@kehto/paja 0.8.0 → 0.10.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 === "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.31.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,18 +4862,69 @@ 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 hasOwn2 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
4866
+ const normalizeRequest = (value) => {
4867
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4868
+ throw new TypeError("Intent request must be an object");
4869
+ }
4870
+ const supplied = value;
4871
+ if (hasOwn2(supplied, "sender")) {
4872
+ throw new TypeError("Intent callers cannot supply sender");
4873
+ }
4874
+ const allowed22 = ["archetype", "action", "convention", "payload", "handler", "behavior"];
4875
+ if (Object.keys(supplied).some((field) => !allowed22.includes(field))) {
4876
+ throw new TypeError("Intent request contains unsupported fields");
4877
+ }
4878
+ if (typeof supplied.archetype !== "string" || supplied.archetype.length === 0) {
4879
+ throw new TypeError("Intent request requires an archetype");
4880
+ }
4881
+ const action = supplied.action === void 0 ? "open" : supplied.action;
4882
+ if (typeof action !== "string" || action.length === 0) {
4883
+ throw new TypeError("Intent action must be a non-empty string");
4884
+ }
4885
+ if (supplied.convention !== void 0 && (typeof supplied.convention !== "string" || !/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(supplied.convention))) {
4886
+ throw new TypeError("Intent convention must be queryless");
4887
+ }
4888
+ if (supplied.handler !== void 0 && (typeof supplied.handler !== "string" || supplied.handler.length === 0)) {
4889
+ throw new TypeError("Intent handler must be a non-empty string");
4890
+ }
4891
+ const behaviorValue = supplied?.behavior;
4892
+ const behavior = behaviorValue && typeof behaviorValue === "object" ? behaviorValue : void 0;
4893
+ if (behaviorValue !== void 0 && (!behavior || Array.isArray(behaviorValue) || Object.keys(behavior).some((field) => field !== "focus" && field !== "newWindow" && field !== "reuse") || hasOwn2(behavior, "focus") && typeof behavior.focus !== "boolean" || hasOwn2(behavior, "newWindow") && typeof behavior.newWindow !== "boolean" || hasOwn2(behavior, "reuse") && typeof behavior.reuse !== "boolean")) {
4894
+ throw new TypeError("Intent behavior contains unsupported fields");
4895
+ }
4896
+ const sanitizedBehavior = {
4897
+ ...typeof behavior?.focus === "boolean" ? { focus: behavior.focus } : {},
4898
+ ...typeof behavior?.newWindow === "boolean" ? { newWindow: behavior.newWindow } : {},
4899
+ ...typeof behavior?.reuse === "boolean" ? { reuse: behavior.reuse } : {}
4900
+ };
4901
+ return {
4902
+ archetype: supplied.archetype,
4903
+ action,
4904
+ ...typeof supplied.convention === "string" ? { convention: supplied.convention } : {},
4905
+ ...hasOwn2(supplied, "payload") ? { payload: supplied.payload } : {},
4906
+ ...typeof supplied.handler === "string" ? { handler: supplied.handler } : {},
4907
+ ...Object.keys(sanitizedBehavior).length === 0 ? {} : { behavior: sanitizedBehavior }
4908
+ };
4909
+ };
4910
+ const invoke = (value) => {
4911
+ const normalized = normalizeRequest(value);
4912
+ return request(
4913
+ {
4914
+ type: "intent.invoke",
4915
+ request: normalized
4916
+ },
4917
+ "intent.invoke.result",
4918
+ (msg) => fieldOrThrow(msg, "result", "intent.invoke.result missing result")
4919
+ );
4920
+ };
4306
4921
  return {
4307
4922
  invoke,
4308
4923
  open: (archetype, payload, opts) => invoke({
4309
4924
  archetype,
4310
4925
  action: "open",
4311
- payload,
4312
- ...opts && typeof opts === "object" ? opts : {}
4926
+ ...payload === void 0 ? {} : { payload },
4927
+ ...opts
4313
4928
  }),
4314
4929
  available: (archetype) => request(
4315
4930
  { type: "intent.available", archetype },
@@ -4460,14 +5075,40 @@ function nappletNamespacePrelude(domains) {
4460
5075
  };
4461
5076
  }
4462
5077
  const shell = makeShell();
5078
+ let inc;
5079
+ let identity;
5080
+ let theme;
5081
+ let intent;
5082
+ function makeProtectedInc(existing) {
5083
+ const extensions = existing && typeof existing === "object" ? existing : {};
5084
+ inc ??= makeInc();
5085
+ return { ...extensions, ...inc };
5086
+ }
5087
+ function makeProtectedIdentity() {
5088
+ identity ??= makeIdentity();
5089
+ return identity;
5090
+ }
5091
+ function makeProtectedTheme() {
5092
+ theme ??= Object.freeze(makeTheme());
5093
+ return theme;
5094
+ }
5095
+ function makeProtectedIntent() {
5096
+ intent ??= Object.freeze(makeIntent());
5097
+ return intent;
5098
+ }
5099
+ function isProtectedDomain(domain) {
5100
+ return domain === "shell" || domain === "inc" || domain === "identity" || domain === "theme" || domain === "intent";
5101
+ }
4463
5102
  function makeDomain(domain, existing) {
4464
5103
  if (domain === "shell") return shell;
5104
+ if (domain === "inc") return makeProtectedInc(existing);
5105
+ if (domain === "identity") return makeProtectedIdentity();
5106
+ if (domain === "theme") return makeProtectedTheme();
5107
+ if (domain === "intent") return makeProtectedIntent();
4465
5108
  if (existing && typeof existing === "object" && Object.keys(existing).length > 0) return existing;
4466
5109
  switch (domain) {
4467
5110
  case "relay":
4468
5111
  return makeRelay();
4469
- case "inc":
4470
- return makeInc();
4471
5112
  case "storage":
4472
5113
  return makeStorage();
4473
5114
  case "keys":
@@ -4476,10 +5117,6 @@ function nappletNamespacePrelude(domains) {
4476
5117
  return makeMedia();
4477
5118
  case "notify":
4478
5119
  return makeNotify();
4479
- case "identity":
4480
- return makeIdentity();
4481
- case "theme":
4482
- return makeTheme();
4483
5120
  case "config":
4484
5121
  return makeConfig();
4485
5122
  case "resource":
@@ -4490,8 +5127,6 @@ function nappletNamespacePrelude(domains) {
4490
5127
  return makeOutbox();
4491
5128
  case "upload":
4492
5129
  return makeUpload();
4493
- case "intent":
4494
- return makeIntent();
4495
5130
  case "webrtc":
4496
5131
  return makeWebrtc();
4497
5132
  case "ble":
@@ -4521,10 +5156,22 @@ function nappletNamespacePrelude(domains) {
4521
5156
  },
4522
5157
  defineProperty(obj, prop, descriptor) {
4523
5158
  if (typeof prop !== "string" || !allowed2.has(prop)) return true;
5159
+ if (isProtectedDomain(prop)) {
5160
+ return Reflect.defineProperty(obj, prop, {
5161
+ value: makeDomain(prop, descriptor.value),
5162
+ enumerable: true,
5163
+ configurable: true,
5164
+ writable: true
5165
+ });
5166
+ }
4524
5167
  return Reflect.defineProperty(obj, prop, {
4525
5168
  ...descriptor,
4526
5169
  value: makeDomain(prop, descriptor.value)
4527
5170
  });
5171
+ },
5172
+ deleteProperty(obj, prop) {
5173
+ if (typeof prop === "string" && allowed2.has(prop) && isProtectedDomain(prop)) return true;
5174
+ return Reflect.deleteProperty(obj, prop);
4528
5175
  }
4529
5176
  });
4530
5177
  }
@@ -4554,7 +5201,7 @@ function nappletNamespacePrelude(domains) {
4554
5201
  root = buildNappletNamespace(value);
4555
5202
  },
4556
5203
  enumerable: false,
4557
- configurable: true
5204
+ configurable: false
4558
5205
  });
4559
5206
  }
4560
5207
 
@@ -8159,14 +8806,11 @@ function enforceLimit(store2, list) {
8159
8806
  list.shift();
8160
8807
  }
8161
8808
  }
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;
8809
+ function findById(store2, windowId, id) {
8810
+ const list = store2.notifications.get(windowId);
8811
+ const index = list?.findIndex((notification) => notification.id === id) ?? -1;
8812
+ if (!list || index === -1) return void 0;
8813
+ return [list[index], index];
8170
8814
  }
