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,45 @@
|
||||
import { Capability, MatrixCapabilities } from 'matrix-widget-api';
|
||||
|
||||
// Conservative v1 capability policy: approve only benign display capabilities.
|
||||
// Everything else (room-event send/receive, to-device, uploads, user-directory,
|
||||
// delayed events, TURN servers) is denied — a random widget must not be able to
|
||||
// act as the user or read room data without an explicit consent flow (follow-up).
|
||||
export const ALLOWED_WIDGET_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
|
||||
MatrixCapabilities.AlwaysOnScreen,
|
||||
MatrixCapabilities.RequiresClient,
|
||||
MatrixCapabilities.Screenshots,
|
||||
]);
|
||||
|
||||
export const filterWidgetCapabilities = (requested: Set<Capability>): Set<Capability> =>
|
||||
new Set([...requested].filter((cap) => ALLOWED_WIDGET_CAPABILITIES.has(cap)));
|
||||
|
||||
export type WidgetUrlError = 'empty' | 'invalid' | 'not-https' | 'same-origin';
|
||||
|
||||
// A widget URL to ADD must be https and NOT our own origin: a same-origin frame
|
||||
// with allow-same-origin + allow-scripts can break out of the sandbox against us.
|
||||
export const validateWidgetUrl = (raw: string, appOrigin: string): WidgetUrlError | undefined => {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return 'empty';
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(trimmed);
|
||||
} catch {
|
||||
return 'invalid';
|
||||
}
|
||||
if (url.protocol !== 'https:') return 'not-https';
|
||||
if (url.origin === appOrigin) return 'same-origin';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Is an already-resolved (complete) widget URL safe to render in a sandboxed
|
||||
// iframe that carries allow-same-origin? Rejects same-origin URLs (breakout).
|
||||
export const isWidgetUrlSafe = (completeUrl: string, appOrigin: string): boolean => {
|
||||
try {
|
||||
return new URL(completeUrl).origin !== appOrigin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const generateWidgetId = (): string =>
|
||||
`lotus_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
Reference in New Issue
Block a user