@crup/port
v0.1.4
Published
A lightweight protocol-first iframe runtime for building secure host/child embeds with explicit lifecycle and messaging.
Maintainers
Readme
@crup/port
Protocol-first iframe runtime for explicit host/child communication.
@crup/port exists for the part of embedded app work that usually rots first: lifecycle, handshake timing, and message discipline. It gives the host page a small runtime for mounting an iframe, opening it inline or in a modal, pinning exact origins, and exchanging request/response/error messages without ad hoc postMessage glue.
Package: https://www.npmjs.com/package/@crup/port
Live demo: https://crup.github.io/port/
Install
npm install @crup/portpnpm add @crup/portyarn add @crup/portImport the host runtime from @crup/port and the child runtime from @crup/port/child.
Quick Links
- npm package: https://www.npmjs.com/package/@crup/port
- live demo: https://crup.github.io/port/
- source: https://github.com/crup/port
- issues: https://github.com/crup/port/issues
Why This Package Exists
- Raw
postMessageis low-level and easy to drift across products. - Iframe lifecycle bugs usually hide in timing and cleanup paths.
- Cross-window integrations need explicit origin pinning and state transitions.
- Small embed runtimes should stay tiny, predictable, and framework-agnostic.
Quick Start
Host
import { createPort } from '@crup/port';
const port = createPort({
url: 'https://example.com/embed',
allowedOrigin: 'https://example.com',
target: '#embed-root',
mode: 'inline',
minHeight: 360,
maxHeight: 720
});
await port.mount();
port.on('widget:loaded', (payload) => {
console.log('child event', payload);
});
const result = await port.call<{ ok: boolean }>('system:ping', {
requestedAt: Date.now()
});
console.log(result.ok);Child
import { createChildPort } from '@crup/port/child';
const child = createChildPort({
allowedOrigin: 'https://host.example.com'
});
child.on('request:system:ping', (message) => {
const request = message as { messageId: string; payload?: unknown };
if (!request.payload) {
child.reject(request.messageId, 'missing ping payload');
return;
}
child.respond(request.messageId, {
ok: true,
receivedAt: Date.now()
});
});
child.emit('widget:loaded', { version: '1' });
child.resize(document.body.scrollHeight);What You Get
- Explicit lifecycle:
idle -> mounting -> mounted -> handshaking -> ready -> open -> closed -> destroyed - Explicit origin pinning on both host and child
- Inline and modal host modes
- Event emission plus request/response/error RPC
- Child-driven height updates
- Small ESM-only bundle built with
tsup
API Surface
createPort(config)
Host runtime with:
mount()open()close()destroy()send(type, payload?)call<T>(type, payload?)on(type, handler)off(type, handler)update(partialConfig)getState()
createChildPort(config)
Child runtime with:
ready()emit(type, payload?)on(type, handler)respond(messageId, payload)reject(messageId, payload?)resize(height)destroy()
Message Shape
type PortMessage = {
protocol: 'crup.port';
version: '1';
instanceId: string;
messageId: string;
replyTo?: string;
kind: 'event' | 'request' | 'response' | 'error' | 'system';
type: string;
payload?: unknown;
};Demo And Examples
- Live GitHub Pages docs and demo: https://crup.github.io/port/
- Inline example:
examples/host-inline.ts - Modal example:
examples/host-modal.ts - Child example:
examples/child-basic.ts - Example overview:
examples/README.md
Documentation
- Getting started:
docs/getting-started.md - API reference:
docs/api-reference.md - Lifecycle guide:
docs/lifecycle.md - Events and RPC:
docs/events-and-rpc.md - Protocol notes:
docs/protocol.md - Example patterns:
docs/examples.md - Security guidance:
docs/security.md - Release process:
docs/releasing.md - Contributing:
CONTRIBUTING.md
Positioning
@crup/port stays intentionally narrow:
- it is a protocol runtime for iframe lifecycle, handshake, resize, and correlated messaging
- it is not a framework adapter layer for React, Vue, or Web Components
- it is not a generic method bridge that reaches into arbitrary child code
- it is not an automatic DOM sync system beyond explicit child-driven
resize()
That scope is what keeps the package small and predictable.
Why Not Penpal, Zoid, Or Postmate?
Those libraries solve adjacent problems, but with a broader or different abstraction:
- Penpal is strong when you want promise-based remote method calls across iframes, windows, and workers.
- Zoid is strong when you want full cross-domain components with props, callbacks, and framework-facing integration patterns.
- Postmate is strong when you want a small promise-based parent/child model API over
postMessage.
@crup/port exists for a narrower use case:
- explicit iframe lifecycle and handshake control
- exact origin pinning on both sides
- simple event plus request/response/error messaging
- explicit child-driven resize instead of a larger component abstraction
If you want a minimal protocol runtime rather than a remote method bridge or cross-domain component toolkit, that is the gap this package is targeting.
Local Development
pnpm install
pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm demo:devUseful scripts:
pnpm docs:buildbuilds the GitHub Pages site intodemo-dist/pnpm sizereports raw and gzip bundle sizes fordist/pnpm changesetadds a release note entry when you want to track pending package notespnpm readme:checkvalidates the README install and package links
Release Model
ci.ymlvalidates lint, types, tests, package build, demo build, README checks, size output, and package packing.docs.ymldeploys the Vite demo to GitHub Pages athttps://crup.github.io/port/.release.ymlis a guarded manual stable release workflow modeled oncrup/react-timer-hook, and it persists the published version back tomain.prerelease.ymlpublishes a manual alpha prerelease from thenextbranch.
Security
This package helps enforce the runtime boundary, but it cannot secure a weak embed strategy on its own. Always pin allowedOrigin, set restrictive iframe attributes, and validate application-level payloads. The practical guidance lives in docs/security.md.
License
Released under the MIT License.
