@kehto/paja 0.8.0 → 0.9.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.
@@ -196,6 +196,9 @@ function countMap(_action) {
196
196
  return { senderCap: "relay:read", recipientCap: null };
197
197
  }
198
198
  function identityMap(action) {
199
+ if (action === "changed") {
200
+ return { senderCap: null, recipientCap: "identity:read" };
201
+ }
199
202
  if (action === "getPublicKey" || action === "getRelays") {
200
203
  return { senderCap: null, recipientCap: null };
201
204
  }
@@ -219,9 +222,12 @@ function storageMap(action) {
219
222
  return { senderCap: null, recipientCap: null };
220
223
  }
221
224
  function incMap(action) {
222
- if (action === "emit" || action === "channel.emit" || action === "channel.broadcast") {
225
+ if (action === "emit") {
223
226
  return { senderCap: "relay:write", recipientCap: "relay:read" };
224
227
  }
228
+ if (action === "channel.emit" || action === "channel.broadcast" || action === "channel.list" || action === "channel.close") {
229
+ return { senderCap: null, recipientCap: null };
230
+ }
225
231
  return { senderCap: "relay:read", recipientCap: null };
226
232
  }
227
233
  function configMap(action) {
@@ -256,11 +262,14 @@ function uploadMap(action) {
256
262
  return { senderCap: "upload:write", recipientCap: null };
257
263
  }
258
264
  function intentMap(action) {
259
- if (action === "changed" || action.endsWith(".result") || action.endsWith(".error")) {
265
+ if (action === "changed" || action === "deliver" || action === "invoke.result" || action === "available.result" || action === "handlers.result") {
260
266
  return { senderCap: null, recipientCap: "intent:read" };
261
267
  }
262
268
  if (action === "invoke") return { senderCap: "intent:write", recipientCap: null };
263
- return { senderCap: "intent:read", recipientCap: null };
269
+ if (action === "available" || action === "handlers") {
270
+ return { senderCap: "intent:read", recipientCap: null };
271
+ }
272
+ return { senderCap: null, recipientCap: null };
264
273
  }
265
274
  function dmMap(action) {
266
275
  if (action === "message" || action.endsWith(".result") || action.endsWith(".error")) {
@@ -363,12 +372,14 @@ function evaluate(config, state, observation) {
363
372
  }
364
373
  const { initElapsedMs } = observation;
365
374
  if (initElapsedMs !== void 0 && initElapsedMs < config.burstGuard.windowMs) {
366
- const existingBurst = state.bursts[napplet];
375
+ const burstKey = observation.initKey ?? napplet;
376
+ const existingBurst = state.bursts[burstKey];
377
+ const activeBurst = existingBurst !== void 0 && now2 - existingBurst.windowStart < config.burstGuard.windowMs ? existingBurst : void 0;
367
378
  const newBurst = {
368
- count: (existingBurst?.count ?? 0) + 1,
369
- windowStart: existingBurst?.windowStart ?? now2
379
+ count: (activeBurst?.count ?? 0) + 1,
380
+ windowStart: activeBurst?.windowStart ?? now2
370
381
  };
371
- const newBursts = { ...state.bursts, [napplet]: newBurst };
382
+ const newBursts = { ...state.bursts, [burstKey]: newBurst };
372
383
  if (newBurst.count > config.burstGuard.maxOps) {
373
384
  const burstAction = config.burstGuard.action;
374
385
  return {
@@ -612,7 +623,7 @@ function deserialize2(json) {
612
623
  return defaultConfig();
613
624
  }
614
625
 
615
- // ../../node_modules/.pnpm/@napplet+core@0.28.0/node_modules/@napplet/core/dist/index.js
626
+ // ../../node_modules/.pnpm/@napplet+core@0.29.0/node_modules/@napplet/core/dist/index.js
616
627
  function createDispatch() {
617
628
  const handlers = /* @__PURE__ */ new Map();
618
629
  function registerNap2(domain, handler) {
@@ -705,11 +716,20 @@ function createSessionRegistry(notifier) {
705
716
  getWindowId(pubkey) {
706
717
  return byPubkey2.get(pubkey)?.windowId;
707
718
  },
719
+ getWindowIdByDTag(dTag) {
720
+ let resolvedWindowId;
721
+ for (const [windowId, entry] of byWindowIdEntry) {
722
+ if (entry.dTag !== dTag) continue;
723
+ if (resolvedWindowId !== void 0) return void 0;
724
+ resolvedWindowId = windowId;
725
+ }
726
+ return resolvedWindowId;
727
+ },
708
728
  isRegistered(windowId) {
709
729
  return byWindowId2.has(windowId);
710
730
  },
711
731
  getAllEntries() {
712
- return Array.from(byPubkey2.values());
732
+ return Array.from(byWindowIdEntry.values());
713
733
  },
714
734
  getInstanceId(windowId) {
715
735
  const pubkey = byWindowId2.get(windowId);
@@ -795,7 +815,7 @@ function bitsToCapabilities(bits) {
795
815
  function toIdentity(pubkey, dTag, hash) {
796
816
  return { pubkey, dTag, hash };
797
817
  }
798
- function createAclState(persistence, defaultPolicy = "permissive") {
818
+ function createAclState(persistence, defaultPolicy = "permissive", onMutation) {
799
819
  let state = createState(defaultPolicy);
800
820
  function ensureRuntimeDefaultEntry(id) {
801
821
  if (state.defaultPolicy !== "permissive") return;
@@ -816,11 +836,13 @@ function createAclState(persistence, defaultPolicy = "permissive") {
816
836
  const id = toIdentity(pubkey, dTag, aggregateHash);
817
837
  ensureRuntimeDefaultEntry(id);
818
838
  state = revoke(state, id, capToBit(capability));
839
+ onMutation?.({ type: "revoke", dTag, aggregateHash, capability });
819
840
  },
820
841
  block(pubkey, dTag, aggregateHash) {
821
842
  const id = toIdentity(pubkey, dTag, aggregateHash);
822
843
  ensureRuntimeDefaultEntry(id);
823
844
  state = block(state, id);
845
+ onMutation?.({ type: "block", dTag, aggregateHash });
824
846
  },
825
847
  unblock(pubkey, dTag, aggregateHash) {
826
848
  const id = toIdentity(pubkey, dTag, aggregateHash);
@@ -999,18 +1021,39 @@ function createManifestCache(persistence) {
999
1021
  var REPLAY_WINDOW_SECONDS = 30;
1000
1022
  function createReplayDetector(getReplayWindow) {
1001
1023
  const seenEventIds = /* @__PURE__ */ new Map();
1024
+ function checkAndTrack(event, state) {
1025
+ const replayWindow = getReplayWindow?.() ?? REPLAY_WINDOW_SECONDS;
1026
+ const now2 = Math.floor(Date.now() / 1e3);
1027
+ for (const [id, entry] of seenEventIds) {
1028
+ if (entry.state === "committed" && now2 - entry.timestamp > replayWindow) {
1029
+ seenEventIds.delete(id);
1030
+ }
1031
+ }
1032
+ if (now2 - event.created_at > replayWindow) return "invalid: event created_at too old";
1033
+ if (event.created_at - now2 > 10) return "invalid: event created_at in the future";
1034
+ if (seenEventIds.has(event.id)) return "duplicate: already processed";
1035
+ seenEventIds.set(event.id, { state, timestamp: now2 });
1036
+ return null;
1037
+ }
1002
1038
  return {
1003
1039
  check(event) {
1004
- const replayWindow = getReplayWindow?.() ?? REPLAY_WINDOW_SECONDS;
1005
- const now2 = Math.floor(Date.now() / 1e3);
1006
- if (now2 - event.created_at > replayWindow) return "invalid: event created_at too old";
1007
- if (event.created_at - now2 > 10) return "invalid: event created_at in the future";
1008
- if (seenEventIds.has(event.id)) return "duplicate: already processed";
1009
- seenEventIds.set(event.id, now2);
1010
- for (const [id, timestamp] of seenEventIds) {
1011
- if (now2 - timestamp > replayWindow) seenEventIds.delete(id);
1040
+ return checkAndTrack(event, "committed");
1041
+ },
1042
+ reserve(event) {
1043
+ return checkAndTrack(event, "pending");
1044
+ },
1045
+ commit(eventId) {
1046
+ const entry = seenEventIds.get(eventId);
1047
+ if (entry?.state !== "pending") return;
1048
+ seenEventIds.set(eventId, {
1049
+ state: "committed",
1050
+ timestamp: Math.floor(Date.now() / 1e3)
1051
+ });
1052
+ },
1053
+ release(eventId) {
1054
+ if (seenEventIds.get(eventId)?.state === "pending") {
1055
+ seenEventIds.delete(eventId);
1012
1056
  }
1013
- return null;
1014
1057
  },
1015
1058
  clear() {
1016
1059
  seenEventIds.clear();
@@ -1135,7 +1178,7 @@ function createRelayHandler(context) {
1135
1178
  handleRelayClose(context, windowId, msg, m);
1136
1179
  return;
1137
1180
  case "publish":
1138
- handleRelayPublish(context, windowId, msg, m);
1181
+ handleRelayPublish(context, windowId, m);
1139
1182
  return;
1140
1183
  case "publishEncrypted":
1141
1184
  handleRelayPublishEncrypted(context, windowId, msg);
@@ -1259,29 +1302,106 @@ function handleRelayClose(context, windowId, msg, m) {
1259
1302
  hooks.relayPool?.untrackSubscription(subKey);
1260
1303
  hooks.sendToNapplet(windowId, { type: "relay.closed", subId, message: "" });
1261
1304
  }
1262
- function handleRelayPublish(context, windowId, msg, m) {
1263
- const { eventBuffer, hooks, replayDetector } = context;
1264
- const event = m.event;
1305
+ function handleRelayPublish(context, windowId, m) {
1306
+ const { hooks, replayDetector } = context;
1307
+ const eventTemplate = m.event;
1265
1308
  const id = m.id ?? "";
1266
- if (!event || typeof event !== "object") {
1267
- hooks.sendToNapplet(windowId, { type: "relay.publish.error", id, error: "invalid event" });
1309
+ if (!eventTemplate || typeof eventTemplate !== "object") {
1310
+ sendRelayPublishResult(hooks, windowId, id, false, void 0, "invalid event template");
1268
1311
  return;
1269
1312
  }
1270
- const replayResult = replayDetector.check(event);
1271
- if (replayResult !== null) {
1272
- hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: false, message: replayResult });
1313
+ const signer = hooks.auth.getSigner();
1314
+ const signEvent = signer?.signEvent?.bind(signer);
1315
+ if (!signEvent) {
1316
+ sendRelayPublishResult(hooks, windowId, id, false, void 0, "no signer configured");
1273
1317
  return;
1274
1318
  }
1319
+ void (async () => {
1320
+ try {
1321
+ const signed = await signEvent(eventTemplate);
1322
+ if (!signed) {
1323
+ sendRelayPublishResult(hooks, windowId, id, false, void 0, "signEvent returned no event");
1324
+ return;
1325
+ }
1326
+ const replayResult = replayDetector.reserve(signed);
1327
+ if (replayResult !== null) {
1328
+ sendRelayPublishResult(hooks, windowId, id, false, void 0, replayResult);
1329
+ return;
1330
+ }
1331
+ publishSignedRelayEvent(context, windowId, id, signed, (ok, error) => {
1332
+ if (ok) replayDetector.commit(signed.id);
1333
+ else replayDetector.release(signed.id);
1334
+ sendRelayPublishResult(hooks, windowId, id, ok, ok ? signed : void 0, error);
1335
+ });
1336
+ } catch (error) {
1337
+ sendRelayPublishResult(
1338
+ hooks,
1339
+ windowId,
1340
+ id,
1341
+ false,
1342
+ void 0,
1343
+ error instanceof Error && error.message ? error.message : "event signing failed"
1344
+ );
1345
+ }
1346
+ })();
1347
+ }
1348
+ function sendRelayPublishResult(hooks, windowId, id, ok, event, error) {
1349
+ const result2 = ok && event ? { type: "relay.publish.result", id, ok: true, event, eventId: event.id } : { type: "relay.publish.result", id, ok: false, error: error ?? "publish failed" };
1350
+ hooks.sendToNapplet(windowId, result2);
1351
+ }
1352
+ function publishSignedRelayEvent(context, windowId, id, signed, reply) {
1353
+ const { eventBuffer, hooks } = context;
1354
+ let settled = false;
1355
+ const settle6 = (ok, error) => {
1356
+ if (settled) return;
1357
+ settled = true;
1358
+ if (ok) {
1359
+ try {
1360
+ eventBuffer.bufferAndDeliver(signed, windowId);
1361
+ } catch {
1362
+ }
1363
+ }
1364
+ reply(ok, error);
1365
+ };
1275
1366
  const relayService = relayServiceFrom(context);
1276
1367
  if (relayService) {
1277
- relayService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
1278
- } else if (hooks.relayPool?.isAvailable()) {
1279
- hooks.relayPool.publish(event);
1280
- hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: true });
1281
- } else {
1282
- hooks.sendToNapplet(windowId, { type: "relay.publish.result", id, accepted: false, message: "no relay pool available" });
1368
+ const publishMessage = { type: "relay.publish", id, event: signed };
1369
+ try {
1370
+ relayService.handleMessage(windowId, publishMessage, (response) => {
1371
+ if (response.type !== "relay.publish.result" && response.type !== "relay.publish.error") {
1372
+ return;
1373
+ }
1374
+ const result2 = response;
1375
+ const ok = response.type === "relay.publish.result" && (result2.ok ?? result2.accepted ?? false);
1376
+ settle6(ok, ok ? void 0 : result2.error ?? result2.message ?? "publish failed");
1377
+ });
1378
+ } catch (error) {
1379
+ settle6(
1380
+ false,
1381
+ error instanceof Error && error.message ? error.message : "relay service failed"
1382
+ );
1383
+ }
1384
+ return;
1385
+ }
1386
+ if (!hooks.relayPool?.isAvailable()) {
1387
+ settle6(false, "no relay pool available");
1388
+ return;
1389
+ }
1390
+ try {
1391
+ const publishResult = hooks.relayPool.publish(signed);
1392
+ void Promise.resolve(publishResult).then(
1393
+ () => settle6(true),
1394
+ (error) => settle6(
1395
+ false,
1396
+ error instanceof Error && error.message ? error.message : "publish failed"
1397
+ )
1398
+ );
1399
+ } catch (error) {
1400
+ settle6(
1401
+ false,
1402
+ error instanceof Error && error.message ? error.message : "publish failed"
1403
+ );
1283
1404
  }
1284
- eventBuffer.bufferAndDeliver(event, windowId);
1285
1405
  }
1286
1406
  function handleRelayPublishEncrypted(context, windowId, msg) {
1287
1407
  const { hooks } = context;
@@ -1313,7 +1433,7 @@ function handleRelayPublishEncrypted(context, windowId, msg) {
1313
1433
  publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe);
1314
1434
  }
1315
1435
  function publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe) {
1316
- const { eventBuffer, hooks } = context;
1436
+ const { hooks } = context;
1317
1437
  const peSigner = hooks.auth.getSigner();
1318
1438
  if (!peSigner) return;
1319
1439
  (async () => {
@@ -1326,46 +1446,14 @@ function publishEncrypted(context, windowId, id, recipient, encryption, eventTem
1326
1446
  replyPe(false, { error: "signEvent returned null" });
1327
1447
  return;
1328
1448
  }
1329
- publishSignedEncrypted(context, windowId, id, signed, replyPe);
1330
- try {
1331
- eventBuffer.bufferAndDeliver(signed, windowId);
1332
- } catch {
1333
- return;
1334
- }
1449
+ publishSignedRelayEvent(context, windowId, id, signed, (ok, error) => {
1450
+ replyPe(ok, ok ? { event: signed, eventId: signed.id } : { error: error ?? "publish failed" });
1451
+ });
1335
1452
  } catch (err) {
1336
1453
  replyPe(false, { error: err?.message ?? "encryption failed" });
1337
1454
  }
1338
1455
  })();
1339
1456
  }
1340
- function publishSignedEncrypted(context, windowId, id, signed, replyPe) {
1341
- const { hooks } = context;
1342
- const relayService = relayServiceFrom(context);
1343
- if (!relayService) {
1344
- if (hooks.relayPool?.isAvailable()) {
1345
- hooks.relayPool.publish(signed);
1346
- replyPe(true, { event: signed, eventId: signed.id });
1347
- } else {
1348
- replyPe(false, { error: "no relay pool available" });
1349
- }
1350
- return;
1351
- }
1352
- const publishMsg = { type: "relay.publish", id, event: signed };
1353
- let replied = false;
1354
- relayService.handleMessage(windowId, publishMsg, (resp) => {
1355
- if (replied) return;
1356
- const r = resp;
1357
- if (typeof r.type !== "string" || !r.type.startsWith("relay.publish")) return;
1358
- const okVal = r.ok ?? r.accepted ?? false;
1359
- replied = true;
1360
- const publishResult = { event: signed, eventId: signed.id };
1361
- if (!okVal) publishResult.error = r.error ?? r.message ?? "publish failed";
1362
- replyPe(okVal, publishResult);
1363
- });
1364
- if (!replied) {
1365
- replied = true;
1366
- replyPe(true, { event: signed, eventId: signed.id });
1367
- }
1368
- }
1369
1457
  function handleRelayQuery(context, windowId, m) {
1370
1458
  const { hooks, serviceRegistry, eventBuffer } = context;
1371
1459
  const id = m.id ?? "";
@@ -1491,6 +1579,61 @@ function handleRelayQuery(context, windowId, m) {
1491
1579
  }
1492
1580
  }, relayUrls);
1493
1581
  }
1582
+ var RUNTIME_THEME_FALLBACK = {
1583
+ colors: { background: "#0a0a0a", text: "#e0e0e0", primary: "#7aa2f7" }
1584
+ };
1585
+ var IDENTITY_SAFE_DEFAULTS = {
1586
+ "identity.getPublicKey": { pubkey: "" },
1587
+ "identity.getRelays": { relays: {} },
1588
+ "identity.getProfile": { profile: null },
1589
+ "identity.getFollows": { pubkeys: [] },
1590
+ "identity.getList": { entries: [] },
1591
+ "identity.getZaps": { zaps: [] },
1592
+ "identity.getMutes": { pubkeys: [] },
1593
+ "identity.getBlocked": { pubkeys: [] },
1594
+ "identity.getBadges": { badges: [] }
1595
+ };
1596
+ function isRuntimeOwnedIdentityOrThemeRequest(type) {
1597
+ return type === "theme.get" || type in IDENTITY_SAFE_DEFAULTS;
1598
+ }
1599
+ function createRuntimeOwnedResultEnvelope(type, id) {
1600
+ return Object.assign({ type }, { id });
1601
+ }
1602
+ function isIdentityOrThemeMessage(message) {
1603
+ return message.type.startsWith("identity.") || message.type.startsWith("theme.");
1604
+ }
1605
+ function createIntentPolicyDenial(message) {
1606
+ const id = message.id ?? "";
1607
+ if (message.type === "intent.invoke") {
1608
+ return {
1609
+ type: "intent.invoke.result",
1610
+ id,
1611
+ result: { ok: false, error: "invoke rejected" }
1612
+ };
1613
+ }
1614
+ if (message.type === "intent.available" || message.type === "intent.handlers") {
1615
+ return {
1616
+ type: `${message.type}.result`,
1617
+ id,
1618
+ error: "intent request denied"
1619
+ };
1620
+ }
1621
+ return void 0;
1622
+ }
1623
+ function createCanonicalDomainResult(message) {
1624
+ if (!isRuntimeOwnedIdentityOrThemeRequest(message.type)) return void 0;
1625
+ const id = message.id ?? "";
1626
+ if (message.type === "theme.get") {
1627
+ return Object.assign(
1628
+ createRuntimeOwnedResultEnvelope("theme.get.result", id),
1629
+ { theme: RUNTIME_THEME_FALLBACK }
1630
+ );
1631
+ }
1632
+ return Object.assign(
1633
+ createRuntimeOwnedResultEnvelope(`${message.type}.result`, id),
1634
+ IDENTITY_SAFE_DEFAULTS[message.type]
1635
+ );
1636
+ }
1494
1637
  function createIdentityHandler(context) {
1495
1638
  return function handleIdentityMessage(windowId, msg) {
1496
1639
  const { hooks, serviceRegistry } = context;
@@ -1499,14 +1642,12 @@ function createIdentityHandler(context) {
1499
1642
  identityService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
1500
1643
  return;
1501
1644
  }
1502
- const id = msg.id ?? "";
1503
1645
  const action = msg.type.slice("identity.".length);
1504
1646
  const signer = hooks.auth.getSigner();
1505
- const sendError = (error) => {
1506
- hooks.sendToNapplet(windowId, { type: `${msg.type}.error`, id, error });
1507
- };
1508
- const sendResult = (payload) => {
1509
- hooks.sendToNapplet(windowId, { type: `${msg.type}.result`, id, ...payload });
1647
+ const fallback = createCanonicalDomainResult(msg);
1648
+ if (!fallback) return;
1649
+ const sendResult = (payload = {}) => {
1650
+ hooks.sendToNapplet(windowId, { ...fallback, ...payload });
1510
1651
  };
1511
1652
  switch (action) {
1512
1653
  case "getPublicKey":
@@ -1514,14 +1655,14 @@ function createIdentityHandler(context) {
1514
1655
  sendResult({ pubkey: "" });
1515
1656
  return;
1516
1657
  }
1517
- Promise.resolve(signer.getPublicKey?.()).then((pubkey) => sendResult({ pubkey: pubkey ?? "" })).catch((err) => sendError(err?.message ?? "getPublicKey failed"));
1658
+ Promise.resolve(signer.getPublicKey?.()).then((pubkey) => sendResult({ pubkey: pubkey ?? "" })).catch(() => sendResult({ pubkey: "" }));
1518
1659
  return;
1519
1660
  case "getRelays":
1520
1661
  if (!signer) {
1521
- sendError("no signer configured");
1662
+ sendResult();
1522
1663
  return;
1523
1664
  }
1524
- Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => sendResult({ relays })).catch((err) => sendError(err?.message ?? "getRelays failed"));
1665
+ Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => sendResult({ relays })).catch(() => sendResult());
1525
1666
  return;
1526
1667
  case "getProfile":
1527
1668
  sendResult({ profile: null });
@@ -1545,7 +1686,7 @@ function createIdentityHandler(context) {
1545
1686
  sendResult({ badges: [] });
1546
1687
  return;
1547
1688
  default:
1548
- sendError(`Unknown identity action: ${action}`);
1689
+ return;
1549
1690
  }
1550
1691
  };
1551
1692
  }
@@ -1608,7 +1749,8 @@ function isValidFilterArray(value) {
1608
1749
  function isPlainObject(value) {
1609
1750
  return typeof value === "object" && value !== null && !Array.isArray(value);
1610
1751
  }
1611
- function createIncRuntime(hooks, sessionRegistry2) {
1752
+ var MAX_CHANNELS_PER_WINDOW = 32;
1753
+ function createIncRuntime(hooks, sessionRegistry2, authorizeChannelOpen) {
1612
1754
  const state = {
1613
1755
  subscriptions: /* @__PURE__ */ new Map(),
1614
1756
  channels: /* @__PURE__ */ new Map(),
@@ -1616,12 +1758,15 @@ function createIncRuntime(hooks, sessionRegistry2) {
1616
1758
  };
1617
1759
  return {
1618
1760
  handleMessage(windowId, msg) {
1619
- handleIncMessage(state, hooks, sessionRegistry2, windowId, msg);
1761
+ handleIncMessage(state, hooks, sessionRegistry2, authorizeChannelOpen, windowId, msg);
1620
1762
  },
1621
1763
  destroyWindow(windowId) {
1622
1764
  removeWindowChannels(state, hooks, windowId);
1623
1765
  removeWindowSubscriptions(state, windowId);
1624
1766
  },
1767
+ revokeWindow(windowId) {
1768
+ revokeWindowChannels(state, hooks, windowId);
1769
+ },
1625
1770
  clear() {
1626
1771
  state.subscriptions.clear();
1627
1772
  state.channels.clear();
@@ -1659,18 +1804,18 @@ function peerOf(state, channelId, self) {
1659
1804
  return null;
1660
1805
  }
1661
1806
  function resolveTarget(sessionRegistry2, target) {
1662
- if (sessionRegistry2.getEntryByWindowId(target)) return target;
1663
- const entries = sessionRegistry2.getAllEntries();
1664
- const byPubkey2 = entries.find((entry) => entry.pubkey === target);
1665
- return byPubkey2?.windowId ?? null;
1807
+ return sessionRegistry2.getWindowIdByDTag(target) ?? null;
1808
+ }
1809
+ function hasReachedChannelLimit(state, windowId) {
1810
+ return (state.channelsByWindow.get(windowId)?.size ?? 0) >= MAX_CHANNELS_PER_WINDOW;
1666
1811
  }
1667
- function handleIncMessage(state, hooks, sessionRegistry2, windowId, msg) {
1812
+ function handleIncMessage(state, hooks, sessionRegistry2, authorizeChannelOpen, windowId, msg) {
1668
1813
  const m = msg;
1669
1814
  const dotIdx = msg.type.indexOf(".");
1670
1815
  const action = msg.type.slice(dotIdx + 1);
1671
1816
  switch (action) {
1672
1817
  case "emit":
1673
- handleEmit(state, hooks, windowId, m);
1818
+ handleEmit(state, hooks, sessionRegistry2, windowId, m);
1674
1819
  return;
1675
1820
  case "subscribe":
1676
1821
  handleSubscribe(state, hooks, windowId, m);
@@ -1679,16 +1824,16 @@ function handleIncMessage(state, hooks, sessionRegistry2, windowId, msg) {
1679
1824
  handleUnsubscribe(state, windowId, m);
1680
1825
  return;
1681
1826
  case "channel.open":
1682
- handleChannelOpen(state, hooks, sessionRegistry2, windowId, m);
1827
+ handleChannelOpen(state, hooks, sessionRegistry2, authorizeChannelOpen, windowId, m);
1683
1828
  return;
1684
1829
  case "channel.emit":
1685
- handleChannelEmit(state, hooks, windowId, m);
1830
+ handleChannelEmit(state, hooks, sessionRegistry2, windowId, m);
1686
1831
  return;
1687
1832
  case "channel.broadcast":
1688
- handleChannelBroadcast(state, hooks, windowId, m);
1833
+ handleChannelBroadcast(state, hooks, sessionRegistry2, windowId, m);
1689
1834
  return;
1690
1835
  case "channel.list":
1691
- handleChannelList(state, hooks, windowId, m);
1836
+ handleChannelList(state, hooks, sessionRegistry2, windowId, m);
1692
1837
  return;
1693
1838
  case "channel.close":
1694
1839
  handleChannelClose(state, hooks, windowId, m);
@@ -1697,14 +1842,16 @@ function handleIncMessage(state, hooks, sessionRegistry2, windowId, msg) {
1697
1842
  return;
1698
1843
  }
1699
1844
  }
1700
- function handleEmit(state, hooks, windowId, m) {
1845
+ function handleEmit(state, hooks, sessionRegistry2, windowId, m) {
1701
1846
  const topic = m.topic ?? "";
1847
+ const sender = sessionRegistry2.getEntryByWindowId(windowId)?.dTag;
1848
+ if (!sender) return;
1702
1849
  if (!topic) return;
1703
1850
  const subscribers = state.subscriptions.get(topic);
1704
1851
  if (!subscribers) return;
1705
1852
  for (const subscriberWindowId of subscribers) {
1706
1853
  if (subscriberWindowId !== windowId) {
1707
- hooks.sendToNapplet(subscriberWindowId, { type: "inc.event", topic, payload: m.payload, sender: windowId });
1854
+ hooks.sendToNapplet(subscriberWindowId, { type: "inc.event", topic, payload: m.payload, sender });
1708
1855
  }
1709
1856
  }
1710
1857
  }
@@ -1731,51 +1878,96 @@ function handleUnsubscribe(state, windowId, m) {
1731
1878
  subscriptions.delete(windowId);
1732
1879
  if (subscriptions.size === 0) state.subscriptions.delete(topic);
1733
1880
  }
1734
- function handleChannelOpen(state, hooks, sessionRegistry2, windowId, m) {
1881
+ function handleChannelOpen(state, hooks, sessionRegistry2, authorizeChannelOpen, windowId, m) {
1735
1882
  const id = m.id ?? "";
1736
1883
  const peerWindow = resolveTarget(sessionRegistry2, m.target ?? "");
1737
- if (!peerWindow) {
1884
+ const openerDTag = sessionRegistry2.getEntryByWindowId(windowId)?.dTag;
1885
+ const peerDTag = peerWindow ? sessionRegistry2.getEntryByWindowId(peerWindow)?.dTag : void 0;
1886
+ if (!peerWindow || !openerDTag || !peerDTag) {
1738
1887
  hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, error: "target not found" });
1739
1888
  return;
1740
1889
  }
1890
+ if (!authorizeChannelOpen(peerWindow, m)) {
1891
+ hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, error: "target denied" });
1892
+ return;
1893
+ }
1894
+ if (hasReachedChannelLimit(state, windowId) || hasReachedChannelLimit(state, peerWindow)) {
1895
+ hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, error: "channel limit reached" });
1896
+ return;
1897
+ }
1741
1898
  const channelId = hooks.crypto.randomUUID().replace(/-/g, "").slice(0, 32);
1742
1899
  addChannel(state, channelId, windowId, peerWindow);
1743
- hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, channelId, peer: peerWindow });
1900
+ try {
1901
+ hooks.sendToNapplet(peerWindow, { type: "inc.channel.opened", channelId, peer: openerDTag });
1902
+ } catch {
1903
+ removeChannel(state, channelId);
1904
+ hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, error: "target unavailable" });
1905
+ return;
1906
+ }
1907
+ try {
1908
+ hooks.sendToNapplet(windowId, { type: "inc.channel.open.result", id, channelId, peer: peerDTag });
1909
+ } catch {
1910
+ teardownChannel(state, hooks, channelId);
1911
+ }
1744
1912
  }
1745
- function handleChannelEmit(state, hooks, windowId, m) {
1913
+ function handleChannelEmit(state, hooks, sessionRegistry2, windowId, m) {
1746
1914
  const peer = peerOf(state, m.channelId ?? "", windowId);
1747
- if (peer) {
1748
- hooks.sendToNapplet(peer, { type: "inc.channel.event", channelId: m.channelId ?? "", sender: windowId, payload: m.payload });
1915
+ const sender = sessionRegistry2.getEntryByWindowId(windowId)?.dTag;
1916
+ if (peer && sender) {
1917
+ hooks.sendToNapplet(peer, { type: "inc.channel.event", channelId: m.channelId ?? "", sender, payload: m.payload });
1749
1918
  }
1750
1919
  }
1751
- function handleChannelBroadcast(state, hooks, windowId, m) {
1920
+ function handleChannelBroadcast(state, hooks, sessionRegistry2, windowId, m) {
1752
1921
  const channels = state.channelsByWindow.get(windowId);
1753
- if (!channels) return;
1922
+ const sender = sessionRegistry2.getEntryByWindowId(windowId)?.dTag;
1923
+ if (!channels || !sender) return;
1754
1924
  for (const channelId of channels) {
1755
1925
  const peer = peerOf(state, channelId, windowId);
1756
1926
  if (peer) {
1757
- hooks.sendToNapplet(peer, { type: "inc.channel.event", channelId, sender: windowId, payload: m.payload });
1927
+ hooks.sendToNapplet(peer, { type: "inc.channel.event", channelId, sender, payload: m.payload });
1758
1928
  }
1759
1929
  }
1760
1930
  }
1761
- function handleChannelList(state, hooks, windowId, m) {
1931
+ function handleChannelList(state, hooks, sessionRegistry2, windowId, m) {
1762
1932
  const channels = [];
1763
1933
  const set = state.channelsByWindow.get(windowId);
1764
1934
  if (set) {
1765
1935
  for (const channelId of set) {
1766
1936
  const peer = peerOf(state, channelId, windowId);
1767
- if (peer) channels.push({ id: channelId, peer });
1937
+ const peerDTag = peer ? sessionRegistry2.getEntryByWindowId(peer)?.dTag : void 0;
1938
+ if (peerDTag) channels.push({ id: channelId, peer: peerDTag });
1768
1939
  }
1769
1940
  }
1770
1941
  hooks.sendToNapplet(windowId, { type: "inc.channel.list.result", id: m.id ?? "", channels });
1771
1942
  }
1772
1943
  function handleChannelClose(state, hooks, windowId, m) {
1773
1944
  const channelId = m.channelId ?? "";
1774
- const peer = peerOf(state, channelId, windowId);
1775
- if (!peer) return;
1776
- hooks.sendToNapplet(windowId, { type: "inc.channel.closed", channelId });
1777
- hooks.sendToNapplet(peer, { type: "inc.channel.closed", channelId });
1778
- removeChannel(state, channelId);
1945
+ if (!peerOf(state, channelId, windowId)) return;
1946
+ teardownChannel(state, hooks, channelId);
1947
+ }
1948
+ function teardownChannel(state, hooks, channelId, destroyedWindowId) {
1949
+ const channel = state.channels.get(channelId);
1950
+ if (!channel) return;
1951
+ try {
1952
+ if (destroyedWindowId) {
1953
+ const survivor = peerOf(state, channelId, destroyedWindowId);
1954
+ if (survivor) {
1955
+ try {
1956
+ hooks.sendToNapplet(survivor, { type: "inc.channel.closed", channelId, reason: "peer destroyed" });
1957
+ } catch {
1958
+ }
1959
+ }
1960
+ return;
1961
+ }
1962
+ for (const peer of [channel.peerA, channel.peerB]) {
1963
+ try {
1964
+ hooks.sendToNapplet(peer, { type: "inc.channel.closed", channelId });
1965
+ } catch {
1966
+ }
1967
+ }
1968
+ } finally {
1969
+ removeChannel(state, channelId);
1970
+ }
1779
1971
  }
1780
1972
  function removeWindowSubscriptions(state, windowId) {
1781
1973
  for (const [topic, subscriptions] of state.subscriptions) {
@@ -1787,11 +1979,14 @@ function removeWindowChannels(state, hooks, windowId) {
1787
1979
  const channelIds = state.channelsByWindow.get(windowId);
1788
1980
  if (!channelIds) return;
1789
1981
  for (const channelId of Array.from(channelIds)) {
1790
- const peer = peerOf(state, channelId, windowId);
1791
- if (peer) {
1792
- hooks.sendToNapplet(peer, { type: "inc.channel.closed", channelId });
1793
- }
1794
- removeChannel(state, channelId);
1982
+ teardownChannel(state, hooks, channelId, windowId);
1983
+ }
1984
+ }
1985
+ function revokeWindowChannels(state, hooks, windowId) {
1986
+ const channelIds = state.channelsByWindow.get(windowId);
1987
+ if (!channelIds) return;
1988
+ for (const channelId of Array.from(channelIds)) {
1989
+ teardownChannel(state, hooks, channelId);
1795
1990
  }
1796
1991
  }
1797
1992
  var INSTANCE_MARKER = "@i/";
@@ -1932,9 +2127,6 @@ function handleStorageNap(windowId, msg, sendToNapplet, sessionRegistry2, aclSta
1932
2127
  break;
1933
2128
  }
1934
2129
  }
1935
- var THEME_FALLBACK_DEFAULT = {
1936
- colors: { background: "#0a0a0a", text: "#e0e0e0", primary: "#7aa2f7" }
1937
- };
1938
2130
  function createRuntimeDomainHandlers(context) {
1939
2131
  return {
1940
2132
  storage: (windowId, msg) => handleStorageMessage(context, windowId, msg),
@@ -2050,16 +2242,13 @@ function handleThemeMessage(context, windowId, msg) {
2050
2242
  themeService.handleMessage(windowId, msg, (resp) => hooks.sendToNapplet(windowId, resp));
2051
2243
  return;
2052
2244
  }
2053
- if (msg.type === "theme.get") {
2054
- const m = msg;
2055
- hooks.sendToNapplet(windowId, {
2056
- type: "theme.get.result",
2057
- id: m.id ?? "",
2058
- theme: THEME_FALLBACK_DEFAULT
2059
- });
2060
- }
2245
+ const fallback = createCanonicalDomainResult(msg);
2246
+ if (fallback) hooks.sendToNapplet(windowId, fallback);
2061
2247
  }
2062
2248
  function handleServiceOnlyMessage(context, name, windowId, msg) {
2249
+ if (name === "intent" && msg.type !== "intent.invoke" && msg.type !== "intent.available" && msg.type !== "intent.handlers") {
2250
+ return;
2251
+ }
2063
2252
  const service = context.serviceRegistry[name];
2064
2253
  if (!service) return;
2065
2254
  service.handleMessage(windowId, msg, (resp) => context.hooks.sendToNapplet(windowId, resp));
@@ -2145,7 +2334,20 @@ function buildObservation(envelope, windowId, senderCap, sessionRegistry2, getFo
2145
2334
  const focus = getFocusContext?.(windowId) ?? { focused: true };
2146
2335
  const opClass = senderCap ?? envelope.type;
2147
2336
  const { kind, size } = extractKindSize(envelope);
2148
- return { napplet, opClass, kind, size, initElapsedMs, focused: focus.focused, msSinceFocusGain: focus.msSinceFocusGain, now: now2 };
2337
+ return {
2338
+ napplet,
2339
+ opClass,
2340
+ kind,
2341
+ size,
2342
+ initElapsedMs,
2343
+ // `windowId` comes from the host's source-bound session lookup, never the
2344
+ // napplet message envelope. It scopes startup burst accounting to this
2345
+ // registered realm while rate limits remain dTag-wide.
2346
+ initKey: windowId,
2347
+ focused: focus.focused,
2348
+ msSinceFocusGain: focus.msSinceFocusGain,
2349
+ now: now2
2350
+ };
2149
2351
  }
2150
2352
  function createFirewallGate(config) {
2151
2353
  const { firewallState, sessionRegistry: sessionRegistry2, hooks, fireConsent } = config;
@@ -2156,10 +2358,25 @@ function createFirewallGate(config) {
2156
2358
  const napplet = obs.napplet;
2157
2359
  const opClass = obs.opClass;
2158
2360
  if (decision === "reject" || decision === "prompt") {
2361
+ const intentDenial = createIntentPolicyDenial(envelope);
2362
+ if (intentDenial) {
2363
+ hooks.sendToNapplet(windowId, intentDenial);
2364
+ hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
2365
+ if (decision === "prompt") fireConsent(windowId, napplet);
2366
+ return "drop";
2367
+ }
2368
+ const canonicalResult = createCanonicalDomainResult(envelope);
2369
+ if (canonicalResult) {
2370
+ hooks.sendToNapplet(windowId, canonicalResult);
2371
+ hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
2372
+ if (decision === "prompt") fireConsent(windowId, napplet);
2373
+ return "drop";
2374
+ }
2159
2375
  const id = envelope.id ?? "";
2160
- const isStorageEnvelope = envelope.type.startsWith("storage.");
2161
- const type = isStorageEnvelope ? `${envelope.type}.result` : `${envelope.type}.error`;
2162
- hooks.sendToNapplet(windowId, { type, id, error: `firewall: ${reason}` });
2376
+ const type = denialResponseType(envelope);
2377
+ if (type) {
2378
+ hooks.sendToNapplet(windowId, { type, id, error: `firewall: ${reason}` });
2379
+ }
2163
2380
  hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
2164
2381
  if (decision === "prompt") {
2165
2382
  fireConsent(windowId, napplet);
@@ -2172,21 +2389,45 @@ function createFirewallGate(config) {
2172
2389
  return "dispatch";
2173
2390
  };
2174
2391
  }
2175
- function createMessageHandler(hooks, enforceNap, dispatchNapEnvelope, firewallGate) {
2392
+ function denialResponseType(envelope) {
2393
+ if (envelope.type.startsWith("intent.")) return null;
2394
+ if (envelope.type.startsWith("storage.")) return `${envelope.type}.result`;
2395
+ if (envelope.type === "inc.subscribe" || envelope.type === "inc.channel.open") {
2396
+ return `${envelope.type}.result`;
2397
+ }
2398
+ if (envelope.type.startsWith("inc.")) return null;
2399
+ return `${envelope.type}.error`;
2400
+ }
2401
+ function createMessageHandler(hooks, sessionRegistry2, enforceNap, dispatchNapEnvelope, firewallGate) {
2176
2402
  return (windowId, msg) => {
2177
2403
  if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
2178
2404
  const envelope = msg;
2179
2405
  const dotIdx = envelope.type.indexOf(".");
2180
2406
  if (dotIdx === -1) return;
2407
+ if (!sessionRegistry2.getEntryByWindowId(windowId)) return;
2408
+ const domain = envelope.type.slice(0, dotIdx);
2409
+ if (hooks.isDomainAllowed && !hooks.isDomainAllowed(windowId, domain)) return;
2410
+ if (isIdentityOrThemeMessage(envelope) && !createCanonicalDomainResult(envelope)) return;
2181
2411
  const caps = resolveCapabilitiesNap(envelope);
2182
2412
  if (caps.senderCap) {
2183
2413
  const result2 = enforceNap(windowId, caps.senderCap, envelope);
2184
2414
  if (!result2.allowed) {
2415
+ const intentDenial = createIntentPolicyDenial(envelope);
2416
+ if (intentDenial) {
2417
+ hooks.sendToNapplet(windowId, intentDenial);
2418
+ return;
2419
+ }
2420
+ const canonicalResult = createCanonicalDomainResult(envelope);
2421
+ if (canonicalResult) {
2422
+ hooks.sendToNapplet(windowId, canonicalResult);
2423
+ return;
2424
+ }
2185
2425
  const id = envelope.id ?? "";
2186
- const isStorageEnvelope = envelope.type.startsWith("storage.");
2187
2426
  const error = formatDenialReason(result2.capability);
2188
- const type = isStorageEnvelope ? `${envelope.type}.result` : `${envelope.type}.error`;
2189
- hooks.sendToNapplet(windowId, { type, id, error });
2427
+ const type = denialResponseType(envelope);
2428
+ if (type) {
2429
+ hooks.sendToNapplet(windowId, { type, id, error });
2430
+ }
2190
2431
  return;
2191
2432
  }
2192
2433
  }
@@ -2223,12 +2464,64 @@ function createRuntimeInstance(context) {
2223
2464
  consentHandlerRef
2224
2465
  } = context;
2225
2466
  const undeclaredServiceConsents = /* @__PURE__ */ new Set();
2467
+ const attachedServices = /* @__PURE__ */ new Map();
2468
+ const isDomainAllowed = hooks.isDomainAllowed;
2469
+ const sendToNapplet = hooks.sendToNapplet;
2470
+ const serviceContext = Object.freeze({
2471
+ resolveDTag(windowId) {
2472
+ return sessionRegistry2.getEntryByWindowId(windowId)?.dTag;
2473
+ },
2474
+ listWindowIds() {
2475
+ return Object.freeze(
2476
+ sessionRegistry2.getAllEntries().map((entry) => entry.windowId)
2477
+ );
2478
+ },
2479
+ sendToEligibleNapplet(windowId, message) {
2480
+ const entry = sessionRegistry2.getEntryByWindowId(windowId);
2481
+ if (!entry || typeof message.type !== "string") return false;
2482
+ const dotIndex = message.type.indexOf(".");
2483
+ if (dotIndex <= 0) return false;
2484
+ const domain = message.type.slice(0, dotIndex);
2485
+ try {
2486
+ if (isDomainAllowed && !isDomainAllowed(windowId, domain)) return false;
2487
+ const { recipientCap } = resolveCapabilitiesNap(message);
2488
+ if (!recipientCap) return false;
2489
+ if (!aclState.check("", entry.dTag, entry.aggregateHash, recipientCap)) {
2490
+ return false;
2491
+ }
2492
+ sendToNapplet(windowId, message);
2493
+ return true;
2494
+ } catch {
2495
+ return false;
2496
+ }
2497
+ }
2498
+ });
2499
+ function attachService(name, handler) {
2500
+ attachedServices.set(name, handler);
2501
+ try {
2502
+ handler.onRegistered?.(serviceContext);
2503
+ } catch {
2504
+ }
2505
+ }
2506
+ function detachService(name) {
2507
+ const handler = attachedServices.get(name);
2508
+ if (!handler) return;
2509
+ attachedServices.delete(name);
2510
+ try {
2511
+ handler.onUnregistered?.();
2512
+ } catch {
2513
+ }
2514
+ }
2515
+ for (const [name, handler] of Object.entries(serviceRegistry)) {
2516
+ attachService(name, handler);
2517
+ }
2226
2518
  return {
2227
2519
  handleMessage: context.handleMessage,
2228
2520
  injectEvent(topic, payload) {
2229
2521
  eventBuffer.bufferAndDeliver(createInjectedEvent(hooks, topic, payload), null);
2230
2522
  },
2231
2523
  destroy() {
2524
+ for (const name of attachedServices.keys()) detachService(name);
2232
2525
  manifestCache2.persist();
2233
2526
  aclState.persist();
2234
2527
  firewallState.persist();
@@ -2243,14 +2536,17 @@ function createRuntimeInstance(context) {
2243
2536
  consentHandlerRef.current = handler;
2244
2537
  },
2245
2538
  registerService(name, handler) {
2539
+ detachService(name);
2246
2540
  serviceRegistry[name] = handler;
2247
2541
  registeredServices.set(name, {
2248
2542
  name: handler.descriptor.name,
2249
2543
  version: handler.descriptor.version,
2250
2544
  description: handler.descriptor.description
2251
2545
  });
2546
+ attachService(name, handler);
2252
2547
  },
2253
2548
  unregisterService(name) {
2549
+ detachService(name);
2254
2550
  delete serviceRegistry[name];
2255
2551
  registeredServices.delete(name);
2256
2552
  },
@@ -2283,7 +2579,15 @@ function createRuntime(hooks) {
2283
2579
  const serviceRegistry = { ...hooks.services };
2284
2580
  const registeredServices = createRegisteredServices(serviceRegistry);
2285
2581
  const sessionRegistry2 = createSessionRegistry(hooks.onPendingUpdate);
2286
- const aclState = createAclState(hooks.aclPersistence);
2582
+ let incRuntime = null;
2583
+ const aclState = createAclState(hooks.aclPersistence, "permissive", (mutation) => {
2584
+ if (mutation.type === "revoke" && mutation.capability !== "relay:read") return;
2585
+ for (const entry of sessionRegistry2.getAllEntries()) {
2586
+ if (entry.dTag === mutation.dTag && entry.aggregateHash === mutation.aggregateHash) {
2587
+ incRuntime?.revokeWindow(entry.windowId);
2588
+ }
2589
+ }
2590
+ });
2287
2591
  const firewallState = createFirewallState(hooks.firewallPersistence);
2288
2592
  const manifestCache2 = createManifestCache(hooks.manifestPersistence);
2289
2593
  const replayDetector = createReplayDetector(
@@ -2320,6 +2624,11 @@ function createRuntime(hooks) {
2320
2624
  },
2321
2625
  onAclCheck: hooks.onAclCheck
2322
2626
  });
2627
+ incRuntime = createIncRuntime(
2628
+ hooks,
2629
+ sessionRegistry2,
2630
+ (targetWindowId, message) => enforceNap(targetWindowId, "relay:read", message).allowed
2631
+ );
2323
2632
  const firewallGate = createFirewallGate({ firewallState, sessionRegistry: sessionRegistry2, hooks, fireConsent });
2324
2633
  const eventBuffer = createEventBuffer(
2325
2634
  hooks.sendToNapplet,
@@ -2331,7 +2640,6 @@ function createRuntime(hooks) {
2331
2640
  aclState.load();
2332
2641
  firewallState.load();
2333
2642
  manifestCache2.load();
2334
- const incRuntime = createIncRuntime(hooks, sessionRegistry2);
2335
2643
  const domainHandlers = createRuntimeDomainHandlers({ hooks, serviceRegistry, sessionRegistry: sessionRegistry2, aclState });
2336
2644
  const dispatchNapEnvelope = createNapEnvelopeDispatcher({
2337
2645
  relay: createRelayHandler({ hooks, serviceRegistry, subscriptions, eventBuffer, replayDetector }),
@@ -2340,7 +2648,7 @@ function createRuntime(hooks) {
2340
2648
  inc: incRuntime.handleMessage,
2341
2649
  ...domainHandlers
2342
2650
  });
2343
- const handleMessage = createMessageHandler(hooks, enforceNap, dispatchNapEnvelope, firewallGate);
2651
+ const handleMessage = createMessageHandler(hooks, sessionRegistry2, enforceNap, dispatchNapEnvelope, firewallGate);
2344
2652
  return createRuntimeInstance({
2345
2653
  hooks,
2346
2654
  serviceRegistry,
@@ -2392,7 +2700,7 @@ function createRelayPoolAdapter(shellHooks, originRegistry2) {
2392
2700
  const pool = shellHooks.relayPool.getRelayPool();
2393
2701
  if (!pool) return;
2394
2702
  const relayUrls = shellHooks.relayPool.selectRelayTier([]);
2395
- pool.publish(relayUrls, event);
2703
+ return pool.publish(relayUrls, event);
2396
2704
  },
2397
2705
  selectRelayTier(filters) {
2398
2706
  return shellHooks.relayPool.selectRelayTier(filters);
@@ -2747,6 +3055,19 @@ var originRegistry = {
2747
3055
  if (!entry?.dTag || !entry?.aggregateHash) return void 0;
2748
3056
  return { dTag: entry.dTag, aggregateHash: entry.aggregateHash };
2749
3057
  },
3058
+ /** Store the immutable environment captured for this exact frame registration. */
3059
+ setEnvironment(win, environment) {
3060
+ const entry = registry.get(win);
3061
+ if (!entry) return;
3062
+ entry.environment = Object.freeze({
3063
+ capabilities: Object.freeze({ domains: Object.freeze([...environment.capabilities.domains]) }),
3064
+ services: Object.freeze([...environment.services])
3065
+ });
3066
+ },
3067
+ /** Return the immutable environment captured before this frame executes. */
3068
+ getEnvironment(win) {
3069
+ return registry.get(win)?.environment;
3070
+ },
2750
3071
  /**
2751
3072
  * Look up the monotonically increasing registration id for a Window.
2752
3073
  *
@@ -3305,111 +3626,151 @@ var audioManager = {
3305
3626
  version = 0;
3306
3627
  }
3307
3628
  };
3308
- var NAP_DOMAINS = [
3629
+ var RUNTIME_NATIVE_DOMAINS = [
3630
+ "relay",
3309
3631
  "identity",
3310
3632
  "storage",
3311
3633
  "inc",
3312
3634
  "theme",
3313
3635
  "keys",
3314
3636
  "media",
3315
- "notify",
3637
+ "notify"
3638
+ ];
3639
+ var SERVICE_ONLY_DOMAINS = [
3640
+ "outbox",
3316
3641
  "config",
3317
3642
  "resource",
3318
- "cvm"
3319
- ];
3320
- var NAP_INC_PROTOCOLS = [
3321
- "inc:NAP-01",
3322
- "inc:NAP-02",
3323
- "inc:NAP-03",
3324
- "inc:NAP-04",
3325
- "inc:NAP-05",
3326
- "inc:NAP-06"
3643
+ "cvm",
3644
+ "upload",
3645
+ "intent",
3646
+ "link",
3647
+ "common",
3648
+ "lists",
3649
+ "serial",
3650
+ "ble",
3651
+ "webrtc",
3652
+ "dm",
3653
+ "count"
3327
3654
  ];
3328
3655
  function buildShellCapabilities(hooks) {
3329
- const domains = hooks.relayPool ? ["relay", "outbox", ...NAP_DOMAINS] : [...NAP_DOMAINS];
3330
- if (hooks.upload) domains.push("upload");
3331
- if (hooks.intent?.isAvailable()) domains.push("intent");
3332
- if (hooks.services?.count) domains.push("count");
3333
- if (hooks.link?.isAvailable()) domains.push("link");
3334
- if (hooks.common?.isAvailable()) domains.push("common");
3335
- if (hooks.lists?.isAvailable()) domains.push("lists");
3336
- if (hooks.serial?.isAvailable()) domains.push("serial");
3337
- if (hooks.ble?.isAvailable()) domains.push("ble");
3338
- if (hooks.webrtc?.isAvailable()) domains.push("webrtc");
3339
- if (hooks.dm) domains.push("dm");
3340
- const sandbox = [];
3341
- const protocols = {};
3342
- for (const entry of NAP_INC_PROTOCOLS) {
3343
- const [domain, protocol] = entry.split(":");
3344
- (protocols[domain] ??= []).push(protocol);
3345
- }
3346
- const naps = hooks.relayPool ? ["relay", "outbox", ...NAP_DOMAINS, ...NAP_INC_PROTOCOLS] : [...NAP_DOMAINS, ...NAP_INC_PROTOCOLS];
3347
- if (hooks.upload) naps.push("upload");
3348
- if (hooks.intent?.isAvailable()) naps.push("intent");
3349
- if (hooks.services?.count) naps.push("count");
3350
- if (hooks.link?.isAvailable()) naps.push("link");
3351
- if (hooks.common?.isAvailable()) naps.push("common");
3352
- if (hooks.lists?.isAvailable()) naps.push("lists");
3353
- if (hooks.serial?.isAvailable()) naps.push("serial");
3354
- if (hooks.ble?.isAvailable()) naps.push("ble");
3355
- if (hooks.webrtc?.isAvailable()) naps.push("webrtc");
3356
- if (hooks.dm) naps.push("dm");
3357
- return applyCapabilityOverrides(
3358
- { domains, protocols, naps, sandbox },
3359
- hooks.capabilities?.disabledDomains ?? []
3360
- );
3656
+ const disabled = new Set(hooks.capabilities?.disabledDomains ?? []);
3657
+ const domains = RUNTIME_NATIVE_DOMAINS.filter((domain) => !disabled.has(domain));
3658
+ for (const domain of SERVICE_ONLY_DOMAINS) {
3659
+ if (!disabled.has(domain) && isServiceDomainLive(hooks, domain)) domains.push(domain);
3660
+ }
3661
+ return Object.freeze({ domains: freezeList(domains) });
3361
3662
  }
3362
- function applyCapabilityOverrides(capabilities, disabledDomains) {
3363
- if (disabledDomains.length === 0) return capabilities;
3364
- const disabled = new Set(disabledDomains);
3365
- const protocols = {};
3366
- for (const [domain, supportedProtocols] of Object.entries(capabilities.protocols)) {
3367
- if (!disabled.has(domain)) protocols[domain] = supportedProtocols;
3663
+ function resolveShellEnvironment(hooks, identity) {
3664
+ const capabilities = buildShellCapabilities(hooks);
3665
+ const disabled = new Set(hooks.capabilities?.disabledDomains ?? []);
3666
+ const services = getLiveServices(hooks, capabilities.domains, disabled);
3667
+ const available = Object.freeze({
3668
+ domains: freezeList(capabilities.domains),
3669
+ services: freezeList(services)
3670
+ });
3671
+ const granted = hooks.capabilities?.resolveEnvironment?.(identity, available) ?? available;
3672
+ return Object.freeze({
3673
+ capabilities: Object.freeze({
3674
+ domains: freezeList(intersectInOrder(available.domains, granted.domains))
3675
+ }),
3676
+ services: freezeList(intersectInOrder(available.services, granted.services))
3677
+ });
3678
+ }
3679
+ function isServiceDomainLive(hooks, domain) {
3680
+ if (!hooks.services?.[domain]) return false;
3681
+ switch (domain) {
3682
+ case "outbox":
3683
+ return Boolean(hooks.relayPool);
3684
+ case "upload":
3685
+ return hooks.upload?.getUploader() !== null && hooks.upload?.getUploader() !== void 0;
3686
+ case "intent":
3687
+ return hooks.intent?.isAvailable() === true;
3688
+ case "link":
3689
+ return hooks.link?.isAvailable() === true;
3690
+ case "common":
3691
+ return hooks.common?.isAvailable() === true;
3692
+ case "lists":
3693
+ return hooks.lists?.isAvailable() === true;
3694
+ case "serial":
3695
+ return hooks.serial?.isAvailable() === true;
3696
+ case "ble":
3697
+ return hooks.ble?.isAvailable() === true;
3698
+ case "webrtc":
3699
+ return hooks.webrtc?.isAvailable() === true;
3700
+ case "dm":
3701
+ return hooks.dm !== void 0;
3702
+ default:
3703
+ return true;
3368
3704
  }
3705
+ }
3706
+ function getLiveServices(hooks, domains, disabled) {
3707
+ const liveDomains = new Set(domains);
3708
+ return Object.keys(hooks.services ?? {}).filter((name) => {
3709
+ if (disabled.has(name)) return false;
3710
+ return !SERVICE_ONLY_DOMAINS.includes(name) || liveDomains.has(name);
3711
+ });
3712
+ }
3713
+ function intersectInOrder(available, requested) {
3714
+ const requestedSet = new Set(requested);
3715
+ return available.filter((entry) => requestedSet.has(entry));
3716
+ }
3717
+ function freezeList(values) {
3718
+ return Object.freeze([...values]);
3719
+ }
3720
+ function createShellReadyState() {
3721
+ const initSent = /* @__PURE__ */ new WeakMap();
3722
+ const sessionRegistration = /* @__PURE__ */ new Map();
3723
+ const environments = /* @__PURE__ */ new Map();
3369
3724
  return {
3370
- domains: capabilities.domains.filter((entry) => !disabled.has(entry)),
3371
- protocols,
3372
- naps: capabilities.naps.filter((entry) => !disabled.has(entry.split(":")[0] ?? entry)),
3373
- sandbox: capabilities.sandbox
3725
+ initSent,
3726
+ sessionRegistration,
3727
+ environments,
3728
+ isDomainAllowed(windowId, domain) {
3729
+ return environments.get(windowId)?.capabilities.domains.includes(domain) ?? false;
3730
+ },
3731
+ clear() {
3732
+ sessionRegistration.clear();
3733
+ environments.clear();
3734
+ }
3374
3735
  };
3375
3736
  }
3376
- var initSent = /* @__PURE__ */ new WeakMap();
3377
3737
  function handleShellReady({
3378
3738
  hooks,
3379
3739
  origin,
3380
3740
  runtime,
3741
+ state,
3381
3742
  sourceRegistrationId,
3382
3743
  sourceWindow,
3383
3744
  windowId
3384
3745
  }) {
3385
- registerNip5dSessionIfNeeded({
3386
- hooks,
3387
- origin,
3388
- runtime,
3389
- sourceRegistrationId,
3390
- sourceWindow,
3391
- windowId
3392
- });
3393
- if (initSent.get(sourceWindow) === sourceRegistrationId) {
3746
+ if (state.initSent.get(sourceWindow) === sourceRegistrationId) {
3394
3747
  return;
3395
3748
  }
3396
- const capabilities = buildShellCapabilities(hooks);
3397
- postShellInit(sourceWindow, capabilities, Object.keys(hooks.services ?? {}));
3398
- initSent.set(sourceWindow, sourceRegistrationId);
3749
+ const identity = resolveNip5dIdentity(hooks, windowId, sourceWindow);
3750
+ if (!identity) return;
3751
+ registerNip5dSessionIfNeeded({ origin, runtime, state, sourceRegistrationId, windowId, identity });
3752
+ const environment = originRegistry.getEnvironment(sourceWindow) ?? resolveShellEnvironment(hooks, identity);
3753
+ state.environments.set(windowId, environment);
3754
+ postShellInit(sourceWindow, environment);
3755
+ state.initSent.set(sourceWindow, sourceRegistrationId);
3399
3756
  }
3400
3757
  function registerNip5dSessionIfNeeded({
3401
- hooks,
3402
3758
  origin,
3403
3759
  runtime,
3404
- windowId
3760
+ state,
3761
+ sourceRegistrationId,
3762
+ windowId,
3763
+ identity
3405
3764
  }) {
3406
- if (runtime.sessionRegistry.getEntryByWindowId(windowId)) {
3765
+ const existing = runtime.sessionRegistry.getEntryByWindowId(windowId);
3766
+ const previousRegistration = state.sessionRegistration.get(windowId);
3767
+ if (existing && previousRegistration === sourceRegistrationId) {
3407
3768
  return;
3408
3769
  }
3409
- const identity = resolveNip5dIdentity(hooks, windowId);
3410
- if (!identity) {
3411
- return;
3770
+ if (existing && previousRegistration !== void 0) {
3771
+ runtime.sessionRegistry.unregister(windowId);
3412
3772
  }
3773
+ if (existing && previousRegistration === void 0) return;
3413
3774
  const entry = {
3414
3775
  pubkey: "",
3415
3776
  windowId,
@@ -3422,33 +3783,30 @@ function registerNip5dSessionIfNeeded({
3422
3783
  provenance: "nip-5d"
3423
3784
  };
3424
3785
  runtime.sessionRegistry.register(windowId, entry);
3786
+ state.sessionRegistration.set(windowId, sourceRegistrationId);
3425
3787
  }
3426
- function resolveNip5dIdentity(hooks, windowId) {
3788
+ function resolveNip5dIdentity(hooks, windowId, sourceWindow) {
3427
3789
  const hookIdentity = hooks.onNip5dIframeCreate?.(windowId);
3428
3790
  if (hookIdentity !== null && hookIdentity !== void 0) {
3429
- return {
3791
+ return Object.freeze({
3430
3792
  dTag: hookIdentity.dTag,
3431
3793
  aggregateHash: hookIdentity.aggregateHash
3432
- };
3433
- }
3434
- const win = originRegistry.getIframeWindow(windowId);
3435
- if (!win) {
3436
- return null;
3794
+ });
3437
3795
  }
3438
- const originIdentity = originRegistry.getIdentity(win);
3796
+ const originIdentity = originRegistry.getIdentity(sourceWindow);
3439
3797
  if (!originIdentity) {
3440
3798
  return null;
3441
3799
  }
3442
- return {
3800
+ return Object.freeze({
3443
3801
  dTag: originIdentity.dTag,
3444
3802
  aggregateHash: originIdentity.aggregateHash
3445
- };
3803
+ });
3446
3804
  }
3447
- function postShellInit(win, capabilities, services) {
3805
+ function postShellInit(win, environment) {
3448
3806
  const initMsg = {
3449
3807
  type: "shell.init",
3450
- capabilities,
3451
- services
3808
+ capabilities: environment.capabilities,
3809
+ services: environment.services
3452
3810
  };
3453
3811
  win.postMessage(initMsg, "*");
3454
3812
  }
@@ -3462,6 +3820,7 @@ function reportUnrouted(hooks, event, reason) {
3462
3820
  }
3463
3821
  }
3464
3822
  function createShellBridge(hooks) {
3823
+ const shellReadyState = createShellReadyState();
3465
3824
  const runtimeHooks = adaptHooks(hooks, {
3466
3825
  originRegistry,
3467
3826
  manifestCache,
@@ -3469,11 +3828,16 @@ function createShellBridge(hooks) {
3469
3828
  audioManager,
3470
3829
  nappKeyRegistry
3471
3830
  });
3472
- const runtime = createRuntime(runtimeHooks);
3473
- function broadcastToNapplets(envelope) {
3474
- const windowIds = originRegistry.getAllWindowIds();
3475
- for (const windowId of windowIds) {
3476
- const win = originRegistry.getIframeWindow(windowId);
3831
+ const runtime = createRuntime({
3832
+ ...runtimeHooks,
3833
+ isDomainAllowed: shellReadyState.isDomainAllowed
3834
+ });
3835
+ function publishToEligibleNapplets(envelope, domain, capability) {
3836
+ for (const entry of runtime.sessionRegistry.getAllEntries()) {
3837
+ const environment = shellReadyState.environments.get(entry.windowId);
3838
+ if (!environment?.capabilities.domains.includes(domain)) continue;
3839
+ if (!runtime.aclState.check(entry.pubkey, entry.dTag, entry.aggregateHash, capability)) continue;
3840
+ const win = originRegistry.getIframeWindow(entry.windowId);
3477
3841
  if (!win) continue;
3478
3842
  win.postMessage(envelope, "*");
3479
3843
  }
@@ -3497,6 +3861,7 @@ function createShellBridge(hooks) {
3497
3861
  hooks,
3498
3862
  origin: event.origin,
3499
3863
  runtime,
3864
+ state: shellReadyState,
3500
3865
  sourceRegistrationId: originRegistry.getRegistrationId(sourceWindow) ?? 0,
3501
3866
  sourceWindow,
3502
3867
  windowId
@@ -3510,17 +3875,18 @@ function createShellBridge(hooks) {
3510
3875
  },
3511
3876
  destroy() {
3512
3877
  runtime.destroy();
3878
+ shellReadyState.clear();
3513
3879
  },
3514
3880
  registerConsentHandler(handler) {
3515
3881
  runtime.registerConsentHandler(handler);
3516
3882
  },
3517
3883
  publishTheme(theme) {
3518
3884
  const envelope = { type: "theme.changed", theme };
3519
- broadcastToNapplets(envelope);
3885
+ publishToEligibleNapplets(envelope, "theme", "theme:read");
3520
3886
  },
3521
3887
  publishIdentityChanged(pubkey) {
3522
3888
  const envelope = { type: "identity.changed", pubkey };
3523
- broadcastToNapplets(envelope);
3889
+ publishToEligibleNapplets(envelope, "identity", "identity:read");
3524
3890
  },
3525
3891
  get runtime() {
3526
3892
  return runtime;
@@ -3582,7 +3948,7 @@ function nappletNamespacePrelude(domains) {
3582
3948
  function request(message, resultType, map, options = {}) {
3583
3949
  const requestId = message.id ?? id();
3584
3950
  const outbound = { ...message, id: requestId };
3585
- return new Promise((resolve, reject) => {
3951
+ return new Promise((resolve, reject2) => {
3586
3952
  const off = listen((event) => {
3587
3953
  if (!isParentMessage(event)) return;
3588
3954
  const incoming = event.data;
@@ -3594,18 +3960,18 @@ function nappletNamespacePrelude(domains) {
3594
3960
  clearTimeout(timer);
3595
3961
  const error = typeof msg.error === "string" ? msg.error : "";
3596
3962
  if (error && options.rejectOnError !== false) {
3597
- reject(new Error(error));
3963
+ reject2(new Error(error));
3598
3964
  return;
3599
3965
  }
3600
3966
  try {
3601
3967
  resolve(map(msg));
3602
3968
  } catch (err) {
3603
- reject(err);
3969
+ reject2(err);
3604
3970
  }
3605
3971
  });
3606
3972
  const timer = setTimeout(() => {
3607
3973
  off();
3608
- reject(new Error(`${message.type} timed out`));
3974
+ reject2(new Error(`${message.type} timed out`));
3609
3975
  }, requestTimeoutMs);
3610
3976
  post(outbound);
3611
3977
  });
@@ -3613,12 +3979,47 @@ function nappletNamespacePrelude(domains) {
3613
3979
  function fire(message) {
3614
3980
  post(message);
3615
3981
  }
3616
- function resultOrError(msg) {
3617
- return msg;
3618
- }
3619
- function withoutEnvelope(msg) {
3620
- const { type: _type, id: _id, ...result2 } = msg;
3621
- return result2;
3982
+ function normalizeConventionUri(topic, hasExplicitPayload) {
3983
+ if (typeof topic !== "string") throw new TypeError("INC topic must be text");
3984
+ if (!topic.startsWith("napplet:")) return { topic };
3985
+ if (topic.includes("#")) throw new TypeError("Convention topics cannot contain fragments");
3986
+ const queryStart = topic.indexOf("?");
3987
+ if (queryStart === -1) {
3988
+ return { topic };
3989
+ }
3990
+ if (hasExplicitPayload) {
3991
+ throw new TypeError("Convention queries cannot be combined with an explicit payload");
3992
+ }
3993
+ const values = {};
3994
+ const names = /* @__PURE__ */ new Set();
3995
+ const query = topic.slice(queryStart + 1);
3996
+ if (query) {
3997
+ for (const pair of query.split("&")) {
3998
+ const separator = pair.indexOf("=");
3999
+ if (separator < 0) {
4000
+ throw new TypeError("Convention query parameters must use name=value form");
4001
+ }
4002
+ let name;
4003
+ let value;
4004
+ try {
4005
+ name = decodeURIComponent(pair.slice(0, separator));
4006
+ value = decodeURIComponent(pair.slice(separator + 1));
4007
+ } catch {
4008
+ throw new TypeError("Convention query contains malformed percent encoding");
4009
+ }
4010
+ if (names.has(name)) throw new TypeError("Convention query names must be unique");
4011
+ names.add(name);
4012
+ values[name] = value;
4013
+ }
4014
+ }
4015
+ return { topic: topic.slice(0, queryStart), payload: values };
4016
+ }
4017
+ function resultOrError(msg) {
4018
+ return msg;
4019
+ }
4020
+ function withoutEnvelope(msg) {
4021
+ const { type: _type, id: _id, ...result2 } = msg;
4022
+ return result2;
3622
4023
  }
3623
4024
  function fieldOrThrow(msg, field, fallback) {
3624
4025
  if (Object.prototype.hasOwnProperty.call(msg, field)) return msg[field];
@@ -3652,11 +4053,13 @@ function nappletNamespacePrelude(domains) {
3652
4053
  if (typeof incoming !== "object" || incoming === null) return;
3653
4054
  const message = incoming;
3654
4055
  if (message.type !== "shell.init") return;
3655
- const services = Object.freeze(
3656
- Array.isArray(message.services) ? message.services.filter((service) => typeof service === "string") : []
3657
- );
4056
+ const rawCapabilities = message.capabilities;
4057
+ const rawDomains = typeof rawCapabilities === "object" && rawCapabilities !== null ? rawCapabilities.domains : void 0;
4058
+ if (!Array.isArray(rawDomains)) return;
4059
+ const domains2 = freezeStrings(rawDomains);
4060
+ const services = freezeStrings(Array.isArray(message.services) ? message.services : []);
3658
4061
  environment = Object.freeze({
3659
- capabilities: message.capabilities ?? {},
4062
+ capabilities: Object.freeze({ domains: domains2 }),
3660
4063
  services
3661
4064
  });
3662
4065
  off();
@@ -3664,20 +4067,18 @@ function nappletNamespacePrelude(domains) {
3664
4067
  for (const handler of readyHandlers) handler(environment);
3665
4068
  readyHandlers.clear();
3666
4069
  });
3667
- function supports(domain, protocol) {
3668
- if (!environment || typeof environment.capabilities !== "object" || environment.capabilities === null) {
3669
- return false;
3670
- }
3671
- const capabilities = environment.capabilities;
3672
- if (protocol !== void 0) {
3673
- const protocols = capabilities.protocols;
3674
- if (typeof protocols === "object" && protocols !== null) {
3675
- const supported = protocols[domain];
3676
- if (Array.isArray(supported) && supported.includes(protocol)) return true;
3677
- }
3678
- return Array.isArray(capabilities.naps) && capabilities.naps.includes(`${domain}:${protocol}`);
4070
+ function freezeStrings(values) {
4071
+ const seen = /* @__PURE__ */ new Set();
4072
+ const strings = [];
4073
+ for (const value of values) {
4074
+ if (typeof value !== "string" || value.length === 0 || seen.has(value)) continue;
4075
+ seen.add(value);
4076
+ strings.push(value);
3679
4077
  }
3680
- return Array.isArray(capabilities.domains) && capabilities.domains.includes(domain) || Array.isArray(capabilities.naps) && capabilities.naps.includes(domain);
4078
+ return Object.freeze(strings);
4079
+ }
4080
+ function supports(domain) {
4081
+ return typeof domain === "string" && environment?.capabilities.domains.includes(domain) === true;
3681
4082
  }
3682
4083
  const shell2 = {
3683
4084
  ready: () => ready,
@@ -3761,38 +4162,205 @@ function nappletNamespacePrelude(domains) {
3761
4162
  };
3762
4163
  }
3763
4164
  function makeInc() {
3764
- return {
3765
- emit(topic, _extraTags, content) {
3766
- let payload = content;
3767
- if (typeof content === "string" && content.length > 0) {
3768
- try {
3769
- payload = JSON.parse(content);
3770
- } catch {
3771
- payload = content;
3772
- }
4165
+ const topicStates = /* @__PURE__ */ new Map();
4166
+ const maxRetainedEvents = 32;
4167
+ const channelStates = /* @__PURE__ */ new Map();
4168
+ const openedHandlers = /* @__PURE__ */ new Set();
4169
+ const pendingOpened = [];
4170
+ const unopenedEvents = /* @__PURE__ */ new Map();
4171
+ const unopenedClosed = /* @__PURE__ */ new Map();
4172
+ function stableSubscriptionTopic(topic) {
4173
+ if (typeof topic !== "string") throw new TypeError("INC topic must be text");
4174
+ if (topic.includes("?") || topic.includes("#")) {
4175
+ throw new TypeError("INC subscriptions require a queryless topic identity");
4176
+ }
4177
+ return topic;
4178
+ }
4179
+ function closeChannelState(state, closed, notifyShell) {
4180
+ if (state.closed) return;
4181
+ state.closed = closed;
4182
+ if (notifyShell) fire({ type: "inc.channel.close", channelId: state.channelId });
4183
+ for (const handler of state.closedHandlers) handler(closed);
4184
+ }
4185
+ function makeChannelHandle(state) {
4186
+ return {
4187
+ id: state.channelId,
4188
+ peer: state.peer,
4189
+ emit(payload) {
4190
+ if (state.closed) return;
4191
+ fire({ type: "inc.channel.emit", channelId: state.channelId, ...payload === void 0 ? {} : { payload } });
4192
+ },
4193
+ on(handler) {
4194
+ if (typeof handler !== "function") throw new TypeError("INC channel event handler must be a function");
4195
+ if (state.closed) return subscriptionHandle(() => void 0);
4196
+ state.eventHandlers.add(handler);
4197
+ const pending = state.pendingEvents.splice(0);
4198
+ for (const event of pending) handler(event);
4199
+ return subscriptionHandle(() => state.eventHandlers.delete(handler));
4200
+ },
4201
+ onClosed(handler) {
4202
+ if (typeof handler !== "function") throw new TypeError("INC channel close handler must be a function");
4203
+ state.closedHandlers.add(handler);
4204
+ if (state.closed) handler(state.closed);
4205
+ return subscriptionHandle(() => state.closedHandlers.delete(handler));
4206
+ },
4207
+ close() {
4208
+ closeChannelState(state, { channelId: state.channelId }, true);
4209
+ }
4210
+ };
4211
+ }
4212
+ function materializeChannel(channelId, peer) {
4213
+ const existing = channelStates.get(channelId);
4214
+ if (existing) return existing;
4215
+ const state = {
4216
+ channelId,
4217
+ peer,
4218
+ eventHandlers: /* @__PURE__ */ new Set(),
4219
+ closedHandlers: /* @__PURE__ */ new Set(),
4220
+ pendingEvents: unopenedEvents.get(channelId) ?? []
4221
+ };
4222
+ unopenedEvents.delete(channelId);
4223
+ channelStates.set(channelId, state);
4224
+ const closed = unopenedClosed.get(channelId);
4225
+ unopenedClosed.delete(channelId);
4226
+ if (closed) closeChannelState(state, closed, false);
4227
+ return state;
4228
+ }
4229
+ function deliverOpened(state) {
4230
+ const handle = makeChannelHandle(state);
4231
+ if (openedHandlers.size > 0) {
4232
+ for (const handler of openedHandlers) handler(handle);
4233
+ return;
4234
+ }
4235
+ pendingOpened.push(state);
4236
+ }
4237
+ function receiveChannelEvent(channelId, sender, payloadPresent, payload) {
4238
+ const event = payloadPresent ? { channelId, sender, payload } : { channelId, sender };
4239
+ const state = channelStates.get(channelId);
4240
+ if (!state) {
4241
+ const pending = unopenedEvents.get(channelId) ?? [];
4242
+ if (pending.length >= maxRetainedEvents) {
4243
+ unopenedEvents.delete(channelId);
4244
+ unopenedClosed.set(channelId, { channelId, reason: "buffer overflow" });
4245
+ fire({ type: "inc.channel.close", channelId });
4246
+ return;
3773
4247
  }
3774
- fire({ type: "inc.emit", topic, ...payload === void 0 || payload === "" ? {} : { payload } });
4248
+ pending.push(event);
4249
+ unopenedEvents.set(channelId, pending);
4250
+ return;
4251
+ }
4252
+ if (state.closed) return;
4253
+ if (state.eventHandlers.size > 0) {
4254
+ for (const handler of state.eventHandlers) handler(event);
4255
+ return;
4256
+ }
4257
+ if (state.pendingEvents.length >= maxRetainedEvents) {
4258
+ closeChannelState(state, { channelId, reason: "buffer overflow" }, true);
4259
+ return;
4260
+ }
4261
+ state.pendingEvents.push(event);
4262
+ }
4263
+ listen((event) => {
4264
+ if (!isParentMessage(event)) return;
4265
+ const msg = event.data;
4266
+ if (typeof msg !== "object" || msg === null || typeof msg.type !== "string") return;
4267
+ if (msg.type === "inc.channel.opened" && typeof msg.channelId === "string" && typeof msg.peer === "string") {
4268
+ deliverOpened(materializeChannel(msg.channelId, msg.peer));
4269
+ return;
4270
+ }
4271
+ if (msg.type === "inc.channel.event" && typeof msg.channelId === "string" && typeof msg.sender === "string") {
4272
+ receiveChannelEvent(msg.channelId, msg.sender, Object.prototype.hasOwnProperty.call(msg, "payload"), msg.payload);
4273
+ return;
4274
+ }
4275
+ if (msg.type === "inc.channel.closed" && typeof msg.channelId === "string") {
4276
+ const closed = typeof msg.reason === "string" ? { channelId: msg.channelId, reason: msg.reason } : { channelId: msg.channelId };
4277
+ const state = channelStates.get(msg.channelId);
4278
+ if (state) closeChannelState(state, closed, false);
4279
+ else unopenedClosed.set(msg.channelId, closed);
4280
+ }
4281
+ });
4282
+ return {
4283
+ emit(topic, payload) {
4284
+ const transposed = normalizeConventionUri(topic, arguments.length > 1);
4285
+ fire({
4286
+ type: "inc.emit",
4287
+ topic: transposed.topic,
4288
+ ...transposed.payload === void 0 && payload === void 0 ? {} : { payload: transposed.payload ?? payload }
4289
+ });
3775
4290
  },
3776
4291
  on(topic, callback) {
3777
- const off = listen((event) => {
3778
- if (!isParentMessage(event)) return;
3779
- const msg = event.data;
3780
- if (typeof msg !== "object" || msg === null || msg.type !== "inc.event" || msg.topic !== topic) return;
3781
- callback(msg.payload ?? {}, {
3782
- id: "",
3783
- pubkey: typeof msg.sender === "string" ? msg.sender : "",
3784
- created_at: Math.floor(Date.now() / 1e3),
3785
- kind: 0,
3786
- tags: [["t", topic]],
3787
- content: typeof msg.payload === "string" ? msg.payload : JSON.stringify(msg.payload ?? {}),
3788
- sig: ""
4292
+ const stableTopic = stableSubscriptionTopic(topic);
4293
+ if (typeof callback !== "function") throw new TypeError("INC subscription handler must be a function");
4294
+ let state = topicStates.get(stableTopic);
4295
+ if (!state) {
4296
+ const handlers = /* @__PURE__ */ new Set();
4297
+ const subscriptionId = id();
4298
+ const offEvent = listen((event) => {
4299
+ if (!isParentMessage(event)) return;
4300
+ const msg = event.data;
4301
+ if (typeof msg !== "object" || msg === null || msg.type !== "inc.event" || msg.topic !== stableTopic || typeof msg.sender !== "string") return;
4302
+ const delivered = Object.prototype.hasOwnProperty.call(msg, "payload") ? { topic: stableTopic, sender: msg.sender, payload: msg.payload } : { topic: stableTopic, sender: msg.sender };
4303
+ for (const handler of handlers) handler(delivered);
3789
4304
  });
3790
- });
3791
- fire({ type: "inc.subscribe", id: id(), topic });
4305
+ const offResult = listen((event) => {
4306
+ if (!isParentMessage(event)) return;
4307
+ const msg = event.data;
4308
+ if (typeof msg !== "object" || msg === null || msg.type !== "inc.subscribe.result" || msg.id !== subscriptionId) return;
4309
+ offResult();
4310
+ if (typeof msg.error === "string" && msg.error) {
4311
+ offEvent();
4312
+ topicStates.delete(stableTopic);
4313
+ }
4314
+ });
4315
+ state = { handlers, offEvent, offResult };
4316
+ topicStates.set(stableTopic, state);
4317
+ fire({ type: "inc.subscribe", id: subscriptionId, topic: stableTopic });
4318
+ }
4319
+ state.handlers.add(callback);
3792
4320
  return subscriptionHandle(() => {
3793
- fire({ type: "inc.unsubscribe", topic });
3794
- off();
4321
+ const current = topicStates.get(stableTopic);
4322
+ if (!current) return;
4323
+ current.handlers.delete(callback);
4324
+ if (current.handlers.size !== 0) return;
4325
+ current.offEvent();
4326
+ current.offResult();
4327
+ topicStates.delete(stableTopic);
4328
+ fire({ type: "inc.unsubscribe", topic: stableTopic });
3795
4329
  });
4330
+ },
4331
+ channel: {
4332
+ open(targetDTag) {
4333
+ return request(
4334
+ { type: "inc.channel.open", target: targetDTag },
4335
+ "inc.channel.open.result",
4336
+ (msg) => {
4337
+ if (typeof msg.channelId !== "string" || typeof msg.peer !== "string") {
4338
+ throw new Error("inc.channel.open.result missing channel identity");
4339
+ }
4340
+ return makeChannelHandle(materializeChannel(msg.channelId, msg.peer));
4341
+ }
4342
+ );
4343
+ },
4344
+ onOpened(handler) {
4345
+ if (typeof handler !== "function") throw new TypeError("INC opened handler must be a function");
4346
+ openedHandlers.add(handler);
4347
+ const pending = pendingOpened.splice(0);
4348
+ for (const state of pending) handler(makeChannelHandle(state));
4349
+ return subscriptionHandle(() => openedHandlers.delete(handler));
4350
+ },
4351
+ list() {
4352
+ return request(
4353
+ { type: "inc.channel.list" },
4354
+ "inc.channel.list.result",
4355
+ (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({
4356
+ id: channel.id,
4357
+ peer: channel.peer
4358
+ })] : []))
4359
+ );
4360
+ },
4361
+ broadcast(payload) {
4362
+ fire({ type: "inc.channel.broadcast", ...payload === void 0 ? {} : { payload } });
4363
+ }
3796
4364
  }
3797
4365
  };
3798
4366
  }
@@ -3830,14 +4398,22 @@ function nappletNamespacePrelude(domains) {
3830
4398
  return { ...makeScope(), instance: makeScope("instance") };
3831
4399
  }
3832
4400
  function makeIdentity() {
3833
- const read = (type, field, fallback) => request(
3834
- { type },
4401
+ const read = (type, field, fallback, payload = {}) => request(
4402
+ { type, ...payload },
3835
4403
  `${type}.result`,
3836
4404
  (msg) => Object.prototype.hasOwnProperty.call(msg, field) ? msg[field] : fallback,
3837
4405
  { rejectOnError: type !== "identity.getPublicKey" }
3838
4406
  );
3839
- return {
4407
+ return Object.freeze({
3840
4408
  getPublicKey: () => read("identity.getPublicKey", "pubkey", ""),
4409
+ getRelays: () => read("identity.getRelays", "relays", {}),
4410
+ getProfile: () => read("identity.getProfile", "profile", null),
4411
+ getFollows: () => read("identity.getFollows", "pubkeys", []),
4412
+ getList: (listType) => read("identity.getList", "entries", [], { listType }),
4413
+ getZaps: () => read("identity.getZaps", "zaps", []),
4414
+ getMutes: () => read("identity.getMutes", "pubkeys", []),
4415
+ getBlocked: () => read("identity.getBlocked", "pubkeys", []),
4416
+ getBadges: () => read("identity.getBadges", "badges", []),
3841
4417
  onChanged(handler) {
3842
4418
  const off = listen((event) => {
3843
4419
  if (!isParentMessage(event)) return;
@@ -3846,20 +4422,8 @@ function nappletNamespacePrelude(domains) {
3846
4422
  if (typeof msg.pubkey === "string") handler(msg.pubkey);
3847
4423
  });
3848
4424
  return subscriptionHandle(off);
3849
- },
3850
- getRelays: () => read("identity.getRelays", "relays", {}),
3851
- getProfile: () => read("identity.getProfile", "profile", null),
3852
- getFollows: () => read("identity.getFollows", "pubkeys", []),
3853
- getList: (listType) => request(
3854
- { type: "identity.getList", listType },
3855
- "identity.getList.result",
3856
- (msg) => Array.isArray(msg.entries) ? msg.entries : []
3857
- ),
3858
- getZaps: () => read("identity.getZaps", "zaps", []),
3859
- getMutes: () => read("identity.getMutes", "pubkeys", []),
3860
- getBlocked: () => read("identity.getBlocked", "pubkeys", []),
3861
- getBadges: () => read("identity.getBadges", "badges", [])
3862
- };
4425
+ }
4426
+ });
3863
4427
  }
3864
4428
  function makeTheme() {
3865
4429
  return {
@@ -4298,19 +4862,85 @@ function nappletNamespacePrelude(domains) {
4298
4862
  };
4299
4863
  }
4300
4864
  function makeIntent() {
4301
- const invoke = (requestPayload) => request(
4302
- { type: "intent.invoke", request: requestPayload },
4303
- "intent.invoke.result",
4304
- (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
4305
- );
4865
+ const deliveryHandlers = /* @__PURE__ */ new Set();
4866
+ const pendingDeliveries = [];
4867
+ const hasOwn2 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
4868
+ const normalizeDelivery = (value) => {
4869
+ if (!value || typeof value !== "object" || typeof value.sender !== "string" || typeof value.archetype !== "string" || typeof value.action !== "string" || typeof value.convention !== "string") {
4870
+ return void 0;
4871
+ }
4872
+ const delivery = value;
4873
+ return {
4874
+ sender: delivery.sender,
4875
+ archetype: delivery.archetype,
4876
+ action: delivery.action,
4877
+ convention: delivery.convention,
4878
+ ...hasOwn2(delivery, "payload") ? { payload: delivery.payload } : {}
4879
+ };
4880
+ };
4881
+ listen((event) => {
4882
+ if (!isParentMessage(event)) return;
4883
+ const msg = event.data;
4884
+ if (typeof msg !== "object" || msg === null || msg.type !== "intent.deliver") return;
4885
+ const delivery = normalizeDelivery(msg.delivery);
4886
+ if (!delivery) return;
4887
+ if (deliveryHandlers.size === 0) pendingDeliveries.push(delivery);
4888
+ else for (const handler of deliveryHandlers) handler(delivery);
4889
+ });
4890
+ function normalizeIntentUri(uri, explicitPayload, actionMustBe) {
4891
+ if (typeof uri !== "string" || !uri.startsWith("napplet:")) {
4892
+ throw new TypeError("Intent convention URI must start with napplet:");
4893
+ }
4894
+ if (uri.includes("#")) throw new TypeError("Intent convention URI cannot contain fragments");
4895
+ const normalized = normalizeConventionUri(uri, explicitPayload);
4896
+ const match = /^napplet:([^/?#]+)\/([^/?#]+)$/.exec(normalized.topic);
4897
+ if (!match) throw new TypeError("Intent convention URI must be napplet:<archetype>/<intent>");
4898
+ if (actionMustBe !== void 0 && match[2] !== actionMustBe) {
4899
+ throw new TypeError(`Intent convention URI must use the ${actionMustBe} intent`);
4900
+ }
4901
+ return {
4902
+ archetype: match[1],
4903
+ action: match[2],
4904
+ convention: normalized.topic,
4905
+ ...normalized.payload === void 0 ? {} : { payload: normalized.payload }
4906
+ };
4907
+ }
4908
+ const invoke = (uri, opts, actionMustBe) => {
4909
+ const supplied = opts && typeof opts === "object" ? opts : void 0;
4910
+ const explicitPayload = supplied?.payload !== void 0;
4911
+ const normalized = normalizeIntentUri(uri, explicitPayload, actionMustBe);
4912
+ if (supplied && hasOwn2(supplied, "sender")) {
4913
+ throw new TypeError("Intent callers cannot supply sender");
4914
+ }
4915
+ for (const field of ["archetype", "action", "convention"]) {
4916
+ if (supplied && hasOwn2(supplied, field) && supplied[field] !== normalized[field]) {
4917
+ throw new TypeError(`Intent options cannot override URI-derived ${field}`);
4918
+ }
4919
+ }
4920
+ const behaviorValue = supplied?.behavior;
4921
+ const behavior = behaviorValue && typeof behaviorValue === "object" ? behaviorValue : void 0;
4922
+ const sanitizedBehavior = {
4923
+ ...typeof behavior?.focus === "boolean" ? { focus: behavior.focus } : {},
4924
+ ...typeof behavior?.reuse === "boolean" ? { reuse: behavior.reuse } : {}
4925
+ };
4926
+ const handler = typeof supplied?.handler === "string" ? supplied.handler : void 0;
4927
+ return request(
4928
+ {
4929
+ type: "intent.invoke",
4930
+ request: {
4931
+ ...normalized,
4932
+ ...explicitPayload ? { payload: supplied?.payload } : {},
4933
+ ...handler === void 0 ? {} : { handler },
4934
+ ...Object.keys(sanitizedBehavior).length === 0 ? {} : { behavior: sanitizedBehavior }
4935
+ }
4936
+ },
4937
+ "intent.invoke.result",
4938
+ (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
4939
+ );
4940
+ };
4306
4941
  return {
4307
4942
  invoke,
4308
- open: (archetype, payload, opts) => invoke({
4309
- archetype,
4310
- action: "open",
4311
- payload,
4312
- ...opts && typeof opts === "object" ? opts : {}
4313
- }),
4943
+ open: (uri, opts) => invoke(uri, opts, "open"),
4314
4944
  available: (archetype) => request(
4315
4945
  { type: "intent.available", archetype },
4316
4946
  "intent.available.result",
@@ -4325,7 +4955,14 @@ function nappletNamespacePrelude(domains) {
4325
4955
  "intent.changed",
4326
4956
  callback,
4327
4957
  (msg) => msg.availability
4328
- )
4958
+ ),
4959
+ onDelivery: (callback) => {
4960
+ if (typeof callback !== "function") throw new TypeError("Intent delivery handler must be a function");
4961
+ deliveryHandlers.add(callback);
4962
+ const pending = pendingDeliveries.splice(0);
4963
+ for (const delivery of pending) callback(delivery);
4964
+ return subscriptionHandle(() => deliveryHandlers.delete(callback));
4965
+ }
4329
4966
  };
4330
4967
  }
4331
4968
  function makeWebrtc() {
@@ -4460,14 +5097,40 @@ function nappletNamespacePrelude(domains) {
4460
5097
  };
4461
5098
  }
4462
5099
  const shell = makeShell();
5100
+ let inc;
5101
+ let identity;
5102
+ let theme;
5103
+ let intent;
5104
+ function makeProtectedInc(existing) {
5105
+ const extensions = existing && typeof existing === "object" ? existing : {};
5106
+ inc ??= makeInc();
5107
+ return { ...extensions, ...inc };
5108
+ }
5109
+ function makeProtectedIdentity() {
5110
+ identity ??= makeIdentity();
5111
+ return identity;
5112
+ }
5113
+ function makeProtectedTheme() {
5114
+ theme ??= Object.freeze(makeTheme());
5115
+ return theme;
5116
+ }
5117
+ function makeProtectedIntent() {
5118
+ intent ??= Object.freeze(makeIntent());
5119
+ return intent;
5120
+ }
5121
+ function isProtectedDomain(domain) {
5122
+ return domain === "shell" || domain === "inc" || domain === "identity" || domain === "theme" || domain === "intent";
5123
+ }
4463
5124
  function makeDomain(domain, existing) {
4464
5125
  if (domain === "shell") return shell;
5126
+ if (domain === "inc") return makeProtectedInc(existing);
5127
+ if (domain === "identity") return makeProtectedIdentity();
5128
+ if (domain === "theme") return makeProtectedTheme();
5129
+ if (domain === "intent") return makeProtectedIntent();
4465
5130
  if (existing && typeof existing === "object" && Object.keys(existing).length > 0) return existing;
4466
5131
  switch (domain) {
4467
5132
  case "relay":
4468
5133
  return makeRelay();
4469
- case "inc":
4470
- return makeInc();
4471
5134
  case "storage":
4472
5135
  return makeStorage();
4473
5136
  case "keys":
@@ -4476,10 +5139,6 @@ function nappletNamespacePrelude(domains) {
4476
5139
  return makeMedia();
4477
5140
  case "notify":
4478
5141
  return makeNotify();
4479
- case "identity":
4480
- return makeIdentity();
4481
- case "theme":
4482
- return makeTheme();
4483
5142
  case "config":
4484
5143
  return makeConfig();
4485
5144
  case "resource":
@@ -4490,8 +5149,6 @@ function nappletNamespacePrelude(domains) {
4490
5149
  return makeOutbox();
4491
5150
  case "upload":
4492
5151
  return makeUpload();
4493
- case "intent":
4494
- return makeIntent();
4495
5152
  case "webrtc":
4496
5153
  return makeWebrtc();
4497
5154
  case "ble":
@@ -4521,10 +5178,22 @@ function nappletNamespacePrelude(domains) {
4521
5178
  },
4522
5179
  defineProperty(obj, prop, descriptor) {
4523
5180
  if (typeof prop !== "string" || !allowed2.has(prop)) return true;
5181
+ if (isProtectedDomain(prop)) {
5182
+ return Reflect.defineProperty(obj, prop, {
5183
+ value: makeDomain(prop, descriptor.value),
5184
+ enumerable: true,
5185
+ configurable: true,
5186
+ writable: true
5187
+ });
5188
+ }
4524
5189
  return Reflect.defineProperty(obj, prop, {
4525
5190
  ...descriptor,
4526
5191
  value: makeDomain(prop, descriptor.value)
4527
5192
  });
5193
+ },
5194
+ deleteProperty(obj, prop) {
5195
+ if (typeof prop === "string" && allowed2.has(prop) && isProtectedDomain(prop)) return true;
5196
+ return Reflect.deleteProperty(obj, prop);
4528
5197
  }
4529
5198
  });
4530
5199
  }
@@ -4554,7 +5223,7 @@ function nappletNamespacePrelude(domains) {
4554
5223
  root = buildNappletNamespace(value);
4555
5224
  },
4556
5225
  enumerable: false,
4557
- configurable: true
5226
+ configurable: false
4558
5227
  });
4559
5228
  }
4560
5229
 
@@ -8159,14 +8828,11 @@ function enforceLimit(store2, list) {
8159
8828
  list.shift();
8160
8829
  }
8161
8830
  }
8162
- function findById(store2, id) {
8163
- for (const [windowId, list] of store2.notifications) {
8164
- const index = list.findIndex((n) => n.id === id);
8165
- if (index !== -1) {
8166
- return [windowId, list[index], index];
8167
- }
8168
- }
8169
- return void 0;
8831
+ function findById(store2, windowId, id) {
8832
+ const list = store2.notifications.get(windowId);
8833
+ const index = list?.findIndex((notification) => notification.id === id) ?? -1;
8834
+ if (!list || index === -1) return void 0;
8835
+ return [list[index], index];
8170
8836
  }
8171
8837
  function createNotification(store2, windowId, title, body) {
8172
8838
  const notification = {
@@ -8183,20 +8849,20 @@ function createNotification(store2, windowId, title, body) {
8183
8849
  notify(store2);
8184
8850
  return notification;
8185
8851
  }
8186
- function dismissNotification(store2, id) {
8187
- const found = findById(store2, id);
8852
+ function dismissNotification(store2, windowId, id) {
8853
+ const found = findById(store2, windowId, id);
8188
8854
  if (!found) return;
8189
- const [foundWindowId, , index] = found;
8190
- const list = store2.notifications.get(foundWindowId);
8855
+ const [, index] = found;
8856
+ const list = store2.notifications.get(windowId);
8191
8857
  if (!list) return;
8192
8858
  list.splice(index, 1);
8193
- if (list.length === 0) store2.notifications.delete(foundWindowId);
8859
+ if (list.length === 0) store2.notifications.delete(windowId);
8194
8860
  notify(store2);
8195
8861
  }
8196
- function markNotificationRead(store2, id) {
8197
- const found = findById(store2, id);
8862
+ function markNotificationRead(store2, windowId, id) {
8863
+ const found = findById(store2, windowId, id);
8198
8864
  if (!found) return;
8199
- const [, notification] = found;
8865
+ const [notification] = found;
8200
8866
  if (!notification.read) {
8201
8867
  notification.read = true;
8202
8868
  notify(store2);
@@ -8213,12 +8879,12 @@ function handleNotifyEnvelope(store2, windowId, action, msg, send) {
8213
8879
  }
8214
8880
  case "dismiss": {
8215
8881
  const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
8216
- if (notifId) dismissNotification(store2, notifId);
8882
+ if (notifId) dismissNotification(store2, windowId, notifId);
8217
8883
  break;
8218
8884
  }
8219
8885
  case "read": {
8220
8886
  const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
8221
- if (notifId) markNotificationRead(store2, notifId);
8887
+ if (notifId) markNotificationRead(store2, windowId, notifId);
8222
8888
  break;
8223
8889
  }
8224
8890
  case "list": {
@@ -8230,34 +8896,6 @@ function handleNotifyEnvelope(store2, windowId, action, msg, send) {
8230
8896
  break;
8231
8897
  }
8232
8898
  }
8233
- function handleIncNotification(store2, windowId, action, payload, send) {
8234
- switch (action) {
8235
- case "create": {
8236
- const title = typeof payload.title === "string" ? payload.title : "";
8237
- const body = typeof payload.body === "string" ? payload.body : "";
8238
- const notification = createNotification(store2, windowId, title, body);
8239
- send({ type: "inc.event", topic: "notifications:created", payload: { id: notification.id } });
8240
- break;
8241
- }
8242
- case "dismiss": {
8243
- const id = typeof payload.id === "string" ? payload.id : "";
8244
- if (id) dismissNotification(store2, id);
8245
- break;
8246
- }
8247
- case "read": {
8248
- const id = typeof payload.id === "string" ? payload.id : "";
8249
- if (id) markNotificationRead(store2, id);
8250
- break;
8251
- }
8252
- case "list": {
8253
- const windowNotifs = store2.notifications.get(windowId) ?? [];
8254
- send({ type: "inc.event", topic: "notifications:listed", payload: { notifications: windowNotifs } });
8255
- break;
8256
- }
8257
- default:
8258
- break;
8259
- }
8260
- }
8261
8899
  function createNotificationService(options) {
8262
8900
  const store2 = {
8263
8901
  notifications: /* @__PURE__ */ new Map(),
@@ -8265,23 +8903,21 @@ function createNotificationService(options) {
8265
8903
  maxPerWindow: options?.maxPerWindow ?? DEFAULT_MAX_PER_WINDOW
8266
8904
  };
8267
8905
  const descriptor = {
8268
- name: "notifications",
8906
+ name: "notify",
8269
8907
  version: NOTIFICATION_SERVICE_VERSION,
8270
8908
  description: "Notification state registry \u2014 tracks notifications per napplet window"
8271
8909
  };
8272
8910
  return {
8273
8911
  descriptor,
8274
8912
  handleMessage(windowId, message, send) {
8275
- const msg = message;
8276
- if (message.type.startsWith("notify.")) {
8277
- handleNotifyEnvelope(store2, windowId, message.type.slice(7), msg, send);
8278
- return;
8279
- }
8280
- if (message.type !== "inc.emit") return;
8281
- const topic = msg.topic;
8282
- if (!topic?.startsWith("notifications:")) return;
8283
- const payload = msg.payload ?? {};
8284
- handleIncNotification(store2, windowId, topic.slice(14), payload, send);
8913
+ if (!message.type.startsWith("notify.")) return;
8914
+ handleNotifyEnvelope(
8915
+ store2,
8916
+ windowId,
8917
+ message.type.slice(7),
8918
+ message,
8919
+ send
8920
+ );
8285
8921
  },
8286
8922
  onWindowDestroyed(windowId) {
8287
8923
  if (store2.notifications.delete(windowId)) {
@@ -8291,16 +8927,16 @@ function createNotificationService(options) {
8291
8927
  };
8292
8928
  }
8293
8929
  var IDENTITY_SERVICE_VERSION = "1.0.0";
8294
- function sendProviderError(send, result2, fallback, err2) {
8930
+ function sendProviderError(send, result2, fallback, _err) {
8295
8931
  send({
8296
8932
  ...result2,
8297
- error: err2?.message ?? fallback
8933
+ error: fallback
8298
8934
  });
8299
8935
  }
8300
8936
  async function getCurrentPubkey(options) {
8301
- const currentSigner = options.getSigner();
8302
- if (!currentSigner?.getPublicKey) return "";
8303
8937
  try {
8938
+ const currentSigner = options.getSigner();
8939
+ if (!currentSigner?.getPublicKey) return "";
8304
8940
  return await currentSigner.getPublicKey() ?? "";
8305
8941
  } catch {
8306
8942
  return "";
@@ -8313,46 +8949,29 @@ function sendOptionalProviderResult(options, send, fallbackResult, errorFallback
8313
8949
  }
8314
8950
  Promise.resolve(getCurrentPubkey(options)).then((pubkey) => buildResult(pubkey)).then((result2) => send(result2)).catch((err2) => sendProviderError(send, fallbackResult, errorFallback, err2));
8315
8951
  }
8316
- function sendIdentityError(send, id, typeBase, error) {
8317
- send({ type: `${typeBase}.error`, id, error });
8318
- }
8319
- function sendSignerError(send, id, typeBase, fallback, err2) {
8320
- sendIdentityError(send, id, typeBase, err2?.message ?? fallback);
8321
- }
8322
8952
  function handleGetPublicKey(options, id, send) {
8323
- const signer = options.getSigner();
8324
- if (!signer) {
8325
- const result2 = {
8326
- type: "identity.getPublicKey.result",
8327
- id,
8328
- pubkey: ""
8329
- };
8330
- send(result2);
8331
- return;
8332
- }
8333
- Promise.resolve(signer.getPublicKey?.()).then((pubkey) => {
8953
+ Promise.resolve().then(() => options.getSigner()).then(async (signer) => signer?.getPublicKey ? signer.getPublicKey() : "").catch(() => "").then((pubkey) => {
8334
8954
  const result2 = {
8335
8955
  type: "identity.getPublicKey.result",
8336
8956
  id,
8337
8957
  pubkey: pubkey ?? ""
8338
8958
  };
8339
8959
  send(result2);
8340
- }).catch((err2) => sendSignerError(send, id, "identity.getPublicKey", "getPublicKey failed", err2));
8960
+ });
8341
8961
  }
8342
8962
  function handleGetRelays(options, id, send) {
8343
- const signer = options.getSigner();
8344
- if (!signer) {
8345
- sendIdentityError(send, id, "identity.getRelays", "no signer configured");
8346
- return;
8347
- }
8348
- Promise.resolve(signer.getRelays?.() ?? {}).then((relays) => {
8349
- const result2 = {
8350
- type: "identity.getRelays.result",
8351
- id,
8352
- relays
8963
+ const fallbackResult = {
8964
+ type: "identity.getRelays.result",
8965
+ id,
8966
+ relays: {}
8967
+ };
8968
+ Promise.resolve().then(() => options.getSigner()).then(async (signer) => {
8969
+ if (!signer?.getRelays) return fallbackResult;
8970
+ return {
8971
+ ...fallbackResult,
8972
+ relays: await signer.getRelays() ?? {}
8353
8973
  };
8354
- send(result2);
8355
- }).catch((err2) => sendSignerError(send, id, "identity.getRelays", "getRelays failed", err2));
8974
+ }).catch(() => ({ ...fallbackResult, error: "identity.getRelays failed" })).then(send);
8356
8975
  }
8357
8976
  function handleReadProvider(options, id, message, send) {
8358
8977
  switch (message.type) {
@@ -8361,11 +8980,11 @@ function handleReadProvider(options, id, message, send) {
8361
8980
  options,
8362
8981
  send,
8363
8982
  { type: "identity.getProfile.result", id, profile: null },
8364
- "getProfile failed",
8983
+ "identity.getProfile failed",
8365
8984
  options.getProfile ? async (pubkey) => ({
8366
8985
  type: "identity.getProfile.result",
8367
8986
  id,
8368
- profile: await options.getProfile(pubkey, message)
8987
+ profile: await options.getProfile(pubkey, message) ?? null
8369
8988
  }) : void 0
8370
8989
  );
8371
8990
  return true;
@@ -8374,11 +8993,11 @@ function handleReadProvider(options, id, message, send) {
8374
8993
  options,
8375
8994
  send,
8376
8995
  { type: "identity.getFollows.result", id, pubkeys: [] },
8377
- "getFollows failed",
8996
+ "identity.getFollows failed",
8378
8997
  options.getFollows ? async (pubkey) => ({
8379
8998
  type: "identity.getFollows.result",
8380
8999
  id,
8381
- pubkeys: await options.getFollows(pubkey, message)
9000
+ pubkeys: await options.getFollows(pubkey, message) ?? []
8382
9001
  }) : void 0
8383
9002
  );
8384
9003
  return true;
@@ -8390,11 +9009,11 @@ function handleReadProvider(options, id, message, send) {
8390
9009
  options,
8391
9010
  send,
8392
9011
  { type: "identity.getZaps.result", id, zaps: [] },
8393
- "getZaps failed",
9012
+ "identity.getZaps failed",
8394
9013
  options.getZaps ? async (pubkey) => ({
8395
9014
  type: "identity.getZaps.result",
8396
9015
  id,
8397
- zaps: await options.getZaps(pubkey, message)
9016
+ zaps: await options.getZaps(pubkey, message) ?? []
8398
9017
  }) : void 0
8399
9018
  );
8400
9019
  return true;
@@ -8403,11 +9022,11 @@ function handleReadProvider(options, id, message, send) {
8403
9022
  options,
8404
9023
  send,
8405
9024
  { type: "identity.getMutes.result", id, pubkeys: [] },
8406
- "getMutes failed",
9025
+ "identity.getMutes failed",
8407
9026
  options.getMutes ? async (pubkey) => ({
8408
9027
  type: "identity.getMutes.result",
8409
9028
  id,
8410
- pubkeys: await options.getMutes(pubkey, message)
9029
+ pubkeys: await options.getMutes(pubkey, message) ?? []
8411
9030
  }) : void 0
8412
9031
  );
8413
9032
  return true;
@@ -8416,11 +9035,11 @@ function handleReadProvider(options, id, message, send) {
8416
9035
  options,
8417
9036
  send,
8418
9037
  { type: "identity.getBlocked.result", id, pubkeys: [] },
8419
- "getBlocked failed",
9038
+ "identity.getBlocked failed",
8420
9039
  options.getBlocked ? async (pubkey) => ({
8421
9040
  type: "identity.getBlocked.result",
8422
9041
  id,
8423
- pubkeys: await options.getBlocked(pubkey, message)
9042
+ pubkeys: await options.getBlocked(pubkey, message) ?? []
8424
9043
  }) : void 0
8425
9044
  );
8426
9045
  return true;
@@ -8429,11 +9048,11 @@ function handleReadProvider(options, id, message, send) {
8429
9048
  options,
8430
9049
  send,
8431
9050
  { type: "identity.getBadges.result", id, badges: [] },
8432
- "getBadges failed",
9051
+ "identity.getBadges failed",
8433
9052
  options.getBadges ? async (pubkey) => ({
8434
9053
  type: "identity.getBadges.result",
8435
9054
  id,
8436
- badges: await options.getBadges(pubkey, message)
9055
+ badges: await options.getBadges(pubkey, message) ?? []
8437
9056
  }) : void 0
8438
9057
  );
8439
9058
  return true;
@@ -8446,11 +9065,11 @@ function handleGetList(options, id, message, send) {
8446
9065
  options,
8447
9066
  send,
8448
9067
  { type: "identity.getList.result", id, entries: [] },
8449
- "getList failed",
9068
+ "identity.getList failed",
8450
9069
  options.getList ? async (pubkey) => ({
8451
9070
  type: "identity.getList.result",
8452
9071
  id,
8453
- entries: await options.getList(message.listType, pubkey, message)
9072
+ entries: await options.getList(message.listType, pubkey, message) ?? []
8454
9073
  }) : void 0
8455
9074
  );
8456
9075
  }
@@ -8464,9 +9083,7 @@ function handleIdentityServiceMessage(options, message, send) {
8464
9083
  handleGetRelays(options, id, send);
8465
9084
  return;
8466
9085
  default:
8467
- if (!handleReadProvider(options, id, message, send)) {
8468
- sendIdentityError(send, id, message.type, `Unknown identity method: ${message.type}`);
8469
- }
9086
+ handleReadProvider(options, id, message, send);
8470
9087
  }
8471
9088
  }
8472
9089
  function createIdentityService(options) {
@@ -8487,6 +9104,37 @@ function createIdentityService(options) {
8487
9104
  var EOSE_FALLBACK_MS = 15e3;
8488
9105
  function createRelayPoolService(options) {
8489
9106
  const tracked = /* @__PURE__ */ new Map();
9107
+ function publishEvent(relayMessage, resultType, send) {
9108
+ const id = typeof relayMessage.id === "string" ? relayMessage.id : "";
9109
+ const event = relayMessage.event;
9110
+ if (!event || typeof event !== "object") {
9111
+ send({ type: resultType, id, ok: false, error: "invalid event" });
9112
+ return;
9113
+ }
9114
+ if (!options.isAvailable()) {
9115
+ send({ type: resultType, id, ok: false, error: "no relay pool available" });
9116
+ return;
9117
+ }
9118
+ void (async () => {
9119
+ try {
9120
+ await options.publish(event);
9121
+ send({
9122
+ type: resultType,
9123
+ id,
9124
+ ok: true,
9125
+ event,
9126
+ eventId: event.id
9127
+ });
9128
+ } catch (error) {
9129
+ send({
9130
+ type: resultType,
9131
+ id,
9132
+ ok: false,
9133
+ error: error instanceof Error && error.message ? error.message : "publish failed"
9134
+ });
9135
+ }
9136
+ })();
9137
+ }
8490
9138
  return {
8491
9139
  descriptor: {
8492
9140
  name: "relay-pool",
@@ -8546,17 +9194,11 @@ function createRelayPoolService(options) {
8546
9194
  return;
8547
9195
  }
8548
9196
  if (message.type === "relay.publish") {
8549
- const event = relayMessage.event;
8550
- if (event && typeof event === "object" && options.isAvailable()) {
8551
- options.publish(event);
8552
- }
9197
+ publishEvent(relayMessage, "relay.publish.result", send);
8553
9198
  return;
8554
9199
  }
8555
9200
  if (message.type === "relay.publishEncrypted") {
8556
- const event = relayMessage.event;
8557
- if (event && typeof event === "object" && options.isAvailable()) {
8558
- options.publish(event);
8559
- }
9201
+ publishEvent(relayMessage, "relay.publishEncrypted.result", send);
8560
9202
  return;
8561
9203
  }
8562
9204
  },
@@ -9488,8 +10130,15 @@ var DEFAULT_THEME = {
9488
10130
  // fonts, background, title intentionally undefined — all optional per
9489
10131
  // @napplet/nap/theme Theme interface.
9490
10132
  };
10133
+ function isCompleteTheme(theme) {
10134
+ const colors = theme?.colors;
10135
+ return typeof colors?.background === "string" && typeof colors.text === "string" && typeof colors.primary === "string";
10136
+ }
10137
+ function normalizeTheme(theme) {
10138
+ return isCompleteTheme(theme) ? theme : DEFAULT_THEME;
10139
+ }
9491
10140
  function createThemeService(options = {}) {
9492
- let currentTheme = options.initialTheme ?? DEFAULT_THEME;
10141
+ let currentTheme = normalizeTheme(options.initialTheme);
9493
10142
  const descriptor = {
9494
10143
  name: "theme",
9495
10144
  version: THEME_SERVICE_VERSION,
@@ -9508,19 +10157,15 @@ function createThemeService(options = {}) {
9508
10157
  send(result2);
9509
10158
  return;
9510
10159
  }
9511
- send({
9512
- type: `${message.type}.error`,
9513
- id,
9514
- error: `Unknown theme method: ${message.type}`
9515
- });
10160
+ return;
9516
10161
  },
9517
10162
  // Theme service has no per-window state to clean up.
9518
10163
  onWindowDestroyed(_windowId) {
9519
10164
  }
9520
10165
  };
9521
10166
  function publishTheme(theme) {
9522
- currentTheme = theme;
9523
- const envelope = { type: "theme.changed", theme };
10167
+ currentTheme = normalizeTheme(theme);
10168
+ const envelope = { type: "theme.changed", theme: currentTheme };
9524
10169
  options.onBroadcast?.(envelope);
9525
10170
  return envelope;
9526
10171
  }
@@ -11071,42 +11716,125 @@ function toErrorMessage4(err2) {
11071
11716
  if (typeof err2 === "string") return err2;
11072
11717
  return "intent request failed";
11073
11718
  }
11719
+ function hasOwn(value, key) {
11720
+ return Object.prototype.hasOwnProperty.call(value, key);
11721
+ }
11722
+ function validateIntentRequest(value) {
11723
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
11724
+ return { error: "invalid convention" };
11725
+ }
11726
+ const request = value;
11727
+ if (typeof request.archetype !== "string" || request.archetype.length === 0 || typeof request.action !== "string" || request.action.length === 0 || typeof request.convention !== "string" || request.convention.length === 0) {
11728
+ return { error: "invalid convention" };
11729
+ }
11730
+ const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(request.convention);
11731
+ if (!match || match[1] !== request.archetype || match[2] !== request.action) {
11732
+ return { error: "invalid convention" };
11733
+ }
11734
+ const allowedKeys = ["archetype", "action", "convention", "payload", "handler", "behavior"];
11735
+ if (Object.keys(request).some((key) => !allowedKeys.includes(key))) {
11736
+ return { error: "invoke rejected" };
11737
+ }
11738
+ if (hasOwn(request, "handler") && request.handler !== void 0 && (typeof request.handler !== "string" || request.handler.length === 0)) {
11739
+ return { error: "invoke rejected" };
11740
+ }
11741
+ let behavior;
11742
+ if (hasOwn(request, "behavior") && request.behavior !== void 0) {
11743
+ if (typeof request.behavior !== "object" || request.behavior === null || Array.isArray(request.behavior)) {
11744
+ return { error: "invoke rejected" };
11745
+ }
11746
+ const record = request.behavior;
11747
+ if (Object.keys(record).some((key) => key !== "focus" && key !== "reuse") || hasOwn(record, "focus") && typeof record.focus !== "boolean" || hasOwn(record, "reuse") && typeof record.reuse !== "boolean") {
11748
+ return { error: "invoke rejected" };
11749
+ }
11750
+ behavior = {
11751
+ ...record.focus === void 0 ? {} : { focus: record.focus },
11752
+ ...record.reuse === void 0 ? {} : { reuse: record.reuse }
11753
+ };
11754
+ }
11755
+ return {
11756
+ request: {
11757
+ archetype: request.archetype,
11758
+ action: request.action,
11759
+ convention: request.convention,
11760
+ ...hasOwn(request, "payload") ? { payload: request.payload } : {},
11761
+ ...typeof request.handler === "string" ? { handler: request.handler } : {},
11762
+ ...behavior === void 0 ? {} : { behavior }
11763
+ }
11764
+ };
11765
+ }
11766
+ function settleResolverCall(call, send, resultType, id, onValue) {
11767
+ let pending;
11768
+ try {
11769
+ pending = Promise.resolve(call());
11770
+ } catch (error) {
11771
+ send({ type: resultType, id, error: toErrorMessage4(error) });
11772
+ return;
11773
+ }
11774
+ pending.then((value) => send(onValue(value))).catch((error) => send({ type: resultType, id, error: toErrorMessage4(error) }));
11775
+ }
11776
+ function rejectInvoke(send, id) {
11777
+ send({
11778
+ type: "intent.invoke.result",
11779
+ id,
11780
+ result: { ok: false, error: "invoke rejected" }
11781
+ });
11782
+ }
11074
11783
  function createIntentService(options) {
11075
11784
  if (!options || typeof options.resolver !== "object" || options.resolver === null) {
11076
11785
  throw new Error("createIntentService: options.resolver is required");
11077
11786
  }
11078
11787
  const { resolver } = options;
11079
- const windows = /* @__PURE__ */ new Map();
11080
- resolver.onChanged?.((availability) => {
11081
- for (const send of windows.values()) {
11082
- send({ type: "intent.changed", availability });
11083
- }
11084
- });
11085
- function settle6(call, send, resultType, id, onValue) {
11086
- let pending;
11087
- try {
11088
- pending = Promise.resolve(call());
11089
- } catch (err2) {
11090
- send({ type: resultType, id, error: toErrorMessage4(err2) });
11091
- return;
11092
- }
11093
- pending.then((value) => send(onValue(value))).catch((err2) => send({ type: resultType, id, error: toErrorMessage4(err2) }));
11094
- }
11788
+ let runtimeContext;
11789
+ let unsubscribeChanged;
11095
11790
  function handleInvoke(windowId, msg, send) {
11096
11791
  const m = msg;
11097
11792
  const id = m.id ?? "";
11098
- const request = m.request;
11099
- if (!request || typeof request !== "object" || typeof request.archetype !== "string" || request.archetype.length === 0) {
11100
- send({ type: "intent.invoke.result", id, error: "invalid request" });
11793
+ const validation = validateIntentRequest(m.request);
11794
+ if ("error" in validation) {
11795
+ send({
11796
+ type: "intent.invoke.result",
11797
+ id,
11798
+ result: { ok: false, error: validation.error }
11799
+ });
11101
11800
  return;
11102
11801
  }
11103
- settle6(
11104
- () => resolver.invoke(request, { windowId }),
11105
- send,
11106
- "intent.invoke.result",
11107
- id,
11108
- (result2) => ({ type: "intent.invoke.result", id, result: result2 })
11109
- );
11802
+ const sender = runtimeContext?.resolveDTag(windowId);
11803
+ if (!sender) {
11804
+ rejectInvoke(send, id);
11805
+ return;
11806
+ }
11807
+ let pending;
11808
+ try {
11809
+ pending = Promise.resolve(resolver.invoke(validation.request, { sender }));
11810
+ } catch {
11811
+ rejectInvoke(send, id);
11812
+ return;
11813
+ }
11814
+ void pending.then(
11815
+ (outcome) => {
11816
+ if (outcome.result.ok && (!("retained" in outcome) || typeof outcome.retained.start !== "function")) {
11817
+ rejectInvoke(send, id);
11818
+ return;
11819
+ }
11820
+ send({
11821
+ type: "intent.invoke.result",
11822
+ id,
11823
+ result: outcome.result
11824
+ });
11825
+ if (!outcome.result.ok || !("retained" in outcome)) return;
11826
+ try {
11827
+ const started = outcome.retained.start();
11828
+ void Promise.resolve(started).catch(() => {
11829
+ });
11830
+ } catch {
11831
+ }
11832
+ },
11833
+ () => {
11834
+ rejectInvoke(send, id);
11835
+ }
11836
+ ).catch(() => {
11837
+ });
11110
11838
  }
11111
11839
  function handleAvailable(msg, send) {
11112
11840
  const m = msg;
@@ -11116,7 +11844,7 @@ function createIntentService(options) {
11116
11844
  return;
11117
11845
  }
11118
11846
  const archetype = m.archetype;
11119
- settle6(
11847
+ settleResolverCall(
11120
11848
  () => resolver.available(archetype),
11121
11849
  send,
11122
11850
  "intent.available.result",
@@ -11127,7 +11855,7 @@ function createIntentService(options) {
11127
11855
  function handleHandlers(msg, send) {
11128
11856
  const m = msg;
11129
11857
  const id = m.id ?? "";
11130
- settle6(
11858
+ settleResolverCall(
11131
11859
  () => resolver.handlers(),
11132
11860
  send,
11133
11861
  "intent.handlers.result",
@@ -11137,8 +11865,31 @@ function createIntentService(options) {
11137
11865
  }
11138
11866
  return {
11139
11867
  descriptor: INTENT_DESCRIPTOR,
11140
- handleMessage(windowId, message, send) {
11141
- windows.set(windowId, send);
11868
+ onRegistered(context) {
11869
+ try {
11870
+ unsubscribeChanged?.();
11871
+ } catch {
11872
+ }
11873
+ runtimeContext = context;
11874
+ unsubscribeChanged = resolver.onChanged?.((availability) => {
11875
+ const current = runtimeContext;
11876
+ if (!current) return;
11877
+ const message = { type: "intent.changed", availability };
11878
+ for (const windowId of current.listWindowIds()) {
11879
+ current.sendToEligibleNapplet(windowId, message);
11880
+ }
11881
+ });
11882
+ },
11883
+ onUnregistered() {
11884
+ const unsubscribe = unsubscribeChanged;
11885
+ unsubscribeChanged = void 0;
11886
+ runtimeContext = void 0;
11887
+ try {
11888
+ unsubscribe?.();
11889
+ } catch {
11890
+ }
11891
+ },
11892
+ handleMessage(windowId, message, send) {
11142
11893
  switch (message.type) {
11143
11894
  case "intent.invoke":
11144
11895
  handleInvoke(windowId, message, send);
@@ -11152,12 +11903,199 @@ function createIntentService(options) {
11152
11903
  default:
11153
11904
  return;
11154
11905
  }
11906
+ }
11907
+ };
11908
+ }
11909
+ function candidatesFor(catalog, archetype, defaultHandler) {
11910
+ const candidates = [];
11911
+ for (const entry of catalog) {
11912
+ const support = entry.archetypes[archetype];
11913
+ if (!support) continue;
11914
+ candidates.push({
11915
+ dTag: entry.dTag,
11916
+ ...entry.title === void 0 ? {} : { title: entry.title },
11917
+ actions: [...support.actions],
11918
+ conventions: [...support.conventions],
11919
+ contracts: support.contracts.map((contract) => ({
11920
+ convention: contract.convention,
11921
+ ...contract.eventKinds === void 0 ? {} : { eventKinds: [...contract.eventKinds] }
11922
+ })),
11923
+ ...entry.dTag === defaultHandler ? { isDefault: true } : {}
11924
+ });
11925
+ }
11926
+ return candidates;
11927
+ }
11928
+ function reject(error) {
11929
+ return { ok: false, error };
11930
+ }
11931
+ function createCatalogIntentResolver(options) {
11932
+ if (!options || typeof options.loadCatalog !== "function") {
11933
+ throw new Error("createCatalogIntentResolver: options.loadCatalog is required");
11934
+ }
11935
+ if (!options.targets || typeof options.targets.retain !== "function") {
11936
+ throw new Error("createCatalogIntentResolver: options.targets is required");
11937
+ }
11938
+ const {
11939
+ loadCatalog,
11940
+ targets,
11941
+ getDefaultHandler,
11942
+ chooseHandler,
11943
+ authorizeExplicitHandler
11944
+ } = options;
11945
+ const listeners = /* @__PURE__ */ new Set();
11946
+ async function availabilityFor(archetype) {
11947
+ const catalog = await loadCatalog();
11948
+ const defaultHandler = getDefaultHandler?.(archetype);
11949
+ const candidates = candidatesFor(catalog, archetype, defaultHandler);
11950
+ return {
11951
+ archetype,
11952
+ available: candidates.length > 0,
11953
+ candidates,
11954
+ hasDefault: defaultHandler !== void 0 && candidates.some((candidate) => candidate.dTag === defaultHandler)
11955
+ };
11956
+ }
11957
+ async function chooseCompatible(request, candidates, sender) {
11958
+ if (!chooseHandler) {
11959
+ return request.handler === "choose" ? { error: "user cancelled" } : { error: "invoke rejected" };
11960
+ }
11961
+ const picked = await chooseHandler(request.archetype, candidates, sender);
11962
+ if (picked === void 0) return { error: "user cancelled" };
11963
+ const candidate = candidates.find((item) => item.dTag === picked);
11964
+ return candidate ? { candidate } : { error: "invoke rejected" };
11965
+ }
11966
+ async function pickHandler(request, compatible, sender) {
11967
+ const preference = request.handler;
11968
+ if (typeof preference === "string" && preference !== "default" && preference !== "choose") {
11969
+ const candidate = compatible.find((item) => item.dTag === preference);
11970
+ if (!candidate || !authorizeExplicitHandler) return { error: "invoke rejected" };
11971
+ let authorized = false;
11972
+ try {
11973
+ authorized = await authorizeExplicitHandler(sender, preference, request, candidate);
11974
+ } catch {
11975
+ return { error: "invoke rejected" };
11976
+ }
11977
+ return authorized ? { candidate } : { error: "invoke rejected" };
11978
+ }
11979
+ if (preference === "choose") {
11980
+ return chooseCompatible(request, compatible, sender);
11981
+ }
11982
+ const defaultCandidate = compatible.find((candidate) => candidate.isDefault === true);
11983
+ if (preference === "default") {
11984
+ return defaultCandidate ? { candidate: defaultCandidate } : { error: "invoke rejected" };
11985
+ }
11986
+ if (defaultCandidate) return { candidate: defaultCandidate };
11987
+ if (compatible.length === 1) return { candidate: compatible[0] };
11988
+ return chooseCompatible(request, compatible, sender);
11989
+ }
11990
+ async function invoke(request, context) {
11991
+ const catalog = await loadCatalog();
11992
+ const defaultHandler = getDefaultHandler?.(request.archetype);
11993
+ const candidates = candidatesFor(catalog, request.archetype, defaultHandler);
11994
+ if (candidates.length === 0) return { result: reject("no handler") };
11995
+ const compatible = candidates.filter((candidate) => candidate.contracts.some((contract) => contract.convention === request.convention));
11996
+ if (compatible.length === 0) return { result: reject("unsupported convention") };
11997
+ const sender = context.sender;
11998
+ if (typeof sender !== "string" || sender.length === 0) {
11999
+ return { result: reject("invoke rejected") };
12000
+ }
12001
+ const selected = await pickHandler(request, compatible, sender);
12002
+ if ("error" in selected) return { result: reject(selected.error) };
12003
+ const delivery = Object.freeze({
12004
+ sender,
12005
+ archetype: request.archetype,
12006
+ action: request.action,
12007
+ convention: request.convention,
12008
+ ...request.payload === void 0 ? {} : { payload: request.payload }
12009
+ });
12010
+ const behavior = request.behavior === void 0 ? void 0 : Object.freeze({
12011
+ ...request.behavior.focus === void 0 ? {} : { focus: request.behavior.focus },
12012
+ ...request.behavior.reuse === void 0 ? {} : { reuse: request.behavior.reuse }
12013
+ });
12014
+ const params = Object.freeze({
12015
+ handler: selected.candidate.dTag,
12016
+ delivery,
12017
+ ...behavior === void 0 ? {} : { behavior }
12018
+ });
12019
+ let retained;
12020
+ try {
12021
+ retained = await targets.retain(params);
12022
+ } catch {
12023
+ return { result: reject("invoke rejected") };
12024
+ }
12025
+ if (!retained || typeof retained.start !== "function") {
12026
+ return { result: reject("invoke rejected") };
12027
+ }
12028
+ return {
12029
+ result: {
12030
+ ok: true,
12031
+ archetype: request.archetype,
12032
+ action: request.action,
12033
+ convention: request.convention,
12034
+ handler: selected.candidate.dTag
12035
+ },
12036
+ retained
12037
+ };
12038
+ }
12039
+ async function handlers() {
12040
+ const catalog = await loadCatalog();
12041
+ const archetypes = /* @__PURE__ */ new Set();
12042
+ for (const entry of catalog) {
12043
+ for (const slug of Object.keys(entry.archetypes)) archetypes.add(slug);
12044
+ }
12045
+ return Promise.all([...archetypes].map((archetype) => availabilityFor(archetype)));
12046
+ }
12047
+ return {
12048
+ invoke,
12049
+ available: availabilityFor,
12050
+ handlers,
12051
+ onChanged(listener) {
12052
+ listeners.add(listener);
12053
+ return () => listeners.delete(listener);
11155
12054
  },
11156
- onWindowDestroyed(windowId) {
11157
- windows.delete(windowId);
12055
+ notifyChanged(archetype) {
12056
+ if (listeners.size === 0) return;
12057
+ void availabilityFor(archetype).then((availability) => {
12058
+ for (const listener of listeners) listener(availability);
12059
+ });
11158
12060
  }
11159
12061
  };
11160
12062
  }
12063
+ function actionFromConvention(slug, convention) {
12064
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
12065
+ throw new TypeError("manifest archetype slug is invalid");
12066
+ }
12067
+ const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(convention);
12068
+ if (!match || match[1] !== slug) {
12069
+ throw new TypeError("manifest archetype convention is invalid or mismatched");
12070
+ }
12071
+ return match[2];
12072
+ }
12073
+ function manifestToIntentCatalogEntry(manifest) {
12074
+ const archetypes = {};
12075
+ for (const { slug, convention, eventKinds } of manifest.archetypes) {
12076
+ const action = actionFromConvention(slug, convention);
12077
+ if (eventKinds?.some((kind) => !Number.isSafeInteger(kind) || kind < 0)) {
12078
+ throw new TypeError("manifest archetype event kinds must be unsigned safe integers");
12079
+ }
12080
+ const support = archetypes[slug] ??= {
12081
+ actions: [],
12082
+ conventions: [],
12083
+ contracts: []
12084
+ };
12085
+ const contract = {
12086
+ convention,
12087
+ ...eventKinds === void 0 || eventKinds.length === 0 ? {} : { eventKinds: [...eventKinds] }
12088
+ };
12089
+ support.contracts.push(contract);
12090
+ if (!support.actions.includes(action)) support.actions.push(action);
12091
+ if (!support.conventions.includes(convention)) support.conventions.push(convention);
12092
+ }
12093
+ return {
12094
+ dTag: manifest.dTag,
12095
+ ...manifest.title === void 0 ? {} : { title: manifest.title },
12096
+ archetypes
12097
+ };
12098
+ }
11161
12099
  var CVM_SERVICE_VERSION = "1.0.0";
11162
12100
  var CVM_DESCRIPTOR = {
11163
12101
  name: "cvm",
@@ -12148,6 +13086,200 @@ function createDevWebrtcController() {
12148
13086
  };
12149
13087
  }
12150
13088
 
13089
+ // src/browser-intent-controller.ts
13090
+ var MAX_INTENT_DELIVERY_ATTEMPTS = 10;
13091
+ var BrowserIntentController = class {
13092
+ constructor(options) {
13093
+ this.options = options;
13094
+ this.maxAttempts = normalizeAttempts(options.maxAttempts);
13095
+ }
13096
+ options;
13097
+ maxAttempts;
13098
+ retain(params) {
13099
+ const retained = freezeRetention(params);
13100
+ let started;
13101
+ let delivered = false;
13102
+ return {
13103
+ start: () => {
13104
+ started ??= this.start(retained, () => delivered, () => {
13105
+ delivered = true;
13106
+ });
13107
+ return started;
13108
+ }
13109
+ };
13110
+ }
13111
+ async start(params, isDelivered, markDelivered) {
13112
+ let reason = "no-current-target";
13113
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt += 1) {
13114
+ let generation;
13115
+ try {
13116
+ generation = await this.options.openOrReuse(params, attempt);
13117
+ } catch {
13118
+ reason = "open-failed";
13119
+ continue;
13120
+ }
13121
+ if (!generation) {
13122
+ reason = "open-failed";
13123
+ continue;
13124
+ }
13125
+ try {
13126
+ await this.options.waitForReady(generation);
13127
+ } catch {
13128
+ reason = "ready-failed";
13129
+ continue;
13130
+ }
13131
+ if (!await this.options.isCurrent(generation)) {
13132
+ reason = "no-current-target";
13133
+ continue;
13134
+ }
13135
+ if (isDelivered()) return;
13136
+ markDelivered();
13137
+ try {
13138
+ await this.options.send(generation, params.delivery);
13139
+ return;
13140
+ } catch {
13141
+ this.options.onTerminal?.(params, "send-failed");
13142
+ return;
13143
+ }
13144
+ }
13145
+ this.options.onTerminal?.(params, reason);
13146
+ }
13147
+ };
13148
+ function normalizeAttempts(value) {
13149
+ if (value === void 0) return 2;
13150
+ if (!Number.isFinite(value)) throw new TypeError("maxAttempts must be finite");
13151
+ return Math.min(MAX_INTENT_DELIVERY_ATTEMPTS, Math.max(1, Math.floor(value)));
13152
+ }
13153
+ function freezeRetention(params) {
13154
+ if (!params || typeof params.handler !== "string" || params.handler.length === 0) {
13155
+ throw new TypeError("Intent retention requires a handler");
13156
+ }
13157
+ const delivery = freezeValue({ ...params.delivery });
13158
+ const behavior = params.behavior === void 0 ? void 0 : freezeValue({ ...params.behavior });
13159
+ return Object.freeze({
13160
+ handler: params.handler,
13161
+ delivery,
13162
+ ...behavior === void 0 ? {} : { behavior }
13163
+ });
13164
+ }
13165
+ function freezeValue(value, seen = /* @__PURE__ */ new WeakMap()) {
13166
+ if (typeof value !== "object" || value === null) return value;
13167
+ const existing = seen.get(value);
13168
+ if (existing !== void 0) return existing;
13169
+ if (Array.isArray(value)) {
13170
+ const copy2 = [];
13171
+ seen.set(value, copy2);
13172
+ for (const item of value) copy2.push(freezeValue(item, seen));
13173
+ return Object.freeze(copy2);
13174
+ }
13175
+ const copy = {};
13176
+ seen.set(value, copy);
13177
+ for (const [key, item] of Object.entries(value)) copy[key] = freezeValue(item, seen);
13178
+ return Object.freeze(copy);
13179
+ }
13180
+
13181
+ // src/installed-napplet-catalog.ts
13182
+ function matchesInstalledNappletRecord(record, target) {
13183
+ return target.dTag === record.dTag && target.aggregateHash === record.aggregateHash;
13184
+ }
13185
+ var InstalledNappletCatalog = class {
13186
+ records = /* @__PURE__ */ new Map();
13187
+ listeners = /* @__PURE__ */ new Set();
13188
+ /** Insert or replace a record from an already resolver-verified pointer. */
13189
+ install(resolved) {
13190
+ const record = freezeRecord({
13191
+ dTag: resolved.dTag,
13192
+ aggregateHash: resolved.aggregateHash,
13193
+ pointer: copyPointer(resolved.pointer),
13194
+ ...resolved.manifest.title === void 0 ? {} : { title: resolved.manifest.title },
13195
+ archetypes: resolved.manifest.archetypes.map((archetype) => ({
13196
+ slug: archetype.slug,
13197
+ convention: archetype.convention,
13198
+ ...archetype.eventKinds === void 0 ? {} : { eventKinds: [...archetype.eventKinds] }
13199
+ }))
13200
+ });
13201
+ this.records.set(record.dTag, record);
13202
+ this.notify(record.dTag);
13203
+ return record;
13204
+ }
13205
+ /** Remove an artifact explicitly; frame teardown never calls this method. */
13206
+ remove(dTag) {
13207
+ const removed = this.records.delete(dTag);
13208
+ if (removed) this.notify(dTag);
13209
+ return removed;
13210
+ }
13211
+ /** Return whether a verified installation exists for a d-tag. */
13212
+ has(dTag) {
13213
+ return this.records.has(dTag);
13214
+ }
13215
+ /** Return immutable serializable installed-artifact facts. */
13216
+ installed() {
13217
+ return [...this.records.values()];
13218
+ }
13219
+ /** Return a verified record by d-tag. */
13220
+ get(dTag) {
13221
+ return this.records.get(dTag);
13222
+ }
13223
+ /**
13224
+ * Atomically confirm that a resolved target still belongs to the exact record
13225
+ * selected before an async operation. The record object is the catalog version
13226
+ * token, so even a same-identity replacement cannot pass this check.
13227
+ */
13228
+ validateCurrent(selected, target) {
13229
+ if (this.records.get(selected.dTag) !== selected) return null;
13230
+ return matchesInstalledNappletRecord(selected, target) ? selected : null;
13231
+ }
13232
+ /** Return exact handler candidates derived only from installed manifests. */
13233
+ intentCatalog() {
13234
+ return this.installed().map((record) => manifestToIntentCatalogEntry({
13235
+ dTag: record.dTag,
13236
+ ...record.title === void 0 ? {} : { title: record.title },
13237
+ archetypes: record.archetypes.map((archetype) => ({
13238
+ slug: archetype.slug,
13239
+ convention: archetype.convention,
13240
+ ...archetype.eventKinds === void 0 ? {} : { eventKinds: [...archetype.eventKinds] }
13241
+ }))
13242
+ }));
13243
+ }
13244
+ /** Subscribe to explicit install and artifact-removal changes. */
13245
+ onChanged(listener) {
13246
+ this.listeners.add(listener);
13247
+ return () => this.listeners.delete(listener);
13248
+ }
13249
+ notify(dTag) {
13250
+ for (const listener of this.listeners) listener(dTag);
13251
+ }
13252
+ };
13253
+ function copyPointer(pointer) {
13254
+ if (pointer.type === "naddr") {
13255
+ return Object.freeze({
13256
+ type: "naddr",
13257
+ value: pointer.value,
13258
+ identifier: pointer.identifier,
13259
+ pubkey: pointer.pubkey,
13260
+ kind: pointer.kind,
13261
+ relays: Object.freeze([...pointer.relays])
13262
+ });
13263
+ }
13264
+ return Object.freeze({
13265
+ type: "nevent",
13266
+ value: pointer.value,
13267
+ id: pointer.id,
13268
+ ...pointer.author === void 0 ? {} : { author: pointer.author },
13269
+ ...pointer.kind === void 0 ? {} : { kind: pointer.kind },
13270
+ relays: Object.freeze([...pointer.relays])
13271
+ });
13272
+ }
13273
+ function freezeRecord(record) {
13274
+ return Object.freeze({
13275
+ ...record,
13276
+ archetypes: Object.freeze(record.archetypes.map((archetype) => Object.freeze({
13277
+ ...archetype,
13278
+ ...archetype.eventKinds === void 0 ? {} : { eventKinds: Object.freeze([...archetype.eventKinds]) }
13279
+ })))
13280
+ });
13281
+ }
13282
+
12151
13283
  // src/simulation.ts
12152
13284
  var PajaSimulationError = class extends Error {
12153
13285
  constructor(message) {
@@ -12725,10 +13857,10 @@ var AbstractRelay = class {
12725
13857
  this.challenge = void 0;
12726
13858
  this.authPromise = void 0;
12727
13859
  this.skipReconnection = false;
12728
- this.connectionPromise = new Promise((resolve, reject) => {
13860
+ this.connectionPromise = new Promise((resolve, reject2) => {
12729
13861
  if (opts?.timeout) {
12730
13862
  connectionTimeoutHandle = setTimeout(() => {
12731
- reject("connection timed out");
13863
+ reject2("connection timed out");
12732
13864
  this.connectionPromise = void 0;
12733
13865
  this.skipReconnection = true;
12734
13866
  this.onclose?.();
@@ -12736,13 +13868,13 @@ var AbstractRelay = class {
12736
13868
  }, opts.timeout);
12737
13869
  }
12738
13870
  if (opts?.abort) {
12739
- opts.abort.onabort = reject;
13871
+ opts.abort.onabort = reject2;
12740
13872
  }
12741
13873
  try {
12742
13874
  this.ws = new this._WebSocket(this.url);
12743
13875
  } catch (err) {
12744
13876
  clearTimeout(connectionTimeoutHandle);
12745
- reject(err);
13877
+ reject2(err);
12746
13878
  return;
12747
13879
  }
12748
13880
  this.ws.onopen = () => {
@@ -12772,7 +13904,7 @@ var AbstractRelay = class {
12772
13904
  };
12773
13905
  this.ws.onerror = () => {
12774
13906
  clearTimeout(connectionTimeoutHandle);
12775
- reject("connection failed");
13907
+ reject2("connection failed");
12776
13908
  this.connectionPromise = void 0;
12777
13909
  this.skipReconnection = true;
12778
13910
  this.onclose?.();
@@ -12780,7 +13912,7 @@ var AbstractRelay = class {
12780
13912
  };
12781
13913
  this.ws.onclose = (ev) => {
12782
13914
  clearTimeout(connectionTimeoutHandle);
12783
- reject(ev.message || "websocket closed");
13915
+ reject2(ev.message || "websocket closed");
12784
13916
  this.handleHardClose("relay connection closed");
12785
13917
  };
12786
13918
  this.ws.onmessage = this._onmessage.bind(this);
@@ -12795,9 +13927,9 @@ var AbstractRelay = class {
12795
13927
  });
12796
13928
  }
12797
13929
  waitForDummyReq() {
12798
- return new Promise((resolve, reject) => {
13930
+ return new Promise((resolve, reject2) => {
12799
13931
  if (!this.connectionPromise)
12800
- return reject(new Error(`no connection to ${this.url}, can't ping`));
13932
+ return reject2(new Error(`no connection to ${this.url}, can't ping`));
12801
13933
  try {
12802
13934
  const sub = this.subscribe(
12803
13935
  [{ ids: ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], limit: 0 }],
@@ -12814,7 +13946,7 @@ var AbstractRelay = class {
12814
13946
  }
12815
13947
  );
12816
13948
  } catch (err) {
12817
- reject(err);
13949
+ reject2(err);
12818
13950
  }
12819
13951
  });
12820
13952
  }
@@ -12844,7 +13976,7 @@ var AbstractRelay = class {
12844
13976
  throw new Error("can't perform auth, no challenge was received");
12845
13977
  if (this.authPromise)
12846
13978
  return this.authPromise;
12847
- this.authPromise = new Promise(async (resolve, reject) => {
13979
+ this.authPromise = new Promise(async (resolve, reject2) => {
12848
13980
  try {
12849
13981
  let evt = await signAuthEvent(makeAuthEvent(this.url, challenge2));
12850
13982
  let timeout = setTimeout(() => {
@@ -12854,7 +13986,7 @@ var AbstractRelay = class {
12854
13986
  this.openEventPublishes.delete(evt.id);
12855
13987
  }
12856
13988
  }, this.publishTimeout);
12857
- this.openEventPublishes.set(evt.id, { resolve, reject, timeout });
13989
+ this.openEventPublishes.set(evt.id, { resolve, reject: reject2, timeout });
12858
13990
  this.send('["AUTH",' + JSON.stringify(evt) + "]");
12859
13991
  } catch (err) {
12860
13992
  console.warn("subscribe auth function failed:", err);
@@ -12865,7 +13997,7 @@ var AbstractRelay = class {
12865
13997
  async publish(event) {
12866
13998
  this.idleSince = void 0;
12867
13999
  this.ongoingOperations++;
12868
- const ret = new Promise((resolve, reject) => {
14000
+ const ret = new Promise((resolve, reject2) => {
12869
14001
  const timeout = setTimeout(() => {
12870
14002
  const ep = this.openEventPublishes.get(event.id);
12871
14003
  if (ep) {
@@ -12873,7 +14005,7 @@ var AbstractRelay = class {
12873
14005
  this.openEventPublishes.delete(event.id);
12874
14006
  }
12875
14007
  }, this.publishTimeout);
12876
- this.openEventPublishes.set(event.id, { resolve, reject, timeout });
14008
+ this.openEventPublishes.set(event.id, { resolve, reject: reject2, timeout });
12877
14009
  });
12878
14010
  this.send('["EVENT",' + JSON.stringify(event) + "]");
12879
14011
  this.ongoingOperations--;
@@ -12884,8 +14016,8 @@ var AbstractRelay = class {
12884
14016
  async count(filters, params) {
12885
14017
  this.serial++;
12886
14018
  const id = params?.id || "count:" + this.serial;
12887
- const ret = new Promise((resolve, reject) => {
12888
- this.openCountRequests.set(id, { resolve, reject });
14019
+ const ret = new Promise((resolve, reject2) => {
14020
+ this.openCountRequests.set(id, { resolve, reject: reject2 });
12889
14021
  });
12890
14022
  this.send('["COUNT","' + id + '",' + JSON.stringify(filters).substring(1));
12891
14023
  return ret;
@@ -13472,9 +14604,8 @@ async function publishLive(pool, relayUrls, event) {
13472
14604
  const results = await Promise.allSettled(pool.publish(relayUrls, event));
13473
14605
  return Object.fromEntries(relayUrls.map((relayUrl, index) => [relayUrl, results[index]?.status === "fulfilled"]));
13474
14606
  }
13475
- function createPajaRelayBackend(getSimulation, confirmRequest) {
14607
+ function createPajaRelayBackend(getSimulation, confirmRequest, livePool = new SimplePool()) {
13476
14608
  const events = getSimulation().relay.fixtures.flatMap(toNostrEvent);
13477
- const livePool = new SimplePool();
13478
14609
  const subscribers = /* @__PURE__ */ new Set();
13479
14610
  const isAvailable = () => getSimulation().relay.mode !== "disabled" && getPajaRelayUrls(getSimulation()).length > 0;
13480
14611
  const query = async (relayUrls, filters, maxWaitMs) => {
@@ -13485,6 +14616,28 @@ function createPajaRelayBackend(getSimulation, confirmRequest) {
13485
14616
  for (const event of [...memoryEvents, ...liveEvents]) out.set(event.id, event);
13486
14617
  return [...out.values()].sort((a, b) => b.created_at - a.created_at);
13487
14618
  };
14619
+ function retainPublishedEvent(event) {
14620
+ events.push(event);
14621
+ for (const subscriber of subscribers) {
14622
+ if (matchesAnyFilter2(event, subscriber.filters)) subscriber.next(event);
14623
+ }
14624
+ }
14625
+ async function attemptPublish(relayUrls, event) {
14626
+ const simulation = getSimulation();
14627
+ const rejected = Object.fromEntries(relayUrls.map((url) => [url, false]));
14628
+ if (simulation.relay.mode === "disabled") {
14629
+ return { outcomes: rejected, error: "relay unavailable" };
14630
+ }
14631
+ if (!confirmRequest({ action: "publish", event })) {
14632
+ return { outcomes: rejected, error: "publish denied" };
14633
+ }
14634
+ const outcomes = simulation.relay.mode === "live" ? await publishLive(livePool, relayUrls, event) : Object.fromEntries(relayUrls.map((url) => [url, true]));
14635
+ if (!Object.values(outcomes).some(Boolean)) {
14636
+ return { outcomes, error: "publish failed" };
14637
+ }
14638
+ retainPublishedEvent(event);
14639
+ return { outcomes };
14640
+ }
13488
14641
  const backend = {
13489
14642
  subscription(relayUrls, filtersInput) {
13490
14643
  return {
@@ -13509,16 +14662,9 @@ function createPajaRelayBackend(getSimulation, confirmRequest) {
13509
14662
  }
13510
14663
  };
13511
14664
  },
13512
- publish(relayUrls, event) {
13513
- if (getSimulation().relay.mode === "disabled") return;
13514
- if (!confirmRequest({ action: "publish", event })) return;
13515
- events.push(event);
13516
- for (const subscriber of subscribers) {
13517
- if (matchesAnyFilter2(event, subscriber.filters)) subscriber.next(event);
13518
- }
13519
- if (getSimulation().relay.mode === "live") {
13520
- void publishLive(livePool, relayUrls, event);
13521
- }
14665
+ async publish(relayUrls, event) {
14666
+ const attempt = await attemptPublish(relayUrls, event);
14667
+ if (attempt.error) throw new Error(attempt.error);
13522
14668
  },
13523
14669
  request(relayUrls, filtersInput) {
13524
14670
  return {
@@ -13538,14 +14684,7 @@ function createPajaRelayBackend(getSimulation, confirmRequest) {
13538
14684
  },
13539
14685
  query,
13540
14686
  async publishToRelays(relayUrls, event) {
13541
- if (getSimulation().relay.mode === "disabled") return Object.fromEntries(relayUrls.map((url) => [url, false]));
13542
- if (!confirmRequest({ action: "publish", event })) return Object.fromEntries(relayUrls.map((url) => [url, false]));
13543
- events.push(event);
13544
- for (const subscriber of subscribers) {
13545
- if (matchesAnyFilter2(event, subscriber.filters)) subscriber.next(event);
13546
- }
13547
- if (getSimulation().relay.mode !== "live") return Object.fromEntries(relayUrls.map((url) => [url, true]));
13548
- return publishLive(livePool, relayUrls, event);
14687
+ return (await attemptPublish(relayUrls, event)).outcomes;
13549
14688
  },
13550
14689
  isAvailable,
13551
14690
  close() {
@@ -13666,7 +14805,6 @@ function createPajaIdentityProviders(backend, getSimulation, signerProvider) {
13666
14805
  }
13667
14806
 
13668
14807
  // src/browser-adapter.ts
13669
- var DEV_INTENT_ARCHETYPE = "paja-target";
13670
14808
  var DEV_COMMON_PUBKEY = "1".repeat(64);
13671
14809
  var DEV_COMMON_EVENT_ID = "2".repeat(64);
13672
14810
  var DEV_SIGNER_SECRET_KEY = generateSecretKey();
@@ -13693,10 +14831,14 @@ function createRelayHooks(pool, getSimulation) {
13693
14831
  },
13694
14832
  closeScopedRelay: () => {
13695
14833
  },
13696
- publishToScopedRelay: (_windowId, event) => {
14834
+ publishToScopedRelay: async (_windowId, event) => {
13697
14835
  if (getSimulation().relay.mode === "disabled") return false;
13698
- pool.publish(getPajaRelayUrls(getSimulation()), event);
13699
- return true;
14836
+ try {
14837
+ await pool.publish(getPajaRelayUrls(getSimulation()), event);
14838
+ return true;
14839
+ } catch {
14840
+ return false;
14841
+ }
13700
14842
  },
13701
14843
  selectRelayTier: () => getPajaRelayUrls(getSimulation())
13702
14844
  };
@@ -13749,18 +14891,15 @@ function createDevUploader(getSimulation) {
13749
14891
  }
13750
14892
  };
13751
14893
  }
13752
- function createDevIntentAvailability() {
14894
+ function createDefaultIntentHost() {
13753
14895
  return {
13754
- archetype: DEV_INTENT_ARCHETYPE,
13755
- available: true,
13756
- hasDefault: true,
13757
- candidates: [{
13758
- dTag: "dev-target",
13759
- title: "Dev runtime target",
13760
- actions: ["open"],
13761
- protocols: ["NAP-01"],
13762
- isDefault: true
13763
- }]
14896
+ catalog: new InstalledNappletCatalog(),
14897
+ controller: new BrowserIntentController({
14898
+ openOrReuse: () => null,
14899
+ waitForReady: () => void 0,
14900
+ isCurrent: () => false,
14901
+ send: () => void 0
14902
+ })
13764
14903
  };
13765
14904
  }
13766
14905
  function createDevCvmTransport(getSimulation) {
@@ -13853,12 +14992,11 @@ function createRuntimeSigner(getSimulation, confirmRequest, signerProvider) {
13853
14992
  }
13854
14993
  return signer;
13855
14994
  }
13856
- function createDevServices(backend, getSimulation, onThemeService, confirmRequest, uploadRuntime, signerProvider) {
14995
+ function createDevServices(backend, getSimulation, onThemeService, onThemeBroadcast, confirmRequest, uploadRuntime, signerProvider, intentHost = createDefaultIntentHost()) {
13857
14996
  const notification = createNotificationService({ maxPerWindow: 50 });
13858
14997
  const theme = createThemeService({
13859
14998
  initialTheme: createDevTheme(getSimulation().theme.mode, getSimulation().theme.values),
13860
- onBroadcast: () => {
13861
- }
14999
+ onBroadcast: onThemeBroadcast
13862
15000
  });
13863
15001
  onThemeService(theme);
13864
15002
  const config = createConfigService({
@@ -13938,26 +15076,15 @@ function createDevServices(backend, getSimulation, onThemeService, confirmReques
13938
15076
  });
13939
15077
  }
13940
15078
  if (getSimulation().intent.enabled) {
15079
+ const resolver = createCatalogIntentResolver({
15080
+ loadCatalog: () => intentHost.catalog.intentCatalog(),
15081
+ targets: intentHost.controller,
15082
+ getDefaultHandler: intentHost.getDefaultHandler,
15083
+ chooseHandler: intentHost.chooseHandler,
15084
+ authorizeExplicitHandler: intentHost.authorizeExplicitHandler
15085
+ });
13941
15086
  services.intent = createIntentService({
13942
- resolver: {
13943
- invoke(request) {
13944
- return {
13945
- ok: true,
13946
- archetype: request.archetype,
13947
- action: request.action ?? "open",
13948
- handled: request.archetype === DEV_INTENT_ARCHETYPE,
13949
- handler: "dev-target",
13950
- windowId: "kehto-paja-window",
13951
- protocol: request.protocol ?? "NAP-01"
13952
- };
13953
- },
13954
- available: (archetype) => ({
13955
- ...createDevIntentAvailability(),
13956
- archetype,
13957
- available: archetype === DEV_INTENT_ARCHETYPE
13958
- }),
13959
- handlers: () => [createDevIntentAvailability()]
13960
- }
15087
+ resolver
13961
15088
  });
13962
15089
  }
13963
15090
  if (getSimulation().capabilities.domains.link) {
@@ -13999,7 +15126,7 @@ function createDevServices(backend, getSimulation, onThemeService, confirmReques
13999
15126
  }
14000
15127
  return services;
14001
15128
  }
14002
- function createPajaAdapter(config, getSimulation, onThemeService, confirmRequest, signerProvider, getIdentity) {
15129
+ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadcast, confirmRequest, signerProvider, getIdentity, onEnvironmentChanged, intentHost) {
14003
15130
  const relayBackend = createPajaRelayBackend(getSimulation, confirmRequest);
14004
15131
  const uploadRuntime = getSimulation().upload.mode === "blossom" ? createPajaUploadRuntime({
14005
15132
  getSimulation,
@@ -14015,7 +15142,11 @@ function createPajaAdapter(config, getSimulation, onThemeService, confirmRequest
14015
15142
  subscribeSignerChange: signerProvider?.subscribe?.bind(signerProvider)
14016
15143
  }) : void 0;
14017
15144
  void uploadRuntime?.refreshIdentity();
15145
+ signerProvider?.subscribe?.(() => {
15146
+ void uploadRuntime?.refreshIdentity().finally(() => onEnvironmentChanged?.());
15147
+ });
14018
15148
  const workerRelayEvents = [];
15149
+ const resolvedIntentHost = intentHost ?? createDefaultIntentHost();
14019
15150
  return {
14020
15151
  relayPool: createRelayHooks(relayBackend, getSimulation),
14021
15152
  relayConfig: {
@@ -14034,7 +15165,16 @@ function createPajaAdapter(config, getSimulation, onThemeService, confirmRequest
14034
15165
  getUserPubkey: () => getRuntimePubkey(getSimulation, signerProvider),
14035
15166
  getSigner: () => createRuntimeSigner(getSimulation, confirmRequest, signerProvider)
14036
15167
  },
14037
- services: createDevServices(relayBackend, getSimulation, onThemeService, confirmRequest, uploadRuntime, signerProvider),
15168
+ services: createDevServices(
15169
+ relayBackend,
15170
+ getSimulation,
15171
+ onThemeService,
15172
+ onThemeBroadcast,
15173
+ confirmRequest,
15174
+ uploadRuntime,
15175
+ signerProvider,
15176
+ resolvedIntentHost
15177
+ ),
14038
15178
  get capabilities() {
14039
15179
  return { disabledDomains: getSimulation().capabilities.disabledDomains };
14040
15180
  },
@@ -14052,103 +15192,342 @@ function createPajaAdapter(config, getSimulation, onThemeService, confirmRequest
14052
15192
  webrtc: { isAvailable: () => getSimulation().capabilities.domains.webrtc },
14053
15193
  crypto: {
14054
15194
  verifyEvent: async () => true
14055
- },
14056
- onNip5dIframeCreate: () => {
14057
- const identity = getIdentity?.();
14058
- return {
14059
- dTag: identity?.dTag ?? config.window.dTag,
14060
- aggregateHash: identity?.aggregateHash ?? config.window.aggregateHash
14061
- };
14062
15195
  }
14063
15196
  };
14064
15197
  }
14065
15198
 
14066
- // ../../node_modules/.pnpm/@noble+ciphers@2.1.1/node_modules/@noble/ciphers/aes.js
14067
- var BLOCK_SIZE = 16;
14068
- var POLY = 283;
14069
- function validateKeyLength(key) {
14070
- if (![16, 24, 32].includes(key.length))
14071
- throw new Error('"aes key" expected Uint8Array of length 16/24/32, got length=' + key.length);
15199
+ // src/browser-devtools.ts
15200
+ var PAJA_LOG_LIMIT = 500;
15201
+ var proxyToReal = /* @__PURE__ */ new WeakMap();
15202
+ var realToProxy = /* @__PURE__ */ new WeakMap();
15203
+ var proxyContexts = /* @__PURE__ */ new WeakMap();
15204
+ function isNappletMessage(value) {
15205
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string";
14072
15206
  }
14073
- function mul2(n) {
14074
- return n << 1 ^ POLY & -(n >> 7);
15207
+ function describeMessage(raw) {
15208
+ if (isNappletMessage(raw)) return raw.type;
15209
+ if (Array.isArray(raw) && typeof raw[0] === "string") return raw[0];
15210
+ return "unknown";
14075
15211
  }
14076
- function mul(a, b) {
14077
- let res = 0;
14078
- for (; b > 0; b >>= 1) {
14079
- res ^= a & -(b & 1);
14080
- a = mul2(a);
15212
+ function previewMessage(raw) {
15213
+ try {
15214
+ return JSON.stringify(raw);
15215
+ } catch (error) {
15216
+ return `unserializable:${error instanceof Error ? error.message : String(error)}`;
14081
15217
  }
14082
- return res;
14083
15218
  }
14084
- var sbox = /* @__PURE__ */ (() => {
14085
- const t = new Uint8Array(256);
14086
- for (let i4 = 0, x = 1; i4 < 256; i4++, x ^= mul2(x))
14087
- t[i4] = x;
14088
- const box = new Uint8Array(256);
14089
- box[0] = 99;
14090
- for (let i4 = 0; i4 < 255; i4++) {
14091
- let x = t[255 - i4];
14092
- x |= x << 8;
14093
- box[t[i4]] = (x ^ x >> 4 ^ x >> 5 ^ x >> 6 ^ x >> 7 ^ 99) & 255;
15219
+ function compactMessageDetail(raw) {
15220
+ const detail = {};
15221
+ for (const [key, value] of Object.entries(raw)) {
15222
+ if (key !== "type" && key !== "id" && value !== void 0) detail[key] = value;
14094
15223
  }
14095
- clean2(t);
14096
- return box;
14097
- })();
14098
- var invSbox = /* @__PURE__ */ sbox.map((_, j) => sbox.indexOf(j));
14099
- var rotr32_8 = (n) => n << 24 | n >>> 8;
14100
- var rotl32_8 = (n) => n << 8 | n >>> 24;
14101
- function genTtable(sbox2, fn) {
14102
- if (sbox2.length !== 256)
14103
- throw new Error("Wrong sbox length");
14104
- const T0 = new Uint32Array(256).map((_, j) => fn(sbox2[j]));
14105
- const T1 = T0.map(rotl32_8);
14106
- const T2 = T1.map(rotl32_8);
14107
- const T3 = T2.map(rotl32_8);
14108
- const T01 = new Uint32Array(256 * 256);
14109
- const T23 = new Uint32Array(256 * 256);
14110
- const sbox22 = new Uint16Array(256 * 256);
14111
- for (let i4 = 0; i4 < 256; i4++) {
14112
- for (let j = 0; j < 256; j++) {
14113
- const idx = i4 * 256 + j;
14114
- T01[idx] = T0[i4] ^ T1[j];
14115
- T23[idx] = T2[i4] ^ T3[j];
14116
- sbox22[idx] = sbox2[i4] << 8 | sbox2[j];
14117
- }
15224
+ if (Object.keys(detail).length === 0) return "";
15225
+ try {
15226
+ return JSON.stringify(detail);
15227
+ } catch {
15228
+ return "";
14118
15229
  }
14119
- return { sbox: sbox2, sbox2: sbox22, T0, T1, T2, T3, T01, T23 };
14120
15230
  }
14121
- var tableEncoding = /* @__PURE__ */ genTtable(sbox, (s) => mul(s, 3) << 24 | s << 16 | s << 8 | mul(s, 2));
14122
- var tableDecoding = /* @__PURE__ */ genTtable(invSbox, (s) => mul(s, 11) << 24 | mul(s, 13) << 16 | mul(s, 9) << 8 | mul(s, 14));
14123
- var xPowers = /* @__PURE__ */ (() => {
14124
- const p = new Uint8Array(16);
14125
- for (let i4 = 0, x = 1; i4 < 16; i4++, x = mul2(x))
14126
- p[i4] = x;
14127
- return p;
14128
- })();
14129
- function expandKeyLE(key) {
14130
- abytes3(key);
14131
- const len = key.length;
14132
- validateKeyLength(key);
14133
- const { sbox2 } = tableEncoding;
14134
- const toClean = [];
14135
- if (!isAligned32(key))
14136
- toClean.push(key = copyBytes2(key));
14137
- const k32 = u32(key);
14138
- const Nk = k32.length;
14139
- const subByte = (n) => applySbox(sbox2, n, n, n, n);
14140
- const xk = new Uint32Array(len + 28);
14141
- xk.set(k32);
14142
- for (let i4 = Nk; i4 < xk.length; i4++) {
14143
- let t = xk[i4 - 1];
14144
- if (i4 % Nk === 0)
14145
- t = subByte(rotr32_8(t)) ^ xPowers[i4 / Nk - 1];
14146
- else if (Nk > 6 && i4 % Nk === 4)
14147
- t = subByte(t);
14148
- xk[i4] = xk[i4 - Nk] ^ t;
15231
+ function describeMessageDetail(raw) {
15232
+ if (!isNappletMessage(raw)) return "";
15233
+ const value = raw;
15234
+ for (const key of ["error", "message", "reason"]) {
15235
+ const detail = value[key];
15236
+ if (typeof detail === "string" && detail.length > 0) return detail;
14149
15237
  }
14150
- clean2(...toClean);
14151
- return xk;
15238
+ return raw.type.endsWith(".error") ? compactMessageDetail(value) : "";
15239
+ }
15240
+ function domainEnabled(simulation, domain) {
15241
+ return simulation.capabilities.domains[domain] === true;
15242
+ }
15243
+ function getTargetIdentity(config) {
15244
+ return {
15245
+ pubkey: "",
15246
+ dTag: config.window.dTag,
15247
+ aggregateHash: config.window.aggregateHash
15248
+ };
15249
+ }
15250
+ function appendPajaMessageLog(state, direction, raw, windowId) {
15251
+ if (!state) return;
15252
+ state.messageLog.push({
15253
+ index: state.messageLog.length,
15254
+ timestamp: Date.now(),
15255
+ direction,
15256
+ type: describeMessage(raw),
15257
+ windowId,
15258
+ preview: previewMessage(raw),
15259
+ detail: describeMessageDetail(raw)
15260
+ });
15261
+ if (state.messageLog.length > PAJA_LOG_LIMIT) {
15262
+ state.messageLog.splice(0, state.messageLog.length - PAJA_LOG_LIMIT);
15263
+ }
15264
+ renderPajaMessageLog(state);
15265
+ }
15266
+ function createPajaPostMessageProxy(realWin, state, windowId) {
15267
+ const existing = realToProxy.get(realWin);
15268
+ if (existing) {
15269
+ proxyContexts.set(existing, { state, windowId });
15270
+ return existing;
15271
+ }
15272
+ const proxy = new Proxy(realWin, {
15273
+ get(target, prop) {
15274
+ if (prop === "postMessage") {
15275
+ return (msg, targetOrigin, transfer) => {
15276
+ const context = proxyContexts.get(proxy);
15277
+ appendPajaMessageLog(
15278
+ context?.state ?? state,
15279
+ "shell->napplet",
15280
+ msg,
15281
+ context?.windowId ?? windowId
15282
+ );
15283
+ return target.postMessage(msg, targetOrigin, transfer);
15284
+ };
15285
+ }
15286
+ try {
15287
+ const val = Reflect.get(target, prop, target);
15288
+ return typeof val === "function" ? val.bind(target) : val;
15289
+ } catch {
15290
+ return void 0;
15291
+ }
15292
+ }
15293
+ });
15294
+ proxyToReal.set(proxy, realWin);
15295
+ realToProxy.set(realWin, proxy);
15296
+ proxyContexts.set(proxy, { state, windowId });
15297
+ return proxy;
15298
+ }
15299
+ function installPajaOriginRegistryProxy(originRegistry2, stateRef) {
15300
+ const originalGetIframeWindow = originRegistry2.getIframeWindow.bind(originRegistry2);
15301
+ originRegistry2.getIframeWindow = (windowId) => {
15302
+ const win = originalGetIframeWindow(windowId);
15303
+ if (!win) return null;
15304
+ return createPajaPostMessageProxy(win, stateRef(), windowId);
15305
+ };
15306
+ const originalGetWindowId = originRegistry2.getWindowId.bind(originRegistry2);
15307
+ originRegistry2.getWindowId = (win) => {
15308
+ const result2 = originalGetWindowId(win);
15309
+ if (result2) return result2;
15310
+ const real = proxyToReal.get(win);
15311
+ return real ? originalGetWindowId(real) : void 0;
15312
+ };
15313
+ const originalGetIdentity = originRegistry2.getIdentity.bind(originRegistry2);
15314
+ originRegistry2.getIdentity = (win) => {
15315
+ const real = proxyToReal.get(win);
15316
+ return originalGetIdentity(win) ?? (real ? originalGetIdentity(real) : void 0);
15317
+ };
15318
+ const originalGetRegistrationId = originRegistry2.getRegistrationId.bind(originRegistry2);
15319
+ originRegistry2.getRegistrationId = (win) => {
15320
+ const real = proxyToReal.get(win);
15321
+ return originalGetRegistrationId(win) ?? (real ? originalGetRegistrationId(real) : void 0);
15322
+ };
15323
+ const originalGetEnvironment = originRegistry2.getEnvironment.bind(originRegistry2);
15324
+ originRegistry2.getEnvironment = (win) => {
15325
+ const real = proxyToReal.get(win);
15326
+ return originalGetEnvironment(win) ?? (real ? originalGetEnvironment(real) : void 0);
15327
+ };
15328
+ }
15329
+ function renderPajaDevtools(state, options) {
15330
+ renderInterfaceControls(state);
15331
+ renderAclControls(state, options.bridge);
15332
+ renderSignerStatus(state, options.devSignerPubkey);
15333
+ }
15334
+ function renderPajaMessageLog(state) {
15335
+ const container = document.getElementById("message-log");
15336
+ if (!container) return;
15337
+ const filter = state.messageFilter.trim().toLowerCase();
15338
+ const rows = state.messageLog.filter((entry) => filter.length === 0 || `${entry.direction} ${entry.type} ${entry.detail} ${entry.preview}`.toLowerCase().includes(filter)).slice(-120).map((entry) => {
15339
+ const row = document.createElement("div");
15340
+ row.className = "log-row";
15341
+ row.dataset.messageType = entry.type;
15342
+ if (entry.type.endsWith(".error")) row.dataset.error = "true";
15343
+ const dir = document.createElement("div");
15344
+ dir.className = "log-dir";
15345
+ dir.textContent = entry.direction;
15346
+ const body = document.createElement("div");
15347
+ body.className = "log-body";
15348
+ const type = document.createElement("div");
15349
+ type.className = "log-type";
15350
+ type.title = entry.preview;
15351
+ type.textContent = entry.type;
15352
+ body.append(type);
15353
+ if (entry.detail) {
15354
+ const detail = document.createElement("div");
15355
+ detail.className = "log-detail";
15356
+ detail.textContent = entry.detail;
15357
+ detail.title = entry.preview;
15358
+ body.append(detail);
15359
+ }
15360
+ row.append(dir, body);
15361
+ return row;
15362
+ });
15363
+ container.replaceChildren(...rows);
15364
+ container.scrollTop = container.scrollHeight;
15365
+ }
15366
+ function renderInterfaceControls(state) {
15367
+ const container = document.getElementById("interface-toggles");
15368
+ if (!container) return;
15369
+ container.replaceChildren(...PAJA_SIMULATION_DOMAINS.map((domain) => {
15370
+ const enabled = domainEnabled(state.simulation, domain);
15371
+ const button = document.createElement("button");
15372
+ button.type = "button";
15373
+ button.className = "toggle";
15374
+ button.dataset.interfaceDomain = domain;
15375
+ button.dataset.enabled = String(enabled);
15376
+ button.textContent = domain;
15377
+ button.title = `${domain} injection ${enabled ? "enabled" : "disabled"}`;
15378
+ button.addEventListener("click", () => {
15379
+ state.setDomainEnabled(domain, !domainEnabled(state.simulation, domain));
15380
+ });
15381
+ return button;
15382
+ }));
15383
+ }
15384
+ function renderAclControls(state, bridge) {
15385
+ const container = document.getElementById("acl-controls");
15386
+ if (!container) return;
15387
+ const identity = getTargetIdentity(state.config);
15388
+ container.replaceChildren(...ALL_CAPABILITIES.map((capability) => {
15389
+ const allowed2 = bridge?.runtime.aclState.check(
15390
+ identity.pubkey,
15391
+ identity.dTag,
15392
+ identity.aggregateHash,
15393
+ capability
15394
+ ) ?? true;
15395
+ const button = document.createElement("button");
15396
+ button.type = "button";
15397
+ button.className = "toggle";
15398
+ button.dataset.aclCapability = capability;
15399
+ button.dataset.enabled = String(allowed2);
15400
+ button.textContent = capability;
15401
+ button.title = `${capability} ${allowed2 ? "allowed" : "blocked"}`;
15402
+ button.addEventListener("click", () => state.setAclCapability(capability, !allowed2));
15403
+ return button;
15404
+ }));
15405
+ }
15406
+ function renderSignerStatus(state, signerPubkey) {
15407
+ const el = document.getElementById("signer-status");
15408
+ const controls = document.getElementById("signer-controls");
15409
+ if (!el || !controls) return;
15410
+ const fallbackPubkey = state.simulation.identity.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
15411
+ const pubkey = state.simulation.identity.pubkey || state.signer.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
15412
+ const methodLabel = state.signer.method === "nip07" ? "NIP-07" : state.signer.method === "nip46" ? "bunker" : state.signer.method === "dev" ? "dev" : "none";
15413
+ const relay = state.signer.relay ? ` \xB7 relay ${state.signer.relay}` : "";
15414
+ const prefix = state.signer.status === "disconnected" ? "no signer connected" : state.signer.status === "error" ? `${methodLabel} error: ${state.signer.error ?? "unknown error"} \xB7 fallback pubkey ${fallbackPubkey}` : `${methodLabel} ${state.signer.status} \xB7 pubkey ${pubkey || "(none)"}${relay}`;
15415
+ el.textContent = `${prefix} \xB7 every sign/publish request prompts`;
15416
+ const previousInput = controls.querySelector("#signer-bunker-uri")?.value ?? "";
15417
+ const devButton = document.createElement("button");
15418
+ devButton.type = "button";
15419
+ devButton.id = "signer-dev";
15420
+ devButton.dataset.active = String(state.signer.method === "dev");
15421
+ devButton.textContent = "Dev";
15422
+ devButton.addEventListener("click", () => state.useDevSigner());
15423
+ const nip07Button = document.createElement("button");
15424
+ nip07Button.type = "button";
15425
+ nip07Button.id = "signer-nip07";
15426
+ nip07Button.dataset.active = String(state.signer.method === "nip07");
15427
+ nip07Button.textContent = "NIP-07";
15428
+ nip07Button.addEventListener("click", () => void state.connectNip07());
15429
+ const bunkerInput = document.createElement("input");
15430
+ bunkerInput.id = "signer-bunker-uri";
15431
+ bunkerInput.type = "text";
15432
+ bunkerInput.autocomplete = "off";
15433
+ bunkerInput.placeholder = "bunker://...";
15434
+ bunkerInput.setAttribute("aria-label", "Bunker connection URI");
15435
+ bunkerInput.value = previousInput;
15436
+ const bunkerButton = document.createElement("button");
15437
+ bunkerButton.type = "button";
15438
+ bunkerButton.id = "signer-bunker";
15439
+ bunkerButton.dataset.active = String(state.signer.method === "nip46");
15440
+ bunkerButton.textContent = "Bunker";
15441
+ bunkerButton.addEventListener("click", () => void state.connectBunker(bunkerInput.value));
15442
+ controls.replaceChildren(devButton, nip07Button, bunkerInput, bunkerButton);
15443
+ }
15444
+
15445
+ // ../../node_modules/.pnpm/@noble+ciphers@2.1.1/node_modules/@noble/ciphers/aes.js
15446
+ var BLOCK_SIZE = 16;
15447
+ var POLY = 283;
15448
+ function validateKeyLength(key) {
15449
+ if (![16, 24, 32].includes(key.length))
15450
+ throw new Error('"aes key" expected Uint8Array of length 16/24/32, got length=' + key.length);
15451
+ }
15452
+ function mul2(n) {
15453
+ return n << 1 ^ POLY & -(n >> 7);
15454
+ }
15455
+ function mul(a, b) {
15456
+ let res = 0;
15457
+ for (; b > 0; b >>= 1) {
15458
+ res ^= a & -(b & 1);
15459
+ a = mul2(a);
15460
+ }
15461
+ return res;
15462
+ }
15463
+ var sbox = /* @__PURE__ */ (() => {
15464
+ const t = new Uint8Array(256);
15465
+ for (let i4 = 0, x = 1; i4 < 256; i4++, x ^= mul2(x))
15466
+ t[i4] = x;
15467
+ const box = new Uint8Array(256);
15468
+ box[0] = 99;
15469
+ for (let i4 = 0; i4 < 255; i4++) {
15470
+ let x = t[255 - i4];
15471
+ x |= x << 8;
15472
+ box[t[i4]] = (x ^ x >> 4 ^ x >> 5 ^ x >> 6 ^ x >> 7 ^ 99) & 255;
15473
+ }
15474
+ clean2(t);
15475
+ return box;
15476
+ })();
15477
+ var invSbox = /* @__PURE__ */ sbox.map((_, j) => sbox.indexOf(j));
15478
+ var rotr32_8 = (n) => n << 24 | n >>> 8;
15479
+ var rotl32_8 = (n) => n << 8 | n >>> 24;
15480
+ function genTtable(sbox2, fn) {
15481
+ if (sbox2.length !== 256)
15482
+ throw new Error("Wrong sbox length");
15483
+ const T0 = new Uint32Array(256).map((_, j) => fn(sbox2[j]));
15484
+ const T1 = T0.map(rotl32_8);
15485
+ const T2 = T1.map(rotl32_8);
15486
+ const T3 = T2.map(rotl32_8);
15487
+ const T01 = new Uint32Array(256 * 256);
15488
+ const T23 = new Uint32Array(256 * 256);
15489
+ const sbox22 = new Uint16Array(256 * 256);
15490
+ for (let i4 = 0; i4 < 256; i4++) {
15491
+ for (let j = 0; j < 256; j++) {
15492
+ const idx = i4 * 256 + j;
15493
+ T01[idx] = T0[i4] ^ T1[j];
15494
+ T23[idx] = T2[i4] ^ T3[j];
15495
+ sbox22[idx] = sbox2[i4] << 8 | sbox2[j];
15496
+ }
15497
+ }
15498
+ return { sbox: sbox2, sbox2: sbox22, T0, T1, T2, T3, T01, T23 };
15499
+ }
15500
+ var tableEncoding = /* @__PURE__ */ genTtable(sbox, (s) => mul(s, 3) << 24 | s << 16 | s << 8 | mul(s, 2));
15501
+ var tableDecoding = /* @__PURE__ */ genTtable(invSbox, (s) => mul(s, 11) << 24 | mul(s, 13) << 16 | mul(s, 9) << 8 | mul(s, 14));
15502
+ var xPowers = /* @__PURE__ */ (() => {
15503
+ const p = new Uint8Array(16);
15504
+ for (let i4 = 0, x = 1; i4 < 16; i4++, x = mul2(x))
15505
+ p[i4] = x;
15506
+ return p;
15507
+ })();
15508
+ function expandKeyLE(key) {
15509
+ abytes3(key);
15510
+ const len = key.length;
15511
+ validateKeyLength(key);
15512
+ const { sbox2 } = tableEncoding;
15513
+ const toClean = [];
15514
+ if (!isAligned32(key))
15515
+ toClean.push(key = copyBytes2(key));
15516
+ const k32 = u32(key);
15517
+ const Nk = k32.length;
15518
+ const subByte = (n) => applySbox(sbox2, n, n, n, n);
15519
+ const xk = new Uint32Array(len + 28);
15520
+ xk.set(k32);
15521
+ for (let i4 = Nk; i4 < xk.length; i4++) {
15522
+ let t = xk[i4 - 1];
15523
+ if (i4 % Nk === 0)
15524
+ t = subByte(rotr32_8(t)) ^ xPowers[i4 / Nk - 1];
15525
+ else if (Nk > 6 && i4 % Nk === 4)
15526
+ t = subByte(t);
15527
+ xk[i4] = xk[i4 - Nk] ^ t;
15528
+ }
15529
+ clean2(...toClean);
15530
+ return xk;
14152
15531
  }
14153
15532
  function expandKeyDecLE(key) {
14154
15533
  const encKey = expandKeyLE(key);
@@ -16890,12 +18269,12 @@ async function sendRequestWithId(state, correlationId, method, params) {
16890
18269
  tags: [["p", state.bunkerPubkey]],
16891
18270
  content: encryptedContent
16892
18271
  }, state.localSecretKey);
16893
- return new Promise((resolve, reject) => {
18272
+ return new Promise((resolve, reject2) => {
16894
18273
  const timer = setTimeout(() => {
16895
18274
  state.pending.delete(correlationId);
16896
- reject(new Error(`NIP-46 request timed out: ${method}`));
18275
+ reject2(new Error(`NIP-46 request timed out: ${method}`));
16897
18276
  }, state.timeout);
16898
- state.pending.set(correlationId, { resolve, reject, timer });
18277
+ state.pending.set(correlationId, { resolve, reject: reject2, timer });
16899
18278
  state.ws.send(JSON.stringify(["EVENT", signedEvent]));
16900
18279
  });
16901
18280
  }
@@ -16934,7 +18313,7 @@ function rejectPendingOnClose(state) {
16934
18313
  state.pending.clear();
16935
18314
  }
16936
18315
  function openWebSocket(state) {
16937
- return new Promise((resolve, reject) => {
18316
+ return new Promise((resolve, reject2) => {
16938
18317
  state.ws = new WebSocket(state.relayUrl);
16939
18318
  state.ws.onopen = () => {
16940
18319
  state.ws.send(JSON.stringify([
@@ -16948,7 +18327,7 @@ function openWebSocket(state) {
16948
18327
  handleRelayMessage(state, event.data).catch(() => {
16949
18328
  });
16950
18329
  };
16951
- state.ws.onerror = () => reject(new Error(`NIP-46 WebSocket connection failed: ${state.relayUrl}`));
18330
+ state.ws.onerror = () => reject2(new Error(`NIP-46 WebSocket connection failed: ${state.relayUrl}`));
16952
18331
  state.ws.onclose = () => rejectPendingOnClose(state);
16953
18332
  });
16954
18333
  }
@@ -17123,245 +18502,77 @@ function createPajaSignerController(options) {
17123
18502
  };
17124
18503
  }
17125
18504
 
17126
- // src/browser-devtools.ts
17127
- var PAJA_LOG_LIMIT = 500;
17128
- var proxyToReal = /* @__PURE__ */ new WeakMap();
17129
- var realToProxy = /* @__PURE__ */ new WeakMap();
17130
- var proxyContexts = /* @__PURE__ */ new WeakMap();
17131
- function isNappletMessage(value) {
17132
- return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string";
17133
- }
17134
- function describeMessage(raw) {
17135
- if (isNappletMessage(raw)) return raw.type;
17136
- if (Array.isArray(raw) && typeof raw[0] === "string") return raw[0];
17137
- return "unknown";
17138
- }
17139
- function previewMessage(raw) {
17140
- try {
17141
- return JSON.stringify(raw);
17142
- } catch (error) {
17143
- return `unserializable:${error instanceof Error ? error.message : String(error)}`;
17144
- }
17145
- }
17146
- function compactMessageDetail(raw) {
17147
- const detail = {};
17148
- for (const [key, value] of Object.entries(raw)) {
17149
- if (key !== "type" && key !== "id" && value !== void 0) detail[key] = value;
17150
- }
17151
- if (Object.keys(detail).length === 0) return "";
17152
- try {
17153
- return JSON.stringify(detail);
17154
- } catch {
17155
- return "";
17156
- }
17157
- }
17158
- function describeMessageDetail(raw) {
17159
- if (!isNappletMessage(raw)) return "";
17160
- const value = raw;
17161
- for (const key of ["error", "message", "reason"]) {
17162
- const detail = value[key];
17163
- if (typeof detail === "string" && detail.length > 0) return detail;
17164
- }
17165
- return raw.type.endsWith(".error") ? compactMessageDetail(value) : "";
17166
- }
17167
- function domainEnabled(simulation, domain) {
17168
- return simulation.capabilities.domains[domain] === true;
17169
- }
17170
- function getTargetIdentity(config) {
17171
- return {
17172
- pubkey: "",
17173
- dTag: config.window.dTag,
17174
- aggregateHash: config.window.aggregateHash
17175
- };
17176
- }
17177
- function appendPajaMessageLog(state, direction, raw, windowId) {
17178
- if (!state) return;
17179
- state.messageLog.push({
17180
- index: state.messageLog.length,
17181
- timestamp: Date.now(),
17182
- direction,
17183
- type: describeMessage(raw),
17184
- windowId,
17185
- preview: previewMessage(raw),
17186
- detail: describeMessageDetail(raw)
17187
- });
17188
- if (state.messageLog.length > PAJA_LOG_LIMIT) {
17189
- state.messageLog.splice(0, state.messageLog.length - PAJA_LOG_LIMIT);
17190
- }
17191
- renderPajaMessageLog(state);
17192
- }
17193
- function createPajaPostMessageProxy(realWin, state, windowId) {
17194
- const existing = realToProxy.get(realWin);
17195
- if (existing) {
17196
- proxyContexts.set(existing, { state, windowId });
17197
- return existing;
18505
+ // src/browser-host-signer.ts
18506
+ function confirmPajaRequest(state, request) {
18507
+ if (request.action === "upload") {
18508
+ const filename = request.filename ?? "(unnamed blob)";
18509
+ const mimeType = request.mimeType ?? "application/octet-stream";
18510
+ const allowed3 = window.confirm([
18511
+ "Paja upload request",
18512
+ `napplet: ${request.napplet.dTag} (${request.windowId})`,
18513
+ `file: ${filename}`,
18514
+ `size: ${request.size} bytes`,
18515
+ `type: ${mimeType}`,
18516
+ `server: ${request.server}`,
18517
+ request.warning
18518
+ ].join("\n"));
18519
+ appendPajaMessageLog(state, "paja", {
18520
+ type: `paja.upload.${allowed3 ? "confirmed" : "denied"}`,
18521
+ windowId: request.windowId,
18522
+ dTag: request.napplet.dTag,
18523
+ aggregateHash: request.napplet.aggregateHash,
18524
+ filename,
18525
+ size: request.size,
18526
+ mimeType,
18527
+ server: request.server,
18528
+ warning: request.warning
18529
+ });
18530
+ return allowed3;
17198
18531
  }
17199
- const proxy = new Proxy(realWin, {
17200
- get(target, prop) {
17201
- if (prop === "postMessage") {
17202
- return (msg, targetOrigin, transfer) => {
17203
- const context = proxyContexts.get(proxy);
17204
- appendPajaMessageLog(
17205
- context?.state ?? state,
17206
- "shell->napplet",
17207
- msg,
17208
- context?.windowId ?? windowId
17209
- );
17210
- return target.postMessage(msg, targetOrigin, transfer);
17211
- };
17212
- }
17213
- try {
17214
- const val = Reflect.get(target, prop, target);
17215
- return typeof val === "function" ? val.bind(target) : val;
17216
- } catch {
17217
- return void 0;
17218
- }
17219
- }
18532
+ const event = request.event;
18533
+ const kind = typeof event.kind === "number" ? event.kind : "unknown";
18534
+ const content = typeof event.content === "string" && event.content.length > 0 ? `
18535
+
18536
+ ${event.content.slice(0, 240)}` : "";
18537
+ const allowed2 = window.confirm(`Paja ${request.action} request
18538
+ kind: ${kind}${content}`);
18539
+ appendPajaMessageLog(state, "paja", {
18540
+ type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
18541
+ kind
17220
18542
  });
17221
- proxyToReal.set(proxy, realWin);
17222
- realToProxy.set(realWin, proxy);
17223
- proxyContexts.set(proxy, { state, windowId });
17224
- return proxy;
17225
- }
17226
- function installPajaOriginRegistryProxy(originRegistry2, stateRef) {
17227
- const originalGetIframeWindow = originRegistry2.getIframeWindow.bind(originRegistry2);
17228
- originRegistry2.getIframeWindow = (windowId) => {
17229
- const win = originalGetIframeWindow(windowId);
17230
- if (!win) return null;
17231
- return createPajaPostMessageProxy(win, stateRef(), windowId);
17232
- };
17233
- const originalGetWindowId = originRegistry2.getWindowId.bind(originRegistry2);
17234
- originRegistry2.getWindowId = (win) => {
17235
- const result2 = originalGetWindowId(win);
17236
- if (result2) return result2;
17237
- const real = proxyToReal.get(win);
17238
- return real ? originalGetWindowId(real) : void 0;
17239
- };
17240
- const originalGetIdentity = originRegistry2.getIdentity.bind(originRegistry2);
17241
- originRegistry2.getIdentity = (win) => {
17242
- const real = proxyToReal.get(win);
17243
- return originalGetIdentity(win) ?? (real ? originalGetIdentity(real) : void 0);
17244
- };
17245
- const originalGetRegistrationId = originRegistry2.getRegistrationId.bind(originRegistry2);
17246
- originRegistry2.getRegistrationId = (win) => {
17247
- const real = proxyToReal.get(win);
17248
- return originalGetRegistrationId(win) ?? (real ? originalGetRegistrationId(real) : void 0);
17249
- };
17250
- }
17251
- function renderPajaDevtools(state, options) {
17252
- renderInterfaceControls(state);
17253
- renderAclControls(state, options.bridge);
17254
- renderSignerStatus(state, options.devSignerPubkey);
18543
+ return allowed2;
17255
18544
  }
17256
- function renderPajaMessageLog(state) {
17257
- const container = document.getElementById("message-log");
17258
- if (!container) return;
17259
- const filter = state.messageFilter.trim().toLowerCase();
17260
- const rows = state.messageLog.filter((entry) => filter.length === 0 || `${entry.direction} ${entry.type} ${entry.detail} ${entry.preview}`.toLowerCase().includes(filter)).slice(-120).map((entry) => {
17261
- const row = document.createElement("div");
17262
- row.className = "log-row";
17263
- row.dataset.messageType = entry.type;
17264
- if (entry.type.endsWith(".error")) row.dataset.error = "true";
17265
- const dir = document.createElement("div");
17266
- dir.className = "log-dir";
17267
- dir.textContent = entry.direction;
17268
- const body = document.createElement("div");
17269
- body.className = "log-body";
17270
- const type = document.createElement("div");
17271
- type.className = "log-type";
17272
- type.title = entry.preview;
17273
- type.textContent = entry.type;
17274
- body.append(type);
17275
- if (entry.detail) {
17276
- const detail = document.createElement("div");
17277
- detail.className = "log-detail";
17278
- detail.textContent = entry.detail;
17279
- detail.title = entry.preview;
17280
- body.append(detail);
18545
+ function createHostSignerController(getState, refreshState) {
18546
+ return createPajaSignerController({
18547
+ confirmRequest: (request) => confirmPajaRequest(getState(), request),
18548
+ onChange(signer) {
18549
+ const state = getState();
18550
+ if (!state) return;
18551
+ state.signer = signer;
18552
+ appendPajaMessageLog(state, "paja", {
18553
+ type: `paja.signer.${signer.method}.${signer.status}`,
18554
+ pubkey: signer.pubkey,
18555
+ relay: signer.relay,
18556
+ error: signer.error
18557
+ });
18558
+ refreshState(state);
18559
+ if (signer.status === "connected") state.reload();
17281
18560
  }
17282
- row.append(dir, body);
17283
- return row;
17284
18561
  });
17285
- container.replaceChildren(...rows);
17286
- container.scrollTop = container.scrollHeight;
17287
- }
17288
- function renderInterfaceControls(state) {
17289
- const container = document.getElementById("interface-toggles");
17290
- if (!container) return;
17291
- container.replaceChildren(...PAJA_SIMULATION_DOMAINS.map((domain) => {
17292
- const enabled = domainEnabled(state.simulation, domain);
17293
- const button = document.createElement("button");
17294
- button.type = "button";
17295
- button.className = "toggle";
17296
- button.dataset.interfaceDomain = domain;
17297
- button.dataset.enabled = String(enabled);
17298
- button.textContent = domain;
17299
- button.title = `${domain} injection ${enabled ? "enabled" : "disabled"}`;
17300
- button.addEventListener("click", () => {
17301
- state.setDomainEnabled(domain, !domainEnabled(state.simulation, domain));
17302
- });
17303
- return button;
17304
- }));
17305
- }
17306
- function renderAclControls(state, bridge) {
17307
- const container = document.getElementById("acl-controls");
17308
- if (!container) return;
17309
- const identity = getTargetIdentity(state.config);
17310
- container.replaceChildren(...ALL_CAPABILITIES.map((capability) => {
17311
- const allowed2 = bridge?.runtime.aclState.check(
17312
- identity.pubkey,
17313
- identity.dTag,
17314
- identity.aggregateHash,
17315
- capability
17316
- ) ?? true;
17317
- const button = document.createElement("button");
17318
- button.type = "button";
17319
- button.className = "toggle";
17320
- button.dataset.aclCapability = capability;
17321
- button.dataset.enabled = String(allowed2);
17322
- button.textContent = capability;
17323
- button.title = `${capability} ${allowed2 ? "allowed" : "blocked"}`;
17324
- button.addEventListener("click", () => state.setAclCapability(capability, !allowed2));
17325
- return button;
17326
- }));
17327
18562
  }
17328
- function renderSignerStatus(state, signerPubkey) {
17329
- const el = document.getElementById("signer-status");
17330
- const controls = document.getElementById("signer-controls");
17331
- if (!el || !controls) return;
17332
- const fallbackPubkey = state.simulation.identity.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
17333
- const pubkey = state.simulation.identity.pubkey || state.signer.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
17334
- const methodLabel = state.signer.method === "nip07" ? "NIP-07" : state.signer.method === "nip46" ? "bunker" : state.signer.method === "dev" ? "dev" : "none";
17335
- const relay = state.signer.relay ? ` \xB7 relay ${state.signer.relay}` : "";
17336
- const prefix = state.signer.status === "disconnected" ? "no signer connected" : state.signer.status === "error" ? `${methodLabel} error: ${state.signer.error ?? "unknown error"} \xB7 fallback pubkey ${fallbackPubkey}` : `${methodLabel} ${state.signer.status} \xB7 pubkey ${pubkey || "(none)"}${relay}`;
17337
- el.textContent = `${prefix} \xB7 every sign/publish request prompts`;
17338
- const previousInput = controls.querySelector("#signer-bunker-uri")?.value ?? "";
17339
- const devButton = document.createElement("button");
17340
- devButton.type = "button";
17341
- devButton.id = "signer-dev";
17342
- devButton.dataset.active = String(state.signer.method === "dev");
17343
- devButton.textContent = "Dev";
17344
- devButton.addEventListener("click", () => state.useDevSigner());
17345
- const nip07Button = document.createElement("button");
17346
- nip07Button.type = "button";
17347
- nip07Button.id = "signer-nip07";
17348
- nip07Button.dataset.active = String(state.signer.method === "nip07");
17349
- nip07Button.textContent = "NIP-07";
17350
- nip07Button.addEventListener("click", () => void state.connectNip07());
17351
- const bunkerInput = document.createElement("input");
17352
- bunkerInput.id = "signer-bunker-uri";
17353
- bunkerInput.type = "text";
17354
- bunkerInput.autocomplete = "off";
17355
- bunkerInput.placeholder = "bunker://...";
17356
- bunkerInput.setAttribute("aria-label", "Bunker connection URI");
17357
- bunkerInput.value = previousInput;
17358
- const bunkerButton = document.createElement("button");
17359
- bunkerButton.type = "button";
17360
- bunkerButton.id = "signer-bunker";
17361
- bunkerButton.dataset.active = String(state.signer.method === "nip46");
17362
- bunkerButton.textContent = "Bunker";
17363
- bunkerButton.addEventListener("click", () => void state.connectBunker(bunkerInput.value));
17364
- controls.replaceChildren(devButton, nip07Button, bunkerInput, bunkerButton);
18563
+ function hasNip07Signer() {
18564
+ const signer = globalThis.nostr;
18565
+ return typeof signer === "object" && signer !== null;
18566
+ }
18567
+
18568
+ // src/browser-host-runtime.ts
18569
+ function unregisterSingleFrameWindow(bridge, runtime, windowId) {
18570
+ if (!windowId) return;
18571
+ bridge.runtime.destroyWindow(windowId);
18572
+ bridge.runtime.sessionRegistry.unregister(windowId);
18573
+ originRegistry.unregister(windowId);
18574
+ runtime.readyWindowIds.delete(windowId);
18575
+ if (runtime.currentWindowId === windowId) runtime.currentWindowId = null;
17365
18576
  }
17366
18577
 
17367
18578
  // src/browser-runtime-tabs.ts
@@ -17374,6 +18585,9 @@ function resolvedTargetKey(target) {
17374
18585
  target.aggregateHash
17375
18586
  ].join(":");
17376
18587
  }
18588
+ function runtimeTabGenerationId(tab) {
18589
+ return `${tab.id}:${tab.generation}`;
18590
+ }
17377
18591
  function getActiveTab(state) {
17378
18592
  return state.tabs.find((tab) => tab.id === state.activeTabId) ?? null;
17379
18593
  }
@@ -17619,11 +18833,13 @@ function resolvedTargetTitle(target) {
17619
18833
  return title && title.length > 0 ? title : target.dTag || target.aggregateHash.slice(0, 12);
17620
18834
  }
17621
18835
  function destroyRuntimeTab(tab, context) {
18836
+ context.onTabDestroyed?.(tab);
17622
18837
  if (tab.windowId) {
17623
18838
  context.bridge.runtime.destroyWindow(tab.windowId);
17624
18839
  context.bridge.runtime.sessionRegistry.unregister(tab.windowId);
17625
18840
  originRegistry.unregister(tab.windowId);
17626
18841
  context.runtime.readyWindowIds.delete(tab.windowId);
18842
+ if (context.runtime.currentWindowId === tab.windowId) context.runtime.currentWindowId = null;
17627
18843
  }
17628
18844
  tab.frame.remove();
17629
18845
  }
@@ -17637,18 +18853,6 @@ function createRuntimeTabFrame(tab, state, context) {
17637
18853
  frame.dataset.tabId = tab.id;
17638
18854
  frame.dataset.targetUrl = tab.pointerValue;
17639
18855
  frame.hidden = true;
17640
- frame.addEventListener("load", () => {
17641
- if (tab.status !== "booting" && tab.status !== "reloading") return;
17642
- tab.windowId = context.registerFrameForGeneration(
17643
- context.bridge,
17644
- frame,
17645
- context.config,
17646
- tab.generation,
17647
- tab.resolvedTarget,
17648
- runtimeTabWindowId(context.config, tab)
17649
- );
17650
- if (state.activeTabId === tab.id) context.runtime.currentWindowId = tab.windowId;
17651
- });
17652
18856
  frame.addEventListener("error", () => {
17653
18857
  tab.status = "error";
17654
18858
  if (state.activeTabId === tab.id) context.setStatus(state, "error");
@@ -17662,14 +18866,18 @@ function startRuntimeTabNavigation(tab, state, context) {
17662
18866
  context.setStatus(state, "booting");
17663
18867
  renderRuntimeTabs(state);
17664
18868
  void context.navigateFrame(
17665
- context.bridge,
17666
18869
  tab.frame,
17667
18870
  context.config,
17668
18871
  generation,
17669
- context.capabilities,
17670
- context.runtime.currentSimulation,
18872
+ context.adapter,
17671
18873
  tab.resolvedTarget,
17672
- runtimeTabWindowId(context.config, tab)
18874
+ runtimeTabWindowId(context.config, tab),
18875
+ () => tab.generation === generation,
18876
+ (windowId) => {
18877
+ if (tab.generation !== generation) return;
18878
+ tab.windowId = windowId;
18879
+ if (state.activeTabId === tab.id) context.runtime.currentWindowId = windowId;
18880
+ }
17673
18881
  ).then((windowId) => {
17674
18882
  if (tab.generation !== generation) return;
17675
18883
  tab.windowId = windowId;
@@ -17692,13 +18900,6 @@ function runtimeTabWindowId(config, tab) {
17692
18900
  return `${config.window.id}:${tab.id}:${tab.generation}`;
17693
18901
  }
17694
18902
 
17695
- // src/parity.ts
17696
- var PAJA_LEGACY_COMPATIBILITY_DOMAIN = `i${"fc"}`;
17697
- var PAJA_HANDSHAKE_DOMAINS = ["shell"];
17698
- var PAJA_COMPATIBILITY_ALIASES = {
17699
- [PAJA_LEGACY_COMPATIBILITY_DOMAIN]: "inc"
17700
- };
17701
-
17702
18903
  // ../nip/dist/chunk-JT5WCCCX.js
17703
18904
  function computeAggregateHash(entries) {
17704
18905
  const lines = entries.map((entry) => `${entry.sha256} ${entry.path}
@@ -17724,7 +18925,7 @@ function aggregateTagValue(tags) {
17724
18925
  return void 0;
17725
18926
  }
17726
18927
 
17727
- // ../nip/dist/chunk-NRUVARZL.js
18928
+ // ../nip/dist/chunk-AGRSQG35.js
17728
18929
  var MIB = 1024 * 1024;
17729
18930
  var NAPPLET_ARTIFACT_CACHE_NAME = "kehto:napplet-artifacts:v1";
17730
18931
  var SCHEMA_VERSION = 1;
@@ -18159,10 +19360,58 @@ function archetypesFromTags(tags) {
18159
19360
  const out = [];
18160
19361
  for (const tag of tags) {
18161
19362
  if (tag[0] !== "archetype") continue;
18162
- if (typeof tag[1] !== "string" || tag[1].length === 0) continue;
18163
- out.push(
18164
- typeof tag[2] === "string" && tag[2].length > 0 ? { slug: tag[1], nap: tag[2] } : { slug: tag[1] }
18165
- );
19363
+ const slug = tag[1];
19364
+ if (typeof slug !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
19365
+ throw new NappletResolutionError(
19366
+ "invalid-manifest",
19367
+ "archetype slug must contain lowercase letters, numbers, and hyphens"
19368
+ );
19369
+ }
19370
+ const convention = tag[2];
19371
+ if (typeof convention !== "string" || convention.length === 0) {
19372
+ throw new NappletResolutionError("invalid-manifest", "archetype convention is required");
19373
+ }
19374
+ if (/^NAP-\d+$/.test(convention)) {
19375
+ throw new NappletResolutionError(
19376
+ "invalid-manifest",
19377
+ "numbered NAP identifier is not an archetype convention"
19378
+ );
19379
+ }
19380
+ const conventionMatch = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(convention);
19381
+ if (!conventionMatch) {
19382
+ throw new NappletResolutionError(
19383
+ "invalid-manifest",
19384
+ "archetype convention must be a queryless napplet:<archetype>/<intent> identity"
19385
+ );
19386
+ }
19387
+ if (conventionMatch[1] !== slug) {
19388
+ throw new NappletResolutionError(
19389
+ "invalid-manifest",
19390
+ "archetype slug must match the convention archetype"
19391
+ );
19392
+ }
19393
+ const eventKinds = tag.slice(3).map((value) => {
19394
+ const match = /^kind:(\d+)$/.exec(value);
19395
+ if (!match) {
19396
+ throw new NappletResolutionError(
19397
+ "invalid-manifest",
19398
+ "archetype trailing fields must use kind:<unsigned-integer>"
19399
+ );
19400
+ }
19401
+ const kind = Number(match[1]);
19402
+ if (!Number.isSafeInteger(kind) || kind < 0) {
19403
+ throw new NappletResolutionError(
19404
+ "invalid-manifest",
19405
+ "archetype event kinds must be unsigned safe integers"
19406
+ );
19407
+ }
19408
+ return kind;
19409
+ });
19410
+ out.push({
19411
+ slug,
19412
+ convention,
19413
+ ...eventKinds.length === 0 ? {} : { eventKinds }
19414
+ });
18166
19415
  }
18167
19416
  return out;
18168
19417
  }
@@ -18295,12 +19544,12 @@ async function resolvePajaPointer(value, options = {}) {
18295
19544
  const pointer = decodePajaPointer(value);
18296
19545
  const relays = uniqueRelayUrls([...pointer.relays, ...options.relays ?? []]);
18297
19546
  if (relays.length === 0) {
18298
- throw new Error("Pointer does not include relay hints; add a relay-bearing naddr or nevent.");
19547
+ throw new Error("Pointer has no relay hints or enabled configured live relays.");
18299
19548
  }
18300
19549
  const pool = options.pool ?? new SimplePool();
18301
19550
  const ownsPool = options.pool === void 0;
18302
19551
  try {
18303
- const event = await resolvePointerEvent(pointer, pool, relays, options.maxWaitMs ?? 5e3);
19552
+ const event = await resolvePointerEvent(pointer, pool, relays, normalizeMaxWaitMs(options.maxWaitMs));
18304
19553
  const blossomServers = [...options.blossomServers ?? []];
18305
19554
  const resolved = await resolveNapplet({
18306
19555
  event,
@@ -18329,7 +19578,26 @@ async function resolvePajaPointer(value, options = {}) {
18329
19578
  }
18330
19579
  }
18331
19580
  function injectPajaRuntimeCsp(html, origins) {
18332
- const value = origins.length > 0 ? `connect-src ${[...new Set(origins)].sort().join(" ")}` : "connect-src 'none'";
19581
+ const grantedOrigins = [...new Set(origins)].sort();
19582
+ const connectSrc = grantedOrigins.length > 0 ? `connect-src ${grantedOrigins.join(" ")}` : "connect-src 'none'";
19583
+ const value = [
19584
+ "default-src 'none'",
19585
+ "script-src 'unsafe-inline'",
19586
+ "style-src 'unsafe-inline'",
19587
+ "img-src data: blob:",
19588
+ "font-src data:",
19589
+ connectSrc,
19590
+ "worker-src 'none'",
19591
+ "child-src 'none'",
19592
+ "frame-src 'none'",
19593
+ "media-src 'none'",
19594
+ "object-src 'none'",
19595
+ "manifest-src 'none'",
19596
+ "prefetch-src 'none'",
19597
+ "base-uri 'none'",
19598
+ "form-action 'none'",
19599
+ "frame-ancestors 'self'"
19600
+ ].join("; ");
18333
19601
  const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(value)}">`;
18334
19602
  if (/<head[^>]*>/i.test(html)) {
18335
19603
  return html.replace(/<head[^>]*>/i, (open) => `${open}${meta}`);
@@ -18341,19 +19609,96 @@ function injectPajaRuntimeCsp(html, origins) {
18341
19609
  }
18342
19610
  async function resolvePointerEvent(pointer, pool, relays, maxWaitMs) {
18343
19611
  const filter = pointer.type === "naddr" ? addressableFilter(pointer) : eventFilter(pointer);
18344
- const events = await pool.querySync([...relays], filter, {
18345
- maxWait: maxWaitMs,
18346
- label: "kehto-paja-runtime"
18347
- });
18348
- const event = events.filter((candidate) => matchesPointer(pointer, candidate)).sort((a, b) => b.created_at - a.created_at)[0];
19612
+ const result2 = await queryPointerRelays(pool, relays, filter, maxWaitMs);
19613
+ const event = result2.events.filter((candidate) => matchesPointer(pointer, candidate)).sort((a, b) => b.created_at - a.created_at)[0];
18349
19614
  if (!event) {
18350
- throw new Error(`No napplet manifest event found for ${pointer.type} pointer.`);
19615
+ if (result2.timedOut) {
19616
+ throw new Error(
19617
+ `Relay resolution timed out after ${maxWaitMs}ms for ${pointer.type} pointer across ${relays.length} relays before complete EOSE.`
19618
+ );
19619
+ }
19620
+ const failures = relayFailureReasons(result2.reasons);
19621
+ if (result2.error || failures.length > 0) {
19622
+ const detail = result2.error instanceof Error ? result2.error.message : result2.error === void 0 ? failures.join("; ") : String(result2.error);
19623
+ throw new Error(`Relay query failed for ${pointer.type} pointer before complete EOSE: ${detail}`);
19624
+ }
19625
+ throw new Error(
19626
+ `All ${relays.length} relays reached EOSE without a matching ${pointer.type} napplet manifest event.`
19627
+ );
18351
19628
  }
18352
19629
  if (!isNappletManifestKind(event.kind)) {
18353
19630
  throw new Error(`Pointer resolved kind ${event.kind}; expected ${formatNappletManifestKinds()}.`);
18354
19631
  }
18355
19632
  return event;
18356
19633
  }
19634
+ function queryPointerRelays(pool, relays, filter, maxWaitMs) {
19635
+ if (pool.subscribeManyEose) {
19636
+ return queryPointerRelaySubscription(pool, relays, filter, maxWaitMs);
19637
+ }
19638
+ return queryPointerRelaySync(pool, relays, filter, maxWaitMs);
19639
+ }
19640
+ function queryPointerRelaySubscription(pool, relays, filter, maxWaitMs) {
19641
+ return new Promise((resolve) => {
19642
+ const events = [];
19643
+ let settled = false;
19644
+ let subscription;
19645
+ const finish = (result2) => {
19646
+ if (settled) return;
19647
+ settled = true;
19648
+ clearTimeout(deadline);
19649
+ resolve({ events, ...result2 });
19650
+ };
19651
+ const deadline = setTimeout(() => {
19652
+ finish({ reasons: [], timedOut: true });
19653
+ void subscription?.close("Paja pointer relay deadline exceeded");
19654
+ }, maxWaitMs);
19655
+ try {
19656
+ subscription = pool.subscribeManyEose([...relays], filter, {
19657
+ maxWait: maxWaitMs,
19658
+ label: "kehto-paja-runtime",
19659
+ onevent(event) {
19660
+ events.push(event);
19661
+ },
19662
+ onclose(reasons) {
19663
+ finish({ reasons, timedOut: false });
19664
+ }
19665
+ });
19666
+ } catch (error) {
19667
+ finish({ reasons: [], timedOut: false, error });
19668
+ }
19669
+ });
19670
+ }
19671
+ function queryPointerRelaySync(pool, relays, filter, maxWaitMs) {
19672
+ return new Promise((resolve) => {
19673
+ let settled = false;
19674
+ const finish = (result2) => {
19675
+ if (settled) return;
19676
+ settled = true;
19677
+ clearTimeout(deadline);
19678
+ resolve(result2);
19679
+ };
19680
+ const deadline = setTimeout(() => {
19681
+ finish({ events: [], reasons: [], timedOut: true });
19682
+ }, maxWaitMs);
19683
+ void pool.querySync([...relays], filter, {
19684
+ maxWait: maxWaitMs,
19685
+ label: "kehto-paja-runtime"
19686
+ }).then(
19687
+ (events) => finish({ events, reasons: [], timedOut: false }),
19688
+ (error) => finish({ events: [], reasons: [], timedOut: false, error })
19689
+ );
19690
+ });
19691
+ }
19692
+ function relayFailureReasons(reasons) {
19693
+ return [...new Set(reasons.map((reason) => reason.trim()).filter((reason) => reason.length > 0 && reason !== "closed automatically on eose").map((reason) => reason.slice(0, 160)))].slice(0, 3);
19694
+ }
19695
+ function normalizeMaxWaitMs(value) {
19696
+ if (value === void 0) return 5e3;
19697
+ if (!Number.isFinite(value) || value <= 0) {
19698
+ throw new Error("Pointer relay deadline must be a positive number of milliseconds.");
19699
+ }
19700
+ return Math.floor(value);
19701
+ }
18357
19702
  function addressableFilter(pointer) {
18358
19703
  if (pointer.kind !== NAPPLET_KIND_NAMED) {
18359
19704
  throw new Error(`naddr kind ${pointer.kind} is not a named NIP-5D napplet manifest kind.`);
@@ -18413,7 +19758,185 @@ function formatNappletManifestKinds() {
18413
19758
  return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
18414
19759
  }
18415
19760
 
19761
+ // src/browser-intent-host.ts
19762
+ function createPajaIntentTargetOptions(getState, getContext, effects = {}) {
19763
+ return {
19764
+ async openOrReuse(params) {
19765
+ const state = getState();
19766
+ const context = getContext();
19767
+ if (!state || !context) return null;
19768
+ const record = context.runtime.catalog.get(params.handler);
19769
+ if (!record || !recordSupportsDelivery(record, params)) return null;
19770
+ for (const stale of state.tabs) {
19771
+ if (stale.resolvedTarget.dTag === params.handler && !matchesInstalledNappletRecord(record, stale.resolvedTarget)) closeRuntimeTab(state, context, stale.id);
19772
+ }
19773
+ const current = state.tabs.find(
19774
+ (tab2) => matchesInstalledNappletRecord(record, tab2.resolvedTarget) && isCurrentRuntimeTabGeneration(state, context, tab2)
19775
+ );
19776
+ if (current && params.behavior?.reuse !== false) {
19777
+ return bindPajaIntentGeneration(current, record, context.runtime);
19778
+ }
19779
+ const resolved = await resolvePajaPointer(record.pointer.value, pajaPointerResolverOptions(context));
19780
+ if (resolved.dTag !== record.dTag || resolved.aggregateHash !== record.aggregateHash || !resolvedSupportsDelivery(resolved, params)) return null;
19781
+ const currentRecord = context.runtime.catalog.validateCurrent(record, resolved);
19782
+ if (!currentRecord) return null;
19783
+ const tab = addRuntimeTab(state, context, currentRecord.pointer.value, resolved);
19784
+ effects.persistTabs?.(state);
19785
+ return bindPajaIntentGeneration(tab, currentRecord, context.runtime);
19786
+ },
19787
+ waitForReady(generation) {
19788
+ const state = getState();
19789
+ const context = getContext();
19790
+ if (!state || !context) return Promise.reject(new Error("Paja host is not available"));
19791
+ const tab = findRuntimeTabGeneration(state, generation);
19792
+ if (!tab || !isCurrentPajaIntentGeneration(generation, state, context, tab)) {
19793
+ invalidatePajaIntentGeneration(generation, context.runtime);
19794
+ return Promise.reject(new Error("intent target generation is not current"));
19795
+ }
19796
+ if (tab.windowId && context.runtime.readyWindowIds.has(tab.windowId)) return void 0;
19797
+ return new Promise((resolve, reject2) => {
19798
+ const waiters = context.runtime.readyWaiters.get(generation) ?? /* @__PURE__ */ new Set();
19799
+ waiters.add({ resolve, reject: reject2 });
19800
+ context.runtime.readyWaiters.set(generation, waiters);
19801
+ });
19802
+ },
19803
+ isCurrent(generation) {
19804
+ const state = getState();
19805
+ const context = getContext();
19806
+ const tab = state && context ? findRuntimeTabGeneration(state, generation) : null;
19807
+ if (!tab || !state || !context || !isCurrentPajaIntentGeneration(generation, state, context, tab)) {
19808
+ if (context) invalidatePajaIntentGeneration(generation, context.runtime);
19809
+ return false;
19810
+ }
19811
+ return true;
19812
+ },
19813
+ send(generation, delivery) {
19814
+ const state = getState();
19815
+ const context = getContext();
19816
+ const tab = state && context ? findRuntimeTabGeneration(state, generation) : null;
19817
+ if (!state || !context || !tab || !tab.windowId || !isCurrentPajaIntentGeneration(generation, state, context, tab)) {
19818
+ if (context) invalidatePajaIntentGeneration(generation, context.runtime);
19819
+ throw new Error("intent target generation is not current and ready");
19820
+ }
19821
+ const source = tab.frame.contentWindow;
19822
+ if (!source || originRegistry.getWindowId(source) !== tab.windowId) {
19823
+ throw new Error("intent target source is no longer registered");
19824
+ }
19825
+ createPajaPostMessageProxy(source, state, tab.windowId).postMessage({
19826
+ type: "intent.deliver",
19827
+ delivery
19828
+ }, "*");
19829
+ }
19830
+ };
19831
+ }
19832
+ function pajaPointerResolverOptions(context) {
19833
+ return {
19834
+ relays: [
19835
+ ...context.config.target.pointer?.relays ?? [],
19836
+ ...getPajaRelayUrls(context.runtime.currentSimulation)
19837
+ ],
19838
+ blossomServers: context.config.target.pointer?.blossomServers ?? [],
19839
+ maxWaitMs: context.config.target.pointer?.maxWaitMs
19840
+ };
19841
+ }
19842
+ function recordSupportsDelivery(record, params) {
19843
+ return record.archetypes.some(
19844
+ (archetype) => archetype.slug === params.delivery.archetype && archetype.convention === params.delivery.convention
19845
+ );
19846
+ }
19847
+ function resolvedSupportsDelivery(resolved, params) {
19848
+ return resolved.manifest.archetypes.some(
19849
+ (archetype) => archetype.slug === params.delivery.archetype && archetype.convention === params.delivery.convention
19850
+ );
19851
+ }
19852
+ function findRuntimeTabGeneration(state, generation) {
19853
+ return state.tabs.find((tab) => runtimeTabGenerationId(tab) === generation.id) ?? null;
19854
+ }
19855
+ function bindPajaIntentGeneration(tab, record, runtime) {
19856
+ const generation = { id: runtimeTabGenerationId(tab) };
19857
+ runtime.intentRecords.set(generation, record);
19858
+ return generation;
19859
+ }
19860
+ function isCurrentPajaIntentGeneration(generation, state, context, tab) {
19861
+ const record = context.runtime.intentRecords.get(generation);
19862
+ return record !== void 0 && context.runtime.catalog.validateCurrent(record, tab.resolvedTarget) !== null && isCurrentRuntimeTabGeneration(state, context, tab);
19863
+ }
19864
+ function invalidatePajaIntentGeneration(generation, runtime) {
19865
+ const waiters = runtime.readyWaiters.get(generation);
19866
+ if (!waiters) return;
19867
+ runtime.readyWaiters.delete(generation);
19868
+ for (const waiter of waiters) waiter.reject(new Error("intent target catalog record was replaced"));
19869
+ }
19870
+ function subscribePajaIntentCatalogChanges(state, context) {
19871
+ return context.runtime.catalog.onChanged((dTag) => {
19872
+ for (const [generation] of context.runtime.readyWaiters) {
19873
+ const record = context.runtime.intentRecords.get(generation);
19874
+ if (!record || record.dTag !== dTag) continue;
19875
+ const tab = findRuntimeTabGeneration(state, generation);
19876
+ if (!tab || !isCurrentPajaIntentGeneration(generation, state, context, tab)) {
19877
+ invalidatePajaIntentGeneration(generation, context.runtime);
19878
+ }
19879
+ }
19880
+ });
19881
+ }
19882
+ function isCurrentRuntimeTabGeneration(state, context, tab) {
19883
+ const source = tab.frame.contentWindow;
19884
+ return state.tabs.includes(tab) && tab.windowId !== null && source !== null && originRegistry.getWindowId(source) === tab.windowId && runtimeTabGenerationId(tab) === `${tab.id}:${tab.generation}`;
19885
+ }
19886
+ function markRuntimeTabReady(state, context, tab, source, registeredWindowId, effects = {}) {
19887
+ if (!tab.windowId || tab.frame.contentWindow !== source || registeredWindowId !== tab.windowId || !isCurrentRuntimeTabGeneration(state, context, tab)) return false;
19888
+ context.runtime.readyWindowIds.add(tab.windowId);
19889
+ for (const [generation, waiters] of context.runtime.readyWaiters) {
19890
+ if (findRuntimeTabGeneration(state, generation) !== tab) continue;
19891
+ context.runtime.readyWaiters.delete(generation);
19892
+ if (!isCurrentPajaIntentGeneration(generation, state, context, tab)) {
19893
+ for (const waiter of waiters) waiter.reject(new Error("intent target catalog record was replaced"));
19894
+ continue;
19895
+ }
19896
+ for (const waiter of waiters) waiter.resolve();
19897
+ }
19898
+ tab.status = "ready";
19899
+ if (state.activeTabId === tab.id) {
19900
+ if (effects.setReadyStatus) effects.setReadyStatus(state);
19901
+ else state.status = "ready";
19902
+ }
19903
+ renderRuntimeTabs(state);
19904
+ return true;
19905
+ }
19906
+ function clearRuntimeTabGeneration(tab, runtime) {
19907
+ for (const [generation, waiters] of runtime.readyWaiters) {
19908
+ if (generation.id !== runtimeTabGenerationId(tab)) continue;
19909
+ runtime.readyWaiters.delete(generation);
19910
+ for (const waiter of waiters) waiter.reject(new Error("intent target generation replaced"));
19911
+ }
19912
+ }
19913
+
19914
+ // src/theme-broadcast.ts
19915
+ function createPajaThemeBroadcastLink() {
19916
+ let attachedBridge;
19917
+ return {
19918
+ attach(bridge) {
19919
+ if (attachedBridge) {
19920
+ throw new Error("Paja theme broadcast link is already attached.");
19921
+ }
19922
+ attachedBridge = bridge;
19923
+ },
19924
+ onBroadcast(envelope) {
19925
+ if (!attachedBridge) {
19926
+ throw new Error("Paja theme broadcast occurred before a ShellBridge is attached.");
19927
+ }
19928
+ attachedBridge.publishTheme(envelope.theme);
19929
+ }
19930
+ };
19931
+ }
19932
+
18416
19933
  // src/browser-target-frame.ts
19934
+ function resolvePajaFrameEnvironment(hooks, identity) {
19935
+ if (!identity.dTag || !identity.aggregateHash) {
19936
+ throw new Error("Paja frame identity requires a dTag and aggregate hash");
19937
+ }
19938
+ return resolveShellEnvironment(hooks, identity);
19939
+ }
18417
19940
  function getTargetIdentity2(config, resolvedTarget) {
18418
19941
  return {
18419
19942
  pubkey: "",
@@ -18421,18 +19944,21 @@ function getTargetIdentity2(config, resolvedTarget) {
18421
19944
  aggregateHash: resolvedTarget?.aggregateHash ?? config.window.aggregateHash
18422
19945
  };
18423
19946
  }
18424
- function registerFrameForGeneration(_bridge, frame, config, generation, resolvedTarget, windowId = `${config.window.id}:${generation}`) {
19947
+ function getTargetOriginIdentity(config, resolvedTarget) {
19948
+ const target = getTargetIdentity2(config, resolvedTarget);
19949
+ return Object.freeze({ dTag: target.dTag, aggregateHash: target.aggregateHash });
19950
+ }
19951
+ function registerFrameForGeneration(frame, config, generation, identity, environment, windowId = `${config.window.id}:${generation}`) {
18425
19952
  const win = frame.contentWindow;
18426
19953
  if (!win) return null;
18427
- const identity = getTargetIdentity2(config, resolvedTarget);
18428
- originRegistry.register(win, windowId, {
18429
- dTag: identity.dTag,
18430
- aggregateHash: identity.aggregateHash
18431
- });
19954
+ originRegistry.register(win, windowId, identity);
19955
+ originRegistry.setEnvironment(win, environment);
18432
19956
  return windowId;
18433
19957
  }
18434
- async function navigateFrame(bridge, frame, config, generation, capabilities, simulation, resolvedTarget, windowId, isCurrent) {
18435
- const domains = getInjectedDomains(capabilities, simulation, resolvedTarget);
19958
+ async function navigateFrame(frame, config, generation, adapter, resolvedTarget, windowId, isCurrent, onRegistered) {
19959
+ const identity = getTargetOriginIdentity(config, resolvedTarget);
19960
+ const environment = resolvePajaFrameEnvironment(adapter, identity);
19961
+ const domains = environment.capabilities.domains;
18436
19962
  if (config.target.mode === "runtime-pointer") {
18437
19963
  if (!resolvedTarget) {
18438
19964
  frame.removeAttribute("src");
@@ -18440,7 +19966,8 @@ async function navigateFrame(bridge, frame, config, generation, capabilities, si
18440
19966
  return null;
18441
19967
  }
18442
19968
  if (isCurrent && !isCurrent()) return null;
18443
- const registeredWindowId2 = registerFrameForGeneration(bridge, frame, config, generation, resolvedTarget, windowId);
19969
+ const registeredWindowId2 = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
19970
+ onRegistered?.(registeredWindowId2);
18444
19971
  frame.removeAttribute("src");
18445
19972
  frame.srcdoc = injectNappletNamespacePrelude(
18446
19973
  injectPajaRuntimeCsp(
@@ -18453,7 +19980,8 @@ async function navigateFrame(bridge, frame, config, generation, capabilities, si
18453
19980
  }
18454
19981
  const html = await fetchTargetHtml();
18455
19982
  if (isCurrent && !isCurrent()) return null;
18456
- const registeredWindowId = registerFrameForGeneration(bridge, frame, config, generation, resolvedTarget, windowId);
19983
+ const registeredWindowId = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
19984
+ onRegistered?.(registeredWindowId);
18457
19985
  frame.removeAttribute("src");
18458
19986
  frame.srcdoc = injectNappletNamespacePrelude(
18459
19987
  injectBaseHref(html, config.target.url),
@@ -18480,21 +20008,6 @@ function connectOrigins(urls) {
18480
20008
  }
18481
20009
  return [...out];
18482
20010
  }
18483
- function getInjectedDomains(capabilities, simulation, resolvedTarget) {
18484
- const available = capabilities.domains.filter((domain) => {
18485
- const knownDomain = domain;
18486
- return simulation.capabilities.domains[knownDomain] !== false;
18487
- });
18488
- const required = resolvedTarget?.manifest.requires ?? [];
18489
- const availableSet = new Set(available);
18490
- const optionalDomains = required.length === 0 ? available : required.filter((domain) => availableSet.has(domain));
18491
- return [
18492
- ...PAJA_HANDSHAKE_DOMAINS,
18493
- ...optionalDomains.filter(
18494
- (domain) => !PAJA_HANDSHAKE_DOMAINS.some((handshakeDomain) => handshakeDomain === domain)
18495
- )
18496
- ];
18497
- }
18498
20011
  async function fetchTargetHtml() {
18499
20012
  const response = await fetch(new URL("./__kehto/target.html", window.location.href), {
18500
20013
  cache: "no-store",
@@ -18524,6 +20037,30 @@ function escapeHtml(value) {
18524
20037
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
18525
20038
  }
18526
20039
 
20040
+ // src/browser-target-diagnostics.ts
20041
+ async function reportTargetCorsDiagnostic(state) {
20042
+ let diagnostic;
20043
+ try {
20044
+ const response = await fetch(new URL("./__kehto/target-cors.json", window.location.href), {
20045
+ cache: "no-store"
20046
+ });
20047
+ if (!response.ok) return;
20048
+ diagnostic = await response.json();
20049
+ } catch {
20050
+ return;
20051
+ }
20052
+ if (diagnostic.status === "allowed") return;
20053
+ appendPajaMessageLog(state, "paja", {
20054
+ type: "paja.target.cors.error",
20055
+ status: diagnostic.status,
20056
+ targetUrl: diagnostic.targetUrl,
20057
+ allowOrigin: diagnostic.allowOrigin,
20058
+ message: `${diagnostic.detail} ${diagnostic.hint ?? ""}`.trim()
20059
+ });
20060
+ console.warn(`[paja] ${diagnostic.detail}
20061
+ [paja] ${diagnostic.hint ?? ""}`.trimEnd());
20062
+ }
20063
+
18527
20064
  // src/browser-host.ts
18528
20065
  var bridgeRef = null;
18529
20066
  function readConfig() {
@@ -18621,68 +20158,22 @@ function getFrame() {
18621
20158
  frame.sandbox.remove("allow-same-origin");
18622
20159
  return frame;
18623
20160
  }
18624
- function confirmPajaRequest(state, request) {
18625
- if (request.action === "upload") {
18626
- const filename = request.filename ?? "(unnamed blob)";
18627
- const mimeType = request.mimeType ?? "application/octet-stream";
18628
- const allowed3 = window.confirm([
18629
- "Paja upload request",
18630
- `napplet: ${request.napplet.dTag} (${request.windowId})`,
18631
- `file: ${filename}`,
18632
- `size: ${request.size} bytes`,
18633
- `type: ${mimeType}`,
18634
- `server: ${request.server}`,
18635
- request.warning
18636
- ].join("\n"));
18637
- appendPajaMessageLog(state, "paja", {
18638
- type: `paja.upload.${allowed3 ? "confirmed" : "denied"}`,
18639
- windowId: request.windowId,
18640
- dTag: request.napplet.dTag,
18641
- aggregateHash: request.napplet.aggregateHash,
18642
- filename,
18643
- size: request.size,
18644
- mimeType,
18645
- server: request.server,
18646
- warning: request.warning
18647
- });
18648
- return allowed3;
18649
- }
18650
- const event = request.event;
18651
- const kind = typeof event.kind === "number" ? event.kind : "unknown";
18652
- const content = typeof event.content === "string" && event.content.length > 0 ? `
18653
-
18654
- ${event.content.slice(0, 240)}` : "";
18655
- const allowed2 = window.confirm(`Paja ${request.action} request
18656
- kind: ${kind}${content}`);
18657
- appendPajaMessageLog(state, "paja", {
18658
- type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
18659
- kind
18660
- });
18661
- return allowed2;
18662
- }
18663
- function unregisterSingleFrameWindow(bridge, runtime, windowId) {
18664
- if (!windowId) return;
18665
- bridge.runtime.destroyWindow(windowId);
18666
- bridge.runtime.sessionRegistry.unregister(windowId);
18667
- originRegistry.unregister(windowId);
18668
- runtime.readyWindowIds.delete(windowId);
18669
- if (runtime.currentWindowId === windowId) runtime.currentWindowId = null;
18670
- }
18671
20161
  function startFrameNavigation(state, context) {
18672
- const { config, frame, bridge, capabilities, runtime } = context;
20162
+ const { config, frame, bridge, adapter, runtime } = context;
18673
20163
  if (!frame) return;
18674
20164
  const generation = state.generation;
18675
20165
  const isCurrentGeneration = () => state.generation === generation;
18676
20166
  void navigateFrame(
18677
- bridge,
18678
20167
  frame,
18679
20168
  config,
18680
20169
  generation,
18681
- capabilities,
18682
- runtime.currentSimulation,
20170
+ adapter,
18683
20171
  state.resolvedTarget,
18684
20172
  void 0,
18685
- isCurrentGeneration
20173
+ isCurrentGeneration,
20174
+ (windowId) => {
20175
+ if (isCurrentGeneration()) runtime.currentWindowId = windowId;
20176
+ }
18686
20177
  ).then((windowId) => {
18687
20178
  if (!isCurrentGeneration()) {
18688
20179
  unregisterSingleFrameWindow(bridge, runtime, windowId);
@@ -18701,28 +20192,6 @@ function startFrameNavigation(state, context) {
18701
20192
  console.error(error);
18702
20193
  });
18703
20194
  }
18704
- function createHostSignerController(getState) {
18705
- return createPajaSignerController({
18706
- confirmRequest: (request) => confirmPajaRequest(getState(), request),
18707
- onChange(signer) {
18708
- const state = getState();
18709
- if (!state) return;
18710
- state.signer = signer;
18711
- appendPajaMessageLog(state, "paja", {
18712
- type: `paja.signer.${signer.method}.${signer.status}`,
18713
- pubkey: signer.pubkey,
18714
- relay: signer.relay,
18715
- error: signer.error
18716
- });
18717
- setSimulationStatus(state);
18718
- if (signer.status === "connected") state.reload();
18719
- }
18720
- });
18721
- }
18722
- function hasNip07Signer() {
18723
- const signer = globalThis.nostr;
18724
- return typeof signer === "object" && signer !== null;
18725
- }
18726
20195
  function installPajaControlListeners(state) {
18727
20196
  document.getElementById("reload-target")?.addEventListener("click", () => {
18728
20197
  state.reload();
@@ -18784,7 +20253,7 @@ function setRuntimeDomainEnabled(state, context, domain, enabled) {
18784
20253
  state.reload();
18785
20254
  }
18786
20255
  async function loadRuntimePointer(state, context, value, options = {}) {
18787
- const { config } = context;
20256
+ const { config, runtime } = context;
18788
20257
  if (config.target.mode !== "runtime-pointer") return;
18789
20258
  const pointer = value.trim();
18790
20259
  const input = document.getElementById("runtime-pointer-input");
@@ -18798,11 +20267,8 @@ async function loadRuntimePointer(state, context, value, options = {}) {
18798
20267
  setStatus(state, "booting");
18799
20268
  appendPajaMessageLog(state, "paja", { type: "paja.pointer.resolve", pointer });
18800
20269
  try {
18801
- const resolvedTarget = await resolvePajaPointer(pointer, {
18802
- relays: config.target.pointer?.relays ?? [],
18803
- blossomServers: config.target.pointer?.blossomServers ?? [],
18804
- maxWaitMs: config.target.pointer?.maxWaitMs
18805
- });
20270
+ const resolvedTarget = await resolvePajaPointer(pointer, pajaPointerResolverOptions(context));
20271
+ runtime.catalog.install(resolvedTarget);
18806
20272
  const pointerStatus = `${resolvedTarget.dTag}:${resolvedTarget.aggregateHash.slice(0, 12)}`;
18807
20273
  setPointerStatus(state, pointerStatus);
18808
20274
  appendPajaMessageLog(state, "paja", {
@@ -18947,16 +20413,30 @@ async function installPajaHost() {
18947
20413
  const runtime = {
18948
20414
  currentSimulation: config.simulation,
18949
20415
  themeService: null,
20416
+ catalog: new InstalledNappletCatalog(),
20417
+ readyWaiters: /* @__PURE__ */ new Map(),
20418
+ intentRecords: /* @__PURE__ */ new WeakMap(),
18950
20419
  currentWindowId: null,
18951
20420
  readyWindowIds: /* @__PURE__ */ new Set()
18952
20421
  };
18953
20422
  const getSimulation = () => runtime.currentSimulation;
20423
+ const themeBroadcast = createPajaThemeBroadcastLink();
18954
20424
  let stateRef = null;
18955
- const signerController = createHostSignerController(() => stateRef);
20425
+ let contextRef = null;
20426
+ const intentController = new BrowserIntentController(createPajaIntentTargetOptions(
20427
+ () => stateRef,
20428
+ () => contextRef,
20429
+ { persistTabs: persistRuntimeTabs }
20430
+ ));
20431
+ const signerController = createHostSignerController(() => stateRef, setSimulationStatus);
18956
20432
  const adapter = createPajaAdapter(config, getSimulation, (theme) => {
18957
20433
  runtime.themeService = theme;
18958
- }, (request) => confirmPajaRequest(stateRef, request), signerController, () => getTargetIdentity2(config, stateRef?.resolvedTarget));
20434
+ }, themeBroadcast.onBroadcast, (request) => confirmPajaRequest(stateRef, request), signerController, () => getTargetIdentity2(config, stateRef?.resolvedTarget), () => stateRef?.reload(), {
20435
+ catalog: runtime.catalog,
20436
+ controller: intentController
20437
+ });
18959
20438
  const bridge = createShellBridge(adapter);
20439
+ themeBroadcast.attach(bridge);
18960
20440
  bridgeRef = bridge;
18961
20441
  installPajaOriginRegistryProxy(originRegistry, () => stateRef);
18962
20442
  const capabilities = buildShellCapabilities(adapter);
@@ -18970,21 +20450,25 @@ async function installPajaHost() {
18970
20450
  capabilities,
18971
20451
  runtime,
18972
20452
  navigateFrame,
18973
- registerFrameForGeneration,
18974
20453
  renderTargetErrorHtml,
20454
+ onTabDestroyed: (tab) => clearRuntimeTabGeneration(tab, runtime),
18975
20455
  setPointerStatus: (state2, message) => setPointerStatus(state2, message),
18976
20456
  setStatus: (state2, status) => setStatus(state2, status)
18977
20457
  };
20458
+ contextRef = context;
18978
20459
  const state = createPajaBrowserState(context);
18979
20460
  stateRef = state;
20461
+ const stopIntentCatalogChanges = subscribePajaIntentCatalogChanges(state, context);
20462
+ window.addEventListener("pagehide", stopIntentCatalogChanges, { once: true });
18980
20463
  window.__KEHTO_PAJA__ = state;
18981
20464
  window.addEventListener("message", (event) => {
18982
20465
  const source = event.source;
18983
20466
  const sourceTab = source ? state.tabs.find((tab) => tab.frame.contentWindow === source) ?? null : null;
18984
20467
  const isSingleFrameMessage = frame ? event.source === frame.contentWindow : false;
18985
20468
  if (!sourceTab && !isSingleFrameMessage) return;
18986
- const registeredWindowId = source ? originRegistry.getWindowId(source) : null;
20469
+ const registeredWindowId = source ? originRegistry.getWindowId(source) ?? null : null;
18987
20470
  const sourceWindowId = sourceTab?.windowId ?? registeredWindowId ?? void 0;
20471
+ if (sourceTab && (!source || !sourceWindowId || registeredWindowId !== sourceWindowId)) return;
18988
20472
  if (isSingleFrameMessage && (!sourceWindowId || sourceWindowId !== runtime.currentWindowId)) return;
18989
20473
  appendPajaMessageLog(state, "napplet->shell", event.data, sourceWindowId);
18990
20474
  const proxiedSource = createPajaPostMessageProxy(event.source, state, sourceWindowId);
@@ -18998,22 +20482,21 @@ async function installPajaHost() {
18998
20482
  bridge.handleMessage(syntheticEvent);
18999
20483
  const data = event.data;
19000
20484
  if (data && typeof data === "object" && data.type === "shell.ready") {
19001
- if (sourceWindowId) runtime.readyWindowIds.add(sourceWindowId);
19002
20485
  if (sourceTab) {
19003
- sourceTab.status = "ready";
19004
- if (state.activeTabId === sourceTab.id) setStatus(state, "ready");
19005
- renderRuntimeTabs(state);
20486
+ if (source && !markRuntimeTabReady(
20487
+ state,
20488
+ context,
20489
+ sourceTab,
20490
+ source,
20491
+ registeredWindowId,
20492
+ { setReadyStatus: (current) => setStatus(current, "ready") }
20493
+ )) return;
19006
20494
  } else {
20495
+ if (sourceWindowId) runtime.readyWindowIds.add(sourceWindowId);
19007
20496
  setStatus(state, "ready");
19008
20497
  }
19009
20498
  }
19010
20499
  });
19011
- frame?.addEventListener("load", () => {
19012
- if (!frame) return;
19013
- if (state.status === "booting" || state.status === "reloading") {
19014
- runtime.currentWindowId = registerFrameForGeneration(bridge, frame, config, state.generation, state.resolvedTarget);
19015
- }
19016
- });
19017
20500
  frame?.addEventListener("error", () => {
19018
20501
  setStatus(state, "error");
19019
20502
  });
@@ -19034,20 +20517,28 @@ async function installPajaHost() {
19034
20517
  }
19035
20518
  } else {
19036
20519
  startFrameNavigation(state, context);
20520
+ void reportTargetCorsDiagnostic(state);
19037
20521
  }
19038
20522
  if (hasNip07Signer()) void state.connectNip07();
19039
20523
  }
19040
- try {
19041
- void installPajaHost().catch((error) => {
20524
+ if (typeof document !== "undefined") {
20525
+ try {
20526
+ void installPajaHost().catch((error) => {
20527
+ const statusEl = document.getElementById("lifecycle-status");
20528
+ if (statusEl) statusEl.textContent = "error";
20529
+ console.error(error);
20530
+ });
20531
+ } catch (error) {
19042
20532
  const statusEl = document.getElementById("lifecycle-status");
19043
20533
  if (statusEl) statusEl.textContent = "error";
19044
20534
  console.error(error);
19045
- });
19046
- } catch (error) {
19047
- const statusEl = document.getElementById("lifecycle-status");
19048
- if (statusEl) statusEl.textContent = "error";
19049
- console.error(error);
20535
+ }
19050
20536
  }
20537
+ export {
20538
+ createPajaIntentTargetOptions,
20539
+ markRuntimeTabReady,
20540
+ subscribePajaIntentCatalogChanges
20541
+ };
19051
20542
  /*! Bundled license information:
19052
20543
 
19053
20544
  @noble/hashes/utils.js: