@kehto/shell 0.17.2 → 0.18.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
@@ -35,7 +35,7 @@ function createRelayPoolAdapter(shellHooks, originRegistry2) {
35
35
  const pool = shellHooks.relayPool.getRelayPool();
36
36
  if (!pool) return;
37
37
  const relayUrls = shellHooks.relayPool.selectRelayTier([]);
38
- pool.publish(relayUrls, event);
38
+ return pool.publish(relayUrls, event);
39
39
  },
40
40
  selectRelayTier(filters) {
41
41
  return shellHooks.relayPool.selectRelayTier(filters);
@@ -392,6 +392,19 @@ var originRegistry = {
392
392
  if (!entry?.dTag || !entry?.aggregateHash) return void 0;
393
393
  return { dTag: entry.dTag, aggregateHash: entry.aggregateHash };
394
394
  },
395
+ /** Store the immutable environment captured for this exact frame registration. */
396
+ setEnvironment(win, environment) {
397
+ const entry = registry.get(win);
398
+ if (!entry) return;
399
+ entry.environment = Object.freeze({
400
+ capabilities: Object.freeze({ domains: Object.freeze([...environment.capabilities.domains]) }),
401
+ services: Object.freeze([...environment.services])
402
+ });
403
+ },
404
+ /** Return the immutable environment captured before this frame executes. */
405
+ getEnvironment(win) {
406
+ return registry.get(win)?.environment;
407
+ },
395
408
  /**
396
409
  * Look up the monotonically increasing registration id for a Window.
397
410
  *
@@ -962,113 +975,153 @@ var audioManager = {
962
975
  };
963
976
 
964
977
  // src/shell-init.ts
965
- var NAP_DOMAINS = [
978
+ var RUNTIME_NATIVE_DOMAINS = [
979
+ "relay",
966
980
  "identity",
967
981
  "storage",
968
982
  "inc",
969
983
  "theme",
970
984
  "keys",
971
985
  "media",
972
- "notify",
986
+ "notify"
987
+ ];
988
+ var SERVICE_ONLY_DOMAINS = [
989
+ "outbox",
973
990
  "config",
974
991
  "resource",
975
- "cvm"
976
- ];
977
- var NAP_INC_PROTOCOLS = [
978
- "inc:NAP-01",
979
- "inc:NAP-02",
980
- "inc:NAP-03",
981
- "inc:NAP-04",
982
- "inc:NAP-05",
983
- "inc:NAP-06"
992
+ "cvm",
993
+ "upload",
994
+ "intent",
995
+ "link",
996
+ "common",
997
+ "lists",
998
+ "serial",
999
+ "ble",
1000
+ "webrtc",
1001
+ "dm",
1002
+ "count"
984
1003
  ];
985
1004
  function buildShellCapabilities(hooks) {
986
- const domains = hooks.relayPool ? ["relay", "outbox", ...NAP_DOMAINS] : [...NAP_DOMAINS];
987
- if (hooks.upload) domains.push("upload");
988
- if (hooks.intent?.isAvailable()) domains.push("intent");
989
- if (hooks.services?.count) domains.push("count");
990
- if (hooks.link?.isAvailable()) domains.push("link");
991
- if (hooks.common?.isAvailable()) domains.push("common");
992
- if (hooks.lists?.isAvailable()) domains.push("lists");
993
- if (hooks.serial?.isAvailable()) domains.push("serial");
994
- if (hooks.ble?.isAvailable()) domains.push("ble");
995
- if (hooks.webrtc?.isAvailable()) domains.push("webrtc");
996
- if (hooks.dm) domains.push("dm");
997
- const sandbox = [];
998
- const protocols = {};
999
- for (const entry of NAP_INC_PROTOCOLS) {
1000
- const [domain, protocol] = entry.split(":");
1001
- (protocols[domain] ??= []).push(protocol);
1002
- }
1003
- const naps = hooks.relayPool ? ["relay", "outbox", ...NAP_DOMAINS, ...NAP_INC_PROTOCOLS] : [...NAP_DOMAINS, ...NAP_INC_PROTOCOLS];
1004
- if (hooks.upload) naps.push("upload");
1005
- if (hooks.intent?.isAvailable()) naps.push("intent");
1006
- if (hooks.services?.count) naps.push("count");
1007
- if (hooks.link?.isAvailable()) naps.push("link");
1008
- if (hooks.common?.isAvailable()) naps.push("common");
1009
- if (hooks.lists?.isAvailable()) naps.push("lists");
1010
- if (hooks.serial?.isAvailable()) naps.push("serial");
1011
- if (hooks.ble?.isAvailable()) naps.push("ble");
1012
- if (hooks.webrtc?.isAvailable()) naps.push("webrtc");
1013
- if (hooks.dm) naps.push("dm");
1014
- return applyCapabilityOverrides(
1015
- { domains, protocols, naps, sandbox },
1016
- hooks.capabilities?.disabledDomains ?? []
1017
- );
1005
+ const disabled = new Set(hooks.capabilities?.disabledDomains ?? []);
1006
+ const domains = RUNTIME_NATIVE_DOMAINS.filter((domain) => !disabled.has(domain));
1007
+ for (const domain of SERVICE_ONLY_DOMAINS) {
1008
+ if (!disabled.has(domain) && isServiceDomainLive(hooks, domain)) domains.push(domain);
1009
+ }
1010
+ return Object.freeze({ domains: freezeList(domains) });
1018
1011
  }
1019
- function applyCapabilityOverrides(capabilities, disabledDomains) {
1020
- if (disabledDomains.length === 0) return capabilities;
1021
- const disabled = new Set(disabledDomains);
1022
- const protocols = {};
1023
- for (const [domain, supportedProtocols] of Object.entries(capabilities.protocols)) {
1024
- if (!disabled.has(domain)) protocols[domain] = supportedProtocols;
1012
+ function resolveShellEnvironment(hooks, identity) {
1013
+ const capabilities = buildShellCapabilities(hooks);
1014
+ const disabled = new Set(hooks.capabilities?.disabledDomains ?? []);
1015
+ const services = getLiveServices(hooks, capabilities.domains, disabled);
1016
+ const available = Object.freeze({
1017
+ domains: freezeList(capabilities.domains),
1018
+ services: freezeList(services)
1019
+ });
1020
+ const granted = hooks.capabilities?.resolveEnvironment?.(identity, available) ?? available;
1021
+ return Object.freeze({
1022
+ capabilities: Object.freeze({
1023
+ domains: freezeList(intersectInOrder(available.domains, granted.domains))
1024
+ }),
1025
+ services: freezeList(intersectInOrder(available.services, granted.services))
1026
+ });
1027
+ }
1028
+ function isServiceDomainLive(hooks, domain) {
1029
+ if (!hooks.services?.[domain]) return false;
1030
+ switch (domain) {
1031
+ case "outbox":
1032
+ return Boolean(hooks.relayPool);
1033
+ case "upload":
1034
+ return hooks.upload?.getUploader() !== null && hooks.upload?.getUploader() !== void 0;
1035
+ case "intent":
1036
+ return hooks.intent?.isAvailable() === true;
1037
+ case "link":
1038
+ return hooks.link?.isAvailable() === true;
1039
+ case "common":
1040
+ return hooks.common?.isAvailable() === true;
1041
+ case "lists":
1042
+ return hooks.lists?.isAvailable() === true;
1043
+ case "serial":
1044
+ return hooks.serial?.isAvailable() === true;
1045
+ case "ble":
1046
+ return hooks.ble?.isAvailable() === true;
1047
+ case "webrtc":
1048
+ return hooks.webrtc?.isAvailable() === true;
1049
+ case "dm":
1050
+ return hooks.dm !== void 0;
1051
+ default:
1052
+ return true;
1025
1053
  }
1026
- return {
1027
- domains: capabilities.domains.filter((entry) => !disabled.has(entry)),
1028
- protocols,
1029
- naps: capabilities.naps.filter((entry) => !disabled.has(entry.split(":")[0] ?? entry)),
1030
- sandbox: capabilities.sandbox
1031
- };
1054
+ }
1055
+ function getLiveServices(hooks, domains, disabled) {
1056
+ const liveDomains = new Set(domains);
1057
+ return Object.keys(hooks.services ?? {}).filter((name) => {
1058
+ if (disabled.has(name)) return false;
1059
+ return !SERVICE_ONLY_DOMAINS.includes(name) || liveDomains.has(name);
1060
+ });
1061
+ }
1062
+ function intersectInOrder(available, requested) {
1063
+ const requestedSet = new Set(requested);
1064
+ return available.filter((entry) => requestedSet.has(entry));
1065
+ }
1066
+ function freezeList(values) {
1067
+ return Object.freeze([...values]);
1032
1068
  }
1033
1069
 
1034
1070
  // src/shell-ready.ts
1035
- var initSent = /* @__PURE__ */ new WeakMap();
1071
+ function createShellReadyState() {
1072
+ const initSent = /* @__PURE__ */ new WeakMap();
1073
+ const sessionRegistration = /* @__PURE__ */ new Map();
1074
+ const environments = /* @__PURE__ */ new Map();
1075
+ return {
1076
+ initSent,
1077
+ sessionRegistration,
1078
+ environments,
1079
+ isDomainAllowed(windowId, domain) {
1080
+ return environments.get(windowId)?.capabilities.domains.includes(domain) ?? false;
1081
+ },
1082
+ clear() {
1083
+ sessionRegistration.clear();
1084
+ environments.clear();
1085
+ }
1086
+ };
1087
+ }
1036
1088
  function handleShellReady({
1037
1089
  hooks,
1038
1090
  origin,
1039
1091
  runtime,
1092
+ state,
1040
1093
  sourceRegistrationId,
1041
1094
  sourceWindow,
1042
1095
  windowId
1043
1096
  }) {
1044
- registerNip5dSessionIfNeeded({
1045
- hooks,
1046
- origin,
1047
- runtime,
1048
- sourceRegistrationId,
1049
- sourceWindow,
1050
- windowId
1051
- });
1052
- if (initSent.get(sourceWindow) === sourceRegistrationId) {
1097
+ if (state.initSent.get(sourceWindow) === sourceRegistrationId) {
1053
1098
  return;
1054
1099
  }
1055
- const capabilities = buildShellCapabilities(hooks);
1056
- postShellInit(sourceWindow, capabilities, Object.keys(hooks.services ?? {}));
1057
- initSent.set(sourceWindow, sourceRegistrationId);
1100
+ const identity = resolveNip5dIdentity(hooks, windowId, sourceWindow);
1101
+ if (!identity) return;
1102
+ registerNip5dSessionIfNeeded({ origin, runtime, state, sourceRegistrationId, windowId, identity });
1103
+ const environment = originRegistry.getEnvironment(sourceWindow) ?? resolveShellEnvironment(hooks, identity);
1104
+ state.environments.set(windowId, environment);
1105
+ postShellInit(sourceWindow, environment);
1106
+ state.initSent.set(sourceWindow, sourceRegistrationId);
1058
1107
  }
1059
1108
  function registerNip5dSessionIfNeeded({
1060
- hooks,
1061
1109
  origin,
1062
1110
  runtime,
1063
- windowId
1111
+ state,
1112
+ sourceRegistrationId,
1113
+ windowId,
1114
+ identity
1064
1115
  }) {
1065
- if (runtime.sessionRegistry.getEntryByWindowId(windowId)) {
1116
+ const existing = runtime.sessionRegistry.getEntryByWindowId(windowId);
1117
+ const previousRegistration = state.sessionRegistration.get(windowId);
1118
+ if (existing && previousRegistration === sourceRegistrationId) {
1066
1119
  return;
1067
1120
  }
1068
- const identity = resolveNip5dIdentity(hooks, windowId);
1069
- if (!identity) {
1070
- return;
1121
+ if (existing && previousRegistration !== void 0) {
1122
+ runtime.sessionRegistry.unregister(windowId);
1071
1123
  }
1124
+ if (existing && previousRegistration === void 0) return;
1072
1125
  const entry = {
1073
1126
  pubkey: "",
1074
1127
  windowId,
@@ -1081,33 +1134,30 @@ function registerNip5dSessionIfNeeded({
1081
1134
  provenance: "nip-5d"
1082
1135
  };
1083
1136
  runtime.sessionRegistry.register(windowId, entry);
1137
+ state.sessionRegistration.set(windowId, sourceRegistrationId);
1084
1138
  }
1085
- function resolveNip5dIdentity(hooks, windowId) {
1139
+ function resolveNip5dIdentity(hooks, windowId, sourceWindow) {
1086
1140
  const hookIdentity = hooks.onNip5dIframeCreate?.(windowId);
1087
1141
  if (hookIdentity !== null && hookIdentity !== void 0) {
1088
- return {
1142
+ return Object.freeze({
1089
1143
  dTag: hookIdentity.dTag,
1090
1144
  aggregateHash: hookIdentity.aggregateHash
1091
- };
1092
- }
1093
- const win = originRegistry.getIframeWindow(windowId);
1094
- if (!win) {
1095
- return null;
1145
+ });
1096
1146
  }
1097
- const originIdentity = originRegistry.getIdentity(win);
1147
+ const originIdentity = originRegistry.getIdentity(sourceWindow);
1098
1148
  if (!originIdentity) {
1099
1149
  return null;
1100
1150
  }
1101
- return {
1151
+ return Object.freeze({
1102
1152
  dTag: originIdentity.dTag,
1103
1153
  aggregateHash: originIdentity.aggregateHash
1104
- };
1154
+ });
1105
1155
  }
1106
- function postShellInit(win, capabilities, services) {
1156
+ function postShellInit(win, environment) {
1107
1157
  const initMsg = {
1108
1158
  type: "shell.init",
1109
- capabilities,
1110
- services
1159
+ capabilities: environment.capabilities,
1160
+ services: environment.services
1111
1161
  };
1112
1162
  win.postMessage(initMsg, "*");
1113
1163
  }
@@ -1123,6 +1173,7 @@ function reportUnrouted(hooks, event, reason) {
1123
1173
  }
1124
1174
  }
1125
1175
  function createShellBridge(hooks) {
1176
+ const shellReadyState = createShellReadyState();
1126
1177
  const runtimeHooks = adaptHooks(hooks, {
1127
1178
  originRegistry,
1128
1179
  manifestCache,
@@ -1130,11 +1181,16 @@ function createShellBridge(hooks) {
1130
1181
  audioManager,
1131
1182
  nappKeyRegistry
1132
1183
  });
1133
- const runtime = createRuntime(runtimeHooks);
1134
- function broadcastToNapplets(envelope) {
1135
- const windowIds = originRegistry.getAllWindowIds();
1136
- for (const windowId of windowIds) {
1137
- const win = originRegistry.getIframeWindow(windowId);
1184
+ const runtime = createRuntime({
1185
+ ...runtimeHooks,
1186
+ isDomainAllowed: shellReadyState.isDomainAllowed
1187
+ });
1188
+ function publishToEligibleNapplets(envelope, domain, capability) {
1189
+ for (const entry of runtime.sessionRegistry.getAllEntries()) {
1190
+ const environment = shellReadyState.environments.get(entry.windowId);
1191
+ if (!environment?.capabilities.domains.includes(domain)) continue;
1192
+ if (!runtime.aclState.check(entry.pubkey, entry.dTag, entry.aggregateHash, capability)) continue;
1193
+ const win = originRegistry.getIframeWindow(entry.windowId);
1138
1194
  if (!win) continue;
1139
1195
  win.postMessage(envelope, "*");
1140
1196
  }
@@ -1158,6 +1214,7 @@ function createShellBridge(hooks) {
1158
1214
  hooks,
1159
1215
  origin: event.origin,
1160
1216
  runtime,
1217
+ state: shellReadyState,
1161
1218
  sourceRegistrationId: originRegistry.getRegistrationId(sourceWindow) ?? 0,
1162
1219
  sourceWindow,
1163
1220
  windowId
@@ -1171,17 +1228,18 @@ function createShellBridge(hooks) {
1171
1228
  },
1172
1229
  destroy() {
1173
1230
  runtime.destroy();
1231
+ shellReadyState.clear();
1174
1232
  },
1175
1233
  registerConsentHandler(handler) {
1176
1234
  runtime.registerConsentHandler(handler);
1177
1235
  },
1178
1236
  publishTheme(theme) {
1179
1237
  const envelope = { type: "theme.changed", theme };
1180
- broadcastToNapplets(envelope);
1238
+ publishToEligibleNapplets(envelope, "theme", "theme:read");
1181
1239
  },
1182
1240
  publishIdentityChanged(pubkey) {
1183
1241
  const envelope = { type: "identity.changed", pubkey };
1184
- broadcastToNapplets(envelope);
1242
+ publishToEligibleNapplets(envelope, "identity", "identity:read");
1185
1243
  },
1186
1244
  get runtime() {
1187
1245
  return runtime;
@@ -1276,6 +1334,41 @@ function nappletNamespacePrelude(domains) {
1276
1334
  function fire(message) {
1277
1335
  post(message);
1278
1336
  }
1337
+ function normalizeConventionUri(topic, hasExplicitPayload) {
1338
+ if (typeof topic !== "string") throw new TypeError("INC topic must be text");
1339
+ if (!topic.startsWith("napplet:")) return { topic };
1340
+ if (topic.includes("#")) throw new TypeError("Convention topics cannot contain fragments");
1341
+ const queryStart = topic.indexOf("?");
1342
+ if (queryStart === -1) {
1343
+ return { topic };
1344
+ }
1345
+ if (hasExplicitPayload) {
1346
+ throw new TypeError("Convention queries cannot be combined with an explicit payload");
1347
+ }
1348
+ const values = {};
1349
+ const names = /* @__PURE__ */ new Set();
1350
+ const query = topic.slice(queryStart + 1);
1351
+ if (query) {
1352
+ for (const pair of query.split("&")) {
1353
+ const separator = pair.indexOf("=");
1354
+ if (separator < 0) {
1355
+ throw new TypeError("Convention query parameters must use name=value form");
1356
+ }
1357
+ let name;
1358
+ let value;
1359
+ try {
1360
+ name = decodeURIComponent(pair.slice(0, separator));
1361
+ value = decodeURIComponent(pair.slice(separator + 1));
1362
+ } catch {
1363
+ throw new TypeError("Convention query contains malformed percent encoding");
1364
+ }
1365
+ if (names.has(name)) throw new TypeError("Convention query names must be unique");
1366
+ names.add(name);
1367
+ values[name] = value;
1368
+ }
1369
+ }
1370
+ return { topic: topic.slice(0, queryStart), payload: values };
1371
+ }
1279
1372
  function resultOrError(msg) {
1280
1373
  return msg;
1281
1374
  }
@@ -1315,11 +1408,13 @@ function nappletNamespacePrelude(domains) {
1315
1408
  if (typeof incoming !== "object" || incoming === null) return;
1316
1409
  const message = incoming;
1317
1410
  if (message.type !== "shell.init") return;
1318
- const services = Object.freeze(
1319
- Array.isArray(message.services) ? message.services.filter((service) => typeof service === "string") : []
1320
- );
1411
+ const rawCapabilities = message.capabilities;
1412
+ const rawDomains = typeof rawCapabilities === "object" && rawCapabilities !== null ? rawCapabilities.domains : void 0;
1413
+ if (!Array.isArray(rawDomains)) return;
1414
+ const domains2 = freezeStrings(rawDomains);
1415
+ const services = freezeStrings(Array.isArray(message.services) ? message.services : []);
1321
1416
  environment = Object.freeze({
1322
- capabilities: message.capabilities ?? {},
1417
+ capabilities: Object.freeze({ domains: domains2 }),
1323
1418
  services
1324
1419
  });
1325
1420
  off();
@@ -1327,20 +1422,18 @@ function nappletNamespacePrelude(domains) {
1327
1422
  for (const handler of readyHandlers) handler(environment);
1328
1423
  readyHandlers.clear();
1329
1424
  });
1330
- function supports(domain, protocol) {
1331
- if (!environment || typeof environment.capabilities !== "object" || environment.capabilities === null) {
1332
- return false;
1425
+ function freezeStrings(values) {
1426
+ const seen = /* @__PURE__ */ new Set();
1427
+ const strings = [];
1428
+ for (const value of values) {
1429
+ if (typeof value !== "string" || value.length === 0 || seen.has(value)) continue;
1430
+ seen.add(value);
1431
+ strings.push(value);
1333
1432
  }
1334
- const capabilities = environment.capabilities;
1335
- if (protocol !== void 0) {
1336
- const protocols = capabilities.protocols;
1337
- if (typeof protocols === "object" && protocols !== null) {
1338
- const supported = protocols[domain];
1339
- if (Array.isArray(supported) && supported.includes(protocol)) return true;
1340
- }
1341
- return Array.isArray(capabilities.naps) && capabilities.naps.includes(`${domain}:${protocol}`);
1342
- }
1343
- return Array.isArray(capabilities.domains) && capabilities.domains.includes(domain) || Array.isArray(capabilities.naps) && capabilities.naps.includes(domain);
1433
+ return Object.freeze(strings);
1434
+ }
1435
+ function supports(domain) {
1436
+ return typeof domain === "string" && environment?.capabilities.domains.includes(domain) === true;
1344
1437
  }
1345
1438
  const shell2 = {
1346
1439
  ready: () => ready,
@@ -1424,38 +1517,205 @@ function nappletNamespacePrelude(domains) {
1424
1517
  };
1425
1518
  }
1426
1519
  function makeInc() {
1427
- return {
1428
- emit(topic, _extraTags, content) {
1429
- let payload = content;
1430
- if (typeof content === "string" && content.length > 0) {
1431
- try {
1432
- payload = JSON.parse(content);
1433
- } catch {
1434
- payload = content;
1435
- }
1520
+ const topicStates = /* @__PURE__ */ new Map();
1521
+ const maxRetainedEvents = 32;
1522
+ const channelStates = /* @__PURE__ */ new Map();
1523
+ const openedHandlers = /* @__PURE__ */ new Set();
1524
+ const pendingOpened = [];
1525
+ const unopenedEvents = /* @__PURE__ */ new Map();
1526
+ const unopenedClosed = /* @__PURE__ */ new Map();
1527
+ function stableSubscriptionTopic(topic) {
1528
+ if (typeof topic !== "string") throw new TypeError("INC topic must be text");
1529
+ if (topic.includes("?") || topic.includes("#")) {
1530
+ throw new TypeError("INC subscriptions require a queryless topic identity");
1531
+ }
1532
+ return topic;
1533
+ }
1534
+ function closeChannelState(state, closed, notifyShell) {
1535
+ if (state.closed) return;
1536
+ state.closed = closed;
1537
+ if (notifyShell) fire({ type: "inc.channel.close", channelId: state.channelId });
1538
+ for (const handler of state.closedHandlers) handler(closed);
1539
+ }
1540
+ function makeChannelHandle(state) {
1541
+ return {
1542
+ id: state.channelId,
1543
+ peer: state.peer,
1544
+ emit(payload) {
1545
+ if (state.closed) return;
1546
+ fire({ type: "inc.channel.emit", channelId: state.channelId, ...payload === void 0 ? {} : { payload } });
1547
+ },
1548
+ on(handler) {
1549
+ if (typeof handler !== "function") throw new TypeError("INC channel event handler must be a function");
1550
+ if (state.closed) return subscriptionHandle(() => void 0);
1551
+ state.eventHandlers.add(handler);
1552
+ const pending = state.pendingEvents.splice(0);
1553
+ for (const event of pending) handler(event);
1554
+ return subscriptionHandle(() => state.eventHandlers.delete(handler));
1555
+ },
1556
+ onClosed(handler) {
1557
+ if (typeof handler !== "function") throw new TypeError("INC channel close handler must be a function");
1558
+ state.closedHandlers.add(handler);
1559
+ if (state.closed) handler(state.closed);
1560
+ return subscriptionHandle(() => state.closedHandlers.delete(handler));
1561
+ },
1562
+ close() {
1563
+ closeChannelState(state, { channelId: state.channelId }, true);
1564
+ }
1565
+ };
1566
+ }
1567
+ function materializeChannel(channelId, peer) {
1568
+ const existing = channelStates.get(channelId);
1569
+ if (existing) return existing;
1570
+ const state = {
1571
+ channelId,
1572
+ peer,
1573
+ eventHandlers: /* @__PURE__ */ new Set(),
1574
+ closedHandlers: /* @__PURE__ */ new Set(),
1575
+ pendingEvents: unopenedEvents.get(channelId) ?? []
1576
+ };
1577
+ unopenedEvents.delete(channelId);
1578
+ channelStates.set(channelId, state);
1579
+ const closed = unopenedClosed.get(channelId);
1580
+ unopenedClosed.delete(channelId);
1581
+ if (closed) closeChannelState(state, closed, false);
1582
+ return state;
1583
+ }
1584
+ function deliverOpened(state) {
1585
+ const handle = makeChannelHandle(state);
1586
+ if (openedHandlers.size > 0) {
1587
+ for (const handler of openedHandlers) handler(handle);
1588
+ return;
1589
+ }
1590
+ pendingOpened.push(state);
1591
+ }
1592
+ function receiveChannelEvent(channelId, sender, payloadPresent, payload) {
1593
+ const event = payloadPresent ? { channelId, sender, payload } : { channelId, sender };
1594
+ const state = channelStates.get(channelId);
1595
+ if (!state) {
1596
+ const pending = unopenedEvents.get(channelId) ?? [];
1597
+ if (pending.length >= maxRetainedEvents) {
1598
+ unopenedEvents.delete(channelId);
1599
+ unopenedClosed.set(channelId, { channelId, reason: "buffer overflow" });
1600
+ fire({ type: "inc.channel.close", channelId });
1601
+ return;
1436
1602
  }
1437
- fire({ type: "inc.emit", topic, ...payload === void 0 || payload === "" ? {} : { payload } });
1603
+ pending.push(event);
1604
+ unopenedEvents.set(channelId, pending);
1605
+ return;
1606
+ }
1607
+ if (state.closed) return;
1608
+ if (state.eventHandlers.size > 0) {
1609
+ for (const handler of state.eventHandlers) handler(event);
1610
+ return;
1611
+ }
1612
+ if (state.pendingEvents.length >= maxRetainedEvents) {
1613
+ closeChannelState(state, { channelId, reason: "buffer overflow" }, true);
1614
+ return;
1615
+ }
1616
+ state.pendingEvents.push(event);
1617
+ }
1618
+ listen((event) => {
1619
+ if (!isParentMessage(event)) return;
1620
+ const msg = event.data;
1621
+ if (typeof msg !== "object" || msg === null || typeof msg.type !== "string") return;
1622
+ if (msg.type === "inc.channel.opened" && typeof msg.channelId === "string" && typeof msg.peer === "string") {
1623
+ deliverOpened(materializeChannel(msg.channelId, msg.peer));
1624
+ return;
1625
+ }
1626
+ if (msg.type === "inc.channel.event" && typeof msg.channelId === "string" && typeof msg.sender === "string") {
1627
+ receiveChannelEvent(msg.channelId, msg.sender, Object.prototype.hasOwnProperty.call(msg, "payload"), msg.payload);
1628
+ return;
1629
+ }
1630
+ if (msg.type === "inc.channel.closed" && typeof msg.channelId === "string") {
1631
+ const closed = typeof msg.reason === "string" ? { channelId: msg.channelId, reason: msg.reason } : { channelId: msg.channelId };
1632
+ const state = channelStates.get(msg.channelId);
1633
+ if (state) closeChannelState(state, closed, false);
1634
+ else unopenedClosed.set(msg.channelId, closed);
1635
+ }
1636
+ });
1637
+ return {
1638
+ emit(topic, payload) {
1639
+ const transposed = normalizeConventionUri(topic, arguments.length > 1);
1640
+ fire({
1641
+ type: "inc.emit",
1642
+ topic: transposed.topic,
1643
+ ...transposed.payload === void 0 && payload === void 0 ? {} : { payload: transposed.payload ?? payload }
1644
+ });
1438
1645
  },
1439
1646
  on(topic, callback) {
1440
- const off = listen((event) => {
1441
- if (!isParentMessage(event)) return;
1442
- const msg = event.data;
1443
- if (typeof msg !== "object" || msg === null || msg.type !== "inc.event" || msg.topic !== topic) return;
1444
- callback(msg.payload ?? {}, {
1445
- id: "",
1446
- pubkey: typeof msg.sender === "string" ? msg.sender : "",
1447
- created_at: Math.floor(Date.now() / 1e3),
1448
- kind: 0,
1449
- tags: [["t", topic]],
1450
- content: typeof msg.payload === "string" ? msg.payload : JSON.stringify(msg.payload ?? {}),
1451
- sig: ""
1647
+ const stableTopic = stableSubscriptionTopic(topic);
1648
+ if (typeof callback !== "function") throw new TypeError("INC subscription handler must be a function");
1649
+ let state = topicStates.get(stableTopic);
1650
+ if (!state) {
1651
+ const handlers = /* @__PURE__ */ new Set();
1652
+ const subscriptionId = id();
1653
+ const offEvent = listen((event) => {
1654
+ if (!isParentMessage(event)) return;
1655
+ const msg = event.data;
1656
+ if (typeof msg !== "object" || msg === null || msg.type !== "inc.event" || msg.topic !== stableTopic || typeof msg.sender !== "string") return;
1657
+ const delivered = Object.prototype.hasOwnProperty.call(msg, "payload") ? { topic: stableTopic, sender: msg.sender, payload: msg.payload } : { topic: stableTopic, sender: msg.sender };
1658
+ for (const handler of handlers) handler(delivered);
1452
1659
  });
1453
- });
1454
- fire({ type: "inc.subscribe", id: id(), topic });
1660
+ const offResult = listen((event) => {
1661
+ if (!isParentMessage(event)) return;
1662
+ const msg = event.data;
1663
+ if (typeof msg !== "object" || msg === null || msg.type !== "inc.subscribe.result" || msg.id !== subscriptionId) return;
1664
+ offResult();
1665
+ if (typeof msg.error === "string" && msg.error) {
1666
+ offEvent();
1667
+ topicStates.delete(stableTopic);
1668
+ }
1669
+ });
1670
+ state = { handlers, offEvent, offResult };
1671
+ topicStates.set(stableTopic, state);
1672
+ fire({ type: "inc.subscribe", id: subscriptionId, topic: stableTopic });
1673
+ }
1674
+ state.handlers.add(callback);
1455
1675
  return subscriptionHandle(() => {
1456
- fire({ type: "inc.unsubscribe", topic });
1457
- off();
1676
+ const current = topicStates.get(stableTopic);
1677
+ if (!current) return;
1678
+ current.handlers.delete(callback);
1679
+ if (current.handlers.size !== 0) return;
1680
+ current.offEvent();
1681
+ current.offResult();
1682
+ topicStates.delete(stableTopic);
1683
+ fire({ type: "inc.unsubscribe", topic: stableTopic });
1458
1684
  });
1685
+ },
1686
+ channel: {
1687
+ open(targetDTag) {
1688
+ return request(
1689
+ { type: "inc.channel.open", target: targetDTag },
1690
+ "inc.channel.open.result",
1691
+ (msg) => {
1692
+ if (typeof msg.channelId !== "string" || typeof msg.peer !== "string") {
1693
+ throw new Error("inc.channel.open.result missing channel identity");
1694
+ }
1695
+ return makeChannelHandle(materializeChannel(msg.channelId, msg.peer));
1696
+ }
1697
+ );
1698
+ },
1699
+ onOpened(handler) {
1700
+ if (typeof handler !== "function") throw new TypeError("INC opened handler must be a function");
1701
+ openedHandlers.add(handler);
1702
+ const pending = pendingOpened.splice(0);
1703
+ for (const state of pending) handler(makeChannelHandle(state));
1704
+ return subscriptionHandle(() => openedHandlers.delete(handler));
1705
+ },
1706
+ list() {
1707
+ return request(
1708
+ { type: "inc.channel.list" },
1709
+ "inc.channel.list.result",
1710
+ (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({
1711
+ id: channel.id,
1712
+ peer: channel.peer
1713
+ })] : []))
1714
+ );
1715
+ },
1716
+ broadcast(payload) {
1717
+ fire({ type: "inc.channel.broadcast", ...payload === void 0 ? {} : { payload } });
1718
+ }
1459
1719
  }
1460
1720
  };
1461
1721
  }
@@ -1493,14 +1753,22 @@ function nappletNamespacePrelude(domains) {
1493
1753
  return { ...makeScope(), instance: makeScope("instance") };
1494
1754
  }
1495
1755
  function makeIdentity() {
1496
- const read = (type, field, fallback) => request(
1497
- { type },
1756
+ const read = (type, field, fallback, payload = {}) => request(
1757
+ { type, ...payload },
1498
1758
  `${type}.result`,
1499
1759
  (msg) => Object.prototype.hasOwnProperty.call(msg, field) ? msg[field] : fallback,
1500
1760
  { rejectOnError: type !== "identity.getPublicKey" }
1501
1761
  );
1502
- return {
1762
+ return Object.freeze({
1503
1763
  getPublicKey: () => read("identity.getPublicKey", "pubkey", ""),
1764
+ getRelays: () => read("identity.getRelays", "relays", {}),
1765
+ getProfile: () => read("identity.getProfile", "profile", null),
1766
+ getFollows: () => read("identity.getFollows", "pubkeys", []),
1767
+ getList: (listType) => read("identity.getList", "entries", [], { listType }),
1768
+ getZaps: () => read("identity.getZaps", "zaps", []),
1769
+ getMutes: () => read("identity.getMutes", "pubkeys", []),
1770
+ getBlocked: () => read("identity.getBlocked", "pubkeys", []),
1771
+ getBadges: () => read("identity.getBadges", "badges", []),
1504
1772
  onChanged(handler) {
1505
1773
  const off = listen((event) => {
1506
1774
  if (!isParentMessage(event)) return;
@@ -1509,20 +1777,8 @@ function nappletNamespacePrelude(domains) {
1509
1777
  if (typeof msg.pubkey === "string") handler(msg.pubkey);
1510
1778
  });
1511
1779
  return subscriptionHandle(off);
1512
- },
1513
- getRelays: () => read("identity.getRelays", "relays", {}),
1514
- getProfile: () => read("identity.getProfile", "profile", null),
1515
- getFollows: () => read("identity.getFollows", "pubkeys", []),
1516
- getList: (listType) => request(
1517
- { type: "identity.getList", listType },
1518
- "identity.getList.result",
1519
- (msg) => Array.isArray(msg.entries) ? msg.entries : []
1520
- ),
1521
- getZaps: () => read("identity.getZaps", "zaps", []),
1522
- getMutes: () => read("identity.getMutes", "pubkeys", []),
1523
- getBlocked: () => read("identity.getBlocked", "pubkeys", []),
1524
- getBadges: () => read("identity.getBadges", "badges", [])
1525
- };
1780
+ }
1781
+ });
1526
1782
  }
1527
1783
  function makeTheme() {
1528
1784
  return {
@@ -1961,19 +2217,85 @@ function nappletNamespacePrelude(domains) {
1961
2217
  };
1962
2218
  }
1963
2219
  function makeIntent() {
1964
- const invoke = (requestPayload) => request(
1965
- { type: "intent.invoke", request: requestPayload },
1966
- "intent.invoke.result",
1967
- (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
1968
- );
2220
+ const deliveryHandlers = /* @__PURE__ */ new Set();
2221
+ const pendingDeliveries = [];
2222
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
2223
+ const normalizeDelivery = (value) => {
2224
+ if (!value || typeof value !== "object" || typeof value.sender !== "string" || typeof value.archetype !== "string" || typeof value.action !== "string" || typeof value.convention !== "string") {
2225
+ return void 0;
2226
+ }
2227
+ const delivery = value;
2228
+ return {
2229
+ sender: delivery.sender,
2230
+ archetype: delivery.archetype,
2231
+ action: delivery.action,
2232
+ convention: delivery.convention,
2233
+ ...hasOwn(delivery, "payload") ? { payload: delivery.payload } : {}
2234
+ };
2235
+ };
2236
+ listen((event) => {
2237
+ if (!isParentMessage(event)) return;
2238
+ const msg = event.data;
2239
+ if (typeof msg !== "object" || msg === null || msg.type !== "intent.deliver") return;
2240
+ const delivery = normalizeDelivery(msg.delivery);
2241
+ if (!delivery) return;
2242
+ if (deliveryHandlers.size === 0) pendingDeliveries.push(delivery);
2243
+ else for (const handler of deliveryHandlers) handler(delivery);
2244
+ });
2245
+ function normalizeIntentUri(uri, explicitPayload, actionMustBe) {
2246
+ if (typeof uri !== "string" || !uri.startsWith("napplet:")) {
2247
+ throw new TypeError("Intent convention URI must start with napplet:");
2248
+ }
2249
+ if (uri.includes("#")) throw new TypeError("Intent convention URI cannot contain fragments");
2250
+ const normalized = normalizeConventionUri(uri, explicitPayload);
2251
+ const match = /^napplet:([^/?#]+)\/([^/?#]+)$/.exec(normalized.topic);
2252
+ if (!match) throw new TypeError("Intent convention URI must be napplet:<archetype>/<intent>");
2253
+ if (actionMustBe !== void 0 && match[2] !== actionMustBe) {
2254
+ throw new TypeError(`Intent convention URI must use the ${actionMustBe} intent`);
2255
+ }
2256
+ return {
2257
+ archetype: match[1],
2258
+ action: match[2],
2259
+ convention: normalized.topic,
2260
+ ...normalized.payload === void 0 ? {} : { payload: normalized.payload }
2261
+ };
2262
+ }
2263
+ const invoke = (uri, opts, actionMustBe) => {
2264
+ const supplied = opts && typeof opts === "object" ? opts : void 0;
2265
+ const explicitPayload = supplied?.payload !== void 0;
2266
+ const normalized = normalizeIntentUri(uri, explicitPayload, actionMustBe);
2267
+ if (supplied && hasOwn(supplied, "sender")) {
2268
+ throw new TypeError("Intent callers cannot supply sender");
2269
+ }
2270
+ for (const field of ["archetype", "action", "convention"]) {
2271
+ if (supplied && hasOwn(supplied, field) && supplied[field] !== normalized[field]) {
2272
+ throw new TypeError(`Intent options cannot override URI-derived ${field}`);
2273
+ }
2274
+ }
2275
+ const behaviorValue = supplied?.behavior;
2276
+ const behavior = behaviorValue && typeof behaviorValue === "object" ? behaviorValue : void 0;
2277
+ const sanitizedBehavior = {
2278
+ ...typeof behavior?.focus === "boolean" ? { focus: behavior.focus } : {},
2279
+ ...typeof behavior?.reuse === "boolean" ? { reuse: behavior.reuse } : {}
2280
+ };
2281
+ const handler = typeof supplied?.handler === "string" ? supplied.handler : void 0;
2282
+ return request(
2283
+ {
2284
+ type: "intent.invoke",
2285
+ request: {
2286
+ ...normalized,
2287
+ ...explicitPayload ? { payload: supplied?.payload } : {},
2288
+ ...handler === void 0 ? {} : { handler },
2289
+ ...Object.keys(sanitizedBehavior).length === 0 ? {} : { behavior: sanitizedBehavior }
2290
+ }
2291
+ },
2292
+ "intent.invoke.result",
2293
+ (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
2294
+ );
2295
+ };
1969
2296
  return {
1970
2297
  invoke,
1971
- open: (archetype, payload, opts) => invoke({
1972
- archetype,
1973
- action: "open",
1974
- payload,
1975
- ...opts && typeof opts === "object" ? opts : {}
1976
- }),
2298
+ open: (uri, opts) => invoke(uri, opts, "open"),
1977
2299
  available: (archetype) => request(
1978
2300
  { type: "intent.available", archetype },
1979
2301
  "intent.available.result",
@@ -1988,7 +2310,14 @@ function nappletNamespacePrelude(domains) {
1988
2310
  "intent.changed",
1989
2311
  callback,
1990
2312
  (msg) => msg.availability
1991
- )
2313
+ ),
2314
+ onDelivery: (callback) => {
2315
+ if (typeof callback !== "function") throw new TypeError("Intent delivery handler must be a function");
2316
+ deliveryHandlers.add(callback);
2317
+ const pending = pendingDeliveries.splice(0);
2318
+ for (const delivery of pending) callback(delivery);
2319
+ return subscriptionHandle(() => deliveryHandlers.delete(callback));
2320
+ }
1992
2321
  };
1993
2322
  }
1994
2323
  function makeWebrtc() {
@@ -2123,14 +2452,40 @@ function nappletNamespacePrelude(domains) {
2123
2452
  };
2124
2453
  }
2125
2454
  const shell = makeShell();
2455
+ let inc;
2456
+ let identity;
2457
+ let theme;
2458
+ let intent;
2459
+ function makeProtectedInc(existing) {
2460
+ const extensions = existing && typeof existing === "object" ? existing : {};
2461
+ inc ??= makeInc();
2462
+ return { ...extensions, ...inc };
2463
+ }
2464
+ function makeProtectedIdentity() {
2465
+ identity ??= makeIdentity();
2466
+ return identity;
2467
+ }
2468
+ function makeProtectedTheme() {
2469
+ theme ??= Object.freeze(makeTheme());
2470
+ return theme;
2471
+ }
2472
+ function makeProtectedIntent() {
2473
+ intent ??= Object.freeze(makeIntent());
2474
+ return intent;
2475
+ }
2476
+ function isProtectedDomain(domain) {
2477
+ return domain === "shell" || domain === "inc" || domain === "identity" || domain === "theme" || domain === "intent";
2478
+ }
2126
2479
  function makeDomain(domain, existing) {
2127
2480
  if (domain === "shell") return shell;
2481
+ if (domain === "inc") return makeProtectedInc(existing);
2482
+ if (domain === "identity") return makeProtectedIdentity();
2483
+ if (domain === "theme") return makeProtectedTheme();
2484
+ if (domain === "intent") return makeProtectedIntent();
2128
2485
  if (existing && typeof existing === "object" && Object.keys(existing).length > 0) return existing;
2129
2486
  switch (domain) {
2130
2487
  case "relay":
2131
2488
  return makeRelay();
2132
- case "inc":
2133
- return makeInc();
2134
2489
  case "storage":
2135
2490
  return makeStorage();
2136
2491
  case "keys":
@@ -2139,10 +2494,6 @@ function nappletNamespacePrelude(domains) {
2139
2494
  return makeMedia();
2140
2495
  case "notify":
2141
2496
  return makeNotify();
2142
- case "identity":
2143
- return makeIdentity();
2144
- case "theme":
2145
- return makeTheme();
2146
2497
  case "config":
2147
2498
  return makeConfig();
2148
2499
  case "resource":
@@ -2153,8 +2504,6 @@ function nappletNamespacePrelude(domains) {
2153
2504
  return makeOutbox();
2154
2505
  case "upload":
2155
2506
  return makeUpload();
2156
- case "intent":
2157
- return makeIntent();
2158
2507
  case "webrtc":
2159
2508
  return makeWebrtc();
2160
2509
  case "ble":
@@ -2184,10 +2533,22 @@ function nappletNamespacePrelude(domains) {
2184
2533
  },
2185
2534
  defineProperty(obj, prop, descriptor) {
2186
2535
  if (typeof prop !== "string" || !allowed.has(prop)) return true;
2536
+ if (isProtectedDomain(prop)) {
2537
+ return Reflect.defineProperty(obj, prop, {
2538
+ value: makeDomain(prop, descriptor.value),
2539
+ enumerable: true,
2540
+ configurable: true,
2541
+ writable: true
2542
+ });
2543
+ }
2187
2544
  return Reflect.defineProperty(obj, prop, {
2188
2545
  ...descriptor,
2189
2546
  value: makeDomain(prop, descriptor.value)
2190
2547
  });
2548
+ },
2549
+ deleteProperty(obj, prop) {
2550
+ if (typeof prop === "string" && allowed.has(prop) && isProtectedDomain(prop)) return true;
2551
+ return Reflect.deleteProperty(obj, prop);
2191
2552
  }
2192
2553
  });
2193
2554
  }
@@ -2217,7 +2578,7 @@ function nappletNamespacePrelude(domains) {
2217
2578
  root = buildNappletNamespace(value);
2218
2579
  },
2219
2580
  enumerable: false,
2220
- configurable: true
2581
+ configurable: false
2221
2582
  });
2222
2583
  }
2223
2584
 
@@ -2234,9 +2595,10 @@ function createIdentityProxy(deps) {
2234
2595
  dispatch(windowId, envelope) {
2235
2596
  deps.runtime.handleMessage(windowId, envelope);
2236
2597
  },
2237
- emit(windowId, envelope) {
2238
- const win = deps.originRegistry.getIframeWindow(windowId);
2239
- if (win) win.postMessage(envelope, "*");
2598
+ emit(_windowId, _envelope) {
2599
+ throw new Error(
2600
+ "Direct identity proxy emit is prohibited; use ShellBridge.publishIdentityChanged()"
2601
+ );
2240
2602
  }
2241
2603
  };
2242
2604
  }
@@ -2247,9 +2609,10 @@ function createThemeProxy(deps) {
2247
2609
  dispatch(windowId, envelope) {
2248
2610
  deps.runtime.handleMessage(windowId, envelope);
2249
2611
  },
2250
- emit(windowId, envelope) {
2251
- const win = deps.originRegistry.getIframeWindow(windowId);
2252
- if (win) win.postMessage(envelope, "*");
2612
+ emit(_windowId, _envelope) {
2613
+ throw new Error(
2614
+ "Direct theme proxy emit is prohibited; use ShellBridge.publishTheme()"
2615
+ );
2253
2616
  }
2254
2617
  };
2255
2618
  }
@@ -2312,6 +2675,7 @@ export {
2312
2675
  nappKeyRegistry,
2313
2676
  originRegistry,
2314
2677
  renderNappletNamespacePrelude,
2678
+ resolveShellEnvironment,
2315
2679
  sessionRegistry
2316
2680
  };
2317
2681
  //# sourceMappingURL=index.js.map