@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.
- package/README.md +28 -0
- package/dist/browser-host.js +395 -60
- package/dist/browser-host.js.map +1 -1
- package/package.json +5 -5
package/dist/browser-host.js
CHANGED
|
@@ -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, {
|
|
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, {
|
|
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, {
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
4078
|
+
entries.push([name, value]);
|
|
4013
4079
|
}
|
|
4014
4080
|
}
|
|
4015
|
-
return { topic: topic.slice(0, queryStart), payload:
|
|
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
|
-
|
|
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(
|
|
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
|
|
12060
|
-
throw new TypeError("manifest archetype convention is invalid
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14764
|
-
|
|
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
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
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-
|
|
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:
|
|
19729
|
+
dTag: dTag ?? "",
|
|
19395
19730
|
paths,
|
|
19396
19731
|
aggregateHash,
|
|
19397
19732
|
servers: allTagValues(event.tags, "server"),
|