8171
8815
  function createNotification(store2, windowId, title, body) {
8172
8816
  const notification = {
@@ -8183,20 +8827,20 @@ function createNotification(store2, windowId, title, body) {
8183
8827
  notify(store2);
8184
8828
  return notification;
8185
8829
  }
8186
- function dismissNotification(store2, id) {
8187
- const found = findById(store2, id);
8830
+ function dismissNotification(store2, windowId, id) {
8831
+ const found = findById(store2, windowId, id);
8188
8832
  if (!found) return;
8189
- const [foundWindowId, , index] = found;
8190
- const list = store2.notifications.get(foundWindowId);
8833
+ const [, index] = found;
8834
+ const list = store2.notifications.get(windowId);
8191
8835
  if (!list) return;
8192
8836
  list.splice(index, 1);
8193
- if (list.length === 0) store2.notifications.delete(foundWindowId);
8837
+ if (list.length === 0) store2.notifications.delete(windowId);
8194
8838
  notify(store2);
8195
8839
  }
8196
- function markNotificationRead(store2, id) {
8197
- const found = findById(store2, id);
8840
+ function markNotificationRead(store2, windowId, id) {
8841
+ const found = findById(store2, windowId, id);
8198
8842
  if (!found) return;
8199
- const [, notification] = found;
8843
+ const [notification] = found;
8200
8844
  if (!notification.read) {
8201
8845
  notification.read = true;
8202
8846
  notify(store2);
@@ -8213,12 +8857,12 @@ function handleNotifyEnvelope(store2, windowId, action, msg, send) {
8213
8857
  }
8214
8858
  case "dismiss": {
8215
8859
  const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
8216
- if (notifId) dismissNotification(store2, notifId);
8860
+ if (notifId) dismissNotification(store2, windowId, notifId);
8217
8861
  break;
8218
8862
  }
8219
8863
  case "read": {
8220
8864
  const notifId = typeof msg.notificationId === "string" ? msg.notificationId : "";
8221
- if (notifId) markNotificationRead(store2, notifId);
8865
+ if (notifId) markNotificationRead(store2, windowId, notifId);
8222
8866
  break;
8223
8867
  }
8224
8868
  case "list": {
@@ -8230,34 +8874,6 @@ function handleNotifyEnvelope(store2, windowId, action, msg, send) {
8230
8874
  break;
8231
8875
  }
8232
8876
  }
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
8877
  function createNotificationService(options) {
8262
8878
  const store2 = {
8263
8879
  notifications: /* @__PURE__ */ new Map(),
@@ -8265,23 +8881,21 @@ function createNotificationService(options) {
8265
8881
  maxPerWindow: options?.maxPerWindow ?? DEFAULT_MAX_PER_WINDOW
8266
8882
  };
8267
8883
  const descriptor = {
8268
- name: "notifications",
8884
+ name: "notify",
8269
8885
  version: NOTIFICATION_SERVICE_VERSION,
8270
8886
  description: "Notification state registry \u2014 tracks notifications per napplet window"
8271
8887
  };
8272
8888
  return {
8273
8889
  descriptor,
8274
8890
  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);
8891
+ if (!message.type.startsWith("notify.")) return;
8892
+ handleNotifyEnvelope(
8893
+ store2,
8894
+ windowId,
8895
+ message.type.slice(7),
8896
+ message,
8897
+ send
8898
+ );
8285
8899
  },
8286
8900
  onWindowDestroyed(windowId) {
8287
8901
  if (store2.notifications.delete(windowId)) {
@@ -8291,16 +8905,16 @@ function createNotificationService(options) {
8291
8905
  };
8292
8906
  }
8293
8907
  var IDENTITY_SERVICE_VERSION = "1.0.0";
8294
- function sendProviderError(send, result2, fallback, err2) {
8908
+ function sendProviderError(send, result2, fallback, _err) {
8295
8909
  send({
8296
8910
  ...result2,
8297
- error: err2?.message ?? fallback
8911
+ error: fallback
8298
8912
  });
8299
8913
  }
8300
8914
  async function getCurrentPubkey(options) {
8301
- const currentSigner = options.getSigner();
8302
- if (!currentSigner?.getPublicKey) return "";
8303
8915
  try {
8916
+ const currentSigner = options.getSigner();
8917
+ if (!currentSigner?.getPublicKey) return "";
8304
8918
  return await currentSigner.getPublicKey() ?? "";
8305
8919
  } catch {
8306
8920
  return "";
@@ -8313,46 +8927,29 @@ function sendOptionalProviderResult(options, send, fallbackResult, errorFallback
8313
8927
  }
8314
8928
  Promise.resolve(getCurrentPubkey(options)).then((pubkey) => buildResult(pubkey)).then((result2) => send(result2)).catch((err2) => sendProviderError(send, fallbackResult, errorFallback, err2));
8315
8929
  }
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
8930
  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) => {
8931
+ Promise.resolve().then(() => options.getSigner()).then(async (signer) => signer?.getPublicKey ? signer.getPublicKey() : "").catch(() => "").then((pubkey) => {
8334
8932
  const result2 = {
8335
8933
  type: "identity.getPublicKey.result",
8336
8934
  id,
8337
8935
  pubkey: pubkey ?? ""
8338
8936
  };
8339
8937
  send(result2);
8340
- }).catch((err2) => sendSignerError(send, id, "identity.getPublicKey", "getPublicKey failed", err2));
8938
+ });
8341
8939
  }
8342
8940
  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
8941
+ const fallbackResult = {
8942
+ type: "identity.getRelays.result",
8943
+ id,
8944
+ relays: {}
8945
+ };
8946
+ Promise.resolve().then(() => options.getSigner()).then(async (signer) => {
8947
+ if (!signer?.getRelays) return fallbackResult;
8948
+ return {
8949
+ ...fallbackResult,
8950
+ relays: await signer.getRelays() ?? {}
8353
8951
  };
8354
- send(result2);
8355
- }).catch((err2) => sendSignerError(send, id, "identity.getRelays", "getRelays failed", err2));
8952
+ }).catch(() => ({ ...fallbackResult, error: "identity.getRelays failed" })).then(send);
8356
8953
  }
8357
8954
  function handleReadProvider(options, id, message, send) {
8358
8955
  switch (message.type) {
@@ -8361,11 +8958,11 @@ function handleReadProvider(options, id, message, send) {
8361
8958
  options,
8362
8959
  send,
8363
8960
  { type: "identity.getProfile.result", id, profile: null },
8364
- "getProfile failed",
8961
+ "identity.getProfile failed",
8365
8962
  options.getProfile ? async (pubkey) => ({
8366
8963
  type: "identity.getProfile.result",
8367
8964
  id,
8368
- profile: await options.getProfile(pubkey, message)
8965
+ profile: await options.getProfile(pubkey, message) ?? null
8369
8966
  }) : void 0
8370
8967
  );
8371
8968
  return true;
@@ -8374,11 +8971,11 @@ function handleReadProvider(options, id, message, send) {
8374
8971
  options,
8375
8972
  send,
8376
8973
  { type: "identity.getFollows.result", id, pubkeys: [] },
8377
- "getFollows failed",
8974
+ "identity.getFollows failed",
8378
8975
  options.getFollows ? async (pubkey) => ({
8379
8976
  type: "identity.getFollows.result",
8380
8977
  id,
8381
- pubkeys: await options.getFollows(pubkey, message)
8978
+ pubkeys: await options.getFollows(pubkey, message) ?? []
8382
8979
  }) : void 0
8383
8980
  );
8384
8981
  return true;
@@ -8390,11 +8987,11 @@ function handleReadProvider(options, id, message, send) {
8390
8987
  options,
8391
8988
  send,
8392
8989
  { type: "identity.getZaps.result", id, zaps: [] },
8393
- "getZaps failed",
8990
+ "identity.getZaps failed",
8394
8991
  options.getZaps ? async (pubkey) => ({
8395
8992
  type: "identity.getZaps.result",
8396
8993
  id,
8397
- zaps: await options.getZaps(pubkey, message)
8994
+ zaps: await options.getZaps(pubkey, message) ?? []
8398
8995
  }) : void 0
8399
8996
  );
8400
8997
  return true;
@@ -8403,11 +9000,11 @@ function handleReadProvider(options, id, message, send) {
8403
9000
  options,
8404
9001
  send,
8405
9002
  { type: "identity.getMutes.result", id, pubkeys: [] },
8406
- "getMutes failed",
9003
+ "identity.getMutes failed",
8407
9004
  options.getMutes ? async (pubkey) => ({
8408
9005
  type: "identity.getMutes.result",
8409
9006
  id,
8410
- pubkeys: await options.getMutes(pubkey, message)
9007
+ pubkeys: await options.getMutes(pubkey, message) ?? []
8411
9008
  }) : void 0
8412
9009
  );
8413
9010
  return true;
@@ -8416,11 +9013,11 @@ function handleReadProvider(options, id, message, send) {
8416
9013
  options,
8417
9014
  send,
8418
9015
  { type: "identity.getBlocked.result", id, pubkeys: [] },
8419
- "getBlocked failed",
9016
+ "identity.getBlocked failed",
8420
9017
  options.getBlocked ? async (pubkey) => ({
8421
9018
  type: "identity.getBlocked.result",
8422
9019
  id,
8423
- pubkeys: await options.getBlocked(pubkey, message)
9020
+ pubkeys: await options.getBlocked(pubkey, message) ?? []
8424
9021
  }) : void 0
8425
9022
  );
8426
9023
  return true;
@@ -8429,11 +9026,11 @@ function handleReadProvider(options, id, message, send) {
8429
9026
  options,
8430
9027
  send,
8431
9028
  { type: "identity.getBadges.result", id, badges: [] },
8432
- "getBadges failed",
9029
+ "identity.getBadges failed",
8433
9030
  options.getBadges ? async (pubkey) => ({
8434
9031
  type: "identity.getBadges.result",
8435
9032
  id,
8436
- badges: await options.getBadges(pubkey, message)
9033
+ badges: await options.getBadges(pubkey, message) ?? []
8437
9034
  }) : void 0
8438
9035
  );
8439
9036
  return true;
@@ -8446,11 +9043,11 @@ function handleGetList(options, id, message, send) {
8446
9043
  options,
8447
9044
  send,
8448
9045
  { type: "identity.getList.result", id, entries: [] },
8449
- "getList failed",
9046
+ "identity.getList failed",
8450
9047
  options.getList ? async (pubkey) => ({
8451
9048
  type: "identity.getList.result",
8452
9049
  id,
8453
- entries: await options.getList(message.listType, pubkey, message)
9050
+ entries: await options.getList(message.listType, pubkey, message) ?? []
8454
9051
  }) : void 0
8455
9052
  );
8456
9053
  }
@@ -8464,9 +9061,7 @@ function handleIdentityServiceMessage(options, message, send) {
8464
9061
  handleGetRelays(options, id, send);
8465
9062
  return;
8466
9063
  default:
8467
- if (!handleReadProvider(options, id, message, send)) {
8468
- sendIdentityError(send, id, message.type, `Unknown identity method: ${message.type}`);
8469
- }
9064
+ handleReadProvider(options, id, message, send);
8470
9065
  }
8471
9066
  }
8472
9067
  function createIdentityService(options) {
@@ -8487,6 +9082,37 @@ function createIdentityService(options) {
8487
9082
  var EOSE_FALLBACK_MS = 15e3;
8488
9083
  function createRelayPoolService(options) {
8489
9084
  const tracked = /* @__PURE__ */ new Map();
9085
+ function publishEvent(relayMessage, resultType, send) {
9086
+ const id = typeof relayMessage.id === "string" ? relayMessage.id : "";
9087
+ const event = relayMessage.event;
9088
+ if (!event || typeof event !== "object") {
9089
+ send({ type: resultType, id, ok: false, error: "invalid event" });
9090
+ return;
9091
+ }
9092
+ if (!options.isAvailable()) {
9093
+ send({ type: resultType, id, ok: false, error: "no relay pool available" });
9094
+ return;
9095
+ }
9096
+ void (async () => {
9097
+ try {
9098
+ await options.publish(event);
9099
+ send({
9100
+ type: resultType,
9101
+ id,
9102
+ ok: true,
9103
+ event,
9104
+ eventId: event.id
9105
+ });
9106
+ } catch (error) {
9107
+ send({
9108
+ type: resultType,
9109
+ id,
9110
+ ok: false,
9111
+ error: error instanceof Error && error.message ? error.message : "publish failed"
9112
+ });
9113
+ }
9114
+ })();
9115
+ }
8490
9116
  return {
8491
9117
  descriptor: {
8492
9118
  name: "relay-pool",
@@ -8546,17 +9172,11 @@ function createRelayPoolService(options) {
8546
9172
  return;
8547
9173
  }
8548
9174
  if (message.type === "relay.publish") {
8549
- const event = relayMessage.event;
8550
- if (event && typeof event === "object" && options.isAvailable()) {
8551
- options.publish(event);
8552
- }
9175
+ publishEvent(relayMessage, "relay.publish.result", send);
8553
9176
  return;
8554
9177
  }
8555
9178
  if (message.type === "relay.publishEncrypted") {
8556
- const event = relayMessage.event;
8557
- if (event && typeof event === "object" && options.isAvailable()) {
8558
- options.publish(event);
8559
- }
9179
+ publishEvent(relayMessage, "relay.publishEncrypted.result", send);
8560
9180
  return;
8561
9181
  }
8562
9182
  },
@@ -9488,8 +10108,15 @@ var DEFAULT_THEME = {
9488
10108
  // fonts, background, title intentionally undefined — all optional per
9489
10109
  // @napplet/nap/theme Theme interface.
9490
10110
  };
10111
+ function isCompleteTheme(theme) {
10112
+ const colors = theme?.colors;
10113
+ return typeof colors?.background === "string" && typeof colors.text === "string" && typeof colors.primary === "string";
10114
+ }
10115
+ function normalizeTheme(theme) {
10116
+ return isCompleteTheme(theme) ? theme : DEFAULT_THEME;
10117
+ }
9491
10118
  function createThemeService(options = {}) {
9492
- let currentTheme = options.initialTheme ?? DEFAULT_THEME;
10119
+ let currentTheme = normalizeTheme(options.initialTheme);
9493
10120
  const descriptor = {
9494
10121
  name: "theme",
9495
10122
  version: THEME_SERVICE_VERSION,
@@ -9508,19 +10135,15 @@ function createThemeService(options = {}) {
9508
10135
  send(result2);
9509
10136
  return;
9510
10137
  }
9511
- send({
9512
- type: `${message.type}.error`,
9513
- id,
9514
- error: `Unknown theme method: ${message.type}`
9515
- });
10138
+ return;
9516
10139
  },
9517
10140
  // Theme service has no per-window state to clean up.
9518
10141
  onWindowDestroyed(_windowId) {
9519
10142
  }
9520
10143
  };
9521
10144
  function publishTheme(theme) {
9522
- currentTheme = theme;
9523
- const envelope = { type: "theme.changed", theme };
10145
+ currentTheme = normalizeTheme(theme);
10146
+ const envelope = { type: "theme.changed", theme: currentTheme };
9524
10147
  options.onBroadcast?.(envelope);
9525
10148
  return envelope;
9526
10149
  }
@@ -11071,42 +11694,132 @@ function toErrorMessage4(err2) {
11071
11694
  if (typeof err2 === "string") return err2;
11072
11695
  return "intent request failed";
11073
11696
  }
11697
+ function hasOwn(value, key) {
11698
+ return Object.prototype.hasOwnProperty.call(value, key);
11699
+ }
11700
+ function validateIntentRequest(value) {
11701
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
11702
+ return { error: "invalid request" };
11703
+ }
11704
+ const request = value;
11705
+ if (typeof request.archetype !== "string" || request.archetype.length === 0) {
11706
+ return { error: "invalid request" };
11707
+ }
11708
+ const action = request.action === void 0 ? "open" : request.action;
11709
+ if (typeof action !== "string" || action.length === 0) {
11710
+ return { error: "invalid request" };
11711
+ }
11712
+ if (request.convention !== void 0 && (typeof request.convention !== "string" || !/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(request.convention))) {
11713
+ return { error: "invalid convention" };
11714
+ }
11715
+ const allowedKeys = ["archetype", "action", "convention", "payload", "handler", "behavior"];
11716
+ if (Object.keys(request).some((key) => !allowedKeys.includes(key))) {
11717
+ return { error: "invoke rejected" };
11718
+ }
11719
+ if (hasOwn(request, "handler") && request.handler !== void 0 && (typeof request.handler !== "string" || request.handler.length === 0)) {
11720
+ return { error: "invoke rejected" };
11721
+ }
11722
+ let behavior;
11723
+ if (hasOwn(request, "behavior") && request.behavior !== void 0) {
11724
+ if (typeof request.behavior !== "object" || request.behavior === null || Array.isArray(request.behavior)) {
11725
+ return { error: "invoke rejected" };
11726
+ }
11727
+ const record = request.behavior;
11728
+ if (Object.keys(record).some((key) => key !== "focus" && key !== "newWindow" && key !== "reuse") || hasOwn(record, "focus") && typeof record.focus !== "boolean" || hasOwn(record, "newWindow") && typeof record.newWindow !== "boolean" || hasOwn(record, "reuse") && typeof record.reuse !== "boolean") {
11729
+ return { error: "invoke rejected" };
11730
+ }
11731
+ behavior = {
11732
+ ...record.focus === void 0 ? {} : { focus: record.focus },
11733
+ ...record.newWindow === void 0 ? {} : { newWindow: record.newWindow },
11734
+ ...record.reuse === void 0 ? {} : { reuse: record.reuse }
11735
+ };
11736
+ }
11737
+ return {
11738
+ request: {
11739
+ archetype: request.archetype,
11740
+ action,
11741
+ ...typeof request.convention === "string" ? { convention: request.convention } : {},
11742
+ ...hasOwn(request, "payload") ? { payload: request.payload } : {},
11743
+ ...typeof request.handler === "string" ? { handler: request.handler } : {},
11744
+ ...behavior === void 0 ? {} : { behavior }
11745
+ }
11746
+ };
11747
+ }
11748
+ function settleResolverCall(call, send, resultType, id, onValue) {
11749
+ let pending;
11750
+ try {
11751
+ pending = Promise.resolve(call());
11752
+ } catch (error) {
11753
+ send({ type: resultType, id, error: toErrorMessage4(error) });
11754
+ return;
11755
+ }
11756
+ pending.then((value) => send(onValue(value))).catch((error) => send({ type: resultType, id, error: toErrorMessage4(error) }));
11757
+ }
11758
+ function rejectedResult(request, error) {
11759
+ return {
11760
+ ok: false,
11761
+ archetype: request.archetype,
11762
+ action: request.action ?? "open",
11763
+ handled: false,
11764
+ error
11765
+ };
11766
+ }
11767
+ function rejectInvoke(send, id, request, error = "invoke rejected") {
11768
+ send({
11769
+ type: "intent.invoke.result",
11770
+ id,
11771
+ result: rejectedResult(request, error)
11772
+ });
11773
+ }
11074
11774
  function createIntentService(options) {
11075
11775
  if (!options || typeof options.resolver !== "object" || options.resolver === null) {
11076
11776
  throw new Error("createIntentService: options.resolver is required");
11077
11777
  }
11078
11778
  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
- }
11779
+ let runtimeContext;
11780
+ let unsubscribeChanged;
11095
11781
  function handleInvoke(windowId, msg, send) {
11096
11782
  const m = msg;
11097
11783
  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" });
11784
+ const validation = validateIntentRequest(m.request);
11785
+ if ("error" in validation) {
11786
+ const raw = typeof m.request === "object" && m.request !== null ? m.request : {};
11787
+ rejectInvoke(
11788
+ send,
11789
+ id,
11790
+ {
11791
+ archetype: typeof raw.archetype === "string" ? raw.archetype : "",
11792
+ action: typeof raw.action === "string" ? raw.action : "open"
11793
+ },
11794
+ validation.error
11795
+ );
11101
11796
  return;
11102
11797
  }
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
- );
11798
+ const sender = runtimeContext?.resolveDTag(windowId);
11799
+ if (!sender) {
11800
+ rejectInvoke(send, id, validation.request);
11801
+ return;
11802
+ }
11803
+ let pending;
11804
+ try {
11805
+ pending = Promise.resolve(resolver.invoke(validation.request, { sender }));
11806
+ } catch {
11807
+ rejectInvoke(send, id, validation.request);
11808
+ return;
11809
+ }
11810
+ void pending.then(
11811
+ (result2) => {
11812
+ send({
11813
+ type: "intent.invoke.result",
11814
+ id,
11815
+ result: result2
11816
+ });
11817
+ },
11818
+ () => {
11819
+ rejectInvoke(send, id, validation.request);
11820
+ }
11821
+ ).catch(() => {
11822
+ });
11110
11823
  }
11111
11824
  function handleAvailable(msg, send) {
11112
11825
  const m = msg;
@@ -11116,7 +11829,7 @@ function createIntentService(options) {
11116
11829
  return;
11117
11830
  }
11118
11831
  const archetype = m.archetype;
11119
- settle6(
11832
+ settleResolverCall(
11120
11833
  () => resolver.available(archetype),
11121
11834
  send,
11122
11835
  "intent.available.result",
@@ -11127,7 +11840,7 @@ function createIntentService(options) {
11127
11840
  function handleHandlers(msg, send) {
11128
11841
  const m = msg;
11129
11842
  const id = m.id ?? "";
11130
- settle6(
11843
+ settleResolverCall(
11131
11844
  () => resolver.handlers(),
11132
11845
  send,
11133
11846
  "intent.handlers.result",
@@ -11137,8 +11850,31 @@ function createIntentService(options) {
11137
11850
  }
11138
11851
  return {
11139
11852
  descriptor: INTENT_DESCRIPTOR,
11853
+ onRegistered(context) {
11854
+ try {
11855
+ unsubscribeChanged?.();
11856
+ } catch {
11857
+ }
11858
+ runtimeContext = context;
11859
+ unsubscribeChanged = resolver.onChanged?.((availability) => {
11860
+ const current = runtimeContext;
11861
+ if (!current) return;
11862
+ const message = { type: "intent.changed", availability };
11863
+ for (const windowId of current.listWindowIds()) {
11864
+ current.sendToEligibleNapplet(windowId, message);
11865
+ }
11866
+ });
11867
+ },
11868
+ onUnregistered() {
11869
+ const unsubscribe = unsubscribeChanged;
11870
+ unsubscribeChanged = void 0;
11871
+ runtimeContext = void 0;
11872
+ try {
11873
+ unsubscribe?.();
11874
+ } catch {
11875
+ }
11876
+ },
11140
11877
  handleMessage(windowId, message, send) {
11141
- windows.set(windowId, send);
11142
11878
  switch (message.type) {
11143
11879
  case "intent.invoke":
11144
11880
  handleInvoke(windowId, message, send);
@@ -11152,12 +11888,196 @@ function createIntentService(options) {
11152
11888
  default:
11153
11889
  return;
11154
11890
  }
11155
- },
11156
- onWindowDestroyed(windowId) {
11157
- windows.delete(windowId);
11158
11891
  }
11159
11892
  };
11160
11893
  }
11894
+ function candidatesFor(catalog, archetype, defaultHandler) {
11895
+ const candidates = [];
11896
+ for (const entry of catalog) {
11897
+ const support = entry.archetypes[archetype];
11898
+ if (!support) continue;
11899
+ candidates.push({
11900
+ dTag: entry.dTag,
11901
+ ...entry.title === void 0 ? {} : { title: entry.title },
11902
+ actions: [...support.actions],
11903
+ conventions: [...support.conventions],
11904
+ ...entry.dTag === defaultHandler ? { isDefault: true } : {}
11905
+ });
11906
+ }
11907
+ return candidates;
11908
+ }
11909
+ function reject(request, error) {
11910
+ return {
11911
+ ok: false,
11912
+ archetype: request.archetype,
11913
+ action: request.action ?? "open",
11914
+ handled: false,
11915
+ error
11916
+ };
11917
+ }
11918
+ async function availabilityForCatalog(options, archetype) {
11919
+ const catalog = await options.loadCatalog();
11920
+ const defaultHandler = options.getDefaultHandler?.(archetype);
11921
+ const candidates = candidatesFor(catalog, archetype, defaultHandler);
11922
+ return {
11923
+ archetype,
11924
+ available: candidates.length > 0,
11925
+ candidates,
11926
+ hasDefault: defaultHandler !== void 0 && candidates.some((candidate) => candidate.dTag === defaultHandler)
11927
+ };
11928
+ }
11929
+ async function chooseCompatible(options, request, candidates, sender) {
11930
+ if (!options.chooseHandler) {
11931
+ return request.handler === "choose" ? { error: "user cancelled" } : { error: "invoke rejected" };
11932
+ }
11933
+ const picked = await options.chooseHandler(request.archetype, candidates, sender);
11934
+ if (picked === void 0) return { error: "user cancelled" };
11935
+ const candidate = candidates.find((item) => item.dTag === picked);
11936
+ return candidate ? { candidate } : { error: "invoke rejected" };
11937
+ }
11938
+ async function pickHandler(options, request, compatible, sender) {
11939
+ const preference = request.handler;
11940
+ if (typeof preference === "string" && preference !== "default" && preference !== "choose") {
11941
+ const candidate = compatible.find((item) => item.dTag === preference);
11942
+ if (!candidate || !options.authorizeExplicitHandler) return { error: "invoke rejected" };
11943
+ let authorized = false;
11944
+ try {
11945
+ authorized = await options.authorizeExplicitHandler(
11946
+ sender,
11947
+ preference,
11948
+ request,
11949
+ candidate
11950
+ );
11951
+ } catch {
11952
+ return { error: "invoke rejected" };
11953
+ }
11954
+ return authorized ? { candidate } : { error: "invoke rejected" };
11955
+ }
11956
+ if (preference === "choose") {
11957
+ return chooseCompatible(options, request, compatible, sender);
11958
+ }
11959
+ const defaultCandidate = compatible.find((candidate) => candidate.isDefault === true);
11960
+ if (preference === "default") {
11961
+ return defaultCandidate ? { candidate: defaultCandidate } : { error: "invoke rejected" };
11962
+ }
11963
+ if (defaultCandidate) return { candidate: defaultCandidate };
11964
+ if (compatible.length === 1) return { candidate: compatible[0] };
11965
+ return chooseCompatible(options, request, compatible, sender);
11966
+ }
11967
+ async function invokeFromCatalog(options, request, context) {
11968
+ const catalog = await options.loadCatalog();
11969
+ const defaultHandler = options.getDefaultHandler?.(request.archetype);
11970
+ const candidates = candidatesFor(catalog, request.archetype, defaultHandler);
11971
+ if (candidates.length === 0) return reject(request, "no handler");
11972
+ const action = request.action ?? "open";
11973
+ const actionCompatible = candidates.filter((candidate) => candidate.actions.includes(action));
11974
+ if (actionCompatible.length === 0) return reject(request, "unsupported action");
11975
+ const compatible = request.convention === void 0 ? actionCompatible : actionCompatible.filter((candidate) => candidate.conventions.includes(request.convention));
11976
+ if (compatible.length === 0) return reject(request, "unsupported convention");
11977
+ const sender = context.sender;
11978
+ if (typeof sender !== "string" || sender.length === 0) {
11979
+ return reject(request, "invoke rejected");
11980
+ }
11981
+ const selected = await pickHandler(options, request, compatible, sender);
11982
+ if ("error" in selected) return reject(request, selected.error);
11983
+ const convention = request.convention ?? selected.candidate.conventions.find((value) => {
11984
+ const match = /^napplet:[^/?#\s]+\/([^/?#\s]+)$/.exec(value);
11985
+ return match?.[1] === action;
11986
+ });
11987
+ if (!convention) return reject(request, "unsupported convention");
11988
+ const behavior = request.behavior === void 0 ? void 0 : Object.freeze({
11989
+ ...request.behavior.focus === void 0 ? {} : { focus: request.behavior.focus },
11990
+ ...request.behavior.newWindow === void 0 ? {} : { newWindow: request.behavior.newWindow },
11991
+ ...request.behavior.reuse === void 0 ? {} : { reuse: request.behavior.reuse }
11992
+ });
11993
+ const params = Object.freeze({
11994
+ handler: selected.candidate.dTag,
11995
+ sender,
11996
+ archetype: request.archetype,
11997
+ action,
11998
+ convention,
11999
+ ...request.payload === void 0 ? {} : { payload: request.payload },
12000
+ ...behavior === void 0 ? {} : { behavior }
12001
+ });
12002
+ let target;
12003
+ try {
12004
+ target = await options.targets.dispatch(params);
12005
+ } catch {
12006
+ return reject(request, "invoke failed");
12007
+ }
12008
+ if (!target || typeof target.windowId !== "string" || target.windowId.length === 0) {
12009
+ return reject(request, "invoke failed");
12010
+ }
12011
+ return {
12012
+ ok: true,
12013
+ archetype: request.archetype,
12014
+ action,
12015
+ handled: true,
12016
+ handler: selected.candidate.dTag,
12017
+ windowId: target.windowId,
12018
+ convention
12019
+ };
12020
+ }
12021
+ function createCatalogIntentResolver(options) {
12022
+ if (!options || typeof options.loadCatalog !== "function") {
12023
+ throw new Error("createCatalogIntentResolver: options.loadCatalog is required");
12024
+ }
12025
+ if (!options.targets || typeof options.targets.dispatch !== "function") {
12026
+ throw new Error("createCatalogIntentResolver: options.targets is required");
12027
+ }
12028
+ const listeners = /* @__PURE__ */ new Set();
12029
+ const availabilityFor = (archetype) => availabilityForCatalog(options, archetype);
12030
+ async function handlers() {
12031
+ const catalog = await options.loadCatalog();
12032
+ const archetypes = /* @__PURE__ */ new Set();
12033
+ for (const entry of catalog) {
12034
+ for (const slug of Object.keys(entry.archetypes)) archetypes.add(slug);
12035
+ }
12036
+ return Promise.all([...archetypes].map((archetype) => availabilityFor(archetype)));
12037
+ }
12038
+ return {
12039
+ invoke: (request, context) => invokeFromCatalog(options, request, context),
12040
+ available: availabilityFor,
12041
+ handlers,
12042
+ onChanged(listener) {
12043
+ listeners.add(listener);
12044
+ return () => listeners.delete(listener);
12045
+ },
12046
+ notifyChanged(archetype) {
12047
+ if (listeners.size === 0) return;
12048
+ void availabilityFor(archetype).then((availability) => {
12049
+ for (const listener of listeners) listener(availability);
12050
+ });
12051
+ }
12052
+ };
12053
+ }
12054
+ function actionFromConvention(slug, convention) {
12055
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
12056
+ throw new TypeError("manifest archetype slug is invalid");
12057
+ }
12058
+ const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(convention);
12059
+ if (!match || match[1] !== slug) {
12060
+ throw new TypeError("manifest archetype convention is invalid or mismatched");
12061
+ }
12062
+ return match[2];
12063
+ }
12064
+ function manifestToIntentCatalogEntry(manifest) {
12065
+ const archetypes = {};
12066
+ for (const { slug, convention } of manifest.archetypes) {
12067
+ const action = actionFromConvention(slug, convention);
12068
+ const support = archetypes[slug] ??= {
12069
+ actions: [],
12070
+ conventions: []
12071
+ };
12072
+ if (!support.actions.includes(action)) support.actions.push(action);
12073
+ if (!support.conventions.includes(convention)) support.conventions.push(convention);
12074
+ }
12075
+ return {
12076
+ dTag: manifest.dTag,
12077
+ ...manifest.title === void 0 ? {} : { title: manifest.title },
12078
+ archetypes
12079
+ };
12080
+ }
11161
12081
  var CVM_SERVICE_VERSION = "1.0.0";
11162
12082
  var CVM_DESCRIPTOR = {
11163
12083
  name: "cvm",
@@ -12148,6 +13068,193 @@ function createDevWebrtcController() {
12148
13068
  };
12149
13069
  }
12150
13070
 
13071
+ // src/browser-intent-controller.ts
13072
+ var MAX_INTENT_DELIVERY_ATTEMPTS = 10;
13073
+ var BrowserIntentController = class {
13074
+ constructor(options) {
13075
+ this.options = options;
13076
+ this.maxAttempts = normalizeAttempts(options.maxAttempts);
13077
+ }
13078
+ options;
13079
+ maxAttempts;
13080
+ async dispatch(params) {
13081
+ const dispatch2 = freezeDispatch(params);
13082
+ let reason = "no-current-target";
13083
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt += 1) {
13084
+ let generation;
13085
+ try {
13086
+ generation = await this.options.openOrReuse(dispatch2, attempt);
13087
+ } catch {
13088
+ reason = "open-failed";
13089
+ continue;
13090
+ }
13091
+ if (!generation) {
13092
+ reason = "open-failed";
13093
+ continue;
13094
+ }
13095
+ try {
13096
+ await this.options.waitForReady(generation);
13097
+ } catch {
13098
+ reason = "ready-failed";
13099
+ continue;
13100
+ }
13101
+ if (!await this.options.isCurrent(generation)) {
13102
+ reason = "no-current-target";
13103
+ continue;
13104
+ }
13105
+ try {
13106
+ await this.options.send(generation, dispatch2);
13107
+ const windowId = this.options.getWindowId(generation);
13108
+ if (!windowId) throw new Error("intent target window is unavailable");
13109
+ return { windowId };
13110
+ } catch {
13111
+ this.options.onTerminal?.(dispatch2, "send-failed");
13112
+ throw new Error("intent target send failed");
13113
+ }
13114
+ }
13115
+ this.options.onTerminal?.(dispatch2, reason);
13116
+ throw new Error(`intent target ${reason}`);
13117
+ }
13118
+ };
13119
+ function normalizeAttempts(value) {
13120
+ if (value === void 0) return 2;
13121
+ if (!Number.isFinite(value)) throw new TypeError("maxAttempts must be finite");
13122
+ return Math.min(MAX_INTENT_DELIVERY_ATTEMPTS, Math.max(1, Math.floor(value)));
13123
+ }
13124
+ function freezeDispatch(params) {
13125
+ if (!params || typeof params.handler !== "string" || params.handler.length === 0) {
13126
+ throw new TypeError("Intent dispatch requires a handler");
13127
+ }
13128
+ if (typeof params.sender !== "string" || params.sender.length === 0 || typeof params.archetype !== "string" || params.archetype.length === 0 || typeof params.action !== "string" || params.action.length === 0 || typeof params.convention !== "string" || params.convention.length === 0) {
13129
+ throw new TypeError("Intent dispatch requires canonical routing fields");
13130
+ }
13131
+ const payload = freezeValue(params.payload);
13132
+ const behavior = params.behavior === void 0 ? void 0 : freezeValue({ ...params.behavior });
13133
+ return Object.freeze({
13134
+ handler: params.handler,
13135
+ sender: params.sender,
13136
+ archetype: params.archetype,
13137
+ action: params.action,
13138
+ convention: params.convention,
13139
+ ...params.payload === void 0 ? {} : { payload },
13140
+ ...behavior === void 0 ? {} : { behavior }
13141
+ });
13142
+ }
13143
+ function freezeValue(value, seen = /* @__PURE__ */ new WeakMap()) {
13144
+ if (typeof value !== "object" || value === null) return value;
13145
+ const existing = seen.get(value);
13146
+ if (existing !== void 0) return existing;
13147
+ if (Array.isArray(value)) {
13148
+ const copy2 = [];
13149
+ seen.set(value, copy2);
13150
+ for (const item of value) copy2.push(freezeValue(item, seen));
13151
+ return Object.freeze(copy2);
13152
+ }
13153
+ const copy = {};
13154
+ seen.set(value, copy);
13155
+ for (const [key, item] of Object.entries(value)) copy[key] = freezeValue(item, seen);
13156
+ return Object.freeze(copy);
13157
+ }
13158
+
13159
+ // src/installed-napplet-catalog.ts
13160
+ function matchesInstalledNappletRecord(record, target) {
13161
+ return target.dTag === record.dTag && target.aggregateHash === record.aggregateHash;
13162
+ }
13163
+ var InstalledNappletCatalog = class {
13164
+ records = /* @__PURE__ */ new Map();
13165
+ listeners = /* @__PURE__ */ new Set();
13166
+ /** Insert or replace a record from an already resolver-verified pointer. */
13167
+ install(resolved) {
13168
+ const record = freezeRecord({
13169
+ dTag: resolved.dTag,
13170
+ aggregateHash: resolved.aggregateHash,
13171
+ pointer: copyPointer(resolved.pointer),
13172
+ ...resolved.manifest.title === void 0 ? {} : { title: resolved.manifest.title },
13173
+ requires: [...resolved.manifest.requires],
13174
+ archetypes: resolved.manifest.archetypes.map((archetype) => ({
13175
+ slug: archetype.slug,
13176
+ convention: archetype.convention
13177
+ }))
13178
+ });
13179
+ this.records.set(record.dTag, record);
13180
+ this.notify(record.dTag);
13181
+ return record;
13182
+ }
13183
+ /** Remove an artifact explicitly; frame teardown never calls this method. */
13184
+ remove(dTag) {
13185
+ const removed = this.records.delete(dTag);
13186
+ if (removed) this.notify(dTag);
13187
+ return removed;
13188
+ }
13189
+ /** Return whether a verified installation exists for a d-tag. */
13190
+ has(dTag) {
13191
+ return this.records.has(dTag);
13192
+ }
13193
+ /** Return immutable serializable installed-artifact facts. */
13194
+ installed() {
13195
+ return [...this.records.values()];
13196
+ }
13197
+ /** Return a verified record by d-tag. */
13198
+ get(dTag) {
13199
+ return this.records.get(dTag);
13200
+ }
13201
+ /**
13202
+ * Atomically confirm that a resolved target still belongs to the exact record
13203
+ * selected before an async operation. The record object is the catalog version
13204
+ * token, so even a same-identity replacement cannot pass this check.
13205
+ */
13206
+ validateCurrent(selected, target) {
13207
+ if (this.records.get(selected.dTag) !== selected) return null;
13208
+ return matchesInstalledNappletRecord(selected, target) ? selected : null;
13209
+ }
13210
+ /** Return exact handler candidates derived only from installed manifests. */
13211
+ intentCatalog() {
13212
+ return this.installed().filter((record) => record.requires.includes("inc")).map((record) => manifestToIntentCatalogEntry({
13213
+ dTag: record.dTag,
13214
+ ...record.title === void 0 ? {} : { title: record.title },
13215
+ archetypes: record.archetypes.map((archetype) => ({
13216
+ slug: archetype.slug,
13217
+ convention: archetype.convention
13218
+ }))
13219
+ }));
13220
+ }
13221
+ /** Subscribe to explicit install and artifact-removal changes. */
13222
+ onChanged(listener) {
13223
+ this.listeners.add(listener);
13224
+ return () => this.listeners.delete(listener);
13225
+ }
13226
+ notify(dTag) {
13227
+ for (const listener of this.listeners) listener(dTag);
13228
+ }
13229
+ };
13230
+ function copyPointer(pointer) {
13231
+ if (pointer.type === "naddr") {
13232
+ return Object.freeze({
13233
+ type: "naddr",
13234
+ value: pointer.value,
13235
+ identifier: pointer.identifier,
13236
+ pubkey: pointer.pubkey,
13237
+ kind: pointer.kind,
13238
+ relays: Object.freeze([...pointer.relays])
13239
+ });
13240
+ }
13241
+ return Object.freeze({
13242
+ type: "nevent",
13243
+ value: pointer.value,
13244
+ id: pointer.id,
13245
+ ...pointer.author === void 0 ? {} : { author: pointer.author },
13246
+ ...pointer.kind === void 0 ? {} : { kind: pointer.kind },
13247
+ relays: Object.freeze([...pointer.relays])
13248
+ });
13249
+ }
13250
+ function freezeRecord(record) {
13251
+ return Object.freeze({
13252
+ ...record,
13253
+ requires: Object.freeze([...record.requires]),
13254
+ archetypes: Object.freeze(record.archetypes.map((archetype) => Object.freeze({ ...archetype })))
13255
+ });
13256
+ }
13257
+
12151
13258
  // src/simulation.ts
12152
13259
  var PajaSimulationError = class extends Error {
12153
13260
  constructor(message) {
@@ -12725,10 +13832,10 @@ var AbstractRelay = class {
12725
13832
  this.challenge = void 0;
12726
13833
  this.authPromise = void 0;
12727
13834
  this.skipReconnection = false;
12728
- this.connectionPromise = new Promise((resolve, reject) => {
13835
+ this.connectionPromise = new Promise((resolve, reject2) => {
12729
13836
  if (opts?.timeout) {
12730
13837
  connectionTimeoutHandle = setTimeout(() => {
12731
- reject("connection timed out");
13838
+ reject2("connection timed out");
12732
13839
  this.connectionPromise = void 0;
12733
13840
  this.skipReconnection = true;
12734
13841
  this.onclose?.();
@@ -12736,13 +13843,13 @@ var AbstractRelay = class {
12736
13843
  }, opts.timeout);
12737
13844
  }
12738
13845
  if (opts?.abort) {
12739
- opts.abort.onabort = reject;
13846
+ opts.abort.onabort = reject2;
12740
13847
  }
12741
13848
  try {
12742
13849
  this.ws = new this._WebSocket(this.url);
12743
13850
  } catch (err) {
12744
13851
  clearTimeout(connectionTimeoutHandle);
12745
- reject(err);
13852
+ reject2(err);
12746
13853
  return;
12747
13854
  }
12748
13855
  this.ws.onopen = () => {
@@ -12772,7 +13879,7 @@ var AbstractRelay = class {
12772
13879
  };
12773
13880
  this.ws.onerror = () => {
12774
13881
  clearTimeout(connectionTimeoutHandle);
12775
- reject("connection failed");
13882
+ reject2("connection failed");
12776
13883
  this.connectionPromise = void 0;
12777
13884
  this.skipReconnection = true;
12778
13885
  this.onclose?.();
@@ -12780,7 +13887,7 @@ var AbstractRelay = class {
12780
13887
  };
12781
13888
  this.ws.onclose = (ev) => {
12782
13889
  clearTimeout(connectionTimeoutHandle);
12783
- reject(ev.message || "websocket closed");
13890
+ reject2(ev.message || "websocket closed");
12784
13891
  this.handleHardClose("relay connection closed");
12785
13892
  };
12786
13893
  this.ws.onmessage = this._onmessage.bind(this);
@@ -12795,9 +13902,9 @@ var AbstractRelay = class {
12795
13902
  });
12796
13903
  }
12797
13904
  waitForDummyReq() {
12798
- return new Promise((resolve, reject) => {
13905
+ return new Promise((resolve, reject2) => {
12799
13906
  if (!this.connectionPromise)
12800
- return reject(new Error(`no connection to ${this.url}, can't ping`));
13907
+ return reject2(new Error(`no connection to ${this.url}, can't ping`));
12801
13908
  try {
12802
13909
  const sub = this.subscribe(
12803
13910
  [{ ids: ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], limit: 0 }],
@@ -12814,7 +13921,7 @@ var AbstractRelay = class {
12814
13921
  }
12815
13922
  );
12816
13923
  } catch (err) {
12817
- reject(err);
13924
+ reject2(err);
12818
13925
  }
12819
13926
  });
12820
13927
  }
@@ -12844,7 +13951,7 @@ var AbstractRelay = class {
12844
13951
  throw new Error("can't perform auth, no challenge was received");
12845
13952
  if (this.authPromise)
12846
13953
  return this.authPromise;
12847
- this.authPromise = new Promise(async (resolve, reject) => {
13954
+ this.authPromise = new Promise(async (resolve, reject2) => {
12848
13955
  try {
12849
13956
  let evt = await signAuthEvent(makeAuthEvent(this.url, challenge2));
12850
13957
  let timeout = setTimeout(() => {
@@ -12854,7 +13961,7 @@ var AbstractRelay = class {
12854
13961
  this.openEventPublishes.delete(evt.id);
12855
13962
  }
12856
13963
  }, this.publishTimeout);
12857
- this.openEventPublishes.set(evt.id, { resolve, reject, timeout });
13964
+ this.openEventPublishes.set(evt.id, { resolve, reject: reject2, timeout });
12858
13965
  this.send('["AUTH",' + JSON.stringify(evt) + "]");
12859
13966
  } catch (err) {
12860
13967
  console.warn("subscribe auth function failed:", err);
@@ -12865,7 +13972,7 @@ var AbstractRelay = class {
12865
13972
  async publish(event) {
12866
13973
  this.idleSince = void 0;
12867
13974
  this.ongoingOperations++;
12868
- const ret = new Promise((resolve, reject) => {
13975
+ const ret = new Promise((resolve, reject2) => {
12869
13976
  const timeout = setTimeout(() => {
12870
13977
  const ep = this.openEventPublishes.get(event.id);
12871
13978
  if (ep) {
@@ -12873,7 +13980,7 @@ var AbstractRelay = class {
12873
13980
  this.openEventPublishes.delete(event.id);
12874
13981
  }
12875
13982
  }, this.publishTimeout);
12876
- this.openEventPublishes.set(event.id, { resolve, reject, timeout });
13983
+ this.openEventPublishes.set(event.id, { resolve, reject: reject2, timeout });
12877
13984
  });
12878
13985
  this.send('["EVENT",' + JSON.stringify(event) + "]");
12879
13986
  this.ongoingOperations--;
@@ -12884,8 +13991,8 @@ var AbstractRelay = class {
12884
13991
  async count(filters, params) {
12885
13992
  this.serial++;
12886
13993
  const id = params?.id || "count:" + this.serial;
12887
- const ret = new Promise((resolve, reject) => {
12888
- this.openCountRequests.set(id, { resolve, reject });
13994
+ const ret = new Promise((resolve, reject2) => {
13995
+ this.openCountRequests.set(id, { resolve, reject: reject2 });
12889
13996
  });
12890
13997
  this.send('["COUNT","' + id + '",' + JSON.stringify(filters).substring(1));
12891
13998
  return ret;
@@ -13472,9 +14579,8 @@ async function publishLive(pool, relayUrls, event) {
13472
14579
  const results = await Promise.allSettled(pool.publish(relayUrls, event));
13473
14580
  return Object.fromEntries(relayUrls.map((relayUrl, index) => [relayUrl, results[index]?.status === "fulfilled"]));
13474
14581
  }
13475
- function createPajaRelayBackend(getSimulation, confirmRequest) {
14582
+ function createPajaRelayBackend(getSimulation, confirmRequest, livePool = new SimplePool()) {
13476
14583
  const events = getSimulation().relay.fixtures.flatMap(toNostrEvent);
13477
- const livePool = new SimplePool();
13478
14584
  const subscribers = /* @__PURE__ */ new Set();
13479
14585
  const isAvailable = () => getSimulation().relay.mode !== "disabled" && getPajaRelayUrls(getSimulation()).length > 0;
13480
14586
  const query = async (relayUrls, filters, maxWaitMs) => {
@@ -13485,6 +14591,28 @@ function createPajaRelayBackend(getSimulation, confirmRequest) {
13485
14591
  for (const event of [...memoryEvents, ...liveEvents]) out.set(event.id, event);
13486
14592
  return [...out.values()].sort((a, b) => b.created_at - a.created_at);
13487
14593
  };
14594
+ function retainPublishedEvent(event) {
14595
+ events.push(event);
14596
+ for (const subscriber of subscribers) {
14597
+ if (matchesAnyFilter2(event, subscriber.filters)) subscriber.next(event);
14598
+ }
14599
+ }
14600
+ async function attemptPublish(relayUrls, event) {
14601
+ const simulation = getSimulation();
14602
+ const rejected = Object.fromEntries(relayUrls.map((url) => [url, false]));
14603
+ if (simulation.relay.mode === "disabled") {
14604
+ return { outcomes: rejected, error: "relay unavailable" };
14605
+ }
14606
+ if (!confirmRequest({ action: "publish", event })) {
14607
+ return { outcomes: rejected, error: "publish denied" };
14608
+ }
14609
+ const outcomes = simulation.relay.mode === "live" ? await publishLive(livePool, relayUrls, event) : Object.fromEntries(relayUrls.map((url) => [url, true]));
14610
+ if (!Object.values(outcomes).some(Boolean)) {
14611
+ return { outcomes, error: "publish failed" };
14612
+ }
14613
+ retainPublishedEvent(event);
14614
+ return { outcomes };
14615
+ }
13488
14616
  const backend = {
13489
14617
  subscription(relayUrls, filtersInput) {
13490
14618
  return {
@@ -13509,16 +14637,9 @@ function createPajaRelayBackend(getSimulation, confirmRequest) {
13509
14637
  }
13510
14638
  };
13511
14639
  },
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
- }
14640
+ async publish(relayUrls, event) {
14641
+ const attempt = await attemptPublish(relayUrls, event);
14642
+ if (attempt.error) throw new Error(attempt.error);
13522
14643
  },
13523
14644
  request(relayUrls, filtersInput) {
13524
14645
  return {
@@ -13538,14 +14659,7 @@ function createPajaRelayBackend(getSimulation, confirmRequest) {
13538
14659
  },
13539
14660
  query,
13540
14661
  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);
14662
+ return (await attemptPublish(relayUrls, event)).outcomes;
13549
14663
  },
13550
14664
  isAvailable,
13551
14665
  close() {
@@ -13666,7 +14780,6 @@ function createPajaIdentityProviders(backend, getSimulation, signerProvider) {
13666
14780
  }
13667
14781
 
13668
14782
  // src/browser-adapter.ts
13669
- var DEV_INTENT_ARCHETYPE = "paja-target";
13670
14783
  var DEV_COMMON_PUBKEY = "1".repeat(64);
13671
14784
  var DEV_COMMON_EVENT_ID = "2".repeat(64);
13672
14785
  var DEV_SIGNER_SECRET_KEY = generateSecretKey();
@@ -13693,10 +14806,14 @@ function createRelayHooks(pool, getSimulation) {
13693
14806
  },
13694
14807
  closeScopedRelay: () => {
13695
14808
  },
13696
- publishToScopedRelay: (_windowId, event) => {
14809
+ publishToScopedRelay: async (_windowId, event) => {
13697
14810
  if (getSimulation().relay.mode === "disabled") return false;
13698
- pool.publish(getPajaRelayUrls(getSimulation()), event);
13699
- return true;
14811
+ try {
14812
+ await pool.publish(getPajaRelayUrls(getSimulation()), event);
14813
+ return true;
14814
+ } catch {
14815
+ return false;
14816
+ }
13700
14817
  },
13701
14818
  selectRelayTier: () => getPajaRelayUrls(getSimulation())
13702
14819
  };
@@ -13749,18 +14866,16 @@ function createDevUploader(getSimulation) {
13749
14866
  }
13750
14867
  };
13751
14868
  }
13752
- function createDevIntentAvailability() {
14869
+ function createDefaultIntentHost() {
13753
14870
  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
- }]
14871
+ catalog: new InstalledNappletCatalog(),
14872
+ controller: new BrowserIntentController({
14873
+ openOrReuse: () => null,
14874
+ waitForReady: () => void 0,
14875
+ isCurrent: () => false,
14876
+ getWindowId: () => null,
14877
+ send: () => void 0
14878
+ })
13764
14879
  };
13765
14880
  }
13766
14881
  function createDevCvmTransport(getSimulation) {
@@ -13853,12 +14968,11 @@ function createRuntimeSigner(getSimulation, confirmRequest, signerProvider) {
13853
14968
  }
13854
14969
  return signer;
13855
14970
  }
13856
- function createDevServices(backend, getSimulation, onThemeService, confirmRequest, uploadRuntime, signerProvider) {
14971
+ function createDevServices(backend, getSimulation, onThemeService, onThemeBroadcast, confirmRequest, uploadRuntime, signerProvider, intentHost = createDefaultIntentHost()) {
13857
14972
  const notification = createNotificationService({ maxPerWindow: 50 });
13858
14973
  const theme = createThemeService({
13859
14974
  initialTheme: createDevTheme(getSimulation().theme.mode, getSimulation().theme.values),
13860
- onBroadcast: () => {
13861
- }
14975
+ onBroadcast: onThemeBroadcast
13862
14976
  });
13863
14977
  onThemeService(theme);
13864
14978
  const config = createConfigService({
@@ -13938,26 +15052,15 @@ function createDevServices(backend, getSimulation, onThemeService, confirmReques
13938
15052
  });
13939
15053
  }
13940
15054
  if (getSimulation().intent.enabled) {
15055
+ const resolver = createCatalogIntentResolver({
15056
+ loadCatalog: () => intentHost.catalog.intentCatalog(),
15057
+ targets: intentHost.controller,
15058
+ getDefaultHandler: intentHost.getDefaultHandler,
15059
+ chooseHandler: intentHost.chooseHandler,
15060
+ authorizeExplicitHandler: intentHost.authorizeExplicitHandler
15061
+ });
13941
15062
  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
- }
15063
+ resolver
13961
15064
  });
13962
15065
  }
13963
15066
  if (getSimulation().capabilities.domains.link) {
@@ -13999,7 +15102,7 @@ function createDevServices(backend, getSimulation, onThemeService, confirmReques
13999
15102
  }
14000
15103
  return services;
14001
15104
  }
14002
- function createPajaAdapter(config, getSimulation, onThemeService, confirmRequest, signerProvider, getIdentity) {
15105
+ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadcast, confirmRequest, signerProvider, getIdentity, onEnvironmentChanged, intentHost) {
14003
15106
  const relayBackend = createPajaRelayBackend(getSimulation, confirmRequest);
14004
15107
  const uploadRuntime = getSimulation().upload.mode === "blossom" ? createPajaUploadRuntime({
14005
15108
  getSimulation,
@@ -14015,7 +15118,11 @@ function createPajaAdapter(config, getSimulation, onThemeService, confirmRequest
14015
15118
  subscribeSignerChange: signerProvider?.subscribe?.bind(signerProvider)
14016
15119
  }) : void 0;
14017
15120
  void uploadRuntime?.refreshIdentity();
15121
+ signerProvider?.subscribe?.(() => {
15122
+ void uploadRuntime?.refreshIdentity().finally(() => onEnvironmentChanged?.());
15123
+ });
14018
15124
  const workerRelayEvents = [];
15125
+ const resolvedIntentHost = intentHost ?? createDefaultIntentHost();
14019
15126
  return {
14020
15127
  relayPool: createRelayHooks(relayBackend, getSimulation),
14021
15128
  relayConfig: {
@@ -14034,7 +15141,16 @@ function createPajaAdapter(config, getSimulation, onThemeService, confirmRequest
14034
15141
  getUserPubkey: () => getRuntimePubkey(getSimulation, signerProvider),
14035
15142
  getSigner: () => createRuntimeSigner(getSimulation, confirmRequest, signerProvider)
14036
15143
  },
14037
- services: createDevServices(relayBackend, getSimulation, onThemeService, confirmRequest, uploadRuntime, signerProvider),
15144
+ services: createDevServices(
15145
+ relayBackend,
15146
+ getSimulation,
15147
+ onThemeService,
15148
+ onThemeBroadcast,
15149
+ confirmRequest,
15150
+ uploadRuntime,
15151
+ signerProvider,
15152
+ resolvedIntentHost
15153
+ ),
14038
15154
  get capabilities() {
14039
15155
  return { disabledDomains: getSimulation().capabilities.disabledDomains };
14040
15156
  },
@@ -14052,119 +15168,358 @@ function createPajaAdapter(config, getSimulation, onThemeService, confirmRequest
14052
15168
  webrtc: { isAvailable: () => getSimulation().capabilities.domains.webrtc },
14053
15169
  crypto: {
14054
15170
  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
15171
  }
14063
15172
  };
14064
15173
  }
14065
15174
 
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);
15175
+ // src/browser-devtools.ts
15176
+ var PAJA_LOG_LIMIT = 500;
15177
+ var proxyToReal = /* @__PURE__ */ new WeakMap();
15178
+ var realToProxy = /* @__PURE__ */ new WeakMap();
15179
+ var proxyContexts = /* @__PURE__ */ new WeakMap();
15180
+ function isNappletMessage(value) {
15181
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string";
14072
15182
  }
14073
- function mul2(n) {
14074
- return n << 1 ^ POLY & -(n >> 7);
15183
+ function describeMessage(raw) {
15184
+ if (isNappletMessage(raw)) return raw.type;
15185
+ if (Array.isArray(raw) && typeof raw[0] === "string") return raw[0];
15186
+ return "unknown";
14075
15187
  }
14076
- function mul(a, b) {
14077
- let res = 0;
14078
- for (; b > 0; b >>= 1) {
14079
- res ^= a & -(b & 1);
14080
- a = mul2(a);
15188
+ function previewMessage(raw) {
15189
+ try {
15190
+ return JSON.stringify(raw);
15191
+ } catch (error) {
15192
+ return `unserializable:${error instanceof Error ? error.message : String(error)}`;
14081
15193
  }
14082
- return res;
14083
15194
  }
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;
15195
+ function compactMessageDetail(raw) {
15196
+ const detail = {};
15197
+ for (const [key, value] of Object.entries(raw)) {
15198
+ if (key !== "type" && key !== "id" && value !== void 0) detail[key] = value;
14094
15199
  }
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
- }
15200
+ if (Object.keys(detail).length === 0) return "";
15201
+ try {
15202
+ return JSON.stringify(detail);
15203
+ } catch {
15204
+ return "";
14118
15205
  }
14119
- return { sbox: sbox2, sbox2: sbox22, T0, T1, T2, T3, T01, T23 };
14120
15206
  }
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;
15207
+ function describeMessageDetail(raw) {
15208
+ if (!isNappletMessage(raw)) return "";
15209
+ const value = raw;
15210
+ for (const key of ["error", "message", "reason"]) {
15211
+ const detail = value[key];
15212
+ if (typeof detail === "string" && detail.length > 0) return detail;
14149
15213
  }
14150
- clean2(...toClean);
14151
- return xk;
15214
+ return raw.type.endsWith(".error") ? compactMessageDetail(value) : "";
14152
15215
  }
14153
- function expandKeyDecLE(key) {
14154
- const encKey = expandKeyLE(key);
14155
- const xk = encKey.slice();
14156
- const Nk = encKey.length;
14157
- const { sbox2 } = tableEncoding;
14158
- const { T0, T1, T2, T3 } = tableDecoding;
14159
- for (let i4 = 0; i4 < Nk; i4 += 4) {
14160
- for (let j = 0; j < 4; j++)
14161
- xk[i4 + j] = encKey[Nk - i4 - 4 + j];
14162
- }
14163
- clean2(encKey);
14164
- for (let i4 = 4; i4 < Nk - 4; i4++) {
14165
- const x = xk[i4];
14166
- const w = applySbox(sbox2, x, x, x, x);
14167
- xk[i4] = T0[w & 255] ^ T1[w >>> 8 & 255] ^ T2[w >>> 16 & 255] ^ T3[w >>> 24];
15216
+ function domainEnabled(simulation, domain) {
15217
+ return simulation.capabilities.domains[domain] === true;
15218
+ }
15219
+ function getTargetIdentity(config) {
15220
+ return {
15221
+ pubkey: "",
15222
+ dTag: config.window.dTag,
15223
+ aggregateHash: config.window.aggregateHash
15224
+ };
15225
+ }
15226
+ function appendPajaMessageLog(state, direction, raw, windowId) {
15227
+ if (!state) return;
15228
+ state.messageLog.push({
15229
+ index: state.messageLog.length,
15230
+ timestamp: Date.now(),
15231
+ direction,
15232
+ type: describeMessage(raw),
15233
+ windowId,
15234
+ preview: previewMessage(raw),
15235
+ detail: describeMessageDetail(raw)
15236
+ });
15237
+ if (state.messageLog.length > PAJA_LOG_LIMIT) {
15238
+ state.messageLog.splice(0, state.messageLog.length - PAJA_LOG_LIMIT);
15239
+ }
15240
+ renderPajaMessageLog(state);
15241
+ }
15242
+ function createPajaPostMessageProxy(realWin, state, windowId) {
15243
+ const existing = realToProxy.get(realWin);
15244
+ if (existing) {
15245
+ proxyContexts.set(existing, { state, windowId });
15246
+ return existing;
15247
+ }
15248
+ const proxy = new Proxy(realWin, {
15249
+ get(target, prop) {
15250
+ if (prop === "postMessage") {
15251
+ return (msg, targetOrigin, transfer) => {
15252
+ const context = proxyContexts.get(proxy);
15253
+ appendPajaMessageLog(
15254
+ context?.state ?? state,
15255
+ "shell->napplet",
15256
+ msg,
15257
+ context?.windowId ?? windowId
15258
+ );
15259
+ return target.postMessage(msg, targetOrigin, transfer);
15260
+ };
15261
+ }
15262
+ try {
15263
+ const val = Reflect.get(target, prop, target);
15264
+ return typeof val === "function" ? val.bind(target) : val;
15265
+ } catch {
15266
+ return void 0;
15267
+ }
15268
+ }
15269
+ });
15270
+ proxyToReal.set(proxy, realWin);
15271
+ realToProxy.set(realWin, proxy);
15272
+ proxyContexts.set(proxy, { state, windowId });
15273
+ return proxy;
15274
+ }
15275
+ function installPajaOriginRegistryProxy(originRegistry2, stateRef) {
15276
+ const originalGetIframeWindow = originRegistry2.getIframeWindow.bind(originRegistry2);
15277
+ originRegistry2.getIframeWindow = (windowId) => {
15278
+ const win = originalGetIframeWindow(windowId);
15279
+ if (!win) return null;
15280
+ return createPajaPostMessageProxy(win, stateRef(), windowId);
15281
+ };
15282
+ const originalGetWindowId = originRegistry2.getWindowId.bind(originRegistry2);
15283
+ originRegistry2.getWindowId = (win) => {
15284
+ const result2 = originalGetWindowId(win);
15285
+ if (result2) return result2;
15286
+ const real = proxyToReal.get(win);
15287
+ return real ? originalGetWindowId(real) : void 0;
15288
+ };
15289
+ const originalGetIdentity = originRegistry2.getIdentity.bind(originRegistry2);
15290
+ originRegistry2.getIdentity = (win) => {
15291
+ const real = proxyToReal.get(win);
15292
+ return originalGetIdentity(win) ?? (real ? originalGetIdentity(real) : void 0);
15293
+ };
15294
+ const originalGetRegistrationId = originRegistry2.getRegistrationId.bind(originRegistry2);
15295
+ originRegistry2.getRegistrationId = (win) => {
15296
+ const real = proxyToReal.get(win);
15297
+ return originalGetRegistrationId(win) ?? (real ? originalGetRegistrationId(real) : void 0);
15298
+ };
15299
+ const originalGetEnvironment = originRegistry2.getEnvironment.bind(originRegistry2);
15300
+ originRegistry2.getEnvironment = (win) => {
15301
+ const real = proxyToReal.get(win);
15302
+ return originalGetEnvironment(win) ?? (real ? originalGetEnvironment(real) : void 0);
15303
+ };
15304
+ }
15305
+ function renderPajaDevtools(state, options) {
15306
+ renderInterfaceControls(state);
15307
+ renderAclControls(state, options.bridge);
15308
+ renderSignerStatus(state, options.devSignerPubkey);
15309
+ }
15310
+ function renderPajaMessageLog(state) {
15311
+ const container = document.getElementById("message-log");
15312
+ if (!container) return;
15313
+ const filter = state.messageFilter.trim().toLowerCase();
15314
+ const rows = state.messageLog.filter((entry) => filter.length === 0 || `${entry.direction} ${entry.type} ${entry.detail} ${entry.preview}`.toLowerCase().includes(filter)).slice(-120).map((entry) => {
15315
+ const row = document.createElement("div");
15316
+ row.className = "log-row";
15317
+ row.dataset.messageType = entry.type;
15318
+ if (entry.type.endsWith(".error")) row.dataset.error = "true";
15319
+ const dir = document.createElement("div");
15320
+ dir.className = "log-dir";
15321
+ dir.textContent = entry.direction;
15322
+ const body = document.createElement("div");
15323
+ body.className = "log-body";
15324
+ const type = document.createElement("div");
15325
+ type.className = "log-type";
15326
+ type.title = entry.preview;
15327
+ type.textContent = entry.type;
15328
+ body.append(type);
15329
+ if (entry.detail) {
15330
+ const detail = document.createElement("div");
15331
+ detail.className = "log-detail";
15332
+ detail.textContent = entry.detail;
15333
+ detail.title = entry.preview;
15334
+ body.append(detail);
15335
+ }
15336
+ row.append(dir, body);
15337
+ return row;
15338
+ });
15339
+ container.replaceChildren(...rows);
15340
+ container.scrollTop = container.scrollHeight;
15341
+ }
15342
+ function renderInterfaceControls(state) {
15343
+ const container = document.getElementById("interface-toggles");
15344
+ if (!container) return;
15345
+ container.replaceChildren(...PAJA_SIMULATION_DOMAINS.map((domain) => {
15346
+ const enabled = domainEnabled(state.simulation, domain);
15347
+ const button = document.createElement("button");
15348
+ button.type = "button";
15349
+ button.className = "toggle";
15350
+ button.dataset.interfaceDomain = domain;
15351
+ button.dataset.enabled = String(enabled);
15352
+ button.textContent = domain;
15353
+ button.title = `${domain} injection ${enabled ? "enabled" : "disabled"}`;
15354
+ button.addEventListener("click", () => {
15355
+ state.setDomainEnabled(domain, !domainEnabled(state.simulation, domain));
15356
+ });
15357
+ return button;
15358
+ }));
15359
+ }
15360
+ function renderAclControls(state, bridge) {
15361
+ const container = document.getElementById("acl-controls");
15362
+ if (!container) return;
15363
+ const identity = getTargetIdentity(state.config);
15364
+ container.replaceChildren(...ALL_CAPABILITIES.map((capability) => {
15365
+ const allowed2 = bridge?.runtime.aclState.check(
15366
+ identity.pubkey,
15367
+ identity.dTag,
15368
+ identity.aggregateHash,
15369
+ capability
15370
+ ) ?? true;
15371
+ const button = document.createElement("button");
15372
+ button.type = "button";
15373
+ button.className = "toggle";
15374
+ button.dataset.aclCapability = capability;
15375
+ button.dataset.enabled = String(allowed2);
15376
+ button.textContent = capability;
15377
+ button.title = `${capability} ${allowed2 ? "allowed" : "blocked"}`;
15378
+ button.addEventListener("click", () => state.setAclCapability(capability, !allowed2));
15379
+ return button;
15380
+ }));
15381
+ }
15382
+ function renderSignerStatus(state, signerPubkey) {
15383
+ const el = document.getElementById("signer-status");
15384
+ const controls = document.getElementById("signer-controls");
15385
+ if (!el || !controls) return;
15386
+ const fallbackPubkey = state.simulation.identity.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
15387
+ const pubkey = state.simulation.identity.pubkey || state.signer.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
15388
+ const methodLabel = state.signer.method === "nip07" ? "NIP-07" : state.signer.method === "nip46" ? "bunker" : state.signer.method === "dev" ? "dev" : "none";
15389
+ const relay = state.signer.relay ? ` \xB7 relay ${state.signer.relay}` : "";
15390
+ 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}`;
15391
+ el.textContent = `${prefix} \xB7 every sign/publish request prompts`;
15392
+ const previousInput = controls.querySelector("#signer-bunker-uri")?.value ?? "";
15393
+ const devButton = document.createElement("button");
15394
+ devButton.type = "button";
15395
+ devButton.id = "signer-dev";
15396
+ devButton.dataset.active = String(state.signer.method === "dev");
15397
+ devButton.textContent = "Dev";
15398
+ devButton.addEventListener("click", () => state.useDevSigner());
15399
+ const nip07Button = document.createElement("button");
15400
+ nip07Button.type = "button";
15401
+ nip07Button.id = "signer-nip07";
15402
+ nip07Button.dataset.active = String(state.signer.method === "nip07");
15403
+ nip07Button.textContent = "NIP-07";
15404
+ nip07Button.addEventListener("click", () => void state.connectNip07());
15405
+ const bunkerInput = document.createElement("input");
15406
+ bunkerInput.id = "signer-bunker-uri";
15407
+ bunkerInput.type = "text";
15408
+ bunkerInput.autocomplete = "off";
15409
+ bunkerInput.placeholder = "bunker://...";
15410
+ bunkerInput.setAttribute("aria-label", "Bunker connection URI");
15411
+ bunkerInput.value = previousInput;
15412
+ const bunkerButton = document.createElement("button");
15413
+ bunkerButton.type = "button";
15414
+ bunkerButton.id = "signer-bunker";
15415
+ bunkerButton.dataset.active = String(state.signer.method === "nip46");
15416
+ bunkerButton.textContent = "Bunker";
15417
+ bunkerButton.addEventListener("click", () => void state.connectBunker(bunkerInput.value));
15418
+ controls.replaceChildren(devButton, nip07Button, bunkerInput, bunkerButton);
15419
+ }
15420
+
15421
+ // ../../node_modules/.pnpm/@noble+ciphers@2.1.1/node_modules/@noble/ciphers/aes.js
15422
+ var BLOCK_SIZE = 16;
15423
+ var POLY = 283;
15424
+ function validateKeyLength(key) {
15425
+ if (![16, 24, 32].includes(key.length))
15426
+ throw new Error('"aes key" expected Uint8Array of length 16/24/32, got length=' + key.length);
15427
+ }
15428
+ function mul2(n) {
15429
+ return n << 1 ^ POLY & -(n >> 7);
15430
+ }
15431
+ function mul(a, b) {
15432
+ let res = 0;
15433
+ for (; b > 0; b >>= 1) {
15434
+ res ^= a & -(b & 1);
15435
+ a = mul2(a);
15436
+ }
15437
+ return res;
15438
+ }
15439
+ var sbox = /* @__PURE__ */ (() => {
15440
+ const t = new Uint8Array(256);
15441
+ for (let i4 = 0, x = 1; i4 < 256; i4++, x ^= mul2(x))
15442
+ t[i4] = x;
15443
+ const box = new Uint8Array(256);
15444
+ box[0] = 99;
15445
+ for (let i4 = 0; i4 < 255; i4++) {
15446
+ let x = t[255 - i4];
15447
+ x |= x << 8;
15448
+ box[t[i4]] = (x ^ x >> 4 ^ x >> 5 ^ x >> 6 ^ x >> 7 ^ 99) & 255;
15449
+ }
15450
+ clean2(t);
15451
+ return box;
15452
+ })();
15453
+ var invSbox = /* @__PURE__ */ sbox.map((_, j) => sbox.indexOf(j));
15454
+ var rotr32_8 = (n) => n << 24 | n >>> 8;
15455
+ var rotl32_8 = (n) => n << 8 | n >>> 24;
15456
+ function genTtable(sbox2, fn) {
15457
+ if (sbox2.length !== 256)
15458
+ throw new Error("Wrong sbox length");
15459
+ const T0 = new Uint32Array(256).map((_, j) => fn(sbox2[j]));
15460
+ const T1 = T0.map(rotl32_8);
15461
+ const T2 = T1.map(rotl32_8);
15462
+ const T3 = T2.map(rotl32_8);
15463
+ const T01 = new Uint32Array(256 * 256);
15464
+ const T23 = new Uint32Array(256 * 256);
15465
+ const sbox22 = new Uint16Array(256 * 256);
15466
+ for (let i4 = 0; i4 < 256; i4++) {
15467
+ for (let j = 0; j < 256; j++) {
15468
+ const idx = i4 * 256 + j;
15469
+ T01[idx] = T0[i4] ^ T1[j];
15470
+ T23[idx] = T2[i4] ^ T3[j];
15471
+ sbox22[idx] = sbox2[i4] << 8 | sbox2[j];
15472
+ }
15473
+ }
15474
+ return { sbox: sbox2, sbox2: sbox22, T0, T1, T2, T3, T01, T23 };
15475
+ }
15476
+ var tableEncoding = /* @__PURE__ */ genTtable(sbox, (s) => mul(s, 3) << 24 | s << 16 | s << 8 | mul(s, 2));
15477
+ var tableDecoding = /* @__PURE__ */ genTtable(invSbox, (s) => mul(s, 11) << 24 | mul(s, 13) << 16 | mul(s, 9) << 8 | mul(s, 14));
15478
+ var xPowers = /* @__PURE__ */ (() => {
15479
+ const p = new Uint8Array(16);
15480
+ for (let i4 = 0, x = 1; i4 < 16; i4++, x = mul2(x))
15481
+ p[i4] = x;
15482
+ return p;
15483
+ })();
15484
+ function expandKeyLE(key) {
15485
+ abytes3(key);
15486
+ const len = key.length;
15487
+ validateKeyLength(key);
15488
+ const { sbox2 } = tableEncoding;
15489
+ const toClean = [];
15490
+ if (!isAligned32(key))
15491
+ toClean.push(key = copyBytes2(key));
15492
+ const k32 = u32(key);
15493
+ const Nk = k32.length;
15494
+ const subByte = (n) => applySbox(sbox2, n, n, n, n);
15495
+ const xk = new Uint32Array(len + 28);
15496
+ xk.set(k32);
15497
+ for (let i4 = Nk; i4 < xk.length; i4++) {
15498
+ let t = xk[i4 - 1];
15499
+ if (i4 % Nk === 0)
15500
+ t = subByte(rotr32_8(t)) ^ xPowers[i4 / Nk - 1];
15501
+ else if (Nk > 6 && i4 % Nk === 4)
15502
+ t = subByte(t);
15503
+ xk[i4] = xk[i4 - Nk] ^ t;
15504
+ }
15505
+ clean2(...toClean);
15506
+ return xk;
15507
+ }
15508
+ function expandKeyDecLE(key) {
15509
+ const encKey = expandKeyLE(key);
15510
+ const xk = encKey.slice();
15511
+ const Nk = encKey.length;
15512
+ const { sbox2 } = tableEncoding;
15513
+ const { T0, T1, T2, T3 } = tableDecoding;
15514
+ for (let i4 = 0; i4 < Nk; i4 += 4) {
15515
+ for (let j = 0; j < 4; j++)
15516
+ xk[i4 + j] = encKey[Nk - i4 - 4 + j];
15517
+ }
15518
+ clean2(encKey);
15519
+ for (let i4 = 4; i4 < Nk - 4; i4++) {
15520
+ const x = xk[i4];
15521
+ const w = applySbox(sbox2, x, x, x, x);
15522
+ xk[i4] = T0[w & 255] ^ T1[w >>> 8 & 255] ^ T2[w >>> 16 & 255] ^ T3[w >>> 24];
14168
15523
  }
14169
15524
  return xk;
14170
15525
  }
@@ -16890,12 +18245,12 @@ async function sendRequestWithId(state, correlationId, method, params) {
16890
18245
  tags: [["p", state.bunkerPubkey]],
16891
18246
  content: encryptedContent
16892
18247
  }, state.localSecretKey);
16893
- return new Promise((resolve, reject) => {
18248
+ return new Promise((resolve, reject2) => {
16894
18249
  const timer = setTimeout(() => {
16895
18250
  state.pending.delete(correlationId);
16896
- reject(new Error(`NIP-46 request timed out: ${method}`));
18251
+ reject2(new Error(`NIP-46 request timed out: ${method}`));
16897
18252
  }, state.timeout);
16898
- state.pending.set(correlationId, { resolve, reject, timer });
18253
+ state.pending.set(correlationId, { resolve, reject: reject2, timer });
16899
18254
  state.ws.send(JSON.stringify(["EVENT", signedEvent]));
16900
18255
  });
16901
18256
  }
@@ -16934,7 +18289,7 @@ function rejectPendingOnClose(state) {
16934
18289
  state.pending.clear();
16935
18290
  }
16936
18291
  function openWebSocket(state) {
16937
- return new Promise((resolve, reject) => {
18292
+ return new Promise((resolve, reject2) => {
16938
18293
  state.ws = new WebSocket(state.relayUrl);
16939
18294
  state.ws.onopen = () => {
16940
18295
  state.ws.send(JSON.stringify([
@@ -16948,7 +18303,7 @@ function openWebSocket(state) {
16948
18303
  handleRelayMessage(state, event.data).catch(() => {
16949
18304
  });
16950
18305
  };
16951
- state.ws.onerror = () => reject(new Error(`NIP-46 WebSocket connection failed: ${state.relayUrl}`));
18306
+ state.ws.onerror = () => reject2(new Error(`NIP-46 WebSocket connection failed: ${state.relayUrl}`));
16952
18307
  state.ws.onclose = () => rejectPendingOnClose(state);
16953
18308
  });
16954
18309
  }
@@ -17123,245 +18478,77 @@ function createPajaSignerController(options) {
17123
18478
  };
17124
18479
  }
17125
18480
 
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;
18481
+ // src/browser-host-signer.ts
18482
+ function confirmPajaRequest(state, request) {
18483
+ if (request.action === "upload") {
18484
+ const filename = request.filename ?? "(unnamed blob)";
18485
+ const mimeType = request.mimeType ?? "application/octet-stream";
18486
+ const allowed3 = window.confirm([
18487
+ "Paja upload request",
18488
+ `napplet: ${request.napplet.dTag} (${request.windowId})`,
18489
+ `file: ${filename}`,
18490
+ `size: ${request.size} bytes`,
18491
+ `type: ${mimeType}`,
18492
+ `server: ${request.server}`,
18493
+ request.warning
18494
+ ].join("\n"));
18495
+ appendPajaMessageLog(state, "paja", {
18496
+ type: `paja.upload.${allowed3 ? "confirmed" : "denied"}`,
18497
+ windowId: request.windowId,
18498
+ dTag: request.napplet.dTag,
18499
+ aggregateHash: request.napplet.aggregateHash,
18500
+ filename,
18501
+ size: request.size,
18502
+ mimeType,
18503
+ server: request.server,
18504
+ warning: request.warning
18505
+ });
18506
+ return allowed3;
17198
18507
  }
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
- }
17220
- });
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);
17255
- }
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);
17281
- }
17282
- row.append(dir, body);
17283
- return row;
18508
+ const event = request.event;
18509
+ const kind = typeof event.kind === "number" ? event.kind : "unknown";
18510
+ const content = typeof event.content === "string" && event.content.length > 0 ? `
18511
+
18512
+ ${event.content.slice(0, 240)}` : "";
18513
+ const allowed2 = window.confirm(`Paja ${request.action} request
18514
+ kind: ${kind}${content}`);
18515
+ appendPajaMessageLog(state, "paja", {
18516
+ type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
18517
+ kind
17284
18518
  });
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
- }));
18519
+ return allowed2;
17305
18520
  }
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
- }));
18521
+ function createHostSignerController(getState, refreshState) {
18522
+ return createPajaSignerController({
18523
+ confirmRequest: (request) => confirmPajaRequest(getState(), request),
18524
+ onChange(signer) {
18525
+ const state = getState();
18526
+ if (!state) return;
18527
+ state.signer = signer;
18528
+ appendPajaMessageLog(state, "paja", {
18529
+ type: `paja.signer.${signer.method}.${signer.status}`,
18530
+ pubkey: signer.pubkey,
18531
+ relay: signer.relay,
18532
+ error: signer.error
18533
+ });
18534
+ refreshState(state);
18535
+ if (signer.status === "connected") state.reload();
18536
+ }
18537
+ });
17327
18538
  }
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);
18539
+ function hasNip07Signer() {
18540
+ const signer = globalThis.nostr;
18541
+ return typeof signer === "object" && signer !== null;
18542
+ }
18543
+
18544
+ // src/browser-host-runtime.ts
18545
+ function unregisterSingleFrameWindow(bridge, runtime, windowId) {
18546
+ if (!windowId) return;
18547
+ bridge.runtime.destroyWindow(windowId);
18548
+ bridge.runtime.sessionRegistry.unregister(windowId);
18549
+ originRegistry.unregister(windowId);
18550
+ runtime.readyWindowIds.delete(windowId);
18551
+ if (runtime.currentWindowId === windowId) runtime.currentWindowId = null;
17365
18552
  }
17366
18553
 
17367
18554
  // src/browser-runtime-tabs.ts
@@ -17374,6 +18561,9 @@ function resolvedTargetKey(target) {
17374
18561
  target.aggregateHash
17375
18562
  ].join(":");
17376
18563
  }
18564
+ function runtimeTabGenerationId(tab) {
18565
+ return `${tab.id}:${tab.generation}`;
18566
+ }
17377
18567
  function getActiveTab(state) {
17378
18568
  return state.tabs.find((tab) => tab.id === state.activeTabId) ?? null;
17379
18569
  }
@@ -17619,11 +18809,13 @@ function resolvedTargetTitle(target) {
17619
18809
  return title && title.length > 0 ? title : target.dTag || target.aggregateHash.slice(0, 12);
17620
18810
  }
17621
18811
  function destroyRuntimeTab(tab, context) {
18812
+ context.onTabDestroyed?.(tab);
17622
18813
  if (tab.windowId) {
17623
18814
  context.bridge.runtime.destroyWindow(tab.windowId);
17624
18815
  context.bridge.runtime.sessionRegistry.unregister(tab.windowId);
17625
18816
  originRegistry.unregister(tab.windowId);
17626
18817
  context.runtime.readyWindowIds.delete(tab.windowId);
18818
+ if (context.runtime.currentWindowId === tab.windowId) context.runtime.currentWindowId = null;
17627
18819
  }
17628
18820
  tab.frame.remove();
17629
18821
  }
@@ -17637,18 +18829,6 @@ function createRuntimeTabFrame(tab, state, context) {
17637
18829
  frame.dataset.tabId = tab.id;
17638
18830
  frame.dataset.targetUrl = tab.pointerValue;
17639
18831
  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
18832
  frame.addEventListener("error", () => {
17653
18833
  tab.status = "error";
17654
18834
  if (state.activeTabId === tab.id) context.setStatus(state, "error");
@@ -17662,14 +18842,18 @@ function startRuntimeTabNavigation(tab, state, context) {
17662
18842
  context.setStatus(state, "booting");
17663
18843
  renderRuntimeTabs(state);
17664
18844
  void context.navigateFrame(
17665
- context.bridge,
17666
18845
  tab.frame,
17667
18846
  context.config,
17668
18847
  generation,
17669
- context.capabilities,
17670
- context.runtime.currentSimulation,
18848
+ context.adapter,
17671
18849
  tab.resolvedTarget,
17672
- runtimeTabWindowId(context.config, tab)
18850
+ runtimeTabWindowId(context.config, tab),
18851
+ () => tab.generation === generation,
18852
+ (windowId) => {
18853
+ if (tab.generation !== generation) return;
18854
+ tab.windowId = windowId;
18855
+ if (state.activeTabId === tab.id) context.runtime.currentWindowId = windowId;
18856
+ }
17673
18857
  ).then((windowId) => {
17674
18858
  if (tab.generation !== generation) return;
17675
18859
  tab.windowId = windowId;
@@ -17692,13 +18876,6 @@ function runtimeTabWindowId(config, tab) {
17692
18876
  return `${config.window.id}:${tab.id}:${tab.generation}`;
17693
18877
  }
17694
18878
 
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
18879
  // ../nip/dist/chunk-JT5WCCCX.js
17703
18880
  function computeAggregateHash(entries) {
17704
18881
  const lines = entries.map((entry) => `${entry.sha256} ${entry.path}
@@ -17724,7 +18901,7 @@ function aggregateTagValue(tags) {
17724
18901
  return void 0;
17725
18902
  }
17726
18903
 
17727
- // ../nip/dist/chunk-NRUVARZL.js
18904
+ // ../nip/dist/chunk-ZQSTY6HR.js
17728
18905
  var MIB = 1024 * 1024;
17729
18906
  var NAPPLET_ARTIFACT_CACHE_NAME = "kehto:napplet-artifacts:v1";
17730
18907
  var SCHEMA_VERSION = 1;
@@ -18159,10 +19336,43 @@ function archetypesFromTags(tags) {
18159
19336
  const out = [];
18160
19337
  for (const tag of tags) {
18161
19338
  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
- );
19339
+ const slug = tag[1];
19340
+ if (typeof slug !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
19341
+ throw new NappletResolutionError(
19342
+ "invalid-manifest",
19343
+ "archetype slug must contain lowercase letters, numbers, and hyphens"
19344
+ );
19345
+ }
19346
+ const convention = tag[2];
19347
+ if (typeof convention !== "string" || convention.length === 0) {
19348
+ throw new NappletResolutionError("invalid-manifest", "archetype convention is required");
19349
+ }
19350
+ if (/^NAP-\d+$/.test(convention)) {
19351
+ throw new NappletResolutionError(
19352
+ "invalid-manifest",
19353
+ "numbered NAP identifier is not an archetype convention"
19354
+ );
19355
+ }
19356
+ const conventionMatch = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(convention);
19357
+ if (!conventionMatch) {
19358
+ throw new NappletResolutionError(
19359
+ "invalid-manifest",
19360
+ "archetype convention must be a queryless napplet:<archetype>/<intent> identity"
19361
+ );
19362
+ }
19363
+ if (conventionMatch[1] !== slug) {
19364
+ throw new NappletResolutionError(
19365
+ "invalid-manifest",
19366
+ "archetype slug must match the convention archetype"
19367
+ );
19368
+ }
19369
+ if (tag.length !== 3) {
19370
+ throw new NappletResolutionError(
19371
+ "invalid-manifest",
19372
+ "archetype tags must contain exactly slug and convention"
19373
+ );
19374
+ }
19375
+ out.push({ slug, convention });
18166
19376
  }
18167
19377
  return out;
18168
19378
  }
@@ -18295,12 +19505,12 @@ async function resolvePajaPointer(value, options = {}) {
18295
19505
  const pointer = decodePajaPointer(value);
18296
19506
  const relays = uniqueRelayUrls([...pointer.relays, ...options.relays ?? []]);
18297
19507
  if (relays.length === 0) {
18298
- throw new Error("Pointer does not include relay hints; add a relay-bearing naddr or nevent.");
19508
+ throw new Error("Pointer has no relay hints or enabled configured live relays.");
18299
19509
  }
18300
19510
  const pool = options.pool ?? new SimplePool();
18301
19511
  const ownsPool = options.pool === void 0;
18302
19512
  try {
18303
- const event = await resolvePointerEvent(pointer, pool, relays, options.maxWaitMs ?? 5e3);
19513
+ const event = await resolvePointerEvent(pointer, pool, relays, normalizeMaxWaitMs(options.maxWaitMs));
18304
19514
  const blossomServers = [...options.blossomServers ?? []];
18305
19515
  const resolved = await resolveNapplet({
18306
19516
  event,
@@ -18329,7 +19539,26 @@ async function resolvePajaPointer(value, options = {}) {
18329
19539
  }
18330
19540
  }
18331
19541
  function injectPajaRuntimeCsp(html, origins) {
18332
- const value = origins.length > 0 ? `connect-src ${[...new Set(origins)].sort().join(" ")}` : "connect-src 'none'";
19542
+ const grantedOrigins = [...new Set(origins)].sort();
19543
+ const connectSrc = grantedOrigins.length > 0 ? `connect-src ${grantedOrigins.join(" ")}` : "connect-src 'none'";
19544
+ const value = [
19545
+ "default-src 'none'",
19546
+ "script-src 'unsafe-inline'",
19547
+ "style-src 'unsafe-inline'",
19548
+ "img-src data: blob:",
19549
+ "font-src data:",
19550
+ connectSrc,
19551
+ "worker-src 'none'",
19552
+ "child-src 'none'",
19553
+ "frame-src 'none'",
19554
+ "media-src 'none'",
19555
+ "object-src 'none'",
19556
+ "manifest-src 'none'",
19557
+ "prefetch-src 'none'",
19558
+ "base-uri 'none'",
19559
+ "form-action 'none'",
19560
+ "frame-ancestors 'self'"
19561
+ ].join("; ");
18333
19562
  const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(value)}">`;
18334
19563
  if (/<head[^>]*>/i.test(html)) {
18335
19564
  return html.replace(/<head[^>]*>/i, (open) => `${open}${meta}`);
@@ -18341,19 +19570,96 @@ function injectPajaRuntimeCsp(html, origins) {
18341
19570
  }
18342
19571
  async function resolvePointerEvent(pointer, pool, relays, maxWaitMs) {
18343
19572
  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];
19573
+ const result2 = await queryPointerRelays(pool, relays, filter, maxWaitMs);
19574
+ const event = result2.events.filter((candidate) => matchesPointer(pointer, candidate)).sort((a, b) => b.created_at - a.created_at)[0];
18349
19575
  if (!event) {
18350
- throw new Error(`No napplet manifest event found for ${pointer.type} pointer.`);
19576
+ if (result2.timedOut) {
19577
+ throw new Error(
19578
+ `Relay resolution timed out after ${maxWaitMs}ms for ${pointer.type} pointer across ${relays.length} relays before complete EOSE.`
19579
+ );
19580
+ }
19581
+ const failures = relayFailureReasons(result2.reasons);
19582
+ if (result2.error || failures.length > 0) {
19583
+ const detail = result2.error instanceof Error ? result2.error.message : result2.error === void 0 ? failures.join("; ") : String(result2.error);
19584
+ throw new Error(`Relay query failed for ${pointer.type} pointer before complete EOSE: ${detail}`);
19585
+ }
19586
+ throw new Error(
19587
+ `All ${relays.length} relays reached EOSE without a matching ${pointer.type} napplet manifest event.`
19588
+ );
18351
19589
  }
18352
19590
  if (!isNappletManifestKind(event.kind)) {
18353
19591
  throw new Error(`Pointer resolved kind ${event.kind}; expected ${formatNappletManifestKinds()}.`);
18354
19592
  }
18355
19593
  return event;
18356
19594
  }
19595
+ function queryPointerRelays(pool, relays, filter, maxWaitMs) {
19596
+ if (pool.subscribeManyEose) {
19597
+ return queryPointerRelaySubscription(pool, relays, filter, maxWaitMs);
19598
+ }
19599
+ return queryPointerRelaySync(pool, relays, filter, maxWaitMs);
19600
+ }
19601
+ function queryPointerRelaySubscription(pool, relays, filter, maxWaitMs) {
19602
+ return new Promise((resolve) => {
19603
+ const events = [];
19604
+ let settled = false;
19605
+ let subscription;
19606
+ const finish = (result2) => {
19607
+ if (settled) return;
19608
+ settled = true;
19609
+ clearTimeout(deadline);
19610
+ resolve({ events, ...result2 });
19611
+ };
19612
+ const deadline = setTimeout(() => {
19613
+ finish({ reasons: [], timedOut: true });
19614
+ void subscription?.close("Paja pointer relay deadline exceeded");
19615
+ }, maxWaitMs);
19616
+ try {
19617
+ subscription = pool.subscribeManyEose([...relays], filter, {
19618
+ maxWait: maxWaitMs,
19619
+ label: "kehto-paja-runtime",
19620
+ onevent(event) {
19621
+ events.push(event);
19622
+ },
19623
+ onclose(reasons) {
19624
+ finish({ reasons, timedOut: false });
19625
+ }
19626
+ });
19627
+ } catch (error) {
19628
+ finish({ reasons: [], timedOut: false, error });
19629
+ }
19630
+ });
19631
+ }
19632
+ function queryPointerRelaySync(pool, relays, filter, maxWaitMs) {
19633
+ return new Promise((resolve) => {
19634
+ let settled = false;
19635
+ const finish = (result2) => {
19636
+ if (settled) return;
19637
+ settled = true;
19638
+ clearTimeout(deadline);
19639
+ resolve(result2);
19640
+ };
19641
+ const deadline = setTimeout(() => {
19642
+ finish({ events: [], reasons: [], timedOut: true });
19643
+ }, maxWaitMs);
19644
+ void pool.querySync([...relays], filter, {
19645
+ maxWait: maxWaitMs,
19646
+ label: "kehto-paja-runtime"
19647
+ }).then(
19648
+ (events) => finish({ events, reasons: [], timedOut: false }),
19649
+ (error) => finish({ events: [], reasons: [], timedOut: false, error })
19650
+ );
19651
+ });
19652
+ }
19653
+ function relayFailureReasons(reasons) {
19654
+ 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);
19655
+ }
19656
+ function normalizeMaxWaitMs(value) {
19657
+ if (value === void 0) return 5e3;
19658
+ if (!Number.isFinite(value) || value <= 0) {
19659
+ throw new Error("Pointer relay deadline must be a positive number of milliseconds.");
19660
+ }
19661
+ return Math.floor(value);
19662
+ }
18357
19663
  function addressableFilter(pointer) {
18358
19664
  if (pointer.kind !== NAPPLET_KIND_NAMED) {
18359
19665
  throw new Error(`naddr kind ${pointer.kind} is not a named NIP-5D napplet manifest kind.`);
@@ -18413,7 +19719,192 @@ function formatNappletManifestKinds() {
18413
19719
  return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
18414
19720
  }
18415
19721
 
19722
+ // src/browser-intent-host.ts
19723
+ function createPajaIntentTargetOptions(getState, getContext, effects = {}) {
19724
+ return {
19725
+ async openOrReuse(params) {
19726
+ const state = getState();
19727
+ const context = getContext();
19728
+ if (!state || !context) return null;
19729
+ const record = context.runtime.catalog.get(params.handler);
19730
+ if (!record || !recordSupportsDelivery(record, params)) return null;
19731
+ for (const stale of state.tabs) {
19732
+ if (stale.resolvedTarget.dTag === params.handler && !matchesInstalledNappletRecord(record, stale.resolvedTarget)) closeRuntimeTab(state, context, stale.id);
19733
+ }
19734
+ const current = state.tabs.find(
19735
+ (tab2) => matchesInstalledNappletRecord(record, tab2.resolvedTarget) && isCurrentRuntimeTabGeneration(state, context, tab2)
19736
+ );
19737
+ if (current && params.behavior?.newWindow !== true && params.behavior?.reuse !== false) {
19738
+ return bindPajaIntentGeneration(current, record, context.runtime);
19739
+ }
19740
+ const resolved = await resolvePajaPointer(record.pointer.value, pajaPointerResolverOptions(context));
19741
+ if (resolved.dTag !== record.dTag || resolved.aggregateHash !== record.aggregateHash || !resolvedSupportsDelivery(resolved, params)) return null;
19742
+ const currentRecord = context.runtime.catalog.validateCurrent(record, resolved);
19743
+ if (!currentRecord) return null;
19744
+ const tab = addRuntimeTab(state, context, currentRecord.pointer.value, resolved);
19745
+ effects.persistTabs?.(state);
19746
+ return bindPajaIntentGeneration(tab, currentRecord, context.runtime);
19747
+ },
19748
+ waitForReady(generation) {
19749
+ const state = getState();
19750
+ const context = getContext();
19751
+ if (!state || !context) return Promise.reject(new Error("Paja host is not available"));
19752
+ const tab = findRuntimeTabGeneration(state, generation);
19753
+ if (!tab || !isCurrentPajaIntentGeneration(generation, state, context, tab)) {
19754
+ invalidatePajaIntentGeneration(generation, context.runtime);
19755
+ return Promise.reject(new Error("intent target generation is not current"));
19756
+ }
19757
+ if (tab.windowId && context.runtime.readyWindowIds.has(tab.windowId)) return void 0;
19758
+ return new Promise((resolve, reject2) => {
19759
+ const waiters = context.runtime.readyWaiters.get(generation) ?? /* @__PURE__ */ new Set();
19760
+ waiters.add({ resolve, reject: reject2 });
19761
+ context.runtime.readyWaiters.set(generation, waiters);
19762
+ });
19763
+ },
19764
+ isCurrent(generation) {
19765
+ const state = getState();
19766
+ const context = getContext();
19767
+ const tab = state && context ? findRuntimeTabGeneration(state, generation) : null;
19768
+ if (!tab || !state || !context || !isCurrentPajaIntentGeneration(generation, state, context, tab)) {
19769
+ if (context) invalidatePajaIntentGeneration(generation, context.runtime);
19770
+ return false;
19771
+ }
19772
+ return true;
19773
+ },
19774
+ getWindowId(generation) {
19775
+ const state = getState();
19776
+ const tab = state ? findRuntimeTabGeneration(state, generation) : null;
19777
+ return tab?.windowId ?? null;
19778
+ },
19779
+ send(generation, params) {
19780
+ const state = getState();
19781
+ const context = getContext();
19782
+ const tab = state && context ? findRuntimeTabGeneration(state, generation) : null;
19783
+ if (!state || !context || !tab || !tab.windowId || !isCurrentPajaIntentGeneration(generation, state, context, tab)) {
19784
+ if (context) invalidatePajaIntentGeneration(generation, context.runtime);
19785
+ throw new Error("intent target generation is not current and ready");
19786
+ }
19787
+ const source = tab.frame.contentWindow;
19788
+ if (!source || originRegistry.getWindowId(source) !== tab.windowId) {
19789
+ throw new Error("intent target source is no longer registered");
19790
+ }
19791
+ createPajaPostMessageProxy(source, state, tab.windowId).postMessage({
19792
+ type: "inc.event",
19793
+ topic: params.convention,
19794
+ sender: params.sender,
19795
+ ...params.payload === void 0 ? {} : { payload: params.payload }
19796
+ }, "*");
19797
+ }
19798
+ };
19799
+ }
19800
+ function pajaPointerResolverOptions(context) {
19801
+ return {
19802
+ relays: [
19803
+ ...context.config.target.pointer?.relays ?? [],
19804
+ ...getPajaRelayUrls(context.runtime.currentSimulation)
19805
+ ],
19806
+ blossomServers: context.config.target.pointer?.blossomServers ?? [],
19807
+ maxWaitMs: context.config.target.pointer?.maxWaitMs
19808
+ };
19809
+ }
19810
+ function recordSupportsDelivery(record, params) {
19811
+ return record.archetypes.some(
19812
+ (archetype) => archetype.slug === params.archetype && archetype.convention === params.convention
19813
+ );
19814
+ }
19815
+ function resolvedSupportsDelivery(resolved, params) {
19816
+ return resolved.manifest.archetypes.some(
19817
+ (archetype) => archetype.slug === params.archetype && archetype.convention === params.convention
19818
+ );
19819
+ }
19820
+ function findRuntimeTabGeneration(state, generation) {
19821
+ return state.tabs.find((tab) => runtimeTabGenerationId(tab) === generation.id) ?? null;
19822
+ }
19823
+ function bindPajaIntentGeneration(tab, record, runtime) {
19824
+ const generation = { id: runtimeTabGenerationId(tab) };
19825
+ runtime.intentRecords.set(generation, record);
19826
+ return generation;
19827
+ }
19828
+ function isCurrentPajaIntentGeneration(generation, state, context, tab) {
19829
+ const record = context.runtime.intentRecords.get(generation);
19830
+ return record !== void 0 && context.runtime.catalog.validateCurrent(record, tab.resolvedTarget) !== null && isCurrentRuntimeTabGeneration(state, context, tab);
19831
+ }
19832
+ function invalidatePajaIntentGeneration(generation, runtime) {
19833
+ const waiters = runtime.readyWaiters.get(generation);
19834
+ if (!waiters) return;
19835
+ runtime.readyWaiters.delete(generation);
19836
+ for (const waiter of waiters) waiter.reject(new Error("intent target catalog record was replaced"));
19837
+ }
19838
+ function subscribePajaIntentCatalogChanges(state, context) {
19839
+ return context.runtime.catalog.onChanged((dTag) => {
19840
+ for (const [generation] of context.runtime.readyWaiters) {
19841
+ const record = context.runtime.intentRecords.get(generation);
19842
+ if (!record || record.dTag !== dTag) continue;
19843
+ const tab = findRuntimeTabGeneration(state, generation);
19844
+ if (!tab || !isCurrentPajaIntentGeneration(generation, state, context, tab)) {
19845
+ invalidatePajaIntentGeneration(generation, context.runtime);
19846
+ }
19847
+ }
19848
+ });
19849
+ }
19850
+ function isCurrentRuntimeTabGeneration(state, context, tab) {
19851
+ const source = tab.frame.contentWindow;
19852
+ return state.tabs.includes(tab) && tab.windowId !== null && source !== null && originRegistry.getWindowId(source) === tab.windowId && runtimeTabGenerationId(tab) === `${tab.id}:${tab.generation}`;
19853
+ }
19854
+ function markRuntimeTabReady(state, context, tab, source, registeredWindowId, effects = {}) {
19855
+ if (!tab.windowId || tab.frame.contentWindow !== source || registeredWindowId !== tab.windowId || !isCurrentRuntimeTabGeneration(state, context, tab)) return false;
19856
+ context.runtime.readyWindowIds.add(tab.windowId);
19857
+ for (const [generation, waiters] of context.runtime.readyWaiters) {
19858
+ if (findRuntimeTabGeneration(state, generation) !== tab) continue;
19859
+ context.runtime.readyWaiters.delete(generation);
19860
+ if (!isCurrentPajaIntentGeneration(generation, state, context, tab)) {
19861
+ for (const waiter of waiters) waiter.reject(new Error("intent target catalog record was replaced"));
19862
+ continue;
19863
+ }
19864
+ for (const waiter of waiters) waiter.resolve();
19865
+ }
19866
+ tab.status = "ready";
19867
+ if (state.activeTabId === tab.id) {
19868
+ if (effects.setReadyStatus) effects.setReadyStatus(state);
19869
+ else state.status = "ready";
19870
+ }
19871
+ renderRuntimeTabs(state);
19872
+ return true;
19873
+ }
19874
+ function clearRuntimeTabGeneration(tab, runtime) {
19875
+ for (const [generation, waiters] of runtime.readyWaiters) {
19876
+ if (generation.id !== runtimeTabGenerationId(tab)) continue;
19877
+ runtime.readyWaiters.delete(generation);
19878
+ for (const waiter of waiters) waiter.reject(new Error("intent target generation replaced"));
19879
+ }
19880
+ }
19881
+
19882
+ // src/theme-broadcast.ts
19883
+ function createPajaThemeBroadcastLink() {
19884
+ let attachedBridge;
19885
+ return {
19886
+ attach(bridge) {
19887
+ if (attachedBridge) {
19888
+ throw new Error("Paja theme broadcast link is already attached.");
19889
+ }
19890
+ attachedBridge = bridge;
19891
+ },
19892
+ onBroadcast(envelope) {
19893
+ if (!attachedBridge) {
19894
+ throw new Error("Paja theme broadcast occurred before a ShellBridge is attached.");
19895
+ }
19896
+ attachedBridge.publishTheme(envelope.theme);
19897
+ }
19898
+ };
19899
+ }
19900
+
18416
19901
  // src/browser-target-frame.ts
19902
+ function resolvePajaFrameEnvironment(hooks, identity) {
19903
+ if (!identity.dTag || !identity.aggregateHash) {
19904
+ throw new Error("Paja frame identity requires a dTag and aggregate hash");
19905
+ }
19906
+ return resolveShellEnvironment(hooks, identity);
19907
+ }
18417
19908
  function getTargetIdentity2(config, resolvedTarget) {
18418
19909
  return {
18419
19910
  pubkey: "",
@@ -18421,18 +19912,21 @@ function getTargetIdentity2(config, resolvedTarget) {
18421
19912
  aggregateHash: resolvedTarget?.aggregateHash ?? config.window.aggregateHash
18422
19913
  };
18423
19914
  }
18424
- function registerFrameForGeneration(_bridge, frame, config, generation, resolvedTarget, windowId = `${config.window.id}:${generation}`) {
19915
+ function getTargetOriginIdentity(config, resolvedTarget) {
19916
+ const target = getTargetIdentity2(config, resolvedTarget);
19917
+ return Object.freeze({ dTag: target.dTag, aggregateHash: target.aggregateHash });
19918
+ }
19919
+ function registerFrameForGeneration(frame, config, generation, identity, environment, windowId = `${config.window.id}:${generation}`) {
18425
19920
  const win = frame.contentWindow;
18426
19921
  if (!win) return null;
18427
- const identity = getTargetIdentity2(config, resolvedTarget);
18428
- originRegistry.register(win, windowId, {
18429
- dTag: identity.dTag,
18430
- aggregateHash: identity.aggregateHash
18431
- });
19922
+ originRegistry.register(win, windowId, identity);
19923
+ originRegistry.setEnvironment(win, environment);
18432
19924
  return windowId;
18433
19925
  }
18434
- async function navigateFrame(bridge, frame, config, generation, capabilities, simulation, resolvedTarget, windowId, isCurrent) {
18435
- const domains = getInjectedDomains(capabilities, simulation, resolvedTarget);
19926
+ async function navigateFrame(frame, config, generation, adapter, resolvedTarget, windowId, isCurrent, onRegistered) {
19927
+ const identity = getTargetOriginIdentity(config, resolvedTarget);
19928
+ const environment = resolvePajaFrameEnvironment(adapter, identity);
19929
+ const domains = environment.capabilities.domains;
18436
19930
  if (config.target.mode === "runtime-pointer") {
18437
19931
  if (!resolvedTarget) {
18438
19932
  frame.removeAttribute("src");
@@ -18440,7 +19934,8 @@ async function navigateFrame(bridge, frame, config, generation, capabilities, si
18440
19934
  return null;
18441
19935
  }
18442
19936
  if (isCurrent && !isCurrent()) return null;
18443
- const registeredWindowId2 = registerFrameForGeneration(bridge, frame, config, generation, resolvedTarget, windowId);
19937
+ const registeredWindowId2 = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
19938
+ onRegistered?.(registeredWindowId2);
18444
19939
  frame.removeAttribute("src");
18445
19940
  frame.srcdoc = injectNappletNamespacePrelude(
18446
19941
  injectPajaRuntimeCsp(
@@ -18453,7 +19948,8 @@ async function navigateFrame(bridge, frame, config, generation, capabilities, si
18453
19948
  }
18454
19949
  const html = await fetchTargetHtml();
18455
19950
  if (isCurrent && !isCurrent()) return null;
18456
- const registeredWindowId = registerFrameForGeneration(bridge, frame, config, generation, resolvedTarget, windowId);
19951
+ const registeredWindowId = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
19952
+ onRegistered?.(registeredWindowId);
18457
19953
  frame.removeAttribute("src");
18458
19954
  frame.srcdoc = injectNappletNamespacePrelude(
18459
19955
  injectBaseHref(html, config.target.url),
@@ -18480,21 +19976,6 @@ function connectOrigins(urls) {
18480
19976
  }
18481
19977
  return [...out];
18482
19978
  }
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
19979
  async function fetchTargetHtml() {
18499
19980
  const response = await fetch(new URL("./__kehto/target.html", window.location.href), {
18500
19981
  cache: "no-store",
@@ -18524,6 +20005,30 @@ function escapeHtml(value) {
18524
20005
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
18525
20006
  }
18526
20007
 
20008
+ // src/browser-target-diagnostics.ts
20009
+ async function reportTargetCorsDiagnostic(state) {
20010
+ let diagnostic;
20011
+ try {
20012
+ const response = await fetch(new URL("./__kehto/target-cors.json", window.location.href), {
20013
+ cache: "no-store"
20014
+ });
20015
+ if (!response.ok) return;
20016
+ diagnostic = await response.json();
20017
+ } catch {
20018
+ return;
20019
+ }
20020
+ if (diagnostic.status === "allowed") return;
20021
+ appendPajaMessageLog(state, "paja", {
20022
+ type: "paja.target.cors.error",
20023
+ status: diagnostic.status,
20024
+ targetUrl: diagnostic.targetUrl,
20025
+ allowOrigin: diagnostic.allowOrigin,
20026
+ message: `${diagnostic.detail} ${diagnostic.hint ?? ""}`.trim()
20027
+ });
20028
+ console.warn(`[paja] ${diagnostic.detail}
20029
+ [paja] ${diagnostic.hint ?? ""}`.trimEnd());
20030
+ }
20031
+
18527
20032
  // src/browser-host.ts
18528
20033
  var bridgeRef = null;
18529
20034
  function readConfig() {
@@ -18621,68 +20126,22 @@ function getFrame() {
18621
20126
  frame.sandbox.remove("allow-same-origin");
18622
20127
  return frame;
18623
20128
  }
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
20129
  function startFrameNavigation(state, context) {
18672
- const { config, frame, bridge, capabilities, runtime } = context;
20130
+ const { config, frame, bridge, adapter, runtime } = context;
18673
20131
  if (!frame) return;
18674
20132
  const generation = state.generation;
18675
20133
  const isCurrentGeneration = () => state.generation === generation;
18676
20134
  void navigateFrame(
18677
- bridge,
18678
20135
  frame,
18679
20136
  config,
18680
20137
  generation,
18681
- capabilities,
18682
- runtime.currentSimulation,
20138
+ adapter,
18683
20139
  state.resolvedTarget,
18684
20140
  void 0,
18685
- isCurrentGeneration
20141
+ isCurrentGeneration,
20142
+ (windowId) => {
20143
+ if (isCurrentGeneration()) runtime.currentWindowId = windowId;
20144
+ }
18686
20145
  ).then((windowId) => {
18687
20146
  if (!isCurrentGeneration()) {
18688
20147
  unregisterSingleFrameWindow(bridge, runtime, windowId);
@@ -18701,28 +20160,6 @@ function startFrameNavigation(state, context) {
18701
20160
  console.error(error);
18702
20161
  });
18703
20162
  }
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
20163
  function installPajaControlListeners(state) {
18727
20164
  document.getElementById("reload-target")?.addEventListener("click", () => {
18728
20165
  state.reload();
@@ -18784,7 +20221,7 @@ function setRuntimeDomainEnabled(state, context, domain, enabled) {
18784
20221
  state.reload();
18785
20222
  }
18786
20223
  async function loadRuntimePointer(state, context, value, options = {}) {
18787
- const { config } = context;
20224
+ const { config, runtime } = context;
18788
20225
  if (config.target.mode !== "runtime-pointer") return;
18789
20226
  const pointer = value.trim();
18790
20227
  const input = document.getElementById("runtime-pointer-input");
@@ -18798,11 +20235,8 @@ async function loadRuntimePointer(state, context, value, options = {}) {
18798
20235
  setStatus(state, "booting");
18799
20236
  appendPajaMessageLog(state, "paja", { type: "paja.pointer.resolve", pointer });
18800
20237
  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
- });
20238
+ const resolvedTarget = await resolvePajaPointer(pointer, pajaPointerResolverOptions(context));
20239
+ runtime.catalog.install(resolvedTarget);
18806
20240
  const pointerStatus = `${resolvedTarget.dTag}:${resolvedTarget.aggregateHash.slice(0, 12)}`;
18807
20241
  setPointerStatus(state, pointerStatus);
18808
20242
  appendPajaMessageLog(state, "paja", {
@@ -18947,16 +20381,30 @@ async function installPajaHost() {
18947
20381
  const runtime = {
18948
20382
  currentSimulation: config.simulation,
18949
20383
  themeService: null,
20384
+ catalog: new InstalledNappletCatalog(),
20385
+ readyWaiters: /* @__PURE__ */ new Map(),
20386
+ intentRecords: /* @__PURE__ */ new WeakMap(),
18950
20387
  currentWindowId: null,
18951
20388
  readyWindowIds: /* @__PURE__ */ new Set()
18952
20389
  };
18953
20390
  const getSimulation = () => runtime.currentSimulation;
20391
+ const themeBroadcast = createPajaThemeBroadcastLink();
18954
20392
  let stateRef = null;
18955
- const signerController = createHostSignerController(() => stateRef);
20393
+ let contextRef = null;
20394
+ const intentController = new BrowserIntentController(createPajaIntentTargetOptions(
20395
+ () => stateRef,
20396
+ () => contextRef,
20397
+ { persistTabs: persistRuntimeTabs }
20398
+ ));
20399
+ const signerController = createHostSignerController(() => stateRef, setSimulationStatus);
18956
20400
  const adapter = createPajaAdapter(config, getSimulation, (theme) => {
18957
20401
  runtime.themeService = theme;
18958
- }, (request) => confirmPajaRequest(stateRef, request), signerController, () => getTargetIdentity2(config, stateRef?.resolvedTarget));
20402
+ }, themeBroadcast.onBroadcast, (request) => confirmPajaRequest(stateRef, request), signerController, () => getTargetIdentity2(config, stateRef?.resolvedTarget), () => stateRef?.reload(), {
20403
+ catalog: runtime.catalog,
20404
+ controller: intentController
20405
+ });
18959
20406
  const bridge = createShellBridge(adapter);
20407
+ themeBroadcast.attach(bridge);
18960
20408
  bridgeRef = bridge;
18961
20409
  installPajaOriginRegistryProxy(originRegistry, () => stateRef);
18962
20410
  const capabilities = buildShellCapabilities(adapter);
@@ -18970,21 +20418,25 @@ async function installPajaHost() {
18970
20418
  capabilities,
18971
20419
  runtime,
18972
20420
  navigateFrame,
18973
- registerFrameForGeneration,
18974
20421
  renderTargetErrorHtml,
20422
+ onTabDestroyed: (tab) => clearRuntimeTabGeneration(tab, runtime),
18975
20423
  setPointerStatus: (state2, message) => setPointerStatus(state2, message),
18976
20424
  setStatus: (state2, status) => setStatus(state2, status)
18977
20425
  };
20426
+ contextRef = context;
18978
20427
  const state = createPajaBrowserState(context);
18979
20428
  stateRef = state;
20429
+ const stopIntentCatalogChanges = subscribePajaIntentCatalogChanges(state, context);
20430
+ window.addEventListener("pagehide", stopIntentCatalogChanges, { once: true });
18980
20431
  window.__KEHTO_PAJA__ = state;
18981
20432
  window.addEventListener("message", (event) => {
18982
20433
  const source = event.source;
18983
20434
  const sourceTab = source ? state.tabs.find((tab) => tab.frame.contentWindow === source) ?? null : null;
18984
20435
  const isSingleFrameMessage = frame ? event.source === frame.contentWindow : false;
18985
20436
  if (!sourceTab && !isSingleFrameMessage) return;
18986
- const registeredWindowId = source ? originRegistry.getWindowId(source) : null;
20437
+ const registeredWindowId = source ? originRegistry.getWindowId(source) ?? null : null;
18987
20438
  const sourceWindowId = sourceTab?.windowId ?? registeredWindowId ?? void 0;
20439
+ if (sourceTab && (!source || !sourceWindowId || registeredWindowId !== sourceWindowId)) return;
18988
20440
  if (isSingleFrameMessage && (!sourceWindowId || sourceWindowId !== runtime.currentWindowId)) return;
18989
20441
  appendPajaMessageLog(state, "napplet->shell", event.data, sourceWindowId);
18990
20442
  const proxiedSource = createPajaPostMessageProxy(event.source, state, sourceWindowId);
@@ -18998,22 +20450,21 @@ async function installPajaHost() {
18998
20450
  bridge.handleMessage(syntheticEvent);
18999
20451
  const data = event.data;
19000
20452
  if (data && typeof data === "object" && data.type === "shell.ready") {
19001
- if (sourceWindowId) runtime.readyWindowIds.add(sourceWindowId);
19002
20453
  if (sourceTab) {
19003
- sourceTab.status = "ready";
19004
- if (state.activeTabId === sourceTab.id) setStatus(state, "ready");
19005
- renderRuntimeTabs(state);
20454
+ if (source && !markRuntimeTabReady(
20455
+ state,
20456
+ context,
20457
+ sourceTab,
20458
+ source,
20459
+ registeredWindowId,
20460
+ { setReadyStatus: (current) => setStatus(current, "ready") }
20461
+ )) return;
19006
20462
  } else {
20463
+ if (sourceWindowId) runtime.readyWindowIds.add(sourceWindowId);
19007
20464
  setStatus(state, "ready");
19008
20465
  }
19009
20466
  }
19010
20467
  });
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
20468
  frame?.addEventListener("error", () => {
19018
20469
  setStatus(state, "error");
19019
20470
  });
@@ -19034,20 +20485,28 @@ async function installPajaHost() {
19034
20485
  }
19035
20486
  } else {
19036
20487
  startFrameNavigation(state, context);
20488
+ void reportTargetCorsDiagnostic(state);
19037
20489
  }
19038
20490
  if (hasNip07Signer()) void state.connectNip07();
19039
20491
  }
19040
- try {
19041
- void installPajaHost().catch((error) => {
20492
+ if (typeof document !== "undefined") {
20493
+ try {
20494
+ void installPajaHost().catch((error) => {
20495
+ const statusEl = document.getElementById("lifecycle-status");
20496
+ if (statusEl) statusEl.textContent = "error";
20497
+ console.error(error);
20498
+ });
20499
+ } catch (error) {
19042
20500
  const statusEl = document.getElementById("lifecycle-status");
19043
20501
  if (statusEl) statusEl.textContent = "error";
19044
20502
  console.error(error);
19045
- });
19046
- } catch (error) {
19047
- const statusEl = document.getElementById("lifecycle-status");
19048
- if (statusEl) statusEl.textContent = "error";
19049
- console.error(error);
20503
+ }
19050
20504
  }
20505
+ export {
20506
+ createPajaIntentTargetOptions,
20507
+ markRuntimeTabReady,
20508
+ subscribePajaIntentCatalogChanges
20509
+ };
19051
20510
  /*! Bundled license information:
19052
20511
 
19053
20512
  @noble/hashes/utils.js: