@kehto/paja 0.10.0 → 0.11.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.
@@ -1599,17 +1599,29 @@ function isRuntimeOwnedIdentityOrThemeRequest(type) {
1599
1599
  function createRuntimeOwnedResultEnvelope(type, id) {
1600
1600
  return Object.assign({ type }, { id });
1601
1601
  }
1602
+ function intentRequestDetails(message) {
1603
+ const request = message.request;
1604
+ return {
1605
+ archetype: typeof request?.archetype === "string" ? request.archetype : "",
1606
+ action: typeof request?.action === "string" && request.action.length > 0 ? request.action : "open"
1607
+ };
1608
+ }
1609
+ function createIntentInvokeRejection(message, error) {
1610
+ const id = message.id ?? "";
1611
+ const { archetype, action } = intentRequestDetails(message);
1612
+ return {
1613
+ type: "intent.invoke.result",
1614
+ id,
1615
+ result: { ok: false, archetype, action, handled: false, error }
1616
+ };
1617
+ }
1602
1618
  function isIdentityOrThemeMessage(message) {
1603
1619
  return message.type.startsWith("identity.") || message.type.startsWith("theme.");
1604
1620
  }
1605
1621
  function createIntentPolicyDenial(message) {
1606
1622
  const id = message.id ?? "";
1607
1623
  if (message.type === "intent.invoke") {
1608
- return {
1609
- type: "intent.invoke.result",
1610
- id,
1611
- result: { ok: false, error: "invoke rejected" }
1612
- };
1624
+ return createIntentInvokeRejection(message, "invoke rejected");
1613
1625
  }
1614
1626
  if (message.type === "intent.available" || message.type === "intent.handlers") {
1615
1627
  return {
@@ -1620,6 +1632,29 @@ function createIntentPolicyDenial(message) {
1620
1632
  }
1621
1633
  return void 0;
1622
1634
  }
1635
+ function createIntentServiceUnavailableResult(message) {
1636
+ const id = message.id ?? "";
1637
+ if (message.type === "intent.invoke") {
1638
+ return createIntentInvokeRejection(message, "no handler");
1639
+ }
1640
+ if (message.type === "intent.available") {
1641
+ const archetype = message.archetype;
1642
+ return {
1643
+ type: "intent.available.result",
1644
+ id,
1645
+ availability: {
1646
+ archetype: typeof archetype === "string" ? archetype : "",
1647
+ available: false,
1648
+ candidates: [],
1649
+ hasDefault: false
1650
+ }
1651
+ };
1652
+ }
1653
+ if (message.type === "intent.handlers") {
1654
+ return { type: "intent.handlers.result", id, handlers: [] };
1655
+ }
1656
+ return void 0;
1657
+ }
1623
1658
  function createCanonicalDomainResult(message) {
1624
1659
  if (!isRuntimeOwnedIdentityOrThemeRequest(message.type)) return void 0;
1625
1660
  const id = message.id ?? "";
@@ -1766,6 +1801,7 @@ function createIncRuntime(hooks, sessionRegistry2, authorizeChannelOpen) {
1766
1801
  },
1767
1802
  revokeWindow(windowId) {
1768
1803
  revokeWindowChannels(state, hooks, windowId);
1804
+ removeWindowSubscriptions(state, windowId);
1769
1805
  },
1770
1806
  clear() {
1771
1807
  state.subscriptions.clear();
@@ -1851,7 +1887,12 @@ function handleEmit(state, hooks, sessionRegistry2, windowId, m) {
1851
1887
  if (!subscribers) return;
1852
1888
  for (const subscriberWindowId of subscribers) {
1853
1889
  if (subscriberWindowId !== windowId) {
1854
- hooks.sendToNapplet(subscriberWindowId, { type: "inc.event", topic, payload: m.payload, sender });
1890
+ hooks.sendToNapplet(subscriberWindowId, {
1891
+ type: "inc.event",
1892
+ topic,
1893
+ sender,
1894
+ ...m.payload === void 0 ? {} : { payload: m.payload }
1895
+ });
1855
1896
  }
1856
1897
  }
1857
1898
  }
@@ -1914,7 +1955,12 @@ function handleChannelEmit(state, hooks, sessionRegistry2, windowId, m) {
1914
1955
  const peer = peerOf(state, m.channelId ?? "", windowId);
1915
1956
  const sender = sessionRegistry2.getEntryByWindowId(windowId)?.dTag;
1916
1957
  if (peer && sender) {
1917
- hooks.sendToNapplet(peer, { type: "inc.channel.event", channelId: m.channelId ?? "", sender, payload: m.payload });
1958
+ hooks.sendToNapplet(peer, {
1959
+ type: "inc.channel.event",
1960
+ channelId: m.channelId ?? "",
1961
+ sender,
1962
+ ...m.payload === void 0 ? {} : { payload: m.payload }
1963
+ });
1918
1964
  }
1919
1965
  }
1920
1966
  function handleChannelBroadcast(state, hooks, sessionRegistry2, windowId, m) {
@@ -1924,7 +1970,12 @@ function handleChannelBroadcast(state, hooks, sessionRegistry2, windowId, m) {
1924
1970
  for (const channelId of channels) {
1925
1971
  const peer = peerOf(state, channelId, windowId);
1926
1972
  if (peer) {
1927
- hooks.sendToNapplet(peer, { type: "inc.channel.event", channelId, sender, payload: m.payload });
1973
+ hooks.sendToNapplet(peer, {
1974
+ type: "inc.channel.event",
1975
+ channelId,
1976
+ sender,
1977
+ ...m.payload === void 0 ? {} : { payload: m.payload }
1978
+ });
1928
1979
  }
1929
1980
  }
1930
1981
  }
@@ -2250,7 +2301,13 @@ function handleServiceOnlyMessage(context, name, windowId, msg) {
2250
2301
  return;
2251
2302
  }
2252
2303
  const service = context.serviceRegistry[name];
2253
- if (!service) return;
2304
+ if (!service) {
2305
+ if (name === "intent") {
2306
+ const result2 = createIntentServiceUnavailableResult(msg);
2307
+ if (result2) context.hooks.sendToNapplet(windowId, result2);
2308
+ }
2309
+ return;
2310
+ }
2254
2311
  service.handleMessage(windowId, msg, (resp) => context.hooks.sendToNapplet(windowId, resp));
2255
2312
  }
2256
2313
  function createRegisteredServices(serviceRegistry) {
@@ -2476,6 +2533,10 @@ function createRuntimeInstance(context) {
2476
2533
  sessionRegistry2.getAllEntries().map((entry) => entry.windowId)
2477
2534
  );
2478
2535
  },
2536
+ hasCapability(windowId, capability) {
2537
+ const entry = sessionRegistry2.getEntryByWindowId(windowId);
2538
+ return !!entry && aclState.check("", entry.dTag, entry.aggregateHash, capability);
2539
+ },
2479
2540
  sendToEligibleNapplet(windowId, message) {
2480
2541
  const entry = sessionRegistry2.getEntryByWindowId(windowId);
2481
2542
  if (!entry || typeof message.type !== "string") return false;
@@ -3979,9 +4040,14 @@ function nappletNamespacePrelude(domains) {
3979
4040
  function fire(message) {
3980
4041
  post(message);
3981
4042
  }
4043
+ function isConventionTopic(topic) {
4044
+ const pathEnd = topic.search(/[?#]/);
4045
+ const path = pathEnd === -1 ? topic : topic.slice(0, pathEnd);
4046
+ return /^napplet:[^/?#]+\/[^/?#]+$/.test(path);
4047
+ }
3982
4048
  function normalizeConventionUri(topic, hasExplicitPayload) {
3983
4049
  if (typeof topic !== "string") throw new TypeError("INC topic must be text");
3984
- if (!topic.startsWith("napplet:")) return { topic };
4050
+ if (!isConventionTopic(topic)) return { topic };
3985
4051
  if (topic.includes("#")) throw new TypeError("Convention topics cannot contain fragments");
3986
4052
  const queryStart = topic.indexOf("?");
3987
4053
  if (queryStart === -1) {
@@ -3990,7 +4056,7 @@ function nappletNamespacePrelude(domains) {
3990
4056
  if (hasExplicitPayload) {
3991
4057
  throw new TypeError("Convention queries cannot be combined with an explicit payload");
3992
4058
  }
3993
- const values = {};
4059
+ const entries = [];
3994
4060
  const names = /* @__PURE__ */ new Set();
3995
4061
  const query = topic.slice(queryStart + 1);
3996
4062
  if (query) {
@@ -4009,10 +4075,10 @@ function nappletNamespacePrelude(domains) {
4009
4075
  }
4010
4076
  if (names.has(name)) throw new TypeError("Convention query names must be unique");
4011
4077
  names.add(name);
4012
- values[name] = value;
4078
+ entries.push([name, value]);
4013
4079
  }
4014
4080
  }
4015
- return { topic: topic.slice(0, queryStart), payload: values };
4081
+ return { topic: topic.slice(0, queryStart), payload: Object.fromEntries(entries) };
4016
4082
  }
4017
4083
  function resultOrError(msg) {
4018
4084
  return msg;
@@ -4171,7 +4237,7 @@ function nappletNamespacePrelude(domains) {
4171
4237
  const unopenedClosed = /* @__PURE__ */ new Map();
4172
4238
  function stableSubscriptionTopic(topic) {
4173
4239
  if (typeof topic !== "string") throw new TypeError("INC topic must be text");
4174
- if (topic.includes("?") || topic.includes("#")) {
4240
+ if (isConventionTopic(topic) && (topic.includes("?") || topic.includes("#"))) {
4175
4241
  throw new TypeError("INC subscriptions require a queryless topic identity");
4176
4242
  }
4177
4243
  return topic;
@@ -4192,7 +4258,6 @@ function nappletNamespacePrelude(domains) {
4192
4258
  },
4193
4259
  on(handler) {
4194
4260
  if (typeof handler !== "function") throw new TypeError("INC channel event handler must be a function");
4195
- if (state.closed) return subscriptionHandle(() => void 0);
4196
4261
  state.eventHandlers.add(handler);
4197
4262
  const pending = state.pendingEvents.splice(0);
4198
4263
  for (const event of pending) handler(event);
@@ -10719,9 +10784,10 @@ function createOutboxService(options) {
10719
10784
  if (!options || typeof options.router !== "object" || options.router === null) {
10720
10785
  throw new Error("createOutboxService: options.router is required");
10721
10786
  }
10722
- const { router } = options;
10787
+ const { router, getQueryRouter } = options;
10788
+ let runtimeContext;
10723
10789
  const subscriptions = /* @__PURE__ */ new Map();
10724
- function handleQuery(msg, send) {
10790
+ function handleQuery(windowId, msg, send) {
10725
10791
  const m = msg;
10726
10792
  const id = m.id ?? "";
10727
10793
  const filters = normalizeFilters(m.filters);
@@ -10729,7 +10795,8 @@ function createOutboxService(options) {
10729
10795
  send({ type: "outbox.query.result", id, events: [], error: "invalid filter" });
10730
10796
  return;
10731
10797
  }
10732
- void router.query(filters, sanitizeQueryOptions(m.options)).then(
10798
+ const queryRouter = getQueryRouter?.(windowId, runtimeContext) ?? router;
10799
+ void queryRouter.query(filters, sanitizeQueryOptions(m.options)).then(
10733
10800
  (result2) => send({
10734
10801
  type: "outbox.query.result",
10735
10802
  id,
@@ -10806,13 +10873,19 @@ function createOutboxService(options) {
10806
10873
  }
10807
10874
  return {
10808
10875
  descriptor: OUTBOX_DESCRIPTOR,
10876
+ onRegistered(context) {
10877
+ runtimeContext = context;
10878
+ },
10879
+ onUnregistered() {
10880
+ runtimeContext = void 0;
10881
+ },
10809
10882
  handleMessage(windowId, message, send) {
10810
10883
  switch (message.type) {
10811
10884
  case "outbox.getEvent":
10812
10885
  handleGetEvent(router, message, send);
10813
10886
  return;
10814
10887
  case "outbox.query":
10815
- handleQuery(message, send);
10888
+ handleQuery(windowId, message, send);
10816
10889
  return;
10817
10890
  case "outbox.subscribe":
10818
10891
  handleSubscribe2(windowId, message, send);
@@ -12051,20 +12124,20 @@ function createCatalogIntentResolver(options) {
12051
12124
  }
12052
12125
  };
12053
12126
  }
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
- }
12127
+ function actionFromConvention(convention) {
12058
12128
  const match = /^napplet:([^/?#\s]+)\/([^/?#\s]+)$/.exec(convention);
12059
- if (!match || match[1] !== slug) {
12060
- throw new TypeError("manifest archetype convention is invalid or mismatched");
12129
+ if (!match) {
12130
+ throw new TypeError("manifest archetype convention is invalid");
12061
12131
  }
12062
12132
  return match[2];
12063
12133
  }
12064
12134
  function manifestToIntentCatalogEntry(manifest) {
12065
12135
  const archetypes = {};
12066
12136
  for (const { slug, convention } of manifest.archetypes) {
12067
- const action = actionFromConvention(slug, convention);
12137
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
12138
+ throw new TypeError("manifest archetype slug is invalid");
12139
+ }
12140
+ const action = actionFromConvention(convention);
12068
12141
  const support = archetypes[slug] ??= {
12069
12142
  actions: [],
12070
12143
  conventions: []
@@ -14503,7 +14576,9 @@ var SimplePool = class extends AbstractSimplePool {
14503
14576
  // src/browser-relay-runtime.ts
14504
14577
  var PAJA_NIP65_RELAY_LIST_KIND = 10002;
14505
14578
  var PAJA_CONTACT_LIST_KIND = 3;
14579
+ var PAJA_CONTACT_LIST_CANDIDATE_LIMIT = 64;
14506
14580
  var PAJA_LIVE_QUERY_WAIT_MS = 4e3;
14581
+ var contactListCandidateQueries = /* @__PURE__ */ new WeakMap();
14507
14582
  function matchesFilter2(event, filter) {
14508
14583
  const ids = filter.ids;
14509
14584
  if (ids && !ids.some((id) => event.id === id || event.id.startsWith(id))) return false;
@@ -14563,14 +14638,54 @@ function subscribeLive(pool, relayUrls, filters, next) {
14563
14638
  }
14564
14639
  };
14565
14640
  }
14566
- async function queryLive(pool, relayUrls, filters, maxWaitMs = PAJA_LIVE_QUERY_WAIT_MS) {
14641
+ function queryLiveBounded(pool, relayUrls, filter, maxWaitMs, limit, signal) {
14642
+ if (limit === 0 || signal?.aborted) return Promise.resolve([]);
14643
+ return new Promise((resolve) => {
14644
+ const events = [];
14645
+ let settled = false;
14646
+ let closeRequested = false;
14647
+ let closeReason = "paja query limit reached";
14648
+ let subscription;
14649
+ let abort;
14650
+ const finish = () => {
14651
+ if (settled) return;
14652
+ settled = true;
14653
+ if (abort) signal?.removeEventListener("abort", abort);
14654
+ resolve(events);
14655
+ };
14656
+ const close = (reason = closeReason) => {
14657
+ closeRequested = true;
14658
+ closeReason = reason;
14659
+ if (!subscription) return;
14660
+ void Promise.resolve(subscription.close(reason)).catch(() => {
14661
+ });
14662
+ finish();
14663
+ };
14664
+ abort = () => close("paja query aborted");
14665
+ signal?.addEventListener("abort", abort, { once: true });
14666
+ subscription = pool.subscribeEose(relayUrls, filter, {
14667
+ label: "kehto-paja-runtime",
14668
+ maxWait: maxWaitMs,
14669
+ onevent(event) {
14670
+ if (events.length >= limit) return;
14671
+ events.push(event);
14672
+ if (events.length === limit) close();
14673
+ },
14674
+ onclose: finish
14675
+ });
14676
+ if (closeRequested) close();
14677
+ });
14678
+ }
14679
+ async function queryLive(pool, relayUrls, filters, maxWaitMs = PAJA_LIVE_QUERY_WAIT_MS, collectUntilLimit = false, signal) {
14567
14680
  const activeFilters = filters.length > 0 ? filters : [{}];
14568
- const batches = await Promise.all(activeFilters.map(
14569
- (filter) => pool.querySync(relayUrls, filter, {
14681
+ const batches = await Promise.all(activeFilters.map((filter) => {
14682
+ const limit = typeof filter.limit === "number" && filter.limit >= 0 ? filter.limit : void 0;
14683
+ if (collectUntilLimit && limit !== void 0) return queryLiveBounded(pool, relayUrls, filter, maxWaitMs, limit, signal);
14684
+ return pool.querySync(relayUrls, filter, {
14570
14685
  label: "kehto-paja-runtime",
14571
14686
  maxWait: maxWaitMs
14572
- })
14573
- ));
14687
+ });
14688
+ }));
14574
14689
  const out = /* @__PURE__ */ new Map();
14575
14690
  for (const event of batches.flat()) out.set(event.id, event);
14576
14691
  return [...out.values()].sort((a, b) => b.created_at - a.created_at);
@@ -14591,6 +14706,14 @@ function createPajaRelayBackend(getSimulation, confirmRequest, livePool = new Si
14591
14706
  for (const event of [...memoryEvents, ...liveEvents]) out.set(event.id, event);
14592
14707
  return [...out.values()].sort((a, b) => b.created_at - a.created_at);
14593
14708
  };
14709
+ const queryContactListCandidates = async (relayUrls, filters, maxWaitMs, signal) => {
14710
+ const memoryEvents = collectMemoryEvents(events, filters);
14711
+ if (signal?.aborted || getSimulation().relay.mode !== "live" || relayUrls.length === 0) return memoryEvents;
14712
+ const liveEvents = await queryLive(livePool, relayUrls, filters, maxWaitMs, true, signal);
14713
+ const out = /* @__PURE__ */ new Map();
14714
+ for (const event of [...memoryEvents, ...liveEvents]) out.set(event.id, event);
14715
+ return [...out.values()].sort((a, b) => b.created_at - a.created_at);
14716
+ };
14594
14717
  function retainPublishedEvent(event) {
14595
14718
  events.push(event);
14596
14719
  for (const subscriber of subscribers) {
@@ -14666,6 +14789,7 @@ function createPajaRelayBackend(getSimulation, confirmRequest, livePool = new Si
14666
14789
  livePool.destroy();
14667
14790
  }
14668
14791
  };
14792
+ contactListCandidateQueries.set(backend, queryContactListCandidates);
14669
14793
  return backend;
14670
14794
  }
14671
14795
  function toNostrEvent(value) {
@@ -14705,19 +14829,25 @@ async function getBootstrapRelayUrls(getSimulation, signerProvider) {
14705
14829
  ...await getSignerRelayUrls(signerProvider, "read")
14706
14830
  ]);
14707
14831
  }
14832
+ function createPajaContactListLoader(backend, getSimulation, signerProvider) {
14833
+ return async (pubkey, signal) => {
14834
+ if (!/^[0-9a-fA-F]{64}$/.test(pubkey) || signal?.aborted) return [];
14835
+ const relayUrls = await getBootstrapRelayUrls(getSimulation, signerProvider);
14836
+ if (signal?.aborted) return [];
14837
+ const filters = [{
14838
+ kinds: [PAJA_CONTACT_LIST_KIND],
14839
+ authors: [pubkey],
14840
+ limit: PAJA_CONTACT_LIST_CANDIDATE_LIMIT
14841
+ }];
14842
+ const contactQuery = contactListCandidateQueries.get(backend);
14843
+ const candidates = contactQuery ? await contactQuery(relayUrls, filters, PAJA_LIVE_QUERY_WAIT_MS, signal) : await backend.query(relayUrls, filters, PAJA_LIVE_QUERY_WAIT_MS);
14844
+ if (signal?.aborted) return [];
14845
+ return candidates.filter((event) => event.kind === PAJA_CONTACT_LIST_KIND && event.pubkey.toLowerCase() === pubkey.toLowerCase()).sort((left, right) => right.created_at - left.created_at || left.id.localeCompare(right.id)).slice(0, PAJA_CONTACT_LIST_CANDIDATE_LIMIT);
14846
+ };
14847
+ }
14708
14848
  function latestEvent(events, kind, pubkey) {
14709
14849
  return events.filter((event) => event.kind === kind && event.pubkey === pubkey).sort((a, b) => b.created_at - a.created_at)[0];
14710
14850
  }
14711
- function contactPubkeys(event) {
14712
- if (!event) return [];
14713
- const out = /* @__PURE__ */ new Set();
14714
- for (const tag of event.tags) {
14715
- if (tag[0] === "p" && typeof tag[1] === "string" && /^[0-9a-fA-F]{64}$/.test(tag[1])) {
14716
- out.add(tag[1].toLowerCase());
14717
- }
14718
- }
14719
- return [...out];
14720
- }
14721
14851
  function createPajaRelayListLoader(backend, getSimulation, signerProvider) {
14722
14852
  const registry2 = createNip65Registry();
14723
14853
  return async (pubkeys) => {
@@ -14760,24 +14890,210 @@ function createPajaOutboxRelayPool(backend) {
14760
14890
  }
14761
14891
  };
14762
14892
  }
14763
- function createPajaIdentityProviders(backend, getSimulation, signerProvider) {
14764
- const followsCache = /* @__PURE__ */ new Map();
14893
+
14894
+ // src/browser-social-cache.ts
14895
+ var HEX_PUBKEY = /^[0-9a-f]{64}$/i;
14896
+ var FOLLOW_PARSE_YIELD_EVERY = 64;
14897
+ var PROFILE_WARM_YIELD_EVERY = 64;
14898
+ function createPajaSocialCache(options) {
14899
+ let generation = 0;
14900
+ let activeRefreshController;
14901
+ const snapshots = /* @__PURE__ */ new Map();
14902
+ async function loadFollows(capturedPubkey) {
14903
+ const normalizedPubkey = normalizePubkey(capturedPubkey);
14904
+ if (!normalizedPubkey) return [];
14905
+ const existing = snapshots.get(normalizedPubkey);
14906
+ if (existing) return [...existing.follows];
14907
+ const capturedGeneration = generation;
14908
+ const follows = await verifiedFollows(normalizedPubkey);
14909
+ const warmed = snapshots.get(normalizedPubkey);
14910
+ if (warmed) return [...warmed.follows];
14911
+ if (generation === capturedGeneration) {
14912
+ snapshots.set(normalizedPubkey, { follows, profiles: /* @__PURE__ */ new Map() });
14913
+ }
14914
+ return [...follows];
14915
+ }
14916
+ async function verifiedFollows(capturedPubkey, isStillCurrent, signal) {
14917
+ const candidates = await options.loadContactList(capturedPubkey, signal);
14918
+ const verified = [];
14919
+ for (const candidate of candidates) {
14920
+ if (isStillCurrent && !isStillCurrent()) return [];
14921
+ if (!isContactCandidate(candidate, capturedPubkey)) continue;
14922
+ try {
14923
+ if (await options.verifyEvent(candidate)) verified.push(candidate);
14924
+ } catch {
14925
+ }
14926
+ }
14927
+ return contactPubkeys(selectReplacement(verified), isStillCurrent);
14928
+ }
14929
+ async function refreshActiveIdentity() {
14930
+ activeRefreshController?.abort();
14931
+ activeRefreshController = void 0;
14932
+ const currentGeneration = ++generation;
14933
+ const capturedPubkey = normalizePubkey(options.getActivePubkey() ?? "");
14934
+ if (!capturedPubkey) return;
14935
+ const controller = new AbortController();
14936
+ activeRefreshController = controller;
14937
+ const isStillCurrent = () => !controller.signal.aborted && isCurrent(capturedPubkey, currentGeneration);
14938
+ try {
14939
+ const follows = await verifiedFollows(capturedPubkey, isStillCurrent, controller.signal);
14940
+ if (!isStillCurrent()) return;
14941
+ const snapshot = { follows, profiles: /* @__PURE__ */ new Map() };
14942
+ snapshots.set(capturedPubkey, snapshot);
14943
+ for (let index = 0; index < follows.length; index += 1) {
14944
+ if (!isStillCurrent()) return;
14945
+ const author = follows[index];
14946
+ let warmed;
14947
+ try {
14948
+ warmed = await options.baseRouter.query(
14949
+ [{ kinds: [0], authors: [author], limit: 1 }],
14950
+ { authors: [author], limit: 1 }
14951
+ );
14952
+ } catch {
14953
+ }
14954
+ if (!isStillCurrent()) return;
14955
+ if (warmed) {
14956
+ const profile = profileResult(warmed, author);
14957
+ if (profile) snapshot.profiles.set(author, profile);
14958
+ }
14959
+ if ((index + 1) % PROFILE_WARM_YIELD_EVERY === 0 && index + 1 < follows.length) {
14960
+ if (!isStillCurrent()) return;
14961
+ await yieldToEventLoop();
14962
+ if (!isStillCurrent()) return;
14963
+ }
14964
+ }
14965
+ } catch {
14966
+ } finally {
14967
+ if (activeRefreshController === controller) activeRefreshController = void 0;
14968
+ }
14969
+ }
14970
+ function isCurrent(capturedPubkey, currentGeneration) {
14971
+ return generation === currentGeneration && normalizePubkey(options.getActivePubkey() ?? "") === capturedPubkey;
14972
+ }
14973
+ function decorate(router, canReadIdentity) {
14974
+ return {
14975
+ ...router.getEvent ? { getEvent: router.getEvent.bind(router) } : {},
14976
+ async query(filters, queryOptions) {
14977
+ const canAugmentAtStart = canReadIdentity?.() ?? true;
14978
+ const activePubkey = normalizePubkey(options.getActivePubkey() ?? "");
14979
+ const snapshot = canAugmentAtStart && activePubkey ? snapshots.get(activePubkey) : void 0;
14980
+ const base = await router.query(filters, queryOptions);
14981
+ if (!canAugmentAtStart || !(canReadIdentity?.() ?? true)) return base;
14982
+ const cached = matchingCachedProfiles(snapshot?.profiles, filters);
14983
+ return mergeResult(base, cached, filters, queryOptions);
14984
+ },
14985
+ subscribe: router.subscribe.bind(router),
14986
+ publish: router.publish.bind(router),
14987
+ resolveRelays: router.resolveRelays.bind(router)
14988
+ };
14989
+ }
14990
+ const unsubscribe = options.subscribeSignerChange?.(() => {
14991
+ void refreshActiveIdentity();
14992
+ });
14765
14993
  return {
14766
- async getFollows(pubkey) {
14767
- if (!/^[0-9a-fA-F]{64}$/.test(pubkey)) return [];
14768
- const cached = followsCache.get(pubkey);
14769
- if (cached) return [...cached];
14770
- const events = await backend.query(await getBootstrapRelayUrls(getSimulation, signerProvider), [{
14771
- kinds: [PAJA_CONTACT_LIST_KIND],
14772
- authors: [pubkey],
14773
- limit: 1
14774
- }], PAJA_LIVE_QUERY_WAIT_MS);
14775
- const follows = contactPubkeys(latestEvent(events, PAJA_CONTACT_LIST_KIND, pubkey));
14776
- followsCache.set(pubkey, follows);
14777
- return [...follows];
14994
+ getFollows: loadFollows,
14995
+ refreshActiveIdentity,
14996
+ decorate,
14997
+ dispose: () => {
14998
+ activeRefreshController?.abort();
14999
+ unsubscribe?.();
15000
+ }
15001
+ };
15002
+ }
15003
+ function normalizePubkey(value) {
15004
+ return typeof value === "string" && HEX_PUBKEY.test(value) ? value.toLowerCase() : null;
15005
+ }
15006
+ function isContactCandidate(candidate, capturedPubkey) {
15007
+ if (typeof candidate !== "object" || candidate === null) return false;
15008
+ const event = candidate;
15009
+ return event.kind === 3 && normalizePubkey(event.pubkey) === capturedPubkey;
15010
+ }
15011
+ function selectReplacement(events) {
15012
+ return [...events].sort((left, right) => {
15013
+ const timestampOrder = right.created_at - left.created_at;
15014
+ if (timestampOrder !== 0) return timestampOrder;
15015
+ if (left.id === right.id) return 0;
15016
+ return left.id < right.id ? -1 : 1;
15017
+ })[0];
15018
+ }
15019
+ async function contactPubkeys(event, isStillCurrent) {
15020
+ if (!event || !Array.isArray(event.tags)) return [];
15021
+ const follows = /* @__PURE__ */ new Set();
15022
+ for (let index = 0; index < event.tags.length; index += 1) {
15023
+ if (isStillCurrent && !isStillCurrent()) return [];
15024
+ const tag = event.tags[index];
15025
+ if (Array.isArray(tag) && tag[0] === "p") {
15026
+ const pubkey = normalizePubkey(tag[1]);
15027
+ if (pubkey) follows.add(pubkey);
15028
+ }
15029
+ if ((index + 1) % FOLLOW_PARSE_YIELD_EVERY === 0 && index + 1 < event.tags.length) {
15030
+ await yieldToEventLoop();
15031
+ if (isStillCurrent && !isStillCurrent()) return [];
15032
+ }
15033
+ }
15034
+ return [...follows];
15035
+ }
15036
+ function profileResult(result2, author) {
15037
+ let newest;
15038
+ for (const entry of result2.events) {
15039
+ if (entry.event.kind !== 0 || normalizePubkey(entry.event.pubkey) !== author) continue;
15040
+ if (!newest || isNewerProfile(entry, newest)) newest = entry;
15041
+ }
15042
+ return newest;
15043
+ }
15044
+ function isNewerProfile(candidate, existing) {
15045
+ if (candidate.event.created_at !== existing.event.created_at) {
15046
+ return candidate.event.created_at > existing.event.created_at;
15047
+ }
15048
+ return candidate.event.id < existing.event.id;
15049
+ }
15050
+ function yieldToEventLoop() {
15051
+ return new Promise((resolve) => setTimeout(resolve, 0));
15052
+ }
15053
+ function matchingCachedProfiles(profiles, filters) {
15054
+ const readyProfiles = profiles ? [...profiles.values()] : [];
15055
+ if (filters.length === 0) return readyProfiles;
15056
+ const matched = /* @__PURE__ */ new Map();
15057
+ for (const filter of filters) {
15058
+ const limit = typeof filter.limit === "number" && filter.limit >= 0 ? filter.limit : void 0;
15059
+ let count = 0;
15060
+ for (const entry of readyProfiles) {
15061
+ if (!matchesAnyFilter2(entry.event, [filter])) continue;
15062
+ if (limit !== void 0 && count >= limit) continue;
15063
+ count += 1;
15064
+ if (!matched.has(entry.event.id)) matched.set(entry.event.id, entry);
14778
15065
  }
15066
+ }
15067
+ return [...matched.values()];
15068
+ }
15069
+ function mergeResult(base, cached, filters, queryOptions) {
15070
+ const events = /* @__PURE__ */ new Map();
15071
+ for (const entry of base.events) events.set(entry.event.id, entry);
15072
+ for (const entry of cached) {
15073
+ if (!events.has(entry.event.id)) events.set(entry.event.id, entry);
15074
+ }
15075
+ return {
15076
+ events: applyFinalLimits([...events.values()], filters, queryOptions),
15077
+ ...base.incomplete === void 0 ? {} : { incomplete: base.incomplete },
15078
+ ...base.error === void 0 ? {} : { error: base.error }
14779
15079
  };
14780
15080
  }
15081
+ function applyFinalLimits(entries, filters, queryOptions) {
15082
+ const aggregateLimit = typeof queryOptions?.limit === "number" && queryOptions.limit >= 0 ? queryOptions.limit : void 0;
15083
+ const aggregateCapped = aggregateLimit === void 0 ? entries : entries.slice(0, aggregateLimit);
15084
+ if (filters.length === 0) return aggregateCapped;
15085
+ const counts = filters.map(() => 0);
15086
+ return aggregateCapped.filter((entry) => {
15087
+ const matching = filters.flatMap((filter, index) => matchesAnyFilter2(entry.event, [filter]) ? [index] : []);
15088
+ const allowed2 = matching.some((index) => {
15089
+ const limit = filters[index]?.limit;
15090
+ return typeof limit !== "number" || limit < 0 || counts[index] < limit;
15091
+ });
15092
+ if (!allowed2) return false;
15093
+ for (const index of matching) counts[index] += 1;
15094
+ return true;
15095
+ });
15096
+ }
14781
15097
 
14782
15098
  // src/browser-adapter.ts
14783
15099
  var DEV_COMMON_PUBKEY = "1".repeat(64);
@@ -14951,7 +15267,7 @@ function createDevSigner(getSimulation, confirmRequest) {
14951
15267
  };
14952
15268
  }
14953
15269
  function getRuntimePubkey(getSimulation, signerProvider) {
14954
- return getSimulation().identity.pubkey || signerProvider?.getPubkey() || (signerProvider?.getMethod() === "dev" ? PAJA_DEV_SIGNER_PUBKEY : "");
15270
+ return signerProvider?.getPubkey() || (signerProvider?.getMethod() === "dev" ? PAJA_DEV_SIGNER_PUBKEY : "") || getSimulation().identity.pubkey;
14955
15271
  }
14956
15272
  function createRuntimeSigner(getSimulation, confirmRequest, signerProvider) {
14957
15273
  const signer = signerProvider?.getSigner();
@@ -14988,6 +15304,15 @@ function createDevServices(backend, getSimulation, onThemeService, onThemeBroadc
14988
15304
  }
14989
15305
  })
14990
15306
  });
15307
+ const baseOutboxRouter = createOutboxRouter(backend, getSimulation, confirmRequest, signerProvider);
15308
+ const socialCache = createPajaSocialCache({
15309
+ baseRouter: baseOutboxRouter,
15310
+ loadContactList: createPajaContactListLoader(backend, getSimulation, signerProvider),
15311
+ verifyEvent: (event) => verifyEvent(event),
15312
+ getActivePubkey: () => getRuntimePubkey(getSimulation, signerProvider),
15313
+ subscribeSignerChange: signerProvider?.subscribe?.bind(signerProvider)
15314
+ });
15315
+ void socialCache.refreshActiveIdentity();
14991
15316
  const services = {
14992
15317
  keys: createKeysService(),
14993
15318
  resource: createResourceService({
@@ -15008,7 +15333,13 @@ function createDevServices(backend, getSimulation, onThemeService, onThemeBroadc
15008
15333
  selectRelayTier: () => getPajaRelayUrls(getSimulation()),
15009
15334
  isAvailable: () => backend.isAvailable()
15010
15335
  });
15011
- services.outbox = createOutboxService({ router: createOutboxRouter(backend, getSimulation, confirmRequest, signerProvider) });
15336
+ services.outbox = createOutboxService({
15337
+ router: baseOutboxRouter,
15338
+ getQueryRouter: (windowId, context) => socialCache.decorate(
15339
+ baseOutboxRouter,
15340
+ () => context?.hasCapability(windowId, "identity:read") ?? false
15341
+ )
15342
+ });
15012
15343
  }
15013
15344
  if (getSimulation().capabilities.domains.count && typeof backend.count === "function") {
15014
15345
  services.count = createCountService({
@@ -15032,7 +15363,7 @@ function createDevServices(backend, getSimulation, onThemeService, onThemeBroadc
15032
15363
  if (getSimulation().capabilities.domains.identity) {
15033
15364
  services.identity = createIdentityService({
15034
15365
  getSigner: () => createRuntimeSigner(getSimulation, confirmRequest, signerProvider),
15035
- ...createPajaIdentityProviders(backend, getSimulation, signerProvider)
15366
+ getFollows: socialCache.getFollows
15036
15367
  });
15037
15368
  }
15038
15369
  if (getSimulation().notifications.enabled) {
@@ -18901,7 +19232,7 @@ function aggregateTagValue(tags) {
18901
19232
  return void 0;
18902
19233
  }
18903
19234
 
18904
- // ../nip/dist/chunk-ZQSTY6HR.js
19235
+ // ../nip/dist/chunk-YIG2IRN2.js
18905
19236
  var MIB = 1024 * 1024;
18906
19237
  var NAPPLET_ARTIFACT_CACHE_NAME = "kehto:napplet-artifacts:v1";
18907
19238
  var SCHEMA_VERSION = 1;
@@ -19388,10 +19719,14 @@ function parseNappletManifest(event) {
19388
19719
  if (!aggregateHash) {
19389
19720
  throw new NappletResolutionError("invalid-manifest", "manifest has no aggregate x tag");
19390
19721
  }
19722
+ const dTag = firstTagValue(event.tags, "d");
19723
+ if (event.kind !== NAPPLET_KIND_NAMED && dTag !== void 0) {
19724
+ throw new NappletResolutionError("invalid-manifest", "only named manifests may carry a d tag");
19725
+ }
19391
19726
  return {
19392
19727
  kind: event.kind,
19393
19728
  pubkey: event.pubkey,
19394
- dTag: firstTagValue(event.tags, "d") ?? "",
19729
+ dTag: dTag ?? "",
19395
19730
  paths,
19396
19731
  aggregateHash,
19397
19732
  servers: allTagValues(event.tags, "server"),