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 init

Import 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-bridge

Props

PropTypeDefaultDescription
targetWindow | () => Window | null—The other window: the iframe's contentWindow, or window.parent.
originsstring[]—Exact origins from configuration; malformed entries and wildcards are dropped.
messagesRecord<type, { validate; maxBytes?; allowSecrets? }>—Allow-list of accepted types, each with its validator.
maxBytes / timeoutMsnumber65536 / 5000Size 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.