npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ticatec/iframe-message-bridge

v0.2.0

Published

A lightweight TypeScript library for reliable communication between parent window and multiple iframes using postMessage, supporting one-way messages, request-response patterns, broadcast messaging, timeout handling, and automatic resource cleanup.

Readme

iframe-message-bridge

Version License: MIT

中文 | English

A lightweight TypeScript library that implements structured, reliable communication between parent pages and multiple iframes based on postMessage, supporting one-way messages, request-response patterns, and broadcast messages with automatic message tracking and origin validation.

Note: This is a pure ESM (ES Module) package. Only import syntax is supported (no require()). The compiled output uses explicit .js extensions on every relative import, so it loads directly under Node's native ESM loader as well as any bundler.

⚠️ Breaking changes vs. 0.1.x: new MessageBridgeManager() now requires an allowedOrigins whitelist as its first argument, and .broadcast() now requires an explicit targetOrigin (no more silent "*" default). See Security and Migration below.


📦 Features

  • ✅ Support for one-way or two-way communication between parent page and iframes
  • 🔁 Request-response communication with Promise encapsulation
  • 📡 Broadcast messages: Parent page can send messages to all iframes simultaneously
  • 🧩 Multiple iframe support: Parent page can distinguish message sources from each iframe
  • 🔄 Auto iframe discovery: No manual registration required, automatically scans all iframes in the page
  • 🔐 Secure communication: origin whitelist checking and event.source verification are both enforced before any handler runs (see Security)
  • 🧼 Zero runtime dependencies, lightweight and efficient

🚀 Installation

pnpm add @ticatec/iframe-message-bridge

🧠 How It Works

  • MessageBridgeManager — Used in the parent page to receive messages from iframes and respond, while also broadcasting messages to all iframes
  • MessageBridgeClient — Used in iframes to send messages to parent page, wait for responses, and receive broadcast messages

🔧 Usage Examples

In Parent Page

import { MessageBridgeManager } from '@ticatec/iframe-message-bridge';

// allowedOrigins is required: only messages from these origins, AND whose event.source
// is a currently-attached <iframe>'s contentWindow, will ever reach your handlers.
const bridge = new MessageBridgeManager(['https://your-iframe-domain.com']);

// Register request-response event handler
bridge.on('getUserInfo', (data, sourceWindow, sourceOrigin) => {
  console.log('Received data from iframe:', data);
  return { name: 'Alice', role: 'admin' };
});

// Register broadcast message handler (optional, parent page can also receive broadcasts)
bridge.onBroadcast('system-notification', (data) => {
  console.log('Received system notification:', data);
});

// Broadcast message to all iframes -- targetOrigin is required, no more silent "*" default
bridge.broadcast('user-login', {
  userId: 123,
  userName: 'Alice',
  timestamp: Date.now()
}, 'https://your-iframe-domain.com');

// Broadcast theme change
bridge.broadcast('theme-change', { theme: 'dark' }, 'https://your-iframe-domain.com');

In iframe

import { MessageBridgeClient } from '@ticatec/iframe-message-bridge';

const bridge = new MessageBridgeClient('https://your-parent-domain.com');

// Send request and wait for response
bridge.emit('getUserInfo', { id: 123 }).then(response => {
  console.log('Received response from parent page:', response);
}).catch(error => {
  console.error('Request failed or timeout:', error);
});

// Send request with custom timeout (10 seconds)
bridge.emit('slowOperation', { data: 'large' }, 10000).then(response => {
  console.log('Received response:', response);
}).catch(error => {
  if (error.message.includes('timeout')) {
    console.error('Request timed out after 10 seconds');
  }
});

// Send one-way message (no response needed)
bridge.send('logEvent', { action: 'opened-page' });

// Listen to broadcast messages from parent page
bridge.onBroadcast('user-login', (data) => {
  console.log('User login broadcast:', data);
  updateUserInfo(data);
});

bridge.onBroadcast('theme-change', (data) => {
  console.log('Theme change broadcast:', data);
  applyTheme(data.theme);
});

// Unregister specific broadcast event listener
bridge.offBroadcast('theme-change');

// Clear all broadcast listeners
bridge.clearBroadcastHandlers();

// Clean up when component unmounts (important for memory management)
useEffect(() => {
  return () => {
    bridge.destroy(); // Remove all listeners and clear resources
  };
}, []);

📌 API Reference

MessageBridgeManager (Parent Page)

new MessageBridgeManager(allowedOrigins: string[], options?: { debug?: boolean })

Initialize the bridge and register the global message event listener.

  • allowedOrigins: required, non-empty array of trusted origins. Only messages whose event.origin is in this list, and whose event.source is the contentWindow of an <iframe> currently attached to the document, are accepted -- everything else is silently dropped. Pass ['*'] to explicitly disable origin checking (not recommended; only for fully controlled debug/demo scenarios -- doing so logs a console.warn once at construction time).
  • options.debug: when true, logs handler register/unregister activity and rejected-message reasons via console.log/console.warn. Defaults to false so the library stays quiet in production by default.

.on(eventName: string, handler: (data, sourceWindow, sourceOrigin) => any)

Register a request-response event handler. Triggered when an iframe sends a message with the specified event name. Can return data (or a Promise of data) as the response. Each event name supports exactly one handler -- calling .on() again for the same event name overwrites the previous handler and logs a console.warn (it no longer overwrites silently).

.onBroadcast(eventName: string, handler: (data) => void)

Register a broadcast message handler. Same one-handler-per-event, warn-on-overwrite behavior as .on().

.broadcast(eventName: string, data: any, targetOrigin: string)

Send a broadcast message to all iframes currently in the page. Automatically scans and retrieves all <iframe> elements in the current page.

  • eventName: Event name
  • data: Data to send
  • targetOrigin: Required. No more implicit '*' default -- you must consciously decide who's allowed to read the broadcast. Pass '*' explicitly if you really mean "any origin."

.off(eventName: string)

Unregister a specific request-response event handler.

.offBroadcast(eventName: string)

Unregister a specific broadcast message handler.

.clearHandlers()

Clear all request-response event handlers.

.clearBroadcastHandlers()

Clear all broadcast message handlers.

.destroy()

Destroy the manager instance, remove the global message listener, and clear all handlers.


MessageBridgeClient (iframe Page)

new MessageBridgeClient(targetOrigin: string, options?: { debug?: boolean })

Create a client instance, specifying the parent page's origin (e.g., 'https://example.com'). Incoming messages are only accepted when event.origin matches targetOrigin (or targetOrigin is '*') and event.source === window.parent -- this second check matters especially when targetOrigin is '*', since origin checking alone would otherwise accept a forged response from any same-origin window, not just your actual parent page.

  • options.debug: when true, logs rejected/malformed message reasons via console.log. Defaults to false.

.emit(eventName: string, data?: any, timeout?: number): Promise<any>

Send a request-type message and wait for the parent page's response. The pending request is registered before the message is actually posted, so there's no window where a (hypothetically) synchronous response could arrive before it's being listened for.

  • eventName: Event name
  • data: Data to send (optional)
  • timeout: Timeout in milliseconds, defaults to 30000ms (30 seconds)

.send(eventName: string, data?: any): void

Send a one-way message without waiting for a response.

.onBroadcast(eventName: string, handler: (data) => void)

Register a broadcast message handler to listen for broadcast messages from the parent page. One handler per event name; re-registering warns instead of silently overwriting.

.offBroadcast(eventName: string)

Unregister a specific broadcast message handler.

.clearBroadcastHandlers()

Clear all broadcast message handlers.

.clearPendingRequests()

Clear all pending requests and reject their promises, to prevent memory leaks.

.destroy()

Destroy the client instance: remove the global message listener (this now actually works -- see Migration), clear all pending requests, and clear broadcast handlers.


🌟 Communication Patterns

1. Request-Response Pattern (iframe → Parent Page)

// In iframe
const result = await bridge.emit('getData', { id: 123 });

// In parent page
bridge.on('getData', (data) => {
  return fetchDataById(data.id);
});

2. One-way Message (iframe → Parent Page)

// In iframe
bridge.send('analytics', { event: 'page_view' });

// In parent page
bridge.on('analytics', (data) => {
  trackEvent(data.event);
  // No return value needed
});

3. Broadcast Message (Parent Page → All iframes)

// In parent page
bridge.broadcast('global-update', { version: '2.0' }, 'https://your-iframe-domain.com');

// In all iframes
bridge.onBroadcast('global-update', (data) => {
  console.log('Received global update:', data.version);
});

🛡️ Security

The library now enforces two independent checks before any message reaches your handlers, on both ends:

  • MessageBridgeManager: event.origin must be in the allowedOrigins whitelist passed to the constructor, and event.source must be the contentWindow of an <iframe> currently attached to the document (checked via a live scan, so dynamically added/removed iframes are handled correctly). Messages failing either check are dropped before any handler runs.
  • MessageBridgeClient: event.origin must match the targetOrigin passed to the constructor (or targetOrigin is '*'), and event.source === window.parent. This second check matters even when you trust the origin: with targetOrigin: '*', origin checking alone would accept a forged response from any same-origin window, not just your real parent page.
  • All incoming messages also go through runtime shape validation (type, requestId, eventName types are checked, not just the __bridge__ marker), so forged, missing, or mistyped protocol fields are rejected rather than silently mishandled.

Further recommendations:

  • Avoid using "*" as allowedOrigins/targetOrigin/broadcast targetOrigin in production. Every place that accepts "*" does so because you asked for it explicitly -- there's no more silent insecure default anywhere in the library.
  • Recommend adding a sandbox attribute to iframes and limiting permissions.
  • For especially sensitive operations, consider additional application-level authorization inside your handler, not just origin/source trust.

🔄 Automatic iframe Discovery

The library automatically scans all <iframe> elements in the page without manual registration:

// In parent page, these iframes will automatically receive broadcast messages
// <iframe src="module1.html"></iframe>
// <iframe src="module2.html"></iframe>
// <iframe src="module3.html"></iframe>

bridge.broadcast('config-update', newConfig, 'https://your-iframe-domain.com'); // All matching iframes will receive this

Dynamically added iframes will also be automatically discovered on the next broadcast or incoming message (the scan happens live, not once at startup).


🔀 Migrating from 0.1.x

  1. new MessageBridgeManager()new MessageBridgeManager(allowedOrigins). The constructor now requires a non-empty array of trusted origins as its first argument; it throws if you omit it. Pass ['*'] only if you deliberately want to accept messages from any origin.
  2. .broadcast(eventName, data).broadcast(eventName, data, targetOrigin). targetOrigin is now required; pass '*' explicitly if that's really what you want.
  3. MessageBridgeClient.destroy() now actually removes its window message listener. Previously it called removeEventListener with a reference that was never assigned, so the listener leaked forever and kept handling messages after destroy(). If your code relied on (or worked around) that leak, it will now behave correctly instead.
  4. Package import path. Import from @ticatec/iframe-message-bridge (the scoped package name) -- earlier README examples incorrectly showed from 'iframe-message-bridge', which never matched the actual installed package name.
  5. ESM only, require() no longer works. The package is now published as a real ESM module ("type": "module" in package.json). If your project loads this package via CommonJS require(...), that will now throw ERR_REQUIRE_ESM -- switch to import (or a dynamic await import(...)) instead. This library targets browser iframe/parent-window communication, so this mainly affects Node-based tooling (e.g. a test runner) that imports it directly rather than through a bundler.

🧪 Testing

pnpm test        # runs the full vitest suite once
pnpm test:watch  # watch mode

The suite (in tests/) covers normal request/response, a handler throwing, request timeout, destroy() on both MessageBridgeManager and MessageBridgeClient (including that the listener is actually removed), rejecting untrusted origins, rejecting a forged event.source, rejecting malformed protocol fields, one-way messages producing no response, broadcasting to multiple iframes, and multiple manager/client instances coexisting independently.


📜 License

MIT


✨ Author

Developed by Henry Feng
[email protected]