ddf26a177f
Co-Authored-By: Claude Code <noreply@anthropic.com>
62 lines
3.1 KiB
TypeScript
62 lines
3.1 KiB
TypeScript
// 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);
|
||
}
|