@kehto/shell 0.26.0 → 0.27.0

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/dist/index.js CHANGED
@@ -28,8 +28,7 @@ function hexToBytes(hex) {
28
28
  }
29
29
  function createSendToNapplet(originRegistry2) {
30
30
  return (windowId, msg) => {
31
- const win = originRegistry2.getIframeWindow(windowId);
32
- if (win) win.postMessage(msg, "*");
31
+ originRegistry2.postToNapplet(windowId, msg);
33
32
  };
34
33
  }
35
34
  function createRelayPoolAdapter(shellHooks, originRegistry2) {
@@ -61,8 +60,8 @@ function createRelayPoolAdapter(shellHooks, originRegistry2) {
61
60
  shellHooks.relayPool.untrackSubscription(subKey);
62
61
  },
63
62
  openScopedRelay(windowId, relayUrl, subId, filters, _sendFn) {
64
- const win = originRegistry2.getIframeWindow(windowId);
65
- if (win) shellHooks.relayPool.openScopedRelay(windowId, relayUrl, subId, filters, win);
63
+ const target = originRegistry2.getNappletTarget(windowId);
64
+ if (target) shellHooks.relayPool.openScopedRelay(windowId, relayUrl, subId, filters, target);
66
65
  },
67
66
  closeScopedRelay(windowId) {
68
67
  shellHooks.relayPool.closeScopedRelay(windowId);
@@ -329,7 +328,21 @@ function adaptHooks(shellHooks, deps) {
329
328
 
330
329
  // src/origin-registry.ts
331
330
  var registry = /* @__PURE__ */ new Map();
331
+ var spentWindows = /* @__PURE__ */ new WeakSet();
332
332
  var nextRegistrationId = 0;
333
+ function entryFor(windowId) {
334
+ for (const entry of registry.values()) {
335
+ if (entry.windowId === windowId) return entry;
336
+ }
337
+ return void 0;
338
+ }
339
+ function closePort(entry) {
340
+ const port = entry.port;
341
+ if (!port) return;
342
+ entry.port = void 0;
343
+ port.onmessage = null;
344
+ port.close();
345
+ }
333
346
  var originRegistry = {
334
347
  /**
335
348
  * Register a window reference with a windowId and optional identity metadata.
@@ -337,10 +350,17 @@ var originRegistry = {
337
350
  * @param win - The iframe's contentWindow reference
338
351
  * @param windowId - The unique identifier for this napplet window
339
352
  * @param identity - Optional NIP-5D identity metadata (dTag and aggregateHash)
353
+ * @throws {Error} When `win` already bound a NIP-5D port. NIP-5D identity is
354
+ * bound to one document; hosts must create a fresh iframe instead.
340
355
  */
341
356
  register(win, windowId, identity) {
357
+ if (spentWindows.has(win)) {
358
+ throw new Error("NIP-5D: cannot re-bind a napplet identity to an existing iframe; create a fresh iframe");
359
+ }
342
360
  for (const [registeredWin, entry] of registry.entries()) {
343
361
  if (registeredWin === win || entry.windowId === windowId) {
362
+ closePort(entry);
363
+ if (registeredWin !== win) spentWindows.add(registeredWin);
344
364
  registry.delete(registeredWin);
345
365
  }
346
366
  }
@@ -359,10 +379,73 @@ var originRegistry = {
359
379
  unregister(windowId) {
360
380
  for (const [win, entry] of registry.entries()) {
361
381
  if (entry.windowId === windowId) {
382
+ closePort(entry);
383
+ spentWindows.add(win);
362
384
  registry.delete(win);
363
385
  }
364
386
  }
365
387
  },
388
+ /**
389
+ * Bind the NIP-5D port from a frame's single `napplet.hello`.
390
+ *
391
+ * @param windowId - The registered window identifier
392
+ * @param port - The one port transferred with the hello
393
+ * @returns `false` when the window is unknown or already bound
394
+ */
395
+ bindPort(windowId, port) {
396
+ for (const [win, entry] of registry.entries()) {
397
+ if (entry.windowId !== windowId) continue;
398
+ if (entry.port || spentWindows.has(win)) return false;
399
+ entry.port = port;
400
+ spentWindows.add(win);
401
+ return true;
402
+ }
403
+ return false;
404
+ },
405
+ /**
406
+ * Look up the bound NIP-5D port for a window.
407
+ *
408
+ * @param windowId - The window identifier
409
+ * @returns The bound port, or undefined before `napplet.hello`
410
+ */
411
+ getPort(windowId) {
412
+ return entryFor(windowId)?.port;
413
+ },
414
+ /**
415
+ * Send one NAP envelope to a napplet over its bound port. Never falls back
416
+ * to `iframeWindow.postMessage`.
417
+ *
418
+ * @param windowId - The window identifier
419
+ * @param message - The envelope to send
420
+ * @returns `true` when a bound port carried the message
421
+ */
422
+ postToNapplet(windowId, message) {
423
+ const port = entryFor(windowId)?.port;
424
+ if (!port) return false;
425
+ port.postMessage(message);
426
+ return true;
427
+ },
428
+ /**
429
+ * Return a port-backed outbound target for one napplet window.
430
+ *
431
+ * @param windowId - The window identifier
432
+ * @returns A target that sends through {@link OriginRegistry.postToNapplet}, or null when unregistered
433
+ */
434
+ getNappletTarget(windowId) {
435
+ if (!entryFor(windowId)) return null;
436
+ return { postMessage: (message) => {
437
+ originRegistry.postToNapplet(windowId, message);
438
+ } };
439
+ },
440
+ /**
441
+ * Whether a Window already handed over (or lost) its one NIP-5D binding.
442
+ *
443
+ * @param win - The iframe `contentWindow`
444
+ * @returns `true` when the Window can never be registered again
445
+ */
446
+ isSpent(win) {
447
+ return spentWindows.has(win);
448
+ },
366
449
  /**
367
450
  * Look up the windowId for a given Window reference.
368
451
  *
@@ -425,9 +508,11 @@ var originRegistry = {
425
508
  getRegistrationId(win) {
426
509
  return registry.get(win)?.registrationId;
427
510
  },
428
- /** Clear all registrations. */
511
+ /** Clear all registrations and close every bound port. */
429
512
  clear() {
513
+ for (const entry of registry.values()) closePort(entry);
430
514
  registry.clear();
515
+ spentWindows = /* @__PURE__ */ new WeakSet();
431
516
  }
432
517
  };
433
518
 
@@ -913,7 +998,7 @@ var audioManager = {
913
998
  bump();
914
999
  },
915
1000
  /**
916
- * Mute or unmute an audio source and notify the napplet via postMessage.
1001
+ * Mute or unmute an audio source and notify the napplet over its bound NIP-5D port.
917
1002
  *
918
1003
  * @param windowId - The window identifier
919
1004
  * @param muted - True to mute, false to unmute
@@ -929,8 +1014,7 @@ var audioManager = {
929
1014
  src.muted = muted;
930
1015
  bump();
931
1016
  }
932
- const iframeWindow = originRegistry.getIframeWindow(windowId);
933
- if (iframeWindow) {
1017
+ if (originRegistry.getPort(windowId)) {
934
1018
  const muteEvent = {
935
1019
  kind: 29e3,
936
1020
  // INC_PEER — inlined numeric after Phase 24 DRIFT-01 shim removal
@@ -941,7 +1025,7 @@ var audioManager = {
941
1025
  id: `audio-mute-${windowId}-${Date.now()}`,
942
1026
  sig: ""
943
1027
  };
944
- iframeWindow.postMessage(["EVENT", "__shell__", muteEvent], "*");
1028
+ originRegistry.postToNapplet(windowId, ["EVENT", "__shell__", muteEvent]);
945
1029
  }
946
1030
  },
947
1031
  /**
@@ -1120,7 +1204,7 @@ function handleShellReady({
1120
1204
  }
1121
1205
  const environment = registerSourceSession({ hooks, origin, runtime, state, sourceRegistrationId, sourceWindow, windowId });
1122
1206
  if (!environment?.capabilities.domains.includes("shell")) return;
1123
- postShellInit(sourceWindow, environment);
1207
+ postShellInit(windowId, environment);
1124
1208
  state.initSent.set(sourceWindow, sourceRegistrationId);
1125
1209
  }
1126
1210
  function registerSourceSession(options) {
@@ -1187,16 +1271,17 @@ function resolveNip5dIdentity(hooks, windowId, sourceWindow, existingSession) {
1187
1271
  aggregateHash: originIdentity.aggregateHash
1188
1272
  });
1189
1273
  }
1190
- function postShellInit(win, environment) {
1274
+ function postShellInit(windowId, environment) {
1191
1275
  const initMsg = {
1192
1276
  type: "shell.init",
1193
1277
  capabilities: environment.capabilities,
1194
1278
  services: environment.services
1195
1279
  };
1196
- win.postMessage(initMsg, "*");
1280
+ originRegistry.postToNapplet(windowId, initMsg);
1197
1281
  }
1198
1282
 
1199
- // src/shell-bridge.ts
1283
+ // src/nip5d-transport.ts
1284
+ var NAPPLET_HELLO = "napplet.hello";
1200
1285
  function reportUnrouted(hooks, event, reason) {
1201
1286
  if (!hooks.onUnroutedMessage) return;
1202
1287
  const data = event.data;
@@ -1206,6 +1291,138 @@ function reportUnrouted(hooks, event, reason) {
1206
1291
  } catch {
1207
1292
  }
1208
1293
  }
1294
+ var REVOCATION_DETAIL = {
1295
+ "window-message": "A window-level message other than the single napplet.hello arrived from the frame",
1296
+ "second-hello": "A second napplet.hello arrived from the frame",
1297
+ "invalid-hello": "napplet.hello did not carry exactly one MessagePort",
1298
+ "second-load": "The frame loaded a second document (navigation or reload)",
1299
+ "port-closed": "The bound NIP-5D port closed or failed",
1300
+ host: "The host revoked the napplet binding"
1301
+ };
1302
+ function isHello(data) {
1303
+ return typeof data === "object" && data !== null && data.type === NAPPLET_HELLO;
1304
+ }
1305
+ function createNip5dTransport({ hooks, runtime, state: shellReadyState }) {
1306
+ const bindings = /* @__PURE__ */ new Map();
1307
+ function currentBinding(windowId) {
1308
+ const binding = bindings.get(windowId);
1309
+ if (binding && originRegistry.getPort(windowId) !== binding.port) {
1310
+ binding.port.onmessage = null;
1311
+ bindings.delete(windowId);
1312
+ return void 0;
1313
+ }
1314
+ return binding;
1315
+ }
1316
+ function revokeNapplet(windowId, reason = "host") {
1317
+ const binding = currentBinding(windowId);
1318
+ const registered = originRegistry.getIframeWindow(windowId) !== null;
1319
+ if (!binding && !registered) return;
1320
+ bindings.delete(windowId);
1321
+ if (binding) binding.port.onmessage = null;
1322
+ originRegistry.unregister(windowId);
1323
+ runtime.destroyWindow(windowId);
1324
+ runtime.sessionRegistry.unregister(windowId);
1325
+ shellReadyState.environments.delete(windowId);
1326
+ shellReadyState.sessionRegistration.delete(windowId);
1327
+ try {
1328
+ hooks.onNappletRevoked?.({ windowId, reason, detail: REVOCATION_DETAIL[reason] });
1329
+ } catch {
1330
+ }
1331
+ }
1332
+ function dispatchEnvelope(windowId, binding, msg) {
1333
+ if (typeof msg !== "object" || msg === null || typeof msg.type !== "string") return;
1334
+ const envelope = msg;
1335
+ try {
1336
+ hooks.onNappletMessage?.(windowId, envelope);
1337
+ } catch {
1338
+ }
1339
+ const context = {
1340
+ hooks,
1341
+ origin: binding.origin,
1342
+ runtime,
1343
+ state: shellReadyState,
1344
+ sourceRegistrationId: binding.registrationId,
1345
+ sourceWindow: binding.sourceWindow,
1346
+ windowId
1347
+ };
1348
+ if (!registerSourceSession(context)) return;
1349
+ if (envelope.type === "shell.ready") {
1350
+ handleShellReady(context);
1351
+ return;
1352
+ }
1353
+ runtime.handleMessage(windowId, envelope);
1354
+ }
1355
+ function bindPort(windowId, sourceWindow, origin, port) {
1356
+ if (!originRegistry.bindPort(windowId, port)) {
1357
+ port.close();
1358
+ revokeNapplet(windowId, "second-hello");
1359
+ return;
1360
+ }
1361
+ const binding = {
1362
+ port,
1363
+ sourceWindow,
1364
+ origin,
1365
+ registrationId: originRegistry.getRegistrationId(sourceWindow) ?? 0
1366
+ };
1367
+ bindings.set(windowId, binding);
1368
+ const isCurrent = () => currentBinding(windowId) === binding;
1369
+ port.onmessage = (portEvent) => {
1370
+ if (isCurrent()) dispatchEnvelope(windowId, binding, portEvent.data);
1371
+ };
1372
+ const onPortFailure = () => {
1373
+ if (isCurrent()) revokeNapplet(windowId, "port-closed");
1374
+ };
1375
+ port.addEventListener("close", onPortFailure);
1376
+ port.addEventListener("messageerror", onPortFailure);
1377
+ port.start();
1378
+ }
1379
+ function handleMessage(event) {
1380
+ const sourceWindow = event.source;
1381
+ if (!sourceWindow) {
1382
+ reportUnrouted(hooks, event, "no-source-window");
1383
+ return;
1384
+ }
1385
+ const windowId = originRegistry.getWindowId(sourceWindow);
1386
+ if (!windowId) {
1387
+ reportUnrouted(hooks, event, "unregistered-window");
1388
+ return;
1389
+ }
1390
+ if (currentBinding(windowId) || originRegistry.getPort(windowId)) {
1391
+ revokeNapplet(windowId, isHello(event.data) ? "second-hello" : "window-message");
1392
+ return;
1393
+ }
1394
+ if (!isHello(event.data)) {
1395
+ revokeNapplet(windowId, "window-message");
1396
+ return;
1397
+ }
1398
+ const ports = event.ports ?? [];
1399
+ if (ports.length !== 1) {
1400
+ for (const port of ports) port.close();
1401
+ revokeNapplet(windowId, "invalid-hello");
1402
+ return;
1403
+ }
1404
+ bindPort(windowId, sourceWindow, event.origin, ports[0]);
1405
+ }
1406
+ function observeFrame(frame, windowId) {
1407
+ let loads = 0;
1408
+ const onLoad = () => {
1409
+ loads += 1;
1410
+ if (loads > 1) revokeNapplet(windowId, "second-load");
1411
+ };
1412
+ frame.addEventListener("load", onLoad);
1413
+ return () => frame.removeEventListener("load", onLoad);
1414
+ }
1415
+ function destroy() {
1416
+ for (const [windowId, binding] of bindings) {
1417
+ binding.port.onmessage = null;
1418
+ if (originRegistry.getPort(windowId) === binding.port) originRegistry.unregister(windowId);
1419
+ }
1420
+ bindings.clear();
1421
+ }
1422
+ return { handleMessage, revokeNapplet, observeFrame, destroy };
1423
+ }
1424
+
1425
+ // src/shell-bridge.ts
1209
1426
  function createShellBridge(hooks) {
1210
1427
  const shellReadyState = createShellReadyState();
1211
1428
  const runtimeHooks = adaptHooks(hooks, {
@@ -1224,52 +1441,17 @@ function createShellBridge(hooks) {
1224
1441
  const environment = shellReadyState.environments.get(entry.windowId);
1225
1442
  if (!environment?.capabilities.domains.includes(domain)) continue;
1226
1443
  if (!runtime.aclState.check(entry.pubkey, entry.dTag, entry.aggregateHash, capability)) continue;
1227
- const win = originRegistry.getIframeWindow(entry.windowId);
1228
- if (!win) continue;
1229
- win.postMessage(envelope, "*");
1444
+ originRegistry.postToNapplet(entry.windowId, envelope);
1230
1445
  }
1231
1446
  }
1447
+ const transport = createNip5dTransport({ hooks, runtime, state: shellReadyState });
1232
1448
  return {
1233
- handleMessage(event) {
1234
- const sourceWindow = event.source;
1235
- if (!sourceWindow) {
1236
- reportUnrouted(hooks, event, "no-source-window");
1237
- return;
1238
- }
1239
- const windowId = originRegistry.getWindowId(sourceWindow);
1240
- if (!windowId) {
1241
- reportUnrouted(hooks, event, "unregistered-window");
1242
- return;
1243
- }
1244
- const msg = event.data;
1245
- if (typeof msg !== "object" || msg === null || typeof msg.type !== "string") return;
1246
- if (!registerSourceSession({
1247
- hooks,
1248
- origin: event.origin,
1249
- runtime,
1250
- state: shellReadyState,
1251
- sourceRegistrationId: originRegistry.getRegistrationId(sourceWindow) ?? 0,
1252
- sourceWindow,
1253
- windowId
1254
- })) return;
1255
- if (msg.type === "shell.ready") {
1256
- handleShellReady({
1257
- hooks,
1258
- origin: event.origin,
1259
- runtime,
1260
- state: shellReadyState,
1261
- sourceRegistrationId: originRegistry.getRegistrationId(sourceWindow) ?? 0,
1262
- sourceWindow,
1263
- windowId
1264
- });
1265
- return;
1266
- }
1267
- runtime.handleMessage(windowId, msg);
1268
- },
1449
+ handleMessage: transport.handleMessage,
1269
1450
  injectEvent(topic, payload) {
1270
1451
  runtime.injectEvent(topic, payload);
1271
1452
  },
1272
1453
  destroy() {
1454
+ transport.destroy();
1273
1455
  runtime.destroy();
1274
1456
  shellReadyState.clear();
1275
1457
  },
@@ -1284,6 +1466,8 @@ function createShellBridge(hooks) {
1284
1466
  const envelope = { type: "identity.changed", pubkey };
1285
1467
  publishToEligibleNapplets(envelope, "identity", "identity:read");
1286
1468
  },
1469
+ observeFrame: transport.observeFrame,
1470
+ revokeNapplet: transport.revokeNapplet,
1287
1471
  get runtime() {
1288
1472
  return runtime;
1289
1473
  }
@@ -1515,15 +1699,19 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1515
1699
  if (target.crypto?.randomUUID) return target.crypto.randomUUID();
1516
1700
  return `napplet-${Date.now()}-${Math.random().toString(16).slice(2)}`;
1517
1701
  }
1702
+ const channel = new target.MessageChannel();
1703
+ const port = channel.port1;
1704
+ port.start();
1705
+ target.parent.postMessage({ type: "napplet.hello" }, "*", [channel.port2]);
1518
1706
  function post(message) {
1519
- target.parent.postMessage(message, "*");
1707
+ port.postMessage(message);
1520
1708
  }
1521
1709
  function listen(listener) {
1522
- target.addEventListener("message", listener);
1523
- return () => target.removeEventListener("message", listener);
1710
+ port.addEventListener("message", listener);
1711
+ return () => port.removeEventListener("message", listener);
1524
1712
  }
1525
- function isParentMessage(event) {
1526
- return event.source === target.parent;
1713
+ function isShellMessage(event) {
1714
+ return event.currentTarget === port;
1527
1715
  }
1528
1716
  function request(message, resultType, map, options = {}) {
1529
1717
  const requestId = message.id ?? id();
@@ -1544,7 +1732,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1544
1732
  reject(new DOMException("Aborted", "AbortError"));
1545
1733
  };
1546
1734
  off = listen((event) => {
1547
- if (!isParentMessage(event)) return;
1735
+ if (!isShellMessage(event)) return;
1548
1736
  const incoming = event.data;
1549
1737
  if (typeof incoming !== "object" || incoming === null) return;
1550
1738
  const msg = incoming;
@@ -1649,7 +1837,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1649
1837
  resolveReady = resolve;
1650
1838
  });
1651
1839
  const off = listen((event) => {
1652
- if (!isParentMessage(event) || environment) return;
1840
+ if (!isShellMessage(event) || environment) return;
1653
1841
  const incoming = event.data;
1654
1842
  if (typeof incoming !== "object" || incoming === null) return;
1655
1843
  const message = incoming;
@@ -1703,7 +1891,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1703
1891
  }
1704
1892
  function domainEventHandle(eventType, callback, select = (message) => message.event) {
1705
1893
  const off = listen((event) => {
1706
- if (!isParentMessage(event)) return;
1894
+ if (!isShellMessage(event)) return;
1707
1895
  const msg = event.data;
1708
1896
  if (typeof msg === "object" && msg !== null && msg.type === eventType) callback(select(msg));
1709
1897
  });
@@ -1714,7 +1902,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1714
1902
  subscribe(filters, onEvent, onEose, options) {
1715
1903
  const subId = id();
1716
1904
  const off = listen((event) => {
1717
- if (!isParentMessage(event)) return;
1905
+ if (!isShellMessage(event)) return;
1718
1906
  const msg = event.data;
1719
1907
  if (typeof msg !== "object" || msg === null || msg.subId !== subId) return;
1720
1908
  if (msg.type === "relay.event") onEvent(msg.result);
@@ -1861,7 +2049,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1861
2049
  state.pendingEvents.push(event);
1862
2050
  }
1863
2051
  listen((event) => {
1864
- if (!isParentMessage(event)) return;
2052
+ if (!isShellMessage(event)) return;
1865
2053
  const msg = event.data;
1866
2054
  if (typeof msg !== "object" || msg === null || typeof msg.type !== "string") return;
1867
2055
  if (msg.type === "inc.channel.opened" && typeof msg.channelId === "string" && typeof msg.peer === "string") {
@@ -1896,14 +2084,14 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1896
2084
  const handlers = /* @__PURE__ */ new Set();
1897
2085
  const subscriptionId = id();
1898
2086
  const offEvent = listen((event) => {
1899
- if (!isParentMessage(event)) return;
2087
+ if (!isShellMessage(event)) return;
1900
2088
  const msg = event.data;
1901
2089
  if (typeof msg !== "object" || msg === null || msg.type !== "inc.event" || msg.topic !== stableTopic || typeof msg.sender !== "string") return;
1902
2090
  const delivered = Object.prototype.hasOwnProperty.call(msg, "payload") ? { topic: stableTopic, sender: msg.sender, payload: msg.payload } : { topic: stableTopic, sender: msg.sender };
1903
2091
  for (const handler of handlers) handler(delivered);
1904
2092
  });
1905
2093
  const offResult = listen((event) => {
1906
- if (!isParentMessage(event)) return;
2094
+ if (!isShellMessage(event)) return;
1907
2095
  const msg = event.data;
1908
2096
  if (typeof msg !== "object" || msg === null || msg.type !== "inc.subscribe.result" || msg.id !== subscriptionId) return;
1909
2097
  offResult();
@@ -1958,9 +2146,9 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
1958
2146
  return request(
1959
2147
  { type: "inc.channel.list" },
1960
2148
  "inc.channel.list.result",
1961
- (msg) => Object.freeze((Array.isArray(msg.channels) ? msg.channels : []).flatMap((channel) => channel && typeof channel === "object" && typeof channel.id === "string" && typeof channel.peer === "string" ? [Object.freeze({
1962
- id: channel.id,
1963
- peer: channel.peer
2149
+ (msg) => Object.freeze((Array.isArray(msg.channels) ? msg.channels : []).flatMap((channel2) => channel2 && typeof channel2 === "object" && typeof channel2.id === "string" && typeof channel2.peer === "string" ? [Object.freeze({
2150
+ id: channel2.id,
2151
+ peer: channel2.peer
1964
2152
  })] : []))
1965
2153
  );
1966
2154
  },
@@ -2022,7 +2210,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2022
2210
  getBadges: () => read("identity.getBadges", "badges", []),
2023
2211
  onChanged(handler) {
2024
2212
  const off = listen((event) => {
2025
- if (!isParentMessage(event)) return;
2213
+ if (!isShellMessage(event)) return;
2026
2214
  const msg = event.data;
2027
2215
  if (typeof msg !== "object" || msg === null || msg.type !== "identity.changed") return;
2028
2216
  if (typeof msg.pubkey === "string") handler(msg.pubkey);
@@ -2036,7 +2224,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2036
2224
  get: () => request({ type: "theme.get" }, "theme.get.result", (msg) => msg.theme),
2037
2225
  onChanged(handler) {
2038
2226
  const off = listen((event) => {
2039
- if (!isParentMessage(event)) return;
2227
+ if (!isShellMessage(event)) return;
2040
2228
  const msg = event.data;
2041
2229
  if (typeof msg === "object" && msg !== null && msg.type === "theme.changed") handler(msg.theme);
2042
2230
  });
@@ -2152,7 +2340,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2152
2340
  if (keysSmartForwardingInstalled) return;
2153
2341
  keysSmartForwardingInstalled = true;
2154
2342
  listen((event) => {
2155
- if (!isParentMessage(event)) return;
2343
+ if (!isShellMessage(event)) return;
2156
2344
  const msg = event.data;
2157
2345
  if (typeof msg === "object" && msg !== null && msg.type === "keys.bindings") {
2158
2346
  replaceKeyBindings(msg.bindings);
@@ -2206,7 +2394,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2206
2394
  }
2207
2395
  callbacks.add(callback);
2208
2396
  const off = listen((event) => {
2209
- if (!isParentMessage(event)) return;
2397
+ if (!isShellMessage(event)) return;
2210
2398
  const msg = event.data;
2211
2399
  if (typeof msg === "object" && msg !== null && msg.type === "keys.action" && msg.actionId === actionId) callback();
2212
2400
  });
@@ -2230,7 +2418,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2230
2418
  console.warn(`[KEHTO-MEDIA-${name}] Deprecated media API; use ${replacement}. Warned once per endpoint.`);
2231
2419
  };
2232
2420
  listen((event) => {
2233
- if (!isParentMessage(event)) return;
2421
+ if (!isShellMessage(event)) return;
2234
2422
  const msg = event.data;
2235
2423
  if (!msg || typeof msg !== "object" || typeof msg.sessionId !== "string") return;
2236
2424
  const sessionId = msg.sessionId;
@@ -2307,7 +2495,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2307
2495
  function makeNotify() {
2308
2496
  const on = (type, callback) => {
2309
2497
  const off = listen((event) => {
2310
- if (!isParentMessage(event)) return;
2498
+ if (!isShellMessage(event)) return;
2311
2499
  const msg = event.data;
2312
2500
  if (typeof msg === "object" && msg !== null && msg.type === type) callback(msg);
2313
2501
  });
@@ -2321,9 +2509,9 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2321
2509
  ),
2322
2510
  dismiss: (notificationId) => fire({ type: "notify.dismiss", notificationId }),
2323
2511
  badge: (count) => fire({ type: "notify.badge", count }),
2324
- registerChannel: (channel) => fire({ type: "notify.channel.register", ...channel }),
2325
- requestPermission: (channel) => request(
2326
- { type: "notify.permission.request", ...channel ? { channel } : {} },
2512
+ registerChannel: (channel2) => fire({ type: "notify.channel.register", ...channel2 }),
2513
+ requestPermission: (channel2) => request(
2514
+ { type: "notify.permission.request", ...channel2 ? { channel: channel2 } : {} },
2327
2515
  "notify.permission.result",
2328
2516
  (msg) => ({ granted: msg.granted })
2329
2517
  ),
@@ -2347,7 +2535,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2347
2535
  get: () => request({ type: "config.get" }, "config.values", (msg) => msg.values ?? {}),
2348
2536
  subscribe(callback) {
2349
2537
  const off = listen((event) => {
2350
- if (!isParentMessage(event)) return;
2538
+ if (!isShellMessage(event)) return;
2351
2539
  const msg = event.data;
2352
2540
  if (typeof msg === "object" && msg !== null && msg.type === "config.values") callback(msg.values ?? {});
2353
2541
  });
@@ -2360,7 +2548,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2360
2548
  openSettings: (options) => fire({ type: "config.openSettings", ...options && typeof options === "object" ? options : {} }),
2361
2549
  onSchemaError(callback) {
2362
2550
  const off = listen((event) => {
2363
- if (!isParentMessage(event)) return;
2551
+ if (!isShellMessage(event)) return;
2364
2552
  const msg = event.data;
2365
2553
  if (typeof msg === "object" && msg !== null && msg.type === "config.schemaError") callback(msg);
2366
2554
  });
@@ -2473,7 +2661,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2473
2661
  close: (server) => req("cvm.close", { server }, (msg) => voidResult(msg, "cvm close failed")),
2474
2662
  onEvent(callback) {
2475
2663
  const off = listen((event) => {
2476
- if (!isParentMessage(event)) return;
2664
+ if (!isShellMessage(event)) return;
2477
2665
  const msg = event.data;
2478
2666
  if (typeof msg === "object" && msg !== null && msg.type === "cvm.event" && msg.server !== void 0 && msg.message !== void 0) {
2479
2667
  callback(msg.server, msg.message);
@@ -2497,7 +2685,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2497
2685
  const subId = id();
2498
2686
  const handlers = { event: /* @__PURE__ */ new Set(), closed: /* @__PURE__ */ new Set() };
2499
2687
  const off = listen((event) => {
2500
- if (!isParentMessage(event)) return;
2688
+ if (!isShellMessage(event)) return;
2501
2689
  const msg = event.data;
2502
2690
  if (typeof msg !== "object" || msg === null || msg.subId !== subId) return;
2503
2691
  if (msg.type === "outbox.event") for (const handler of handlers.event) handler(msg.result);
@@ -2531,7 +2719,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2531
2719
  status: (uploadId) => request({ type: "upload.status", uploadId }, "upload.status.result", (msg) => fieldOrThrow(msg, "status", "upload status unavailable")),
2532
2720
  onStatus(callback) {
2533
2721
  const off = listen((event) => {
2534
- if (!isParentMessage(event)) return;
2722
+ if (!isShellMessage(event)) return;
2535
2723
  const msg = event.data;
2536
2724
  if (typeof msg === "object" && msg !== null && msg.type === "upload.status.changed" && msg.status !== void 0) callback(msg.status);
2537
2725
  });
@@ -2572,7 +2760,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2572
2760
  }
2573
2761
  }
2574
2762
  listen((event) => {
2575
- if (!isParentMessage(event)) return;
2763
+ if (!isShellMessage(event)) return;
2576
2764
  const message = event.data;
2577
2765
  if (!message || message.type !== "intent.deliver") return;
2578
2766
  const value = message.delivery;
@@ -2846,7 +3034,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2846
3034
  });
2847
3035
  };
2848
3036
  listen((event) => {
2849
- if (!isParentMessage(event)) return;
3037
+ if (!isShellMessage(event)) return;
2850
3038
  const incoming = event.data;
2851
3039
  if (!incoming || incoming.type !== "gamepad.state" || "id" in incoming || !valid(incoming)) return;
2852
3040
  state = copy(incoming);
@@ -2882,7 +3070,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2882
3070
  unsubscribe: (subscriptionId) => req("dm.unsubscribe", { subscriptionId }),
2883
3071
  onMessage(callback) {
2884
3072
  const off = listen((event) => {
2885
- if (!isParentMessage(event)) return;
3073
+ if (!isShellMessage(event)) return;
2886
3074
  const msg = event.data;
2887
3075
  if (typeof msg === "object" && msg !== null && msg.type === "dm.message" && typeof msg.subscriptionId === "string") {
2888
3076
  callback(msg.message, msg.subscriptionId);
@@ -2931,7 +3119,7 @@ function nappletNamespacePrelude(domains, normalizeIntentUri2) {
2931
3119
  post({ type: "llm.cancel", id: id(), requestId });
2932
3120
  };
2933
3121
  const off = listen((event) => {
2934
- if (!isParentMessage(event)) return;
3122
+ if (!isShellMessage(event)) return;
2935
3123
  const msg = event.data;
2936
3124
  if (typeof msg !== "object" || msg === null) return;
2937
3125
  if (type === "llm.generate" && msg.type === "llm.delta" && msg.requestId === requestId) {
@@ -3265,8 +3453,7 @@ function createKeysProxy(deps) {
3265
3453
  deps.runtime.handleMessage(windowId, envelope);
3266
3454
  },
3267
3455
  emit(windowId, envelope) {
3268
- const win = deps.originRegistry.getIframeWindow(windowId);
3269
- if (win) win.postMessage(envelope, "*");
3456
+ deps.originRegistry.postToNapplet(windowId, envelope);
3270
3457
  }
3271
3458
  };
3272
3459
  }
@@ -3278,8 +3465,7 @@ function createMediaProxy(deps) {
3278
3465
  deps.runtime.handleMessage(windowId, envelope);
3279
3466
  },
3280
3467
  emit(windowId, envelope) {
3281
- const win = deps.originRegistry.getIframeWindow(windowId);
3282
- if (win) win.postMessage(envelope, "*");
3468
+ deps.originRegistry.postToNapplet(windowId, envelope);
3283
3469
  }
3284
3470
  };
3285
3471
  }
@@ -3291,13 +3477,13 @@ function createNotifyProxy(deps) {
3291
3477
  deps.runtime.handleMessage(windowId, envelope);
3292
3478
  },
3293
3479
  emit(windowId, envelope) {
3294
- const win = deps.originRegistry.getIframeWindow(windowId);
3295
- if (win) win.postMessage(envelope, "*");
3480
+ deps.originRegistry.postToNapplet(windowId, envelope);
3296
3481
  }
3297
3482
  };
3298
3483
  }
3299
3484
  export {
3300
3485
  ALL_CAPABILITIES2 as ALL_CAPABILITIES,
3486
+ NAPPLET_HELLO,
3301
3487
  TOPICS,
3302
3488
  adaptHooks,
3303
3489
  audioManager,