@crup/port 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,15 +2,14 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/%40crup%2Fport?color=1f8b4c)](https://www.npmjs.com/package/@crup/port)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/%40crup%2Fport?color=0b7285)](https://www.npmjs.com/package/@crup/port)
5
+ [![bundle size](https://deno.bundlejs.com/badge?q=%40crup%2Fport)](https://bundlejs.com/?q=%40crup%2Fport)
5
6
  [![License](https://img.shields.io/github/license/crup/port?color=495057)](https://github.com/crup/port/blob/main/LICENSE)
6
7
  [![CI](https://github.com/crup/port/actions/workflows/ci.yml/badge.svg)](https://github.com/crup/port/actions/workflows/ci.yml)
7
8
  [![Docs](https://github.com/crup/port/actions/workflows/docs.yml/badge.svg)](https://github.com/crup/port/actions/workflows/docs.yml)
8
9
 
9
10
  Protocol-first iframe runtime for explicit host/child communication.
10
11
 
11
- `@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, enforcing origin checks, and exchanging request/response messages without ad hoc `postMessage` glue.
12
-
13
- Tags: `iframe` `postMessage` `embed` `protocol` `TypeScript` `runtime`
12
+ `@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.
14
13
 
15
14
  Package: https://www.npmjs.com/package/@crup/port
16
15
 
@@ -87,6 +86,11 @@ const child = createChildPort({
87
86
  child.on('request:system:ping', (message) => {
88
87
  const request = message as { messageId: string; payload?: unknown };
89
88
 
89
+ if (!request.payload) {
90
+ child.reject(request.messageId, 'missing ping payload');
91
+ return;
92
+ }
93
+
90
94
  child.respond(request.messageId, {
91
95
  ok: true,
92
96
  receivedAt: Date.now()
@@ -100,11 +104,11 @@ child.resize(document.body.scrollHeight);
100
104
  ## What You Get
101
105
 
102
106
  - Explicit lifecycle: `idle -> mounting -> mounted -> handshaking -> ready -> open -> closed -> destroyed`
103
- - Strict origin pinning on both host and child
107
+ - Explicit origin pinning on both host and child
104
108
  - Inline and modal host modes
105
- - Event emission plus request/response RPC
109
+ - Event emission plus request/response/error RPC
106
110
  - Child-driven height updates
107
- - Small ESM-first bundle built with `tsup`
111
+ - Small ESM-only bundle built with `tsup`
108
112
 
109
113
  ## API Surface
110
114
 
@@ -123,7 +127,7 @@ Host runtime with:
123
127
  - `update(partialConfig)`
124
128
  - `getState()`
125
129
 
126
- ### `createChildPort(config?)`
130
+ ### `createChildPort(config)`
127
131
 
128
132
  Child runtime with:
129
133
 
@@ -131,6 +135,7 @@ Child runtime with:
131
135
  - `emit(type, payload?)`
132
136
  - `on(type, handler)`
133
137
  - `respond(messageId, payload)`
138
+ - `reject(messageId, payload?)`
134
139
  - `resize(height)`
135
140
  - `destroy()`
136
141
 
@@ -151,7 +156,7 @@ type PortMessage = {
151
156
 
152
157
  ## Demo And Examples
153
158
 
154
- - Live GitHub Pages demo: https://crup.github.io/port/
159
+ - Live GitHub Pages docs and demo: https://crup.github.io/port/
155
160
  - Inline example: [`examples/host-inline.ts`](examples/host-inline.ts)
156
161
  - Modal example: [`examples/host-modal.ts`](examples/host-modal.ts)
157
162
  - Child example: [`examples/child-basic.ts`](examples/child-basic.ts)
@@ -160,11 +165,26 @@ type PortMessage = {
160
165
  ## Documentation
161
166
 
162
167
  - Getting started: [`docs/getting-started.md`](docs/getting-started.md)
168
+ - API reference: [`docs/api-reference.md`](docs/api-reference.md)
169
+ - Lifecycle guide: [`docs/lifecycle.md`](docs/lifecycle.md)
170
+ - Events and RPC: [`docs/events-and-rpc.md`](docs/events-and-rpc.md)
163
171
  - Protocol notes: [`docs/protocol.md`](docs/protocol.md)
172
+ - Example patterns: [`docs/examples.md`](docs/examples.md)
164
173
  - Security guidance: [`docs/security.md`](docs/security.md)
165
174
  - Release process: [`docs/releasing.md`](docs/releasing.md)
166
175
  - Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
167
176
 
177
+ ## Positioning
178
+
179
+ `@crup/port` stays intentionally narrow:
180
+
181
+ - it is a protocol runtime for iframe lifecycle, handshake, resize, and correlated messaging
182
+ - it is not a framework adapter layer for React, Vue, or Web Components
183
+ - it is not a generic method bridge that reaches into arbitrary child code
184
+ - it is not an automatic DOM sync system beyond explicit child-driven `resize()`
185
+
186
+ That scope is what keeps the package small and predictable.
187
+
168
188
  ## Local Development
169
189
 
170
190
  ```bash
@@ -187,12 +207,12 @@ Useful scripts:
187
207
 
188
208
  - `ci.yml` validates lint, types, tests, package build, demo build, README checks, size output, and package packing.
189
209
  - `docs.yml` deploys the Vite demo to GitHub Pages at `https://crup.github.io/port/`.
190
- - `release.yml` is a guarded manual stable release workflow modeled on `crup/react-timer-hook`.
210
+ - `release.yml` is a guarded manual stable release workflow modeled on `crup/react-timer-hook`, and it persists the published version back to `main`.
191
211
  - `prerelease.yml` publishes a manual alpha prerelease from the `next` branch.
192
212
 
193
213
  ## Security
194
214
 
195
- This package helps with origin checks, 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`](docs/security.md).
215
+ 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`](docs/security.md).
196
216
 
197
217
  ## OSS Baseline
198
218
 
package/dist/child.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  type EventHandler<T = unknown> = (payload: T) => void | Promise<void>;
2
2
  interface ChildPortConfig {
3
- allowedOrigin?: string;
3
+ allowedOrigin: string;
4
4
  }
5
5
 
6
6
  interface ChildPort {
@@ -8,9 +8,10 @@ interface ChildPort {
8
8
  emit(type: string, payload?: unknown): void;
9
9
  on(type: string, handler: EventHandler): void;
10
10
  respond(messageId: string, payload: unknown): void;
11
+ reject(messageId: string, payload?: unknown): void;
11
12
  resize(height: number): void;
12
13
  destroy(): void;
13
14
  }
14
- declare function createChildPort(config?: ChildPortConfig): ChildPort;
15
+ declare function createChildPort(config: ChildPortConfig): ChildPort;
15
16
 
16
17
  export { type ChildPort, createChildPort };
package/dist/child.mjs CHANGED
@@ -16,7 +16,18 @@ var Emitter = class {
16
16
  if (!list) {
17
17
  return;
18
18
  }
19
- await Promise.all([...list].map(async (handler) => handler(payload)));
19
+ for (const handler of list) {
20
+ await handler(payload);
21
+ }
22
+ }
23
+ };
24
+
25
+ // src/errors.ts
26
+ var PortError = class extends Error {
27
+ constructor(code, message) {
28
+ super(message);
29
+ this.code = code;
30
+ this.name = "PortError";
20
31
  }
21
32
  };
22
33
 
@@ -35,23 +46,23 @@ function isPortMessage(value) {
35
46
  }
36
47
 
37
48
  // src/child.ts
38
- function createChildPort(config = {}) {
49
+ function createChildPort(config) {
50
+ validateChildConfig(config);
39
51
  const emitter = new Emitter();
40
52
  let instanceId = null;
41
53
  let hostWindow = null;
42
- let hostOrigin = config.allowedOrigin ?? null;
54
+ const hostOrigin = config.allowedOrigin;
43
55
  const listener = (event) => {
44
56
  if (!isPortMessage(event.data)) {
45
57
  return;
46
58
  }
47
59
  const message = event.data;
48
60
  if (message.kind === "system" && message.type === "port:hello") {
49
- instanceId = message.instanceId;
50
- hostWindow = event.source;
51
- hostOrigin = hostOrigin ?? event.origin;
52
61
  if (hostOrigin !== event.origin) {
53
62
  return;
54
63
  }
64
+ instanceId = message.instanceId;
65
+ hostWindow = event.source;
55
66
  ready();
56
67
  return;
57
68
  }
@@ -97,6 +108,9 @@ function createChildPort(config = {}) {
97
108
  function respond(messageId, payload) {
98
109
  post({ kind: "response", type: "port:response", payload, replyTo: messageId });
99
110
  }
111
+ function reject(messageId, payload = "Rejected") {
112
+ post({ kind: "error", type: "port:error", payload, replyTo: messageId });
113
+ }
100
114
  function resize(height) {
101
115
  if (!Number.isFinite(height) || height < 0) {
102
116
  return;
@@ -106,9 +120,21 @@ function createChildPort(config = {}) {
106
120
  function destroy() {
107
121
  window.removeEventListener("message", listener);
108
122
  }
109
- return { ready, emit, on, respond, resize, destroy };
123
+ return { ready, emit, on, respond, reject, resize, destroy };
124
+ }
125
+ function validateChildConfig(config) {
126
+ if (!config.allowedOrigin) {
127
+ throw new PortError("INVALID_CONFIG", "allowedOrigin is required for child ports");
128
+ }
129
+ try {
130
+ const parsed = new URL(config.allowedOrigin);
131
+ if (parsed.origin !== config.allowedOrigin) {
132
+ throw new Error("Origin must not include a path");
133
+ }
134
+ } catch {
135
+ throw new PortError("INVALID_CONFIG", "allowedOrigin must be an exact origin such as https://host.example.com");
136
+ }
110
137
  }
111
138
  export {
112
139
  createChildPort
113
140
  };
114
- //# sourceMappingURL=child.mjs.map
package/dist/index.d.ts CHANGED
@@ -23,6 +23,9 @@ interface PortConfig {
23
23
  maxHeight?: number;
24
24
  }
25
25
  type EventHandler<T = unknown> = (payload: T) => void | Promise<void>;
26
+ interface ChildPortConfig {
27
+ allowedOrigin: string;
28
+ }
26
29
 
27
30
  interface Port {
28
31
  mount(): Promise<void>;
@@ -43,4 +46,4 @@ declare class PortError extends Error {
43
46
  constructor(code: PortErrorCode, message: string);
44
47
  }
45
48
 
46
- export { type Port, type PortConfig, PortError, type PortErrorCode, type PortMessage, type PortState, createPort };
49
+ export { type ChildPortConfig, type Port, type PortConfig, PortError, type PortErrorCode, type PortMessage, type PortState, createPort };
package/dist/index.mjs CHANGED
@@ -16,7 +16,9 @@ var Emitter = class {
16
16
  if (!list) {
17
17
  return;
18
18
  }
19
- await Promise.all([...list].map(async (handler) => handler(payload)));
19
+ for (const handler of list) {
20
+ await handler(payload);
21
+ }
20
22
  }
21
23
  };
22
24
 
@@ -65,6 +67,9 @@ function createPort(input) {
65
67
  let iframe = null;
66
68
  let targetNode = null;
67
69
  let modalRoot = null;
70
+ let pendingHandshake = null;
71
+ let modalBackdropListener = null;
72
+ let modalKeydownListener = null;
68
73
  const listener = (event) => {
69
74
  if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {
70
75
  return;
@@ -80,8 +85,12 @@ function createPort(input) {
80
85
  return;
81
86
  }
82
87
  if (msg.kind === "system" && msg.type === "port:ready") {
83
- if (state === "handshaking") {
88
+ if (state === "handshaking" && pendingHandshake) {
89
+ clearTimeout(pendingHandshake.timeout);
90
+ const { resolve } = pendingHandshake;
91
+ pendingHandshake = null;
84
92
  state = "ready";
93
+ resolve();
85
94
  }
86
95
  return;
87
96
  }
@@ -133,59 +142,68 @@ function createPort(input) {
133
142
  async function mount() {
134
143
  ensureState(["idle"], "mount");
135
144
  state = "mounting";
136
- targetNode = resolveTarget(config.target);
137
- iframe = document.createElement("iframe");
138
- iframe.src = config.url;
139
- iframe.style.width = "100%";
140
- iframe.style.border = "0";
141
- iframe.style.display = config.mode === "modal" ? "none" : "block";
142
- if (config.mode === "modal") {
143
- modalRoot = document.createElement("div");
144
- modalRoot.style.position = "fixed";
145
- modalRoot.style.inset = "0";
146
- modalRoot.style.background = "rgba(0,0,0,0.5)";
147
- modalRoot.style.display = "none";
148
- modalRoot.style.alignItems = "center";
149
- modalRoot.style.justifyContent = "center";
150
- const container = document.createElement("div");
151
- container.style.width = "min(900px, 95vw)";
152
- container.style.height = "min(85vh, 900px)";
153
- container.style.background = "#fff";
154
- container.style.borderRadius = "8px";
155
- container.style.overflow = "hidden";
156
- iframe.style.display = "block";
157
- iframe.style.height = "100%";
158
- container.append(iframe);
159
- modalRoot.append(container);
160
- targetNode.append(modalRoot);
161
- modalRoot.addEventListener("click", (event) => {
162
- if (event.target === modalRoot) {
163
- void close();
164
- }
165
- });
166
- window.addEventListener("keydown", (event) => {
167
- if (event.key === "Escape" && state === "open") {
168
- void close();
169
- }
145
+ try {
146
+ targetNode = resolveTarget(config.target);
147
+ iframe = document.createElement("iframe");
148
+ iframe.src = config.url;
149
+ iframe.style.width = "100%";
150
+ iframe.style.border = "0";
151
+ iframe.style.display = config.mode === "modal" ? "none" : "block";
152
+ if (config.mode === "modal") {
153
+ modalRoot = document.createElement("div");
154
+ modalRoot.style.position = "fixed";
155
+ modalRoot.style.inset = "0";
156
+ modalRoot.style.background = "rgba(0,0,0,0.5)";
157
+ modalRoot.style.display = "none";
158
+ modalRoot.style.alignItems = "center";
159
+ modalRoot.style.justifyContent = "center";
160
+ const container = document.createElement("div");
161
+ container.style.width = "min(900px, 95vw)";
162
+ container.style.height = "min(85vh, 900px)";
163
+ container.style.background = "#fff";
164
+ container.style.borderRadius = "8px";
165
+ container.style.overflow = "hidden";
166
+ iframe.style.display = "block";
167
+ iframe.style.height = "100%";
168
+ container.append(iframe);
169
+ modalRoot.append(container);
170
+ targetNode.append(modalRoot);
171
+ modalBackdropListener = (event) => {
172
+ if (event.target === modalRoot) {
173
+ void close();
174
+ }
175
+ };
176
+ modalRoot.addEventListener("click", modalBackdropListener);
177
+ modalKeydownListener = (event) => {
178
+ if (event.key === "Escape" && state === "open") {
179
+ void close();
180
+ }
181
+ };
182
+ window.addEventListener("keydown", modalKeydownListener);
183
+ } else {
184
+ targetNode.append(iframe);
185
+ }
186
+ await new Promise((resolve, reject) => {
187
+ const timer = setTimeout(() => {
188
+ reject(new PortError("IFRAME_LOAD_TIMEOUT", "iframe did not load in time"));
189
+ }, config.iframeLoadTimeoutMs);
190
+ iframe?.addEventListener(
191
+ "load",
192
+ () => {
193
+ clearTimeout(timer);
194
+ resolve();
195
+ },
196
+ { once: true }
197
+ );
170
198
  });
171
- } else {
172
- targetNode.append(iframe);
199
+ state = "mounted";
200
+ await handshake();
201
+ } catch (error) {
202
+ cleanupPendingHandshake();
203
+ cleanupMountedElements();
204
+ state = "idle";
205
+ throw error;
173
206
  }
174
- await new Promise((resolve, reject) => {
175
- const timer = setTimeout(() => {
176
- reject(new PortError("IFRAME_LOAD_TIMEOUT", "iframe did not load in time"));
177
- }, config.iframeLoadTimeoutMs);
178
- iframe?.addEventListener(
179
- "load",
180
- () => {
181
- clearTimeout(timer);
182
- resolve();
183
- },
184
- { once: true }
185
- );
186
- });
187
- state = "mounted";
188
- await handshake();
189
207
  }
190
208
  async function handshake() {
191
209
  ensureState(["mounted"], "handshake");
@@ -193,19 +211,25 @@ function createPort(input) {
193
211
  throw new PortError("INVALID_STATE", "iframe is unavailable for handshake");
194
212
  }
195
213
  state = "handshaking";
196
- post({ kind: "system", type: "port:hello" });
197
214
  await new Promise((resolve, reject) => {
198
215
  const timer = setTimeout(() => {
199
- clearInterval(poll);
216
+ pendingHandshake = null;
200
217
  reject(new PortError("HANDSHAKE_TIMEOUT", "handshake timed out"));
201
218
  }, config.handshakeTimeoutMs);
202
- const poll = setInterval(() => {
203
- if (state === "ready") {
219
+ pendingHandshake = {
220
+ resolve,
221
+ reject,
222
+ timeout: timer
223
+ };
224
+ try {
225
+ post({ kind: "system", type: "port:hello" });
226
+ } catch (error) {
227
+ if (pendingHandshake) {
228
+ pendingHandshake = null;
204
229
  clearTimeout(timer);
205
- clearInterval(poll);
206
- resolve();
207
230
  }
208
- }, 10);
231
+ reject(error instanceof Error ? error : new Error(String(error)));
232
+ }
209
233
  });
210
234
  if (config.mode === "inline") {
211
235
  state = "open";
@@ -241,12 +265,9 @@ function createPort(input) {
241
265
  entry.reject(new PortError("PORT_DESTROYED", "Port has been destroyed"));
242
266
  });
243
267
  pending.clear();
268
+ cleanupPendingHandshake(new PortError("PORT_DESTROYED", "Port has been destroyed"));
244
269
  window.removeEventListener("message", listener);
245
- iframe?.remove();
246
- modalRoot?.remove();
247
- iframe = null;
248
- modalRoot = null;
249
- targetNode = null;
270
+ cleanupMountedElements();
250
271
  state = "destroyed";
251
272
  }
252
273
  function post(message) {
@@ -257,7 +278,7 @@ function createPort(input) {
257
278
  protocol: PROTOCOL,
258
279
  version: VERSION,
259
280
  instanceId,
260
- messageId: randomId(),
281
+ messageId: message.messageId ?? randomId(),
261
282
  ...message
262
283
  };
263
284
  iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);
@@ -275,22 +296,19 @@ function createPort(input) {
275
296
  }
276
297
  ensureState(["ready", "open", "closed"], "call");
277
298
  const messageId = randomId();
278
- const message = {
279
- protocol: PROTOCOL,
280
- version: VERSION,
281
- instanceId,
282
- messageId,
283
- kind: "request",
284
- type,
285
- payload
286
- };
287
- iframe?.contentWindow?.postMessage(message, config.allowedOrigin);
288
299
  return new Promise((resolve, reject) => {
289
300
  const timeout = setTimeout(() => {
290
301
  pending.delete(messageId);
291
302
  reject(new PortError("CALL_TIMEOUT", `${type} timed out`));
292
303
  }, config.callTimeoutMs);
293
304
  pending.set(messageId, { resolve, reject, timeout });
305
+ try {
306
+ post({ kind: "request", type, payload, messageId });
307
+ } catch (error) {
308
+ clearTimeout(timeout);
309
+ pending.delete(messageId);
310
+ reject(error instanceof Error ? error : new Error(String(error)));
311
+ }
294
312
  });
295
313
  }
296
314
  function on(type, handler) {
@@ -300,6 +318,14 @@ function createPort(input) {
300
318
  emitter.off(type, handler);
301
319
  }
302
320
  function update(next) {
321
+ validatePartialConfig(next, config);
322
+ if (state !== "idle") {
323
+ for (const key of ["url", "allowedOrigin", "target", "mode"]) {
324
+ if (key in next) {
325
+ throw new PortError("INVALID_STATE", `${key} cannot be updated after mount`);
326
+ }
327
+ }
328
+ }
303
329
  Object.assign(config, next);
304
330
  }
305
331
  function getState() {
@@ -315,6 +341,34 @@ function createPort(input) {
315
341
  const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));
316
342
  iframe.style.height = `${bounded}px`;
317
343
  }
344
+ function cleanupMountedElements() {
345
+ cleanupModalListeners();
346
+ iframe?.remove();
347
+ modalRoot?.remove();
348
+ iframe = null;
349
+ modalRoot = null;
350
+ targetNode = null;
351
+ }
352
+ function cleanupModalListeners() {
353
+ if (modalRoot && modalBackdropListener) {
354
+ modalRoot.removeEventListener("click", modalBackdropListener);
355
+ }
356
+ if (modalKeydownListener) {
357
+ window.removeEventListener("keydown", modalKeydownListener);
358
+ }
359
+ modalBackdropListener = null;
360
+ modalKeydownListener = null;
361
+ }
362
+ function cleanupPendingHandshake(reason) {
363
+ if (!pendingHandshake) {
364
+ return;
365
+ }
366
+ clearTimeout(pendingHandshake.timeout);
367
+ if (reason !== void 0) {
368
+ pendingHandshake.reject(reason);
369
+ }
370
+ pendingHandshake = null;
371
+ }
318
372
  return {
319
373
  mount,
320
374
  open,
@@ -332,9 +386,64 @@ function validateConfig(config) {
332
386
  if (!config.url || !config.allowedOrigin || !config.target) {
333
387
  throw new PortError("INVALID_CONFIG", "url, target, and allowedOrigin are required");
334
388
  }
389
+ validateUrl(config.url);
390
+ validateOrigin(config.allowedOrigin);
391
+ validatePartialConfig(config, {
392
+ minHeight: config.minHeight ?? 0,
393
+ maxHeight: config.maxHeight ?? Number.MAX_SAFE_INTEGER
394
+ });
395
+ }
396
+ function validatePartialConfig(next, current) {
397
+ if (next.mode && next.mode !== "inline" && next.mode !== "modal") {
398
+ throw new PortError("INVALID_CONFIG", "mode must be either inline or modal");
399
+ }
400
+ if (typeof next.url === "string") {
401
+ validateUrl(next.url);
402
+ }
403
+ if (typeof next.allowedOrigin === "string") {
404
+ validateOrigin(next.allowedOrigin);
405
+ }
406
+ for (const [key, value] of [
407
+ ["handshakeTimeoutMs", next.handshakeTimeoutMs],
408
+ ["callTimeoutMs", next.callTimeoutMs],
409
+ ["iframeLoadTimeoutMs", next.iframeLoadTimeoutMs]
410
+ ]) {
411
+ if (value !== void 0 && (!Number.isFinite(value) || value <= 0)) {
412
+ throw new PortError("INVALID_CONFIG", `${key} must be a positive number`);
413
+ }
414
+ }
415
+ for (const [key, value] of [
416
+ ["minHeight", next.minHeight],
417
+ ["maxHeight", next.maxHeight]
418
+ ]) {
419
+ if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
420
+ throw new PortError("INVALID_CONFIG", `${key} must be a non-negative number`);
421
+ }
422
+ }
423
+ const minHeight = next.minHeight ?? current.minHeight;
424
+ const maxHeight = next.maxHeight ?? current.maxHeight;
425
+ if (minHeight > maxHeight) {
426
+ throw new PortError("INVALID_CONFIG", "minHeight cannot be greater than maxHeight");
427
+ }
428
+ }
429
+ function validateOrigin(origin) {
430
+ try {
431
+ const parsed = new URL(origin);
432
+ if (parsed.origin !== origin) {
433
+ throw new Error("Origin must not include a path");
434
+ }
435
+ } catch {
436
+ throw new PortError("INVALID_CONFIG", "allowedOrigin must be an exact origin such as https://child.example.com");
437
+ }
438
+ }
439
+ function validateUrl(url) {
440
+ try {
441
+ void new URL(url, window.location.href);
442
+ } catch {
443
+ throw new PortError("INVALID_CONFIG", "url must be a valid iframe URL");
444
+ }
335
445
  }
336
446
  export {
337
447
  PortError,
338
448
  createPort
339
449
  };
340
- //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crup/port",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "A lightweight protocol-first iframe runtime for building secure host/child embeds with explicit lifecycle and messaging.",
5
5
  "homepage": "https://crup.github.io/port/",
6
6
  "repository": {
@@ -44,6 +44,8 @@
44
44
  },
45
45
  "files": [
46
46
  "dist",
47
+ "!dist/**/*.map",
48
+ "!dist/**/*.global.js",
47
49
  "README.md",
48
50
  "LICENSE"
49
51
  ],
@@ -52,17 +54,17 @@
52
54
  },
53
55
  "devDependencies": {
54
56
  "@changesets/cli": "^2.29.7",
55
- "@eslint/js": "^9.25.1",
56
- "@types/node": "^24.5.2",
57
- "eslint": "^9.25.1",
58
- "globals": "^16.0.0",
57
+ "@eslint/js": "^10.0.1",
58
+ "@types/node": "^25.6.0",
59
+ "eslint": "^10.2.0",
60
+ "globals": "^17.5.0",
59
61
  "husky": "^9.1.7",
60
- "jsdom": "^25.0.1",
62
+ "jsdom": "^29.0.2",
61
63
  "tsup": "^8.3.5",
62
- "typescript": "^5.6.3",
64
+ "typescript": "^6.0.2",
63
65
  "typescript-eslint": "^8.31.1",
64
- "vite": "^7.1.10",
65
- "vitest": "^2.1.8"
66
+ "vite": "^8.0.8",
67
+ "vitest": "^4.1.4"
66
68
  },
67
69
  "engines": {
68
70
  "node": ">=18.0.0"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/emitter.ts","../src/utils.ts","../src/child.ts"],"sourcesContent":["import type { EventHandler } from './types';\n\nexport class Emitter {\n private listeners = new Map<string, Set<EventHandler>>();\n\n on(type: string, handler: EventHandler): void {\n const set = this.listeners.get(type) ?? new Set<EventHandler>();\n set.add(handler);\n this.listeners.set(type, set);\n }\n\n off(type: string, handler: EventHandler): void {\n this.listeners.get(type)?.delete(handler);\n }\n\n async emit(type: string, payload: unknown): Promise<void> {\n const list = this.listeners.get(type);\n if (!list) {\n return;\n }\n await Promise.all([...list].map(async (handler) => handler(payload)));\n }\n}\n","import type { PortMessage } from './types';\n\nexport const PROTOCOL = 'crup.port';\nexport const VERSION = '1';\n\nexport function randomId(prefix = 'msg'): string {\n return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function isPortMessage(value: unknown): value is PortMessage {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const data = value as Partial<PortMessage>;\n return (\n data.protocol === PROTOCOL &&\n data.version === VERSION &&\n typeof data.instanceId === 'string' &&\n typeof data.messageId === 'string' &&\n typeof data.kind === 'string' &&\n typeof data.type === 'string'\n );\n}\n","import { Emitter } from './emitter';\nimport type { ChildPortConfig, EventHandler, PortMessage } from './types';\nimport { isPortMessage, PROTOCOL, randomId, VERSION } from './utils';\n\nexport interface ChildPort {\n ready(): void;\n emit(type: string, payload?: unknown): void;\n on(type: string, handler: EventHandler): void;\n respond(messageId: string, payload: unknown): void;\n resize(height: number): void;\n destroy(): void;\n}\n\nexport function createChildPort(config: ChildPortConfig = {}): ChildPort {\n const emitter = new Emitter();\n let instanceId: string | null = null;\n let hostWindow: Window | null = null;\n let hostOrigin: string | null = config.allowedOrigin ?? null;\n\n const listener = (event: MessageEvent): void => {\n if (!isPortMessage(event.data)) {\n return;\n }\n\n const message = event.data;\n\n if (message.kind === 'system' && message.type === 'port:hello') {\n instanceId = message.instanceId;\n hostWindow = event.source as Window;\n hostOrigin = hostOrigin ?? event.origin;\n\n if (hostOrigin !== event.origin) {\n return;\n }\n\n ready();\n return;\n }\n\n if (!instanceId || !hostWindow || message.instanceId !== instanceId) {\n return;\n }\n\n if (event.source !== hostWindow || event.origin !== hostOrigin) {\n return;\n }\n\n if (message.kind === 'event') {\n void emitter.emit(message.type, message.payload);\n return;\n }\n\n if (message.kind === 'request') {\n void emitter.emit(`request:${message.type}`, message);\n }\n };\n\n window.addEventListener('message', listener);\n\n function post(message: Pick<PortMessage, 'kind' | 'type' | 'payload' | 'replyTo'>): void {\n if (!instanceId || !hostWindow || !hostOrigin) {\n return;\n }\n\n hostWindow.postMessage(\n {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId: randomId(),\n ...message\n } satisfies PortMessage,\n hostOrigin\n );\n }\n\n function ready(): void {\n post({ kind: 'system', type: 'port:ready' });\n }\n\n function emit(type: string, payload?: unknown): void {\n post({ kind: 'event', type, payload });\n }\n\n function on(type: string, handler: EventHandler): void {\n emitter.on(type, handler);\n }\n\n function respond(messageId: string, payload: unknown): void {\n post({ kind: 'response', type: 'port:response', payload, replyTo: messageId });\n }\n\n function resize(height: number): void {\n if (!Number.isFinite(height) || height < 0) {\n return;\n }\n post({ kind: 'event', type: 'port:resize', payload: height });\n }\n\n function destroy(): void {\n window.removeEventListener('message', listener);\n }\n\n return { ready, emit, on, respond, resize, destroy };\n}\n"],"mappings":";AAEO,IAAM,UAAN,MAAc;AAAA,EAAd;AACL,SAAQ,YAAY,oBAAI,IAA+B;AAAA;AAAA,EAEvD,GAAG,MAAc,SAA6B;AAC5C,UAAM,MAAM,KAAK,UAAU,IAAI,IAAI,KAAK,oBAAI,IAAkB;AAC9D,QAAI,IAAI,OAAO;AACf,SAAK,UAAU,IAAI,MAAM,GAAG;AAAA,EAC9B;AAAA,EAEA,IAAI,MAAc,SAA6B;AAC7C,SAAK,UAAU,IAAI,IAAI,GAAG,OAAO,OAAO;AAAA,EAC1C;AAAA,EAEA,MAAM,KAAK,MAAc,SAAiC;AACxD,UAAM,OAAO,KAAK,UAAU,IAAI,IAAI;AACpC,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AACA,UAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,EAAE,IAAI,OAAO,YAAY,QAAQ,OAAO,CAAC,CAAC;AAAA,EACtE;AACF;;;ACpBO,IAAM,WAAW;AACjB,IAAM,UAAU;AAEhB,SAAS,SAAS,SAAS,OAAe;AAC/C,SAAO,GAAG,MAAM,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAC7D;AAEO,SAAS,cAAc,OAAsC;AAClE,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO;AAAA,EACT;AAEA,QAAM,OAAO;AACb,SACE,KAAK,aAAa,YAClB,KAAK,YAAY,WACjB,OAAO,KAAK,eAAe,YAC3B,OAAO,KAAK,cAAc,YAC1B,OAAO,KAAK,SAAS,YACrB,OAAO,KAAK,SAAS;AAEzB;;;ACVO,SAAS,gBAAgB,SAA0B,CAAC,GAAc;AACvE,QAAM,UAAU,IAAI,QAAQ;AAC5B,MAAI,aAA4B;AAChC,MAAI,aAA4B;AAChC,MAAI,aAA4B,OAAO,iBAAiB;AAExD,QAAM,WAAW,CAAC,UAA8B;AAC9C,QAAI,CAAC,cAAc,MAAM,IAAI,GAAG;AAC9B;AAAA,IACF;AAEA,UAAM,UAAU,MAAM;AAEtB,QAAI,QAAQ,SAAS,YAAY,QAAQ,SAAS,cAAc;AAC9D,mBAAa,QAAQ;AACrB,mBAAa,MAAM;AACnB,mBAAa,cAAc,MAAM;AAEjC,UAAI,eAAe,MAAM,QAAQ;AAC/B;AAAA,MACF;AAEA,YAAM;AACN;AAAA,IACF;AAEA,QAAI,CAAC,cAAc,CAAC,cAAc,QAAQ,eAAe,YAAY;AACnE;AAAA,IACF;AAEA,QAAI,MAAM,WAAW,cAAc,MAAM,WAAW,YAAY;AAC9D;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS,SAAS;AAC5B,WAAK,QAAQ,KAAK,QAAQ,MAAM,QAAQ,OAAO;AAC/C;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS,WAAW;AAC9B,WAAK,QAAQ,KAAK,WAAW,QAAQ,IAAI,IAAI,OAAO;AAAA,IACtD;AAAA,EACF;AAEA,SAAO,iBAAiB,WAAW,QAAQ;AAE3C,WAAS,KAAK,SAA2E;AACvF,QAAI,CAAC,cAAc,CAAC,cAAc,CAAC,YAAY;AAC7C;AAAA,IACF;AAEA,eAAW;AAAA,MACT;AAAA,QACE,UAAU;AAAA,QACV,SAAS;AAAA,QACT;AAAA,QACA,WAAW,SAAS;AAAA,QACpB,GAAG;AAAA,MACL;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,WAAS,QAAc;AACrB,SAAK,EAAE,MAAM,UAAU,MAAM,aAAa,CAAC;AAAA,EAC7C;AAEA,WAAS,KAAK,MAAc,SAAyB;AACnD,SAAK,EAAE,MAAM,SAAS,MAAM,QAAQ,CAAC;AAAA,EACvC;AAEA,WAAS,GAAG,MAAc,SAA6B;AACrD,YAAQ,GAAG,MAAM,OAAO;AAAA,EAC1B;AAEA,WAAS,QAAQ,WAAmB,SAAwB;AAC1D,SAAK,EAAE,MAAM,YAAY,MAAM,iBAAiB,SAAS,SAAS,UAAU,CAAC;AAAA,EAC/E;AAEA,WAAS,OAAO,QAAsB;AACpC,QAAI,CAAC,OAAO,SAAS,MAAM,KAAK,SAAS,GAAG;AAC1C;AAAA,IACF;AACA,SAAK,EAAE,MAAM,SAAS,MAAM,eAAe,SAAS,OAAO,CAAC;AAAA,EAC9D;AAEA,WAAS,UAAgB;AACvB,WAAO,oBAAoB,WAAW,QAAQ;AAAA,EAChD;AAEA,SAAO,EAAE,OAAO,MAAM,IAAI,SAAS,QAAQ,QAAQ;AACrD;","names":[]}
@@ -1,365 +0,0 @@
1
- "use strict";
2
- var CrupPort = (() => {
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
-
21
- // src/index.ts
22
- var src_exports = {};
23
- __export(src_exports, {
24
- PortError: () => PortError,
25
- createPort: () => createPort
26
- });
27
-
28
- // src/emitter.ts
29
- var Emitter = class {
30
- constructor() {
31
- this.listeners = /* @__PURE__ */ new Map();
32
- }
33
- on(type, handler) {
34
- const set = this.listeners.get(type) ?? /* @__PURE__ */ new Set();
35
- set.add(handler);
36
- this.listeners.set(type, set);
37
- }
38
- off(type, handler) {
39
- this.listeners.get(type)?.delete(handler);
40
- }
41
- async emit(type, payload) {
42
- const list = this.listeners.get(type);
43
- if (!list) {
44
- return;
45
- }
46
- await Promise.all([...list].map(async (handler) => handler(payload)));
47
- }
48
- };
49
-
50
- // src/errors.ts
51
- var PortError = class extends Error {
52
- constructor(code, message) {
53
- super(message);
54
- this.code = code;
55
- this.name = "PortError";
56
- }
57
- };
58
-
59
- // src/utils.ts
60
- var PROTOCOL = "crup.port";
61
- var VERSION = "1";
62
- function randomId(prefix = "msg") {
63
- return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
64
- }
65
- function isPortMessage(value) {
66
- if (typeof value !== "object" || value === null) {
67
- return false;
68
- }
69
- const data = value;
70
- return data.protocol === PROTOCOL && data.version === VERSION && typeof data.instanceId === "string" && typeof data.messageId === "string" && typeof data.kind === "string" && typeof data.type === "string";
71
- }
72
-
73
- // src/host.ts
74
- var DEFAULT_HANDSHAKE_TIMEOUT = 8e3;
75
- var DEFAULT_CALL_TIMEOUT = 8e3;
76
- var DEFAULT_IFRAME_LOAD_TIMEOUT = 8e3;
77
- function createPort(input) {
78
- validateConfig(input);
79
- const config = {
80
- ...input,
81
- mode: input.mode ?? "inline",
82
- handshakeTimeoutMs: input.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT,
83
- callTimeoutMs: input.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT,
84
- iframeLoadTimeoutMs: input.iframeLoadTimeoutMs ?? DEFAULT_IFRAME_LOAD_TIMEOUT,
85
- minHeight: input.minHeight ?? 0,
86
- maxHeight: input.maxHeight ?? Number.MAX_SAFE_INTEGER
87
- };
88
- const instanceId = randomId("port");
89
- const emitter = new Emitter();
90
- const pending = /* @__PURE__ */ new Map();
91
- let state = "idle";
92
- let iframe = null;
93
- let targetNode = null;
94
- let modalRoot = null;
95
- const listener = (event) => {
96
- if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {
97
- return;
98
- }
99
- if (event.origin !== config.allowedOrigin) {
100
- return;
101
- }
102
- if (!isPortMessage(event.data)) {
103
- return;
104
- }
105
- const msg = event.data;
106
- if (msg.instanceId !== instanceId) {
107
- return;
108
- }
109
- if (msg.kind === "system" && msg.type === "port:ready") {
110
- if (state === "handshaking") {
111
- state = "ready";
112
- }
113
- return;
114
- }
115
- if (msg.kind === "response" && msg.replyTo) {
116
- const call2 = pending.get(msg.replyTo);
117
- if (!call2) {
118
- return;
119
- }
120
- clearTimeout(call2.timeout);
121
- pending.delete(msg.replyTo);
122
- call2.resolve(msg.payload);
123
- return;
124
- }
125
- if (msg.kind === "error" && msg.replyTo) {
126
- const call2 = pending.get(msg.replyTo);
127
- if (!call2) {
128
- return;
129
- }
130
- clearTimeout(call2.timeout);
131
- pending.delete(msg.replyTo);
132
- const reason = typeof msg.payload === "string" ? msg.payload : "Rejected";
133
- call2.reject(new PortError("MESSAGE_REJECTED", reason));
134
- return;
135
- }
136
- if (msg.kind === "event" && msg.type === "port:resize") {
137
- applyResize(msg.payload);
138
- return;
139
- }
140
- if (msg.kind === "event") {
141
- void emitter.emit(msg.type, msg.payload);
142
- }
143
- };
144
- window.addEventListener("message", listener);
145
- function ensureState(valid, nextAction) {
146
- if (!valid.includes(state)) {
147
- throw new PortError("INVALID_STATE", `Cannot ${nextAction} from state ${state}`);
148
- }
149
- }
150
- function resolveTarget(target) {
151
- if (typeof target === "string") {
152
- const node = document.querySelector(target);
153
- if (!node) {
154
- throw new PortError("INVALID_CONFIG", `Target ${target} was not found`);
155
- }
156
- return node;
157
- }
158
- return target;
159
- }
160
- async function mount() {
161
- ensureState(["idle"], "mount");
162
- state = "mounting";
163
- targetNode = resolveTarget(config.target);
164
- iframe = document.createElement("iframe");
165
- iframe.src = config.url;
166
- iframe.style.width = "100%";
167
- iframe.style.border = "0";
168
- iframe.style.display = config.mode === "modal" ? "none" : "block";
169
- if (config.mode === "modal") {
170
- modalRoot = document.createElement("div");
171
- modalRoot.style.position = "fixed";
172
- modalRoot.style.inset = "0";
173
- modalRoot.style.background = "rgba(0,0,0,0.5)";
174
- modalRoot.style.display = "none";
175
- modalRoot.style.alignItems = "center";
176
- modalRoot.style.justifyContent = "center";
177
- const container = document.createElement("div");
178
- container.style.width = "min(900px, 95vw)";
179
- container.style.height = "min(85vh, 900px)";
180
- container.style.background = "#fff";
181
- container.style.borderRadius = "8px";
182
- container.style.overflow = "hidden";
183
- iframe.style.display = "block";
184
- iframe.style.height = "100%";
185
- container.append(iframe);
186
- modalRoot.append(container);
187
- targetNode.append(modalRoot);
188
- modalRoot.addEventListener("click", (event) => {
189
- if (event.target === modalRoot) {
190
- void close();
191
- }
192
- });
193
- window.addEventListener("keydown", (event) => {
194
- if (event.key === "Escape" && state === "open") {
195
- void close();
196
- }
197
- });
198
- } else {
199
- targetNode.append(iframe);
200
- }
201
- await new Promise((resolve, reject) => {
202
- const timer = setTimeout(() => {
203
- reject(new PortError("IFRAME_LOAD_TIMEOUT", "iframe did not load in time"));
204
- }, config.iframeLoadTimeoutMs);
205
- iframe?.addEventListener(
206
- "load",
207
- () => {
208
- clearTimeout(timer);
209
- resolve();
210
- },
211
- { once: true }
212
- );
213
- });
214
- state = "mounted";
215
- await handshake();
216
- }
217
- async function handshake() {
218
- ensureState(["mounted"], "handshake");
219
- if (!iframe?.contentWindow) {
220
- throw new PortError("INVALID_STATE", "iframe is unavailable for handshake");
221
- }
222
- state = "handshaking";
223
- post({ kind: "system", type: "port:hello" });
224
- await new Promise((resolve, reject) => {
225
- const timer = setTimeout(() => {
226
- clearInterval(poll);
227
- reject(new PortError("HANDSHAKE_TIMEOUT", "handshake timed out"));
228
- }, config.handshakeTimeoutMs);
229
- const poll = setInterval(() => {
230
- if (state === "ready") {
231
- clearTimeout(timer);
232
- clearInterval(poll);
233
- resolve();
234
- }
235
- }, 10);
236
- });
237
- if (config.mode === "inline") {
238
- state = "open";
239
- }
240
- }
241
- function open() {
242
- ensureState(["ready", "closed"], "open");
243
- if (config.mode !== "modal") {
244
- state = "open";
245
- return Promise.resolve();
246
- }
247
- if (!modalRoot) {
248
- throw new PortError("INVALID_STATE", "modal root missing");
249
- }
250
- modalRoot.style.display = "flex";
251
- state = "open";
252
- return Promise.resolve();
253
- }
254
- function close() {
255
- ensureState(["open"], "close");
256
- if (config.mode === "modal" && modalRoot) {
257
- modalRoot.style.display = "none";
258
- }
259
- state = "closed";
260
- return Promise.resolve();
261
- }
262
- function destroy() {
263
- if (state === "destroyed") {
264
- return;
265
- }
266
- pending.forEach((entry) => {
267
- clearTimeout(entry.timeout);
268
- entry.reject(new PortError("PORT_DESTROYED", "Port has been destroyed"));
269
- });
270
- pending.clear();
271
- window.removeEventListener("message", listener);
272
- iframe?.remove();
273
- modalRoot?.remove();
274
- iframe = null;
275
- modalRoot = null;
276
- targetNode = null;
277
- state = "destroyed";
278
- }
279
- function post(message) {
280
- if (!iframe?.contentWindow) {
281
- throw new PortError("INVALID_STATE", "iframe is not available");
282
- }
283
- const finalMessage = {
284
- protocol: PROTOCOL,
285
- version: VERSION,
286
- instanceId,
287
- messageId: randomId(),
288
- ...message
289
- };
290
- iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);
291
- }
292
- function send(type, payload) {
293
- if (state === "destroyed") {
294
- throw new PortError("PORT_DESTROYED", "Port is destroyed");
295
- }
296
- ensureState(["ready", "open", "closed"], "send");
297
- post({ kind: "event", type, payload });
298
- }
299
- function call(type, payload) {
300
- if (state === "destroyed") {
301
- return Promise.reject(new PortError("PORT_DESTROYED", "Port is destroyed"));
302
- }
303
- ensureState(["ready", "open", "closed"], "call");
304
- const messageId = randomId();
305
- const message = {
306
- protocol: PROTOCOL,
307
- version: VERSION,
308
- instanceId,
309
- messageId,
310
- kind: "request",
311
- type,
312
- payload
313
- };
314
- iframe?.contentWindow?.postMessage(message, config.allowedOrigin);
315
- return new Promise((resolve, reject) => {
316
- const timeout = setTimeout(() => {
317
- pending.delete(messageId);
318
- reject(new PortError("CALL_TIMEOUT", `${type} timed out`));
319
- }, config.callTimeoutMs);
320
- pending.set(messageId, { resolve, reject, timeout });
321
- });
322
- }
323
- function on(type, handler) {
324
- emitter.on(type, handler);
325
- }
326
- function off(type, handler) {
327
- emitter.off(type, handler);
328
- }
329
- function update(next) {
330
- Object.assign(config, next);
331
- }
332
- function getState() {
333
- return state;
334
- }
335
- function applyResize(payload) {
336
- if (!iframe) {
337
- return;
338
- }
339
- if (typeof payload !== "number" || Number.isNaN(payload)) {
340
- return;
341
- }
342
- const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));
343
- iframe.style.height = `${bounded}px`;
344
- }
345
- return {
346
- mount,
347
- open,
348
- close,
349
- destroy,
350
- send,
351
- call,
352
- on,
353
- off,
354
- update,
355
- getState
356
- };
357
- }
358
- function validateConfig(config) {
359
- if (!config.url || !config.allowedOrigin || !config.target) {
360
- throw new PortError("INVALID_CONFIG", "url, target, and allowedOrigin are required");
361
- }
362
- }
363
- return __toCommonJS(src_exports);
364
- })();
365
- //# sourceMappingURL=index.global.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts","../src/emitter.ts","../src/errors.ts","../src/utils.ts","../src/host.ts"],"sourcesContent":["export { createPort } from './host';\nexport { PortError } from './errors';\nexport type { Port } from './host';\nexport type { PortConfig, PortErrorCode, PortMessage, PortState } from './types';\n","import type { EventHandler } from './types';\n\nexport class Emitter {\n private listeners = new Map<string, Set<EventHandler>>();\n\n on(type: string, handler: EventHandler): void {\n const set = this.listeners.get(type) ?? new Set<EventHandler>();\n set.add(handler);\n this.listeners.set(type, set);\n }\n\n off(type: string, handler: EventHandler): void {\n this.listeners.get(type)?.delete(handler);\n }\n\n async emit(type: string, payload: unknown): Promise<void> {\n const list = this.listeners.get(type);\n if (!list) {\n return;\n }\n await Promise.all([...list].map(async (handler) => handler(payload)));\n }\n}\n","import type { PortErrorCode } from './types';\n\nexport class PortError extends Error {\n readonly code: PortErrorCode;\n\n constructor(code: PortErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'PortError';\n }\n}\n","import type { PortMessage } from './types';\n\nexport const PROTOCOL = 'crup.port';\nexport const VERSION = '1';\n\nexport function randomId(prefix = 'msg'): string {\n return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function isPortMessage(value: unknown): value is PortMessage {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const data = value as Partial<PortMessage>;\n return (\n data.protocol === PROTOCOL &&\n data.version === VERSION &&\n typeof data.instanceId === 'string' &&\n typeof data.messageId === 'string' &&\n typeof data.kind === 'string' &&\n typeof data.type === 'string'\n );\n}\n","import { Emitter } from './emitter';\nimport { PortError } from './errors';\nimport type { EventHandler, PortConfig, PortMessage, PortState } from './types';\nimport { isPortMessage, PROTOCOL, randomId, VERSION } from './utils';\n\ninterface PendingCall {\n resolve: (value: unknown) => void;\n reject: (reason?: unknown) => void;\n timeout: ReturnType<typeof setTimeout>;\n}\n\nconst DEFAULT_HANDSHAKE_TIMEOUT = 8_000;\nconst DEFAULT_CALL_TIMEOUT = 8_000;\nconst DEFAULT_IFRAME_LOAD_TIMEOUT = 8_000;\n\nexport interface Port {\n mount(): Promise<void>;\n open(): Promise<void>;\n close(): Promise<void>;\n destroy(): void;\n send(type: string, payload?: unknown): void;\n call<T = unknown>(type: string, payload?: unknown): Promise<T>;\n on(type: string, handler: EventHandler): void;\n off(type: string, handler: EventHandler): void;\n update(config: Partial<PortConfig>): void;\n getState(): PortState;\n}\n\nexport function createPort(input: PortConfig): Port {\n validateConfig(input);\n\n const config: Required<\n Pick<PortConfig, 'mode' | 'handshakeTimeoutMs' | 'callTimeoutMs' | 'iframeLoadTimeoutMs' | 'minHeight' | 'maxHeight'>\n > &\n PortConfig = {\n ...input,\n mode: input.mode ?? 'inline',\n handshakeTimeoutMs: input.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT,\n callTimeoutMs: input.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT,\n iframeLoadTimeoutMs: input.iframeLoadTimeoutMs ?? DEFAULT_IFRAME_LOAD_TIMEOUT,\n minHeight: input.minHeight ?? 0,\n maxHeight: input.maxHeight ?? Number.MAX_SAFE_INTEGER\n };\n\n const instanceId = randomId('port');\n const emitter = new Emitter();\n const pending = new Map<string, PendingCall>();\n let state: PortState = 'idle';\n let iframe: HTMLIFrameElement | null = null;\n let targetNode: HTMLElement | null = null;\n let modalRoot: HTMLDivElement | null = null;\n\n const listener = (event: MessageEvent): void => {\n if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {\n return;\n }\n\n if (event.origin !== config.allowedOrigin) {\n return;\n }\n\n if (!isPortMessage(event.data)) {\n return;\n }\n\n const msg = event.data;\n if (msg.instanceId !== instanceId) {\n return;\n }\n\n if (msg.kind === 'system' && msg.type === 'port:ready') {\n if (state === 'handshaking') {\n state = 'ready';\n }\n return;\n }\n\n if (msg.kind === 'response' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n call.resolve(msg.payload);\n return;\n }\n\n if (msg.kind === 'error' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n const reason = typeof msg.payload === 'string' ? msg.payload : 'Rejected';\n call.reject(new PortError('MESSAGE_REJECTED', reason));\n return;\n }\n\n if (msg.kind === 'event' && msg.type === 'port:resize') {\n applyResize(msg.payload);\n return;\n }\n\n if (msg.kind === 'event') {\n void emitter.emit(msg.type, msg.payload);\n }\n };\n\n window.addEventListener('message', listener);\n\n function ensureState(valid: PortState[], nextAction: string): void {\n if (!valid.includes(state)) {\n throw new PortError('INVALID_STATE', `Cannot ${nextAction} from state ${state}`);\n }\n }\n\n function resolveTarget(target: PortConfig['target']): HTMLElement {\n if (typeof target === 'string') {\n const node = document.querySelector<HTMLElement>(target);\n if (!node) {\n throw new PortError('INVALID_CONFIG', `Target ${target} was not found`);\n }\n return node;\n }\n return target;\n }\n\n async function mount(): Promise<void> {\n ensureState(['idle'], 'mount');\n state = 'mounting';\n\n targetNode = resolveTarget(config.target);\n iframe = document.createElement('iframe');\n iframe.src = config.url;\n iframe.style.width = '100%';\n iframe.style.border = '0';\n iframe.style.display = config.mode === 'modal' ? 'none' : 'block';\n\n if (config.mode === 'modal') {\n modalRoot = document.createElement('div');\n modalRoot.style.position = 'fixed';\n modalRoot.style.inset = '0';\n modalRoot.style.background = 'rgba(0,0,0,0.5)';\n modalRoot.style.display = 'none';\n modalRoot.style.alignItems = 'center';\n modalRoot.style.justifyContent = 'center';\n\n const container = document.createElement('div');\n container.style.width = 'min(900px, 95vw)';\n container.style.height = 'min(85vh, 900px)';\n container.style.background = '#fff';\n container.style.borderRadius = '8px';\n container.style.overflow = 'hidden';\n iframe.style.display = 'block';\n iframe.style.height = '100%';\n\n container.append(iframe);\n modalRoot.append(container);\n targetNode.append(modalRoot);\n\n modalRoot.addEventListener('click', (event) => {\n if (event.target === modalRoot) {\n void close();\n }\n });\n\n window.addEventListener('keydown', (event) => {\n if (event.key === 'Escape' && state === 'open') {\n void close();\n }\n });\n } else {\n targetNode.append(iframe);\n }\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new PortError('IFRAME_LOAD_TIMEOUT', 'iframe did not load in time'));\n }, config.iframeLoadTimeoutMs);\n\n iframe?.addEventListener(\n 'load',\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true }\n );\n });\n\n state = 'mounted';\n await handshake();\n }\n\n async function handshake(): Promise<void> {\n ensureState(['mounted'], 'handshake');\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is unavailable for handshake');\n }\n\n state = 'handshaking';\n post({ kind: 'system', type: 'port:hello' });\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n clearInterval(poll);\n reject(new PortError('HANDSHAKE_TIMEOUT', 'handshake timed out'));\n }, config.handshakeTimeoutMs);\n\n const poll = setInterval(() => {\n if (state === 'ready') {\n clearTimeout(timer);\n clearInterval(poll);\n resolve();\n }\n }, 10);\n });\n\n if (config.mode === 'inline') {\n state = 'open';\n }\n }\n\n function open(): Promise<void> {\n ensureState(['ready', 'closed'], 'open');\n if (config.mode !== 'modal') {\n state = 'open';\n return Promise.resolve();\n }\n if (!modalRoot) {\n throw new PortError('INVALID_STATE', 'modal root missing');\n }\n modalRoot.style.display = 'flex';\n state = 'open';\n return Promise.resolve();\n }\n\n function close(): Promise<void> {\n ensureState(['open'], 'close');\n if (config.mode === 'modal' && modalRoot) {\n modalRoot.style.display = 'none';\n }\n state = 'closed';\n return Promise.resolve();\n }\n\n function destroy(): void {\n if (state === 'destroyed') {\n return;\n }\n\n pending.forEach((entry) => {\n clearTimeout(entry.timeout);\n entry.reject(new PortError('PORT_DESTROYED', 'Port has been destroyed'));\n });\n pending.clear();\n\n window.removeEventListener('message', listener);\n iframe?.remove();\n modalRoot?.remove();\n iframe = null;\n modalRoot = null;\n targetNode = null;\n state = 'destroyed';\n }\n\n function post(message: Pick<PortMessage, 'kind' | 'type' | 'payload' | 'replyTo'>): void {\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is not available');\n }\n\n const finalMessage: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId: randomId(),\n ...message\n };\n\n iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);\n }\n\n function send(type: string, payload?: unknown): void {\n if (state === 'destroyed') {\n throw new PortError('PORT_DESTROYED', 'Port is destroyed');\n }\n\n ensureState(['ready', 'open', 'closed'], 'send');\n post({ kind: 'event', type, payload });\n }\n\n function call<T = unknown>(type: string, payload?: unknown): Promise<T> {\n if (state === 'destroyed') {\n return Promise.reject(new PortError('PORT_DESTROYED', 'Port is destroyed'));\n }\n\n ensureState(['ready', 'open', 'closed'], 'call');\n\n const messageId = randomId();\n const message: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId,\n kind: 'request',\n type,\n payload\n };\n\n iframe?.contentWindow?.postMessage(message, config.allowedOrigin);\n\n return new Promise<T>((resolve, reject) => {\n const timeout = setTimeout(() => {\n pending.delete(messageId);\n reject(new PortError('CALL_TIMEOUT', `${type} timed out`));\n }, config.callTimeoutMs);\n\n pending.set(messageId, { resolve: resolve as (value: unknown) => void, reject, timeout });\n });\n }\n\n function on(type: string, handler: EventHandler): void {\n emitter.on(type, handler);\n }\n\n function off(type: string, handler: EventHandler): void {\n emitter.off(type, handler);\n }\n\n function update(next: Partial<PortConfig>): void {\n Object.assign(config, next);\n }\n\n function getState(): PortState {\n return state;\n }\n\n function applyResize(payload: unknown): void {\n if (!iframe) {\n return;\n }\n if (typeof payload !== 'number' || Number.isNaN(payload)) {\n return;\n }\n\n const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));\n iframe.style.height = `${bounded}px`;\n }\n\n return {\n mount,\n open,\n close,\n destroy,\n send,\n call,\n on,\n off,\n update,\n getState\n };\n}\n\nfunction validateConfig(config: PortConfig): void {\n if (!config.url || !config.allowedOrigin || !config.target) {\n throw new PortError('INVALID_CONFIG', 'url, target, and allowedOrigin are required');\n }\n}\n"],"mappings":"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","names":["call"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/emitter.ts","../src/errors.ts","../src/utils.ts","../src/host.ts"],"sourcesContent":["import type { EventHandler } from './types';\n\nexport class Emitter {\n private listeners = new Map<string, Set<EventHandler>>();\n\n on(type: string, handler: EventHandler): void {\n const set = this.listeners.get(type) ?? new Set<EventHandler>();\n set.add(handler);\n this.listeners.set(type, set);\n }\n\n off(type: string, handler: EventHandler): void {\n this.listeners.get(type)?.delete(handler);\n }\n\n async emit(type: string, payload: unknown): Promise<void> {\n const list = this.listeners.get(type);\n if (!list) {\n return;\n }\n await Promise.all([...list].map(async (handler) => handler(payload)));\n }\n}\n","import type { PortErrorCode } from './types';\n\nexport class PortError extends Error {\n readonly code: PortErrorCode;\n\n constructor(code: PortErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'PortError';\n }\n}\n","import type { PortMessage } from './types';\n\nexport const PROTOCOL = 'crup.port';\nexport const VERSION = '1';\n\nexport function randomId(prefix = 'msg'): string {\n return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function isPortMessage(value: unknown): value is PortMessage {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const data = value as Partial<PortMessage>;\n return (\n data.protocol === PROTOCOL &&\n data.version === VERSION &&\n typeof data.instanceId === 'string' &&\n typeof data.messageId === 'string' &&\n typeof data.kind === 'string' &&\n typeof data.type === 'string'\n );\n}\n","import { Emitter } from './emitter';\nimport { PortError } from './errors';\nimport type { EventHandler, PortConfig, PortMessage, PortState } from './types';\nimport { isPortMessage, PROTOCOL, randomId, VERSION } from './utils';\n\ninterface PendingCall {\n resolve: (value: unknown) => void;\n reject: (reason?: unknown) => void;\n timeout: ReturnType<typeof setTimeout>;\n}\n\nconst DEFAULT_HANDSHAKE_TIMEOUT = 8_000;\nconst DEFAULT_CALL_TIMEOUT = 8_000;\nconst DEFAULT_IFRAME_LOAD_TIMEOUT = 8_000;\n\nexport interface Port {\n mount(): Promise<void>;\n open(): Promise<void>;\n close(): Promise<void>;\n destroy(): void;\n send(type: string, payload?: unknown): void;\n call<T = unknown>(type: string, payload?: unknown): Promise<T>;\n on(type: string, handler: EventHandler): void;\n off(type: string, handler: EventHandler): void;\n update(config: Partial<PortConfig>): void;\n getState(): PortState;\n}\n\nexport function createPort(input: PortConfig): Port {\n validateConfig(input);\n\n const config: Required<\n Pick<PortConfig, 'mode' | 'handshakeTimeoutMs' | 'callTimeoutMs' | 'iframeLoadTimeoutMs' | 'minHeight' | 'maxHeight'>\n > &\n PortConfig = {\n ...input,\n mode: input.mode ?? 'inline',\n handshakeTimeoutMs: input.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT,\n callTimeoutMs: input.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT,\n iframeLoadTimeoutMs: input.iframeLoadTimeoutMs ?? DEFAULT_IFRAME_LOAD_TIMEOUT,\n minHeight: input.minHeight ?? 0,\n maxHeight: input.maxHeight ?? Number.MAX_SAFE_INTEGER\n };\n\n const instanceId = randomId('port');\n const emitter = new Emitter();\n const pending = new Map<string, PendingCall>();\n let state: PortState = 'idle';\n let iframe: HTMLIFrameElement | null = null;\n let targetNode: HTMLElement | null = null;\n let modalRoot: HTMLDivElement | null = null;\n\n const listener = (event: MessageEvent): void => {\n if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {\n return;\n }\n\n if (event.origin !== config.allowedOrigin) {\n return;\n }\n\n if (!isPortMessage(event.data)) {\n return;\n }\n\n const msg = event.data;\n if (msg.instanceId !== instanceId) {\n return;\n }\n\n if (msg.kind === 'system' && msg.type === 'port:ready') {\n if (state === 'handshaking') {\n state = 'ready';\n }\n return;\n }\n\n if (msg.kind === 'response' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n call.resolve(msg.payload);\n return;\n }\n\n if (msg.kind === 'error' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n const reason = typeof msg.payload === 'string' ? msg.payload : 'Rejected';\n call.reject(new PortError('MESSAGE_REJECTED', reason));\n return;\n }\n\n if (msg.kind === 'event' && msg.type === 'port:resize') {\n applyResize(msg.payload);\n return;\n }\n\n if (msg.kind === 'event') {\n void emitter.emit(msg.type, msg.payload);\n }\n };\n\n window.addEventListener('message', listener);\n\n function ensureState(valid: PortState[], nextAction: string): void {\n if (!valid.includes(state)) {\n throw new PortError('INVALID_STATE', `Cannot ${nextAction} from state ${state}`);\n }\n }\n\n function resolveTarget(target: PortConfig['target']): HTMLElement {\n if (typeof target === 'string') {\n const node = document.querySelector<HTMLElement>(target);\n if (!node) {\n throw new PortError('INVALID_CONFIG', `Target ${target} was not found`);\n }\n return node;\n }\n return target;\n }\n\n async function mount(): Promise<void> {\n ensureState(['idle'], 'mount');\n state = 'mounting';\n\n targetNode = resolveTarget(config.target);\n iframe = document.createElement('iframe');\n iframe.src = config.url;\n iframe.style.width = '100%';\n iframe.style.border = '0';\n iframe.style.display = config.mode === 'modal' ? 'none' : 'block';\n\n if (config.mode === 'modal') {\n modalRoot = document.createElement('div');\n modalRoot.style.position = 'fixed';\n modalRoot.style.inset = '0';\n modalRoot.style.background = 'rgba(0,0,0,0.5)';\n modalRoot.style.display = 'none';\n modalRoot.style.alignItems = 'center';\n modalRoot.style.justifyContent = 'center';\n\n const container = document.createElement('div');\n container.style.width = 'min(900px, 95vw)';\n container.style.height = 'min(85vh, 900px)';\n container.style.background = '#fff';\n container.style.borderRadius = '8px';\n container.style.overflow = 'hidden';\n iframe.style.display = 'block';\n iframe.style.height = '100%';\n\n container.append(iframe);\n modalRoot.append(container);\n targetNode.append(modalRoot);\n\n modalRoot.addEventListener('click', (event) => {\n if (event.target === modalRoot) {\n void close();\n }\n });\n\n window.addEventListener('keydown', (event) => {\n if (event.key === 'Escape' && state === 'open') {\n void close();\n }\n });\n } else {\n targetNode.append(iframe);\n }\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new PortError('IFRAME_LOAD_TIMEOUT', 'iframe did not load in time'));\n }, config.iframeLoadTimeoutMs);\n\n iframe?.addEventListener(\n 'load',\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true }\n );\n });\n\n state = 'mounted';\n await handshake();\n }\n\n async function handshake(): Promise<void> {\n ensureState(['mounted'], 'handshake');\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is unavailable for handshake');\n }\n\n state = 'handshaking';\n post({ kind: 'system', type: 'port:hello' });\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n clearInterval(poll);\n reject(new PortError('HANDSHAKE_TIMEOUT', 'handshake timed out'));\n }, config.handshakeTimeoutMs);\n\n const poll = setInterval(() => {\n if (state === 'ready') {\n clearTimeout(timer);\n clearInterval(poll);\n resolve();\n }\n }, 10);\n });\n\n if (config.mode === 'inline') {\n state = 'open';\n }\n }\n\n function open(): Promise<void> {\n ensureState(['ready', 'closed'], 'open');\n if (config.mode !== 'modal') {\n state = 'open';\n return Promise.resolve();\n }\n if (!modalRoot) {\n throw new PortError('INVALID_STATE', 'modal root missing');\n }\n modalRoot.style.display = 'flex';\n state = 'open';\n return Promise.resolve();\n }\n\n function close(): Promise<void> {\n ensureState(['open'], 'close');\n if (config.mode === 'modal' && modalRoot) {\n modalRoot.style.display = 'none';\n }\n state = 'closed';\n return Promise.resolve();\n }\n\n function destroy(): void {\n if (state === 'destroyed') {\n return;\n }\n\n pending.forEach((entry) => {\n clearTimeout(entry.timeout);\n entry.reject(new PortError('PORT_DESTROYED', 'Port has been destroyed'));\n });\n pending.clear();\n\n window.removeEventListener('message', listener);\n iframe?.remove();\n modalRoot?.remove();\n iframe = null;\n modalRoot = null;\n targetNode = null;\n state = 'destroyed';\n }\n\n function post(message: Pick<PortMessage, 'kind' | 'type' | 'payload' | 'replyTo'>): void {\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is not available');\n }\n\n const finalMessage: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId: randomId(),\n ...message\n };\n\n iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);\n }\n\n function send(type: string, payload?: unknown): void {\n if (state === 'destroyed') {\n throw new PortError('PORT_DESTROYED', 'Port is destroyed');\n }\n\n ensureState(['ready', 'open', 'closed'], 'send');\n post({ kind: 'event', type, payload });\n }\n\n function call<T = unknown>(type: string, payload?: unknown): Promise<T> {\n if (state === 'destroyed') {\n return Promise.reject(new PortError('PORT_DESTROYED', 'Port is destroyed'));\n }\n\n ensureState(['ready', 'open', 'closed'], 'call');\n\n const messageId = randomId();\n const message: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId,\n kind: 'request',\n type,\n payload\n };\n\n iframe?.contentWindow?.postMessage(message, config.allowedOrigin);\n\n return new Promise<T>((resolve, reject) => {\n const timeout = setTimeout(() => {\n pending.delete(messageId);\n reject(new PortError('CALL_TIMEOUT', `${type} timed out`));\n }, config.callTimeoutMs);\n\n pending.set(messageId, { resolve: resolve as (value: unknown) => void, reject, timeout });\n });\n }\n\n function on(type: string, handler: EventHandler): void {\n emitter.on(type, handler);\n }\n\n function off(type: string, handler: EventHandler): void {\n emitter.off(type, handler);\n }\n\n function update(next: Partial<PortConfig>): void {\n Object.assign(config, next);\n }\n\n function getState(): PortState {\n return state;\n }\n\n function applyResize(payload: unknown): void {\n if (!iframe) {\n return;\n }\n if (typeof payload !== 'number' || Number.isNaN(payload)) {\n return;\n }\n\n const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));\n iframe.style.height = `${bounded}px`;\n }\n\n return {\n mount,\n open,\n close,\n destroy,\n send,\n call,\n on,\n off,\n update,\n getState\n };\n}\n\nfunction validateConfig(config: PortConfig): void {\n if (!config.url || !config.allowedOrigin || !config.target) {\n throw new PortError('INVALID_CONFIG', 'url, target, and allowedOrigin are required');\n }\n}\n"],"mappings":"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","names":["call"]}