/** * Pure helpers for the HTML embed node view. Kept out of the React component so * the sandbox srcdoc builder and the render/edit policy can be unit-tested * against a bare environment with no Tiptap/Mantine providers. */ /** postMessage type the sandboxed iframe uses to report its content height. */ export const HTML_EMBED_HEIGHT_MESSAGE = "gitmost-html-embed-height"; // Sane bounds for the auto-resized iframe so a runaway embed cannot blow up the // page layout, and a sensible default before the first height message arrives. export const MIN_IFRAME_HEIGHT = 40; export const MAX_IFRAME_HEIGHT = 4000; export const DEFAULT_IFRAME_HEIGHT = 150; /** * Sandbox tokens for the embed iframe. Intentionally does NOT include * `allow-same-origin`: the content must run in an opaque ("null") origin so it * cannot read the viewer's cookies/session/API. */ export const HTML_EMBED_SANDBOX = "allow-scripts allow-popups allow-forms"; /** Clamp a reported/configured height into the sane iframe bounds. */ export function clampHeight(h: number): number { return Math.min(MAX_IFRAME_HEIGHT, Math.max(MIN_IFRAME_HEIGHT, h)); } /** * Guard for the auto-resize `message` handler. Returns the clamped numeric * height ONLY when the event is a trusted resize report; otherwise null. * * Trusted means ALL of: * - `event.source` is this iframe's own `contentWindow` (the sandboxed srcdoc * has an opaque "null" origin, so we cannot match by `event.origin` — we * match by source instead). A message from any OTHER window is rejected. * - the payload `type` is exactly our agreed resize message type. * - the reported `height` is a finite number (rejects NaN/Infinity). */ export function isTrustedHeightMessage( event: Pick, iframeEl: { contentWindow: Window | null } | null, ): boolean { // Reject when there is no contentWindow to match against; otherwise a `null` // event.source would spuriously equal a `null` contentWindow. if (!iframeEl?.contentWindow) return false; if (event.source !== iframeEl.contentWindow) return false; const data = event.data as { type?: string; height?: number } | null; if (data?.type !== HTML_EMBED_HEIGHT_MESSAGE) return false; return Number.isFinite(Number(data.height)); } /** * Build the `srcdoc` document for the sandboxed embed iframe. * * The user's `source` is placed verbatim, then a small bootstrap `; return `${source || ""}${bootstrap}`; } /** * Render policy split by editor mode: * - READ-ONLY / public-share view: the SERVER already decided whether to * include the embed (it strips htmlEmbed from shared content when the * workspace master toggle is OFF). An anonymous viewer has no workspace and * thus reads `featureEnabled` as false, so we must NOT gate rendering on it * here — we render exactly the `source` the server chose to serve. * - EDITABLE editor: gate on the per-workspace master toggle so an author sees * the inert placeholder when the feature is OFF. */ export function shouldRender( isEditable: boolean, featureEnabled: boolean, ): boolean { return !isEditable || featureEnabled; } /** * The edit affordance is only meaningful in edit mode and is offered only when * the workspace master toggle is ON. The block renders in a sandboxed iframe * (no same-origin access), so authoring is allowed to ANY member — there is no * admin requirement. */ export function canEdit(isEditable: boolean, featureEnabled: boolean): boolean { return isEditable && featureEnabled; }