Files
element-call/src/lotus/lotusWidgetOrigin.test.ts
T
Lotus CIandClaude Opus 5.5 a93bd9d7d8
CI / Build embedded bundle (pull_request) Successful in 3m43s
CI / Publish to Gitea npm registry (pull_request) Skipped
feat(lotus): work when served from its own origin (cinny #43)
Two things tied EC to the host's origin:

- Widget message check: matrix-widget-api's strictOriginCheck compares
  ev.origin with THIS frame's origin, so on call.chat.lotusguild.org every
  message from chat.lotusguild.org would be dropped and calls would not
  start. restrictToHost() instead requires ev.source === window.parent and
  ev.origin === parentUrl's origin. Same-origin deployments keep working
  (the host origin is our own origin there), and it is stricter than
  before: the sender must also be our parent window.
- Soundboard: the host's blob: clip URL is origin-bound. io.lotus.inject_audio
  now accepts the clip's bytes (`audio`, ArrayBuffer, <= 8 MiB) and prefers
  them over `url`; hosts that only send `url` are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-26 23:24:32 -04:00

66 lines
2.0 KiB
TypeScript

/*
Copyright 2026 Lotus Guild
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { describe, expect, it } from "vitest";
import { isFromHost, restrictToHost } from "./lotusWidgetOrigin";
const parent = {} as Window;
const other = {} as Window;
const HOST = "https://chat.example.org";
describe("isFromHost", () => {
it("accepts only the parent window at the host origin", () => {
expect(isFromHost({ source: parent, origin: HOST }, parent, HOST)).toBe(
true,
);
expect(isFromHost({ source: other, origin: HOST }, parent, HOST)).toBe(
false,
);
expect(
isFromHost(
{ source: parent, origin: "https://evil.example" },
parent,
HOST,
),
).toBe(false);
expect(isFromHost({ source: parent, origin: "null" }, parent, HOST)).toBe(
false,
);
});
});
describe("restrictToHost", () => {
it("replaces the listener and drops foreign messages", () => {
const seen: unknown[] = [];
const original = (ev: MessageEvent): void => {
seen.push(ev.data);
};
const transport = { handleMessage: original };
const listeners = new Set<EventListener>([original as EventListener]);
const inbound = {
addEventListener: (_t: string, l: EventListener): void => {
listeners.add(l);
},
removeEventListener: (_t: string, l: EventListener): void => {
listeners.delete(l);
},
} as unknown as Window;
restrictToHost(transport, HOST, parent, inbound);
expect(listeners.has(original as EventListener)).toBe(false);
expect(listeners.size).toBe(1);
const dispatch = (ev: Partial<MessageEvent>): void =>
listeners.forEach((l) => l(ev as unknown as Event));
dispatch({ source: other, origin: HOST, data: "spoof" });
dispatch({ source: parent, origin: "https://evil.example", data: "bad" });
dispatch({ source: parent, origin: HOST, data: "real" });
expect(seen).toEqual(["real"]);
});
});