Files
photo-live/apps/viewer/src/embed/postMessage.ts
T
2026-09-21 18:55:15 +08:00

62 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}