npm.io
0.2.1 • Published 5d ago

iframe-helper-sdk

Licence
MIT
Version
0.2.1
Deps
0
Size
163 kB
Vulns
0
Weekly
443

Iframe Helper SDK

npm version docs types bundle size license

Secure, structured communication between a parent page and cross-domain iframes.

iframe-helper-sdk is a zero-dependency TypeScript SDK for iframe integrations. It wraps window.postMessage with exact origin validation, ready-first handshakes, session routing, bounded queueing, request/response timeouts, fire-and-forget events, opt-in iframe resizing, diagnostics, optional compile-time typed contracts, and an optional child runtime for iframe apps.

Use it when you need a small, auditable bridge between your application and an embedded iframe without building a custom protocol from scratch.

Install

npm install iframe-helper-sdk
pnpm add iframe-helper-sdk
yarn add iframe-helper-sdk

Types are included. No separate @types/* package is required.

Quick Start

Parent Page

The SDK runs in the parent page. Give it a container and an iframe URL, wait for the iframe to complete the handshake, then start communicating.

import { IframeBridgeError, createIframeBridge } from 'iframe-helper-sdk';

const bridge = createIframeBridge({
  container: '#partner-frame',
  src: 'https://partner.example/app',
  iframeAttributes: {
    title: 'Partner application',
  },
  securityProfile: 'strict',
});

try {
  await bridge.whenReady();

  const user = await bridge.request<{ id: string }, { name: string }>('user:get', {
    id: '123',
  });

  console.log(user.name);
} catch (error) {
  if (error instanceof IframeBridgeError) {
    console.error('Bridge failed:', error.code, error.details);
  } else {
    throw error;
  }
}
Iframe Application

The iframe application does not need to install this SDK. It implements the wire protocol directly by reading the bootstrap parameters and replying with protocol envelopes.

const params = new URLSearchParams(window.location.search);
const sessionId = params.get('__iframeBridgeSessionId');
const parentOrigin = params.get('__iframeBridgeParentOrigin');

if (!sessionId || !parentOrigin) {
  throw new Error('Missing iframe bridge bootstrap parameters.');
}

window.parent.postMessage(
  {
    protocol: 'iframe-bridge',
    version: 1,
    sessionId,
    type: 'bridge:ready',
  },
  parentOrigin,
);

window.addEventListener('message', (event) => {
  if (event.origin !== parentOrigin) return;

  const message = event.data;
  if (message?.protocol !== 'iframe-bridge' || message?.version !== 1) return;
  if (message.sessionId !== sessionId) return;

  if (message.type === 'bridge:request' && message.name === 'user:get') {
    window.parent.postMessage(
      {
        protocol: 'iframe-bridge',
        version: 1,
        sessionId,
        type: 'bridge:response',
        requestId: message.requestId,
        payload: { name: 'Ada Lovelace' },
      },
      parentOrigin,
    );
  }
});

See the full Wire Protocol for every envelope type and validation rule.

Iframe Application With Child SDK

If the iframe app can install the package, use the child SDK instead of hand-writing the protocol boilerplate:

import { createIframeChildBridge } from 'iframe-helper-sdk/child';

const bridge = createIframeChildBridge({
  allowedParentOrigins: ['https://host.example'],
});

await bridge.whenConnected();
await bridge.sendEvent('cart:changed', { itemCount: 3 });
bridge.on('theme:changed', (payload) => {});
bridge.handleRequest('user:get', async (payload) => ({ name: 'Ada' }));
bridge.destroy();

allowedParentOrigins?: readonly string[] | null may be omitted or set to null to accept the bootstrap parent origin. Non-empty arrays require exact origin match; empty arrays are invalid. If you omit the allowlist, rely on server-side/browser embedding controls such as CSP frame-ancestors.

The child bridge has no request() method. Child request handlers respond to parent bridge:request; the child does not initiate bridge:request.

Why This SDK

Raw postMessage is intentionally low-level. Every window, iframe, tab, and extension can send messages on the same channel. A production iframe integration needs more structure than a message event listener.

Concern Raw postMessage Iframe Helper SDK
Origin validation Manual and easy to miss Exact origin enforced per bridge
Handshake None or custom Ready-first handshake with timeout
Message format Ad hoc JSON Versioned iframe-bridge envelopes
Request queueing Not built in Bounded pre-ready queue
Timeouts Manual timers Configurable operation timeouts
Typed contracts Manual type assertions Compile-time narrowing via contract maps
Error handling Generic errors IframeBridgeError with code-based branching
Diagnostics Console logging Opt-in diagnostic recorder and logger hooks

Features

  • Strict origin enforcement - exact targetOrigin and allowedOrigin checks. Wildcard origins are rejected.
  • Ready-first handshake - the parent waits for bridge:ready, validates the sender, then responds with bridge:connected.
  • Iframe app SDK - optional iframe-helper-sdk/child runtime for child-side handshake, events, and parent request handlers.
  • Session-scoped routing - every bridge gets a session id so messages from unrelated iframes are ignored.
  • Bounded pre-ready queue - operations called before readiness are queued and flushed after handshake, with a configurable limit.
  • Request/response API - bridge.request() sends a method call and resolves with the iframe response.
  • Event API - bridge.sendEvent(), bridge.on(), and bridge.waitForEvent() cover one-way and inbound event flows.
  • Iframe resize - opt into child-driven width and height updates with parent-side bounds, offsets, and an applied-resize callback.
  • Typed contracts - createTypedIframeBridge narrows method names, payloads, and responses at compile time with no runtime cost.
  • Structured errors - every SDK error is an IframeBridgeError with a stable code and optional details.
  • Diagnostics - use createDiagnosticRecorder or a custom logger to observe lifecycle, handshake, queue, and filtering decisions.
  • Small runtime - zero runtime dependencies, tree-shakable package output, and a compact browser payload.

Communication Patterns

You want to Use
Wait until the iframe is ready bridge.whenReady()
Ask the iframe for a result bridge.request(method, payload)
Notify the iframe without waiting for a body bridge.sendEvent(name, payload)
Subscribe to iframe events bridge.on(name, handler)
Wait for one matching iframe event bridge.waitForEvent(name, options?)
Let the iframe resize itself resizePlugin() + iframe-bridge:resize event
Recreate a failed bridge attempt bridge.remount()
Remove listeners, timers, and the iframe bridge.destroy()

Child-Driven Resize

Resize is opt-in and tree-shakable. Import the resize plugin from the iframe-helper-sdk/resize subpath, register it on the parent, then have the iframe send the reserved iframe-bridge:resize event after the bridge connects.

import { createIframeBridge } from 'iframe-helper-sdk';
import { resizePlugin } from 'iframe-helper-sdk/resize';

const bridge = createIframeBridge(
  {
    container: '#partner-frame',
    src: 'https://partner.example/app',
  },
  {
    plugins: [
      resizePlugin({
        minWidthPx: 320,
        maxWidthPx: 1200,
        minHeightPx: 240,
        maxHeightPx: 900,
        offsetHeightPx: 16,
        onResize({ width, height }) {
          console.log('iframe resized to', width, height);
        },
      }),
    ],
  },
);

The iframe sends the requested dimensions through a standard bridge:event named iframe-bridge:resize. The parent still validates origin, source window, session id, protocol, version, and envelope shape before applying dimensions to the owned iframe element.

Iframe apps that use the child SDK can import the tree-shakable child resize plugin from iframe-helper-sdk/child/resize:

import { createIframeChildBridge } from 'iframe-helper-sdk/child';
import { childResizePlugin } from 'iframe-helper-sdk/child/resize';

const bridge = createIframeChildBridge(
  { allowedParentOrigins: ['https://host.example'] },
  { plugins: [childResizePlugin({ axis: 'both' })] },
);

Child resize must be imported from iframe-helper-sdk/child/resize, not iframe-helper-sdk/child.

offsetWidthPx and offsetHeightPx add fixed parent-side pixels before min/max bounds are applied. In development mode, the SDK warns if resize is enabled without max bounds for every active axis; in securityProfile: 'strict', missing active max bounds throw CONFIG_INVALID_RESIZE.

Full guide: Resize Plugin.

Type-Safe Bridge

Use createTypedIframeBridge when your integration has enough methods or events that repeated generics become noisy. The contract exists only at compile time. It does not validate runtime payloads and is not a security boundary.

import { createTypedIframeBridge } from 'iframe-helper-sdk';

type PartnerContract = {
  requests: {
    'user:get': {
      payload: { id: string };
      response: { name: string; email: string };
    };
    'order:create': {
      payload: { productId: string; quantity: number };
      response: { orderId: string };
    };
  };
  outboundEvents: {
    'analytics:track': { action: string; label?: string };
  };
  inboundEvents: {
    'cart:changed': { itemCount: number };
  };
};

const bridge = createTypedIframeBridge<PartnerContract>({
  container: '#partner-frame',
  src: 'https://partner.example/app',
});

await bridge.whenReady();

const user = await bridge.request('user:get', { id: '123' });
// user is { name: string; email: string }

await bridge.sendEvent('analytics:track', { action: 'opened' });

bridge.on('cart:changed', (payload) => {
  console.log(payload.itemCount);
});

Read more in the Type-Safe Bridge guide.

Security Model

This SDK is a transport and lifecycle layer, not a complete application security solution. It reduces common postMessage mistakes, but authentication, authorization, payload validation, CSRF protection, and server-side business rules remain your responsibility.

Enforced by the SDK:

  • Parent-to-iframe messages always use an exact target origin.
  • Inbound messages are validated against origin, source window, session id, protocol name, protocol version, and envelope shape.
  • HTTPS iframe URLs are required by default. HTTP is only allowed for explicit localhost development mode.
  • Unsafe URL schemes such as javascript:, data:, blob:, and srcdoc are rejected.
  • securityProfile: 'strict' turns risky production settings into config errors.
  • destroy() removes SDK-owned listeners, timers, pending requests, event waits, and the owned iframe.

Production checklist:

  • Use HTTPS for both parent and iframe origins.
  • Set securityProfile: 'strict' once local development is complete.
  • Keep targetOrigin, allowedOrigin, and child allowedParentOrigins exact. Do not use wildcard origins.
  • Add parent-side CSP such as frame-src https://partner.example.
  • Add iframe-side CSP such as frame-ancestors https://host.example.
  • Review iframe sandbox and allow attributes before enabling browser capabilities.
  • Validate critical payloads in your application layer or backend.

Read the full Security guide before shipping a cross-domain integration.

API Surface

Import parent public APIs from the package root. Child APIs and optional plugins use documented subpath exports such as iframe-helper-sdk/child, iframe-helper-sdk/resize, and iframe-helper-sdk/child/resize; never import from internal src or dist paths.

import {
  BRIDGE_MESSAGE_TYPES,
  BRIDGE_PROTOCOL_NAME,
  BRIDGE_PROTOCOL_VERSION,
  IframeBridgeError,
  createDiagnosticRecorder,
  createIframeBridge,
  createTypedIframeBridge,
  isBridgeEnvelope,
  normalizeBridgeRemoteError,
  validateBridgeEnvelope,
} from 'iframe-helper-sdk';

import { resizePlugin } from 'iframe-helper-sdk/resize';
import { createIframeChildBridge } from 'iframe-helper-sdk/child';
import { childResizePlugin } from 'iframe-helper-sdk/child/resize';
Export Kind Purpose
createIframeBridge Function Create a parent-side iframe bridge
createTypedIframeBridge Function Create the same bridge with contract-narrowed types
createIframeChildBridge Function Create a child-side bridge inside an iframe app
createDiagnosticRecorder Function Capture diagnostic events in a bounded recorder
IframeBridgeError Class SDK error with code, message, and details
BRIDGE_MESSAGE_TYPES Constant Tuple of protocol message type strings
BRIDGE_PROTOCOL_NAME Constant Protocol name, currently 'iframe-bridge'
BRIDGE_PROTOCOL_VERSION Constant Protocol version, currently 1
isBridgeEnvelope Function Type guard for bridge envelopes
validateBridgeEnvelope Function Validate and return a typed bridge envelope
normalizeBridgeRemoteError Function Normalize iframe-side error responses
resizePlugin Function Optional child-driven resize plugin from ./resize
childResizePlugin Function Optional child-side resize plugin from ./child/resize
Bridge Instance
type IframeBridge = {
  readonly iframe: HTMLIFrameElement;
  readonly state:
    | 'created'
    | 'mounting'
    | 'waiting_for_handshake'
    | 'ready'
    | 'handshake_failed'
    | 'destroyed';
  request<TPayload, TResponse>(
    method: string,
    payload: TPayload,
    options?: OperationOptions,
  ): Promise<TResponse>;
  sendEvent<TPayload>(name: string, payload: TPayload, options?: OperationOptions): Promise<void>;
  waitForEvent<TPayload>(name: string, options?: OperationOptions): Promise<TPayload>;
  on<TPayload>(name: string, handler: (payload: TPayload) => void): () => void;
  whenReady(): Promise<void>;
  remount(): IframeBridge;
  destroy(): void;
};

Full reference: API Reference.

Error Handling

All SDK errors use IframeBridgeError. Branch on error.code instead of parsing strings.

import { IframeBridgeError } from 'iframe-helper-sdk';

try {
  await bridge.whenReady();
  const result = await bridge.request('report:generate', payload, {
    timeoutMs: 30_000,
  });
  console.log(result);
} catch (error) {
  if (error instanceof IframeBridgeError) {
    switch (error.code) {
      case 'HANDSHAKE_TIMEOUT':
        console.error('The iframe did not complete the handshake.');
        break;
      case 'REQUEST_TIMEOUT':
        console.error('The iframe did not respond in time.');
        break;
      case 'REQUEST_REMOTE_ERROR':
        console.error('The iframe returned an error:', error.details);
        break;
      default:
        console.error(error.code, error.message, error.details);
    }
  } else {
    throw error;
  }
}

Common codes:

Code Meaning
HANDSHAKE_TIMEOUT The iframe did not send a valid bridge:ready in time
REQUEST_TIMEOUT A request was sent, but no matching response arrived in time
REQUEST_REMOTE_ERROR The iframe responded with an explicit error object
EVENT_WAIT_TIMEOUT waitForEvent() did not receive the expected event in time
OPERATION_ABORTED The provided AbortSignal cancelled the operation
BRIDGE_NOT_READY Queueing is disabled and an operation ran before readiness
BRIDGE_DESTROYED The bridge was destroyed before the operation could complete
QUEUE_LIMIT_EXCEEDED Too many operations were queued before the iframe became ready

See the Error Codes reference for the complete list and recovery actions.

Diagnostics

Diagnostics are opt-in. Use the built-in recorder for local debugging or plug your own logger into monitoring.

import { createDiagnosticRecorder, createIframeBridge } from 'iframe-helper-sdk';

const recorder = createDiagnosticRecorder({ maxEntries: 100 });

const bridge = createIframeBridge({
  container: '#partner-frame',
  src: 'https://partner.example/app',
  diagnostics: {
    debug: true,
    logger: recorder.logger,
  },
});

await bridge.whenReady();

console.table(recorder.entries);

Diagnostics are sanitized by design and do not include raw application payloads by default. See Debugging & Diagnostics.

Documentation

Full documentation: furkankaynak.github.io/iframe-helper-sdk

Section Pages
Introduction Home, Getting Started, Core Concepts
Guides Configuration, Type-Safe Bridge, Wire Protocol, Security, Use Cases, Debugging
Child Iframe SDK Overview, Security, Events And Requests, Plugins, Resize
Reference API Reference, Error Codes
Help Troubleshooting, FAQ

Documentation source lives in documentation/docs.

Examples

The repository includes a working parent and iframe playground under playground/manual.

npm run build

Then run the parent and iframe dev servers in separate terminals:

npm run example:manual:parent
npm run example:manual:iframe

Open http://127.0.0.1:5173/. The parent server runs on port 5173, and the iframe server runs on port 5174.

Compatibility

  • Runtime: Browser environments with window, document, HTMLIFrameElement, and postMessage.
  • SSR: Browser-only. Create bridges inside client-only lifecycle hooks such as React useEffect, Vue onMounted, or Svelte onMount.
  • Build target: ES2020 browser output.
  • Package formats: ESM and CommonJS, with TypeScript declarations for both import styles.
  • Node.js: >=18 for development, build, and package tooling.
  • Dependencies: Zero runtime dependencies.

Development

npm install
npm run build
npm run typecheck
npm run test
npm run lint
npm run format:check
Script Description
npm run build Production build with Vite, TypeScript declarations, and CJS type prep
npm run test Vitest test suite
npm run typecheck TypeScript type checking with tsc --noEmit
npm run lint ESLint
npm run format Format source, tests, playground, docs, config, and README files
npm run format:check Prettier format check
npm run verify Full release gate: typecheck, test, lint, format, build, publint, attw, pack
npm run docs:dev Start the Docusaurus documentation server
npm run docs:build Build the documentation site

Contributing

Issues and pull requests are welcome on GitHub. For changes to the public API, update the documentation and README examples in the same pull request.

Before opening a PR, run:

npm run verify
npm run docs:build

License

MIT

Keywords