Files
cinny/src/app/features/room/widgets/widgetUtils.ts
T
jared 43f4ceb45d 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>
2026-07-03 13:27:23 -04:00

46 lines
1.8 KiB
TypeScript

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)}`;