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
66 lines
2.0 KiB
TypeScript
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"]);
|
|
});
|
|
});
|