// apps/viewer/src/embed/postMessage.ts —— 嵌入 iframe ↔ 宿主 loader 消息通道 // (P4b Task 4;UI-01 扩展 config/destroy)。协议与白名单模型见 // packages/embed-loader(README-embed.md):载荷恒为 // {photoliveEmbed: true, type: 'resize'|'collapse'|'config'|'destroy'|...}。 // // 上行(iframe → 宿主): // resize(高度)/ collapse(用户收起,宿主侧高度 0 + 把手); // config(后台设置动态应用,UI-01:configVersion + settings 归一全集, // 宿主侧再验证一次——仅 position/size/offsetX/offsetBottom/zIndex/ // collapsedDefault 等白名单字段,不传任意 CSS/HTML); // destroy(服务端终止展示:槽 404/410/停用/解绑——宿主移除卡片+把手, // 与「用户收起」语义区分,§5.3)。 // 目标 origin 尽量收敛到 document.referrer 的源(loader 的 iframe 带 // no-referrer-when-downgrade,宿主正常配置下可得完整来源 URL);referrer 被宿主 // 隐去时退回 '*'——载荷仅含展示意图/公开设置,无敏感数据,且宿主侧 loader 以 // 「event.source === 已登记 iframe && event.origin === iframe 源」双重严格校验, // 通配目标不构成越权面。 // // 下行(宿主 → iframe):expand(把手点击恢复)——只认 event.source === // window.parent 且载荷形状匹配(source 校验即充分:宿主页内任意脚本无法伪造 // parent 窗口引用;loader 对上行另有 origin 全等白名单)。 import type { EmbedSlotSettings } from '../api/client'; export type EmbedUpMessage = | { photoliveEmbed: true; type: 'resize'; height: number } | { photoliveEmbed: true; type: 'collapse' } | { photoliveEmbed: true; type: 'config'; configVersion: number; settings: EmbedSlotSettings } | { photoliveEmbed: true; type: 'destroy' }; /** 宿主源(referrer 推导);取不到(被隐去/直接打开)为 null。 */ export function embedParentOrigin(): string | null { try { if (document.referrer === '') return null; return new URL(document.referrer).origin; } catch { return null; } } /** 上行发送:无父窗(直接打开开发/测试)静默跳过,绝不抛错。 */ export function postEmbedMessage(message: EmbedUpMessage): void { if (typeof window === 'undefined' || window.parent === window) return; try { window.parent.postMessage(message, embedParentOrigin() ?? '*'); } catch { /* 静默:嵌入环境异常不得外显 */ } } /** 订阅宿主 expand;返回取消函数。 */ export function onEmbedExpand(handler: () => void): () => void { const listener = (event: MessageEvent): void => { if (window.parent === window || event.source !== window.parent) return; const data = event.data as { photoliveEmbed?: unknown; type?: unknown } | null; if (data === null || typeof data !== 'object') return; if (data.photoliveEmbed !== true || data.type !== 'expand') return; handler(); }; window.addEventListener('message', listener); return () => window.removeEventListener('message', listener); }