Files
photo-live/apps/viewer/src/embed/postMessage.ts
T

51 lines
2.4 KiB
TypeScript
Raw Normal View History

// apps/viewer/src/embed/postMessage.ts —— 嵌入 iframe ↔ 宿主 loader 消息通道
// (P4b Task 4)。协议与白名单模型见 packages/embed-loader(README-embed.md):
// 载荷恒为 {photoliveEmbed: true, type: 'resize'|'collapse'|..., ...}。
//
// 上行(iframe → 宿主):resize(高度)/ collapse(收起,宿主侧高度 0 + 把手)。
// 目标 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 全等白名单)。
export type EmbedUpMessage =
| { photoliveEmbed: true; type: 'resize'; height: number }
| { photoliveEmbed: true; type: 'collapse' };
/** 宿主源(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);
}