feat(rooms): Room Widgets (MSC1236 im.vector.modular.widgets)
Phase C.1 of the protocol-gaps roadmap, gate-green (693 tests). Generalizes the Element Call widget host into a general room-widget feature: - StateEvent.Widget + widgetsPanelAtom + useRoomWidgets (WidgetParser). - RoomWidgetView: sandboxed-iframe host via ClientWidgetApi with a conservative GeneralWidgetDriver (approves only benign display caps — no room-event send/read/to-device). Blocks same-origin widget URLs (sandbox breakout guard). - WidgetsPanel: list / open / add / remove, PL-gated on im.vector.modular.widgets, https + non-same-origin URL validation. Mounted like the media gallery (header toggle + 3-way content-panel exclusivity + mobile full-screen overlay). - Tested URL/capability/id helpers. Requires the prod CSP frame-src widening (matrix repo) for external widgets. v1 cuts (capability consent prompt, Jitsi/sticker types, user widgets) noted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { MatrixCapabilities, Capability } from 'matrix-widget-api';
|
||||
import {
|
||||
validateWidgetUrl,
|
||||
isWidgetUrlSafe,
|
||||
filterWidgetCapabilities,
|
||||
generateWidgetId,
|
||||
} from './widgetUtils';
|
||||
|
||||
const APP = 'https://chat.lotusguild.org';
|
||||
|
||||
test('validateWidgetUrl accepts a cross-origin https url', () => {
|
||||
assert.equal(validateWidgetUrl('https://pad.example.org/p/room', APP), undefined);
|
||||
});
|
||||
|
||||
test('validateWidgetUrl rejects empty / invalid / http / same-origin', () => {
|
||||
assert.equal(validateWidgetUrl(' ', APP), 'empty');
|
||||
assert.equal(validateWidgetUrl('not a url', APP), 'invalid');
|
||||
assert.equal(validateWidgetUrl('http://example.org', APP), 'not-https');
|
||||
assert.equal(validateWidgetUrl('https://chat.lotusguild.org/evil', APP), 'same-origin');
|
||||
});
|
||||
|
||||
test('isWidgetUrlSafe rejects same-origin + garbage, accepts cross-origin', () => {
|
||||
assert.equal(isWidgetUrlSafe('https://chat.lotusguild.org/x', APP), false);
|
||||
assert.equal(isWidgetUrlSafe('https://other.example/x', APP), true);
|
||||
assert.equal(isWidgetUrlSafe('garbage', APP), false);
|
||||
});
|
||||
|
||||
test('filterWidgetCapabilities keeps only the benign allowlist', () => {
|
||||
const requested = new Set<Capability>([
|
||||
MatrixCapabilities.AlwaysOnScreen,
|
||||
'm.send.event:m.room.message',
|
||||
'org.matrix.msc2762.receive.state_event:m.room.member',
|
||||
MatrixCapabilities.Screenshots,
|
||||
]);
|
||||
const allowed = filterWidgetCapabilities(requested);
|
||||
assert.ok(allowed.has(MatrixCapabilities.AlwaysOnScreen));
|
||||
assert.ok(allowed.has(MatrixCapabilities.Screenshots));
|
||||
assert.equal(allowed.has('m.send.event:m.room.message'), false);
|
||||
assert.equal(allowed.size, 2);
|
||||
});
|
||||
|
||||
test('generateWidgetId is prefixed and unique across calls', () => {
|
||||
const a = generateWidgetId();
|
||||
const b = generateWidgetId();
|
||||
assert.match(a, /^lotus_/);
|
||||
assert.notEqual(a, b);
|
||||
});
|
||||
Reference in New Issue
Block a user