feat(call): optional Element Call origin via config.json elementCallUrl (#43)
CI / Build & Quality Checks (pull_request) Successful in 3m31s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 12m27s

Groundwork for serving the call page from its own origin
(call.chat.lotusguild.org). Inert until config.json sets `elementCallUrl`:
without it the bundled same-origin page is used exactly as today.

- callPageUrl: resolves `elementCallUrl` — absolute https only (http only on
  localhost for development); anything else, and the desktop app, fall back
  to the bundled page so a bad value can't break calls. Set once from the
  loaded client config.
- CallEmbed builds the widget URL from it; the widget origin (used by the
  message guard and Capability Delegation) follows automatically.
- Soundboard: a host blob: URL can't be fetched from another origin, so
  io.lotus.inject_audio now also carries the clip's bytes (`audio`). Forks
  that predate it ignore the field and use `url`, so this is safe on the
  released fork.

Needs element-call's lotus-call-origin branch (host-origin message check +
inject_audio bytes) released and pinned before `elementCallUrl` is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-26 23:24:24 -04:00
co-authored by Claude Opus 5.5
parent df395776b4
commit 4dcc5176e2
7 changed files with 134 additions and 28 deletions
+14 -6
View File
@@ -564,19 +564,27 @@ export class CallControl extends EventEmitter implements CallControlState {
* track (`io.lotus.inject_audio`) rather than splicing the mic. `url` must be
* an https/blob URL the widget can fetch WITHOUT credentials — the host
* resolves an mxc clip to a `blob:` object URL first (authenticated media
* can't be fetched cross-realm by the widget). `volume` is 0–1.
* can't be fetched cross-realm by the widget) — and `audio` the same clip's
* bytes, which the fork prefers. `volume` is 0–1.
*
* The local user does not hear their own published track, so callers should
* also play the clip locally for feedback.
*/
public injectAudio(url: string, volume = 1): Promise<{ played: boolean; reason?: string }> {
public injectAudio(
url: string,
volume = 1,
audio?: ArrayBuffer,
): Promise<{ played: boolean; reason?: string }> {
// [EC#13] The fork now refuses while the local mic is muted and replies
// { played:false, reason:"muted" }; older forks reply {} (treated as played).
// [Gitea #43] `audio` carries the clip's bytes: a `blob:` URL only works on
// this origin, so a call page on its own origin can't fetch it. Forks that
// predate `audio` ignore it and use `url`.
return this.call.transport
.send<{ url: string; volume: number }, { played?: boolean; reason?: string }>(
'io.lotus.inject_audio',
{ url, volume },
)
.send<
{ url: string; volume: number; audio?: ArrayBuffer },
{ played?: boolean; reason?: string }
>('io.lotus.inject_audio', audio ? { url, volume, audio } : { url, volume })
.then((r) => ({ played: r?.played !== false, reason: r?.reason }))
.catch(() => ({ played: true }));
}