mf-bridge
A typed, origin-checked postMessage channel between a host page and an embedded app.
// Both sides declare what they accept.const isHeight = (p: unknown): p is { height: number } => typeof (p as { height?: unknown })?.height === "number";// Hostconst bridge = createBridge({ target: () => frame.current?.contentWindow ?? null, origins: [process.env.NEXT_PUBLIC_AGENT_ORIGIN!], messages: { resize: { validate: isHeight } },});bridge.on("resize", ({ height }) => setHeight(height));// Embedded appconst parent = createBridge({ target: window.parent, origins: ["https://backoffice.example.com"], messages: {} });await parent.ready;parent.send("resize", { height: document.body.scrollHeight });Installation
Once per app, set up Aedis UI. The CLI verifies the signed release before writing anything and pins every dependency:
npx @aedis/cli@0.1.0 initImport the theme once in your global CSS, after Tailwind:
app/globals.css
@import "tailwindcss";@import "./styles/aedis-theme.css";Then add the component:
npx @aedis/cli@0.1.0 add mf-bridge# pnpm dlx @aedis/cli@0.1.0 add mf-bridge · yarn dlx @aedis/cli@0.1.0 add mf-bridge · bunx @aedis/cli@0.1.0 add mf-bridgeProps
| Prop | Type | Default | Description |
|---|---|---|---|
| target | Window | () => Window | null | — | The other window: the iframe's contentWindow, or window.parent. |
| origins | string[] | — | Exact origins from configuration; malformed entries and wildcards are dropped. |
| messages | Record<type, { validate; maxBytes?; allowSecrets? }> | — | Allow-list of accepted types, each with its validator. |
| maxBytes / timeoutMs | number | 65536 / 5000 | Size limit per message; timeout for requests. |
| returns | { ready; send; on; request; handle; peerOrigin; close } | — | Handshake promise, events and request/response with correlation ids. |
Security
- targetOrigin is always explicit: the peer's origin after the ready/ack handshake, each allowed origin before it. Never "*".
- Every message is checked for event.source (the target window) and event.origin (the allow-list); a peer cannot change origin after the handshake.
- Per type: allow-list, validator and size limit; unknown types and kinds are dropped without an answer.
- No tokens between windows: payloads with secret-looking keys are refused in both directions unless the type opts in with allowSecrets. Use a same-site cookie or a BFF instead.
- A failing handler answers with a bare error; its message never crosses to the other window.