@kehto/paja 0.16.0 → 0.16.2
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 +22 -7
- package/dist/browser-host.js +2371 -2018
- package/dist/browser-host.js.map +1 -1
- package/package.json +3 -3
package/dist/browser-host.js
CHANGED
|
@@ -11234,6 +11234,7 @@ async function fetchResourceItem(options, windowId, info, request, signal) {
|
|
|
11234
11234
|
const response = await options.fetch(url, {
|
|
11235
11235
|
method: "GET",
|
|
11236
11236
|
signal,
|
|
11237
|
+
windowId,
|
|
11237
11238
|
...parsedUrl.protocol === "blossom:" && request.servers !== void 0 ? { servers: [...request.servers] } : {}
|
|
11238
11239
|
});
|
|
11239
11240
|
const buffer = await response.arrayBuffer();
|
|
@@ -11547,7 +11548,7 @@ function createOutboxService(options) {
|
|
|
11547
11548
|
if (!options || typeof options.router !== "object" || options.router === null) {
|
|
11548
11549
|
throw new Error("createOutboxService: options.router is required");
|
|
11549
11550
|
}
|
|
11550
|
-
const { router, getQueryRouter } = options;
|
|
11551
|
+
const { router, getReadRouter, getQueryRouter } = options;
|
|
11551
11552
|
let runtimeContext;
|
|
11552
11553
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
11553
11554
|
function handleQuery(windowId, msg, send) {
|
|
@@ -11558,7 +11559,7 @@ function createOutboxService(options) {
|
|
|
11558
11559
|
send({ type: "outbox.query.result", id, events: [], error: "invalid filter" });
|
|
11559
11560
|
return;
|
|
11560
11561
|
}
|
|
11561
|
-
const queryRouter = getQueryRouter?.(windowId, runtimeContext) ?? router;
|
|
11562
|
+
const queryRouter = getQueryRouter?.(windowId, runtimeContext) ?? getReadRouter?.(windowId, runtimeContext) ?? router;
|
|
11562
11563
|
void queryRouter.query(filters, sanitizeQueryOptions(m.options)).then(
|
|
11563
11564
|
(result2) => send({
|
|
11564
11565
|
type: "outbox.query.result",
|
|
@@ -11590,7 +11591,8 @@ function createOutboxService(options) {
|
|
|
11590
11591
|
send({ type: "outbox.closed", subId, ...reason === void 0 ? {} : { reason } });
|
|
11591
11592
|
}
|
|
11592
11593
|
};
|
|
11593
|
-
|
|
11594
|
+
const readRouter = getReadRouter?.(windowId, runtimeContext) ?? router;
|
|
11595
|
+
subscriptions.set(subKey, readRouter.subscribe(filters, sanitizeSubscribeOptions(m.options), sink));
|
|
11594
11596
|
}
|
|
11595
11597
|
function handleClose(windowId, msg, send) {
|
|
11596
11598
|
const m = msg;
|
|
@@ -11645,7 +11647,7 @@ function createOutboxService(options) {
|
|
|
11645
11647
|
handleMessage(windowId, message, send) {
|
|
11646
11648
|
switch (message.type) {
|
|
11647
11649
|
case "outbox.getEvent":
|
|
11648
|
-
handleGetEvent(router, message, send);
|
|
11650
|
+
handleGetEvent(getReadRouter?.(windowId, runtimeContext) ?? router, message, send);
|
|
11649
11651
|
return;
|
|
11650
11652
|
case "outbox.query":
|
|
11651
11653
|
handleQuery(windowId, message, send);
|
|
@@ -16266,2224 +16268,2557 @@ function cancelled2(uploadId) {
|
|
|
16266
16268
|
return { ok: false, uploadId, status: "cancelled", rail: "blossom", error: "user cancelled" };
|
|
16267
16269
|
}
|
|
16268
16270
|
|
|
16269
|
-
// ../nip/dist/chunk-
|
|
16270
|
-
function
|
|
16271
|
-
|
|
16272
|
-
|
|
16273
|
-
|
|
16274
|
-
|
|
16275
|
-
const write = marker !== "read";
|
|
16276
|
-
return [{ url: tag[1], read, write }];
|
|
16277
|
-
});
|
|
16278
|
-
}
|
|
16279
|
-
function selectWriteRelays(entries) {
|
|
16280
|
-
return dedupe(entries.filter((e) => e.write).map((e) => e.url));
|
|
16281
|
-
}
|
|
16282
|
-
function selectReadRelays(entries) {
|
|
16283
|
-
return dedupe(entries.filter((e) => e.read).map((e) => e.url));
|
|
16271
|
+
// ../nip/dist/chunk-JT5WCCCX.js
|
|
16272
|
+
function computeAggregateHash(entries) {
|
|
16273
|
+
const lines = entries.map((entry) => `${entry.sha256} ${entry.path}
|
|
16274
|
+
`);
|
|
16275
|
+
lines.sort();
|
|
16276
|
+
return bytesToHex2(sha256(new TextEncoder().encode(lines.join(""))));
|
|
16284
16277
|
}
|
|
16285
|
-
function
|
|
16286
|
-
const
|
|
16287
|
-
|
|
16288
|
-
|
|
16289
|
-
|
|
16290
|
-
function ingest(event) {
|
|
16291
|
-
const entries = parseNip65RelayList(event);
|
|
16292
|
-
setRelayList(event.pubkey, entries);
|
|
16293
|
-
return entries;
|
|
16294
|
-
}
|
|
16295
|
-
function getRelayList(pubkey2) {
|
|
16296
|
-
const entries = lists.get(pubkey2);
|
|
16297
|
-
return entries ? entries.map((e) => ({ ...e })) : void 0;
|
|
16298
|
-
}
|
|
16299
|
-
function resolve(pubkeys, want) {
|
|
16300
|
-
const urls = [];
|
|
16301
|
-
const seen = /* @__PURE__ */ new Set();
|
|
16302
|
-
for (const pubkey2 of pubkeys) {
|
|
16303
|
-
const entries = lists.get(pubkey2);
|
|
16304
|
-
if (!entries) continue;
|
|
16305
|
-
for (const entry of entries) {
|
|
16306
|
-
if (entry[want] && !seen.has(entry.url)) {
|
|
16307
|
-
seen.add(entry.url);
|
|
16308
|
-
urls.push(entry.url);
|
|
16309
|
-
}
|
|
16310
|
-
}
|
|
16278
|
+
function pathEntriesFromTags(tags) {
|
|
16279
|
+
const entries = [];
|
|
16280
|
+
for (const tag of tags) {
|
|
16281
|
+
if (tag[0] === "path" && typeof tag[1] === "string" && typeof tag[2] === "string" && tag[1].length > 0 && tag[2].length > 0) {
|
|
16282
|
+
entries.push({ path: tag[1], sha256: tag[2] });
|
|
16311
16283
|
}
|
|
16312
|
-
return urls;
|
|
16313
16284
|
}
|
|
16314
|
-
return
|
|
16315
|
-
ingest,
|
|
16316
|
-
setRelayList,
|
|
16317
|
-
getRelayList,
|
|
16318
|
-
has: (pubkey2) => lists.has(pubkey2),
|
|
16319
|
-
resolveOutboxRelays: (pubkeys) => resolve(pubkeys, "write"),
|
|
16320
|
-
resolveReadRelays: (pubkeys) => resolve(pubkeys, "read"),
|
|
16321
|
-
delete: (pubkey2) => lists.delete(pubkey2),
|
|
16322
|
-
clear: () => lists.clear()
|
|
16323
|
-
};
|
|
16285
|
+
return entries;
|
|
16324
16286
|
}
|
|
16325
|
-
function
|
|
16326
|
-
const
|
|
16327
|
-
|
|
16328
|
-
|
|
16329
|
-
if (!seen.has(url)) {
|
|
16330
|
-
seen.add(url);
|
|
16331
|
-
out.push(url);
|
|
16287
|
+
function aggregateTagValue(tags) {
|
|
16288
|
+
for (const tag of tags) {
|
|
16289
|
+
if (tag[0] === "x" && tag[2] === "aggregate" && typeof tag[1] === "string" && tag[1].length > 0) {
|
|
16290
|
+
return tag[1];
|
|
16332
16291
|
}
|
|
16333
16292
|
}
|
|
16334
|
-
return
|
|
16293
|
+
return void 0;
|
|
16335
16294
|
}
|
|
16336
16295
|
|
|
16337
|
-
//
|
|
16338
|
-
var
|
|
16339
|
-
var
|
|
16340
|
-
var
|
|
16341
|
-
var
|
|
16342
|
-
var
|
|
16343
|
-
var
|
|
16344
|
-
var
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16350
|
-
|
|
16351
|
-
|
|
16352
|
-
|
|
16353
|
-
|
|
16354
|
-
|
|
16355
|
-
if (!key.startsWith("#") || !Array.isArray(value)) continue;
|
|
16356
|
-
const tagName = key.slice(1);
|
|
16357
|
-
const allowed2 = value.filter((item) => typeof item === "string");
|
|
16358
|
-
if (allowed2.length === 0) continue;
|
|
16359
|
-
if (!event.tags.some((tag) => tag[0] === tagName && typeof tag[1] === "string" && allowed2.includes(tag[1]))) return false;
|
|
16360
|
-
}
|
|
16361
|
-
return true;
|
|
16296
|
+
// ../nip/dist/chunk-F6ABV5KZ.js
|
|
16297
|
+
var MIB = 1024 * 1024;
|
|
16298
|
+
var NAPPLET_ARTIFACT_CACHE_NAME = "kehto:napplet-artifacts:v1";
|
|
16299
|
+
var SCHEMA_VERSION = 1;
|
|
16300
|
+
var DEFAULT_BASE_URL = "https://kehto.invalid";
|
|
16301
|
+
var DEFAULT_UNKNOWN_BUDGET_BYTES = 32 * MIB;
|
|
16302
|
+
var DEFAULT_SOFT_CEILING_BYTES = 128 * MIB;
|
|
16303
|
+
var DEFAULT_HARD_CEILING_BYTES = 256 * MIB;
|
|
16304
|
+
var DEFAULT_FOREGROUND_TTL_MS = 15 * 60 * 1e3;
|
|
16305
|
+
var DEFAULT_BACKGROUND_TTL_MS = 60 * 60 * 1e3;
|
|
16306
|
+
function emptyIndex(now2) {
|
|
16307
|
+
return {
|
|
16308
|
+
schemaVersion: SCHEMA_VERSION,
|
|
16309
|
+
updatedAt: now2,
|
|
16310
|
+
blobs: {},
|
|
16311
|
+
aggregates: {},
|
|
16312
|
+
coordinates: {}
|
|
16313
|
+
};
|
|
16362
16314
|
}
|
|
16363
|
-
function
|
|
16364
|
-
return
|
|
16315
|
+
function aggregateKey(dTag, aggregateHash) {
|
|
16316
|
+
return `${encodeURIComponent(dTag)}:${aggregateHash}`;
|
|
16365
16317
|
}
|
|
16366
|
-
function
|
|
16367
|
-
return
|
|
16318
|
+
function coordinateKey(kind, pubkey2, dTag) {
|
|
16319
|
+
return `${kind}:${pubkey2}:${encodeURIComponent(dTag)}`;
|
|
16368
16320
|
}
|
|
16369
|
-
function
|
|
16370
|
-
const
|
|
16371
|
-
|
|
16372
|
-
return
|
|
16321
|
+
function isCoordinateFresh(coordinate, options = {}) {
|
|
16322
|
+
const now2 = options.now ?? Date.now();
|
|
16323
|
+
const ttl = options.background ? coordinate.backgroundTtlMs : coordinate.foregroundTtlMs;
|
|
16324
|
+
return ttl === Number.POSITIVE_INFINITY || now2 - coordinate.lastResolvedAt <= ttl;
|
|
16373
16325
|
}
|
|
16374
|
-
function
|
|
16375
|
-
|
|
16376
|
-
const activeFilters = filters.length > 0 ? filters : [{}];
|
|
16377
|
-
for (const filter of activeFilters) {
|
|
16378
|
-
for (const event of applyFilterLimit(events.filter((candidate) => matchesFilter2(candidate, filter)), filter)) {
|
|
16379
|
-
out.set(event.id, event);
|
|
16380
|
-
}
|
|
16381
|
-
}
|
|
16382
|
-
return [...out.values()].sort((a, b) => b.created_at - a.created_at);
|
|
16326
|
+
function byteLength2(bytes) {
|
|
16327
|
+
return bytes.byteLength;
|
|
16383
16328
|
}
|
|
16384
|
-
function
|
|
16385
|
-
|
|
16386
|
-
const requests = relayUrls2.flatMap((url) => activeFilters.map((filter) => ({ url, filter })));
|
|
16387
|
-
if (requests.length === 0) {
|
|
16388
|
-
queueMicrotask(() => next("EOSE"));
|
|
16389
|
-
return { unsubscribe() {
|
|
16390
|
-
} };
|
|
16391
|
-
}
|
|
16392
|
-
const sub = pool.subscribeMap(requests, {
|
|
16393
|
-
label: "kehto-paja-runtime",
|
|
16394
|
-
maxWait: PAJA_LIVE_QUERY_WAIT_MS,
|
|
16395
|
-
onevent: (event) => next(event),
|
|
16396
|
-
oneose: () => next("EOSE")
|
|
16397
|
-
});
|
|
16398
|
-
return {
|
|
16399
|
-
unsubscribe() {
|
|
16400
|
-
void sub.close("paja unsubscribe");
|
|
16401
|
-
}
|
|
16402
|
-
};
|
|
16329
|
+
function encodeKeyPart(value) {
|
|
16330
|
+
return encodeURIComponent(value).replaceAll("%2F", "%252F");
|
|
16403
16331
|
}
|
|
16404
|
-
function
|
|
16405
|
-
|
|
16406
|
-
return new Promise((resolve) => {
|
|
16407
|
-
const events = [];
|
|
16408
|
-
let settled = false;
|
|
16409
|
-
let closeRequested = false;
|
|
16410
|
-
let closeReason = "paja query limit reached";
|
|
16411
|
-
let subscription;
|
|
16412
|
-
let abort;
|
|
16413
|
-
const finish = () => {
|
|
16414
|
-
if (settled) return;
|
|
16415
|
-
settled = true;
|
|
16416
|
-
if (abort) signal?.removeEventListener("abort", abort);
|
|
16417
|
-
resolve(events);
|
|
16418
|
-
};
|
|
16419
|
-
const close = (reason = closeReason) => {
|
|
16420
|
-
closeRequested = true;
|
|
16421
|
-
closeReason = reason;
|
|
16422
|
-
if (!subscription) return;
|
|
16423
|
-
void Promise.resolve(subscription.close(reason)).catch(() => {
|
|
16424
|
-
});
|
|
16425
|
-
finish();
|
|
16426
|
-
};
|
|
16427
|
-
abort = () => close("paja query aborted");
|
|
16428
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
16429
|
-
subscription = pool.subscribeEose(relayUrls2, filter, {
|
|
16430
|
-
label: "kehto-paja-runtime",
|
|
16431
|
-
maxWait: maxWaitMs,
|
|
16432
|
-
onevent(event) {
|
|
16433
|
-
if (events.length >= limit) return;
|
|
16434
|
-
events.push(event);
|
|
16435
|
-
if (events.length === limit) close();
|
|
16436
|
-
},
|
|
16437
|
-
onclose: finish
|
|
16438
|
-
});
|
|
16439
|
-
if (closeRequested) close();
|
|
16440
|
-
});
|
|
16332
|
+
function cloneBytes(bytes) {
|
|
16333
|
+
return new Uint8Array(bytes);
|
|
16441
16334
|
}
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
const batches = await Promise.all(activeFilters.map((filter) => {
|
|
16445
|
-
const limit = typeof filter.limit === "number" && filter.limit >= 0 ? filter.limit : void 0;
|
|
16446
|
-
if (collectUntilLimit && limit !== void 0) return queryLiveBounded(pool, relayUrls2, filter, maxWaitMs, limit, signal);
|
|
16447
|
-
return pool.querySync(relayUrls2, filter, {
|
|
16448
|
-
label: "kehto-paja-runtime",
|
|
16449
|
-
maxWait: maxWaitMs
|
|
16450
|
-
});
|
|
16451
|
-
}));
|
|
16452
|
-
const out = /* @__PURE__ */ new Map();
|
|
16453
|
-
for (const event of batches.flat()) out.set(event.id, event);
|
|
16454
|
-
return [...out.values()].sort((a, b) => b.created_at - a.created_at);
|
|
16335
|
+
function responseBody(bytes) {
|
|
16336
|
+
return cloneBytes(bytes).buffer;
|
|
16455
16337
|
}
|
|
16456
|
-
|
|
16457
|
-
|
|
16458
|
-
return Object.fromEntries(relayUrls2.map((relayUrl2, index) => [relayUrl2, results[index]?.status === "fulfilled"]));
|
|
16338
|
+
function normalizeBaseUrl(baseUrl) {
|
|
16339
|
+
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
16459
16340
|
}
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
const timedOut = new Promise((_resolve, reject2) => {
|
|
16466
|
-
timeout = setTimeout(() => {
|
|
16467
|
-
controller.abort();
|
|
16468
|
-
reject2(new Error("count timeout"));
|
|
16469
|
-
}, PAJA_LIVE_QUERY_WAIT_MS);
|
|
16470
|
-
});
|
|
16471
|
-
try {
|
|
16472
|
-
const relay = await Promise.race([
|
|
16473
|
-
pool.ensureRelay(relayUrl2, {
|
|
16474
|
-
connectionTimeout: PAJA_LIVE_QUERY_WAIT_MS,
|
|
16475
|
-
abort: controller.signal
|
|
16476
|
-
}),
|
|
16477
|
-
timedOut
|
|
16478
|
-
]);
|
|
16479
|
-
const count = await Promise.race([
|
|
16480
|
-
relay.count(filters, { id: null }),
|
|
16481
|
-
timedOut
|
|
16482
|
-
]);
|
|
16483
|
-
if (!Number.isSafeInteger(count) || count < 0) throw new Error("invalid count");
|
|
16484
|
-
return { count, relay: relayUrl2 };
|
|
16485
|
-
} catch (error) {
|
|
16486
|
-
failure3 = error;
|
|
16487
|
-
} finally {
|
|
16488
|
-
if (timeout !== void 0) clearTimeout(timeout);
|
|
16489
|
-
}
|
|
16490
|
-
}
|
|
16491
|
-
throw failure3;
|
|
16492
|
-
}
|
|
16493
|
-
function createPajaRelayBackend(getSimulation, confirmRequest, livePool = new SimplePool()) {
|
|
16494
|
-
livePool.trackRelays = true;
|
|
16495
|
-
const fixtureEvents = getSimulation().relay.fixtures.flatMap(toNostrEvent);
|
|
16496
|
-
const acceptedMemoryEvents = [];
|
|
16497
|
-
const subscribers = /* @__PURE__ */ new Set();
|
|
16498
|
-
const isAvailable = () => getSimulation().relay.mode !== "disabled" && getPajaRelayUrls(getSimulation()).length > 0;
|
|
16499
|
-
const cachedEvents = () => getSimulation().relay.mode === "memory" ? [...fixtureEvents, ...acceptedMemoryEvents] : [];
|
|
16500
|
-
const query = async (relayUrls2, filters, maxWaitMs) => {
|
|
16501
|
-
const cached = collectMemoryEvents(cachedEvents(), filters);
|
|
16502
|
-
if (getSimulation().relay.mode !== "live" || relayUrls2.length === 0) return cached;
|
|
16503
|
-
const liveEvents = await queryLive(livePool, relayUrls2, filters, maxWaitMs);
|
|
16504
|
-
const out = /* @__PURE__ */ new Map();
|
|
16505
|
-
for (const event of [...cached, ...liveEvents]) out.set(event.id, event);
|
|
16506
|
-
return [...out.values()].sort((a, b) => b.created_at - a.created_at);
|
|
16507
|
-
};
|
|
16508
|
-
const queryContactListCandidates = async (relayUrls2, filters, maxWaitMs, signal) => {
|
|
16509
|
-
const cached = collectMemoryEvents(cachedEvents(), filters);
|
|
16510
|
-
if (signal?.aborted || getSimulation().relay.mode !== "live" || relayUrls2.length === 0) return cached;
|
|
16511
|
-
const liveEvents = await queryLive(livePool, relayUrls2, filters, maxWaitMs, true, signal);
|
|
16512
|
-
const out = /* @__PURE__ */ new Map();
|
|
16513
|
-
for (const event of [...cached, ...liveEvents]) out.set(event.id, event);
|
|
16514
|
-
return [...out.values()].sort((a, b) => b.created_at - a.created_at);
|
|
16515
|
-
};
|
|
16516
|
-
function retainPublishedEvent(event, mode) {
|
|
16517
|
-
if (mode !== "memory") return;
|
|
16518
|
-
acceptedMemoryEvents.push(event);
|
|
16519
|
-
for (const subscriber of subscribers) {
|
|
16520
|
-
if (matchesAnyFilter2(event, subscriber.filters)) subscriber.next(event);
|
|
16521
|
-
}
|
|
16522
|
-
}
|
|
16523
|
-
async function attemptPublish(relayUrls2, event) {
|
|
16524
|
-
const simulation = getSimulation();
|
|
16525
|
-
const rejected = Object.fromEntries(relayUrls2.map((url) => [url, false]));
|
|
16526
|
-
if (simulation.relay.mode === "disabled") {
|
|
16527
|
-
return { outcomes: rejected, error: "relay unavailable" };
|
|
16528
|
-
}
|
|
16529
|
-
if (!await confirmRequest({ action: "publish", event })) {
|
|
16530
|
-
return { outcomes: rejected, error: "publish denied" };
|
|
16531
|
-
}
|
|
16532
|
-
const outcomes = simulation.relay.mode === "live" ? await publishLive(livePool, relayUrls2, event) : Object.fromEntries(relayUrls2.map((url) => [url, true]));
|
|
16533
|
-
if (!Object.values(outcomes).some(Boolean)) {
|
|
16534
|
-
return { outcomes, error: "publish failed" };
|
|
16535
|
-
}
|
|
16536
|
-
retainPublishedEvent(event, simulation.relay.mode);
|
|
16537
|
-
return { outcomes };
|
|
16538
|
-
}
|
|
16539
|
-
const backend = {
|
|
16540
|
-
subscription(relayUrls2, filtersInput) {
|
|
16541
|
-
return {
|
|
16542
|
-
subscribe(next) {
|
|
16543
|
-
const filters = normalizedFilters(filtersInput);
|
|
16544
|
-
const subscriber = {
|
|
16545
|
-
filters,
|
|
16546
|
-
next: (item) => next(item)
|
|
16547
|
-
};
|
|
16548
|
-
subscribers.add(subscriber);
|
|
16549
|
-
for (const event of collectMemoryEvents(cachedEvents(), filters)) next(event);
|
|
16550
|
-
const liveSub = getSimulation().relay.mode === "live" ? subscribeLive(livePool, relayUrls2, filters, subscriber.next) : null;
|
|
16551
|
-
queueMicrotask(() => {
|
|
16552
|
-
if (subscribers.has(subscriber) && !liveSub) next("EOSE");
|
|
16553
|
-
});
|
|
16554
|
-
return {
|
|
16555
|
-
unsubscribe() {
|
|
16556
|
-
subscribers.delete(subscriber);
|
|
16557
|
-
liveSub?.unsubscribe();
|
|
16558
|
-
}
|
|
16559
|
-
};
|
|
16560
|
-
}
|
|
16561
|
-
};
|
|
16562
|
-
},
|
|
16563
|
-
async publish(relayUrls2, event) {
|
|
16564
|
-
const attempt = await attemptPublish(relayUrls2, event);
|
|
16565
|
-
if (attempt.error) throw new Error(attempt.error);
|
|
16566
|
-
},
|
|
16567
|
-
request(relayUrls2, filtersInput) {
|
|
16568
|
-
return {
|
|
16569
|
-
subscribe(observer) {
|
|
16570
|
-
const filters = normalizedFilters(filtersInput);
|
|
16571
|
-
let active = true;
|
|
16572
|
-
query(relayUrls2, filters).then((matched) => {
|
|
16573
|
-
if (!active) return;
|
|
16574
|
-
for (const event of matched) observer.next(event);
|
|
16575
|
-
observer.complete();
|
|
16576
|
-
}).catch(() => {
|
|
16577
|
-
if (active) observer.error();
|
|
16578
|
-
});
|
|
16579
|
-
return { unsubscribe() {
|
|
16580
|
-
active = false;
|
|
16581
|
-
} };
|
|
16582
|
-
}
|
|
16583
|
-
};
|
|
16584
|
-
},
|
|
16585
|
-
async count(relayUrls2, filters) {
|
|
16586
|
-
if (getSimulation().relay.mode !== "live") throw new Error("count unavailable");
|
|
16587
|
-
return (await countLive(livePool, relayUrls2, normalizedFilters(filters))).count;
|
|
16588
|
-
},
|
|
16589
|
-
async countWithRelay(relayUrls2, filters) {
|
|
16590
|
-
if (getSimulation().relay.mode !== "live") throw new Error("count unavailable");
|
|
16591
|
-
return countLive(livePool, relayUrls2, normalizedFilters(filters));
|
|
16592
|
-
},
|
|
16593
|
-
observedRelayUrls(eventId) {
|
|
16594
|
-
return [...livePool.seenOn?.get(eventId) ?? []].map((relay) => relay.url);
|
|
16595
|
-
},
|
|
16596
|
-
query,
|
|
16597
|
-
async publishToRelays(relayUrls2, event) {
|
|
16598
|
-
return (await attemptPublish(relayUrls2, event)).outcomes;
|
|
16599
|
-
},
|
|
16600
|
-
async publishWebrtcSignal(relayUrls2, event) {
|
|
16601
|
-
if (getSimulation().relay.mode !== "live" || relayUrls2.length === 0 || event.kind !== PAJA_WEBRTC_SIGNAL_KIND || !verifyEvent(event)) {
|
|
16602
|
-
throw new Error("signaling unavailable");
|
|
16603
|
-
}
|
|
16604
|
-
const outcomes = await publishLive(livePool, relayUrls2, event);
|
|
16605
|
-
if (!Object.values(outcomes).some(Boolean)) throw new Error("signaling unavailable");
|
|
16606
|
-
retainPublishedEvent(event, "live");
|
|
16607
|
-
},
|
|
16608
|
-
async publishDmGiftWrap(relayUrls2, event) {
|
|
16609
|
-
if (getSimulation().relay.mode !== "live" || relayUrls2.length === 0 || event.kind !== NIP17_GIFT_WRAP_KIND2 || !verifyEvent(event)) {
|
|
16610
|
-
throw new Error("DM relay publication failed");
|
|
16611
|
-
}
|
|
16612
|
-
const outcomes = await publishLive(livePool, relayUrls2, event);
|
|
16613
|
-
if (!Object.values(outcomes).some(Boolean)) throw new Error("DM relay publication failed");
|
|
16614
|
-
retainPublishedEvent(event, "live");
|
|
16615
|
-
},
|
|
16616
|
-
isAvailable,
|
|
16617
|
-
close() {
|
|
16618
|
-
livePool.destroy();
|
|
16619
|
-
}
|
|
16620
|
-
};
|
|
16621
|
-
contactListCandidateQueries.set(backend, queryContactListCandidates);
|
|
16622
|
-
return backend;
|
|
16623
|
-
}
|
|
16624
|
-
function toNostrEvent(value) {
|
|
16625
|
-
if (typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.pubkey === "string" && typeof value.kind === "number" && Array.isArray(value.tags) && typeof value.content === "string" && typeof value.sig === "string") {
|
|
16626
|
-
return [value];
|
|
16627
|
-
}
|
|
16628
|
-
return [];
|
|
16341
|
+
function resolveBaseUrl(baseUrl) {
|
|
16342
|
+
if (baseUrl) return normalizeBaseUrl(baseUrl);
|
|
16343
|
+
const maybeLocation = globalThis;
|
|
16344
|
+
if (maybeLocation.location?.origin) return normalizeBaseUrl(maybeLocation.location.origin);
|
|
16345
|
+
return normalizeBaseUrl(DEFAULT_BASE_URL);
|
|
16629
16346
|
}
|
|
16630
|
-
function
|
|
16631
|
-
return
|
|
16347
|
+
async function responseBytes(response) {
|
|
16348
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
16632
16349
|
}
|
|
16633
|
-
function
|
|
16634
|
-
|
|
16635
|
-
for (const url of urls) {
|
|
16636
|
-
const trimmed = url.trim();
|
|
16637
|
-
if (trimmed.length > 0) out.add(trimmed);
|
|
16638
|
-
}
|
|
16639
|
-
return [...out];
|
|
16350
|
+
async function responseJson(response) {
|
|
16351
|
+
return response.json();
|
|
16640
16352
|
}
|
|
16641
|
-
|
|
16642
|
-
const
|
|
16643
|
-
|
|
16644
|
-
|
|
16645
|
-
const relays = await signer.getRelays();
|
|
16646
|
-
return dedupeRelayUrls(Object.entries(relays).flatMap(([url, permissions]) => {
|
|
16647
|
-
if (direction === "read" && permissions.read) return [url];
|
|
16648
|
-
if (direction === "write" && permissions.write) return [url];
|
|
16649
|
-
return [];
|
|
16650
|
-
}));
|
|
16651
|
-
} catch {
|
|
16652
|
-
return [];
|
|
16353
|
+
function refCounts(index) {
|
|
16354
|
+
const counts = {};
|
|
16355
|
+
for (const aggregate of Object.values(index.aggregates)) {
|
|
16356
|
+
for (const hash of aggregate.blobHashes) counts[hash] = (counts[hash] ?? 0) + 1;
|
|
16653
16357
|
}
|
|
16358
|
+
return counts;
|
|
16654
16359
|
}
|
|
16655
|
-
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
-
|
|
16659
|
-
]);
|
|
16660
|
-
}
|
|
16661
|
-
function createPajaContactListLoader(backend, getSimulation, signerProvider, getHostRelayUrls) {
|
|
16662
|
-
return async (pubkey2, signal) => {
|
|
16663
|
-
if (!/^[0-9a-fA-F]{64}$/.test(pubkey2) || signal?.aborted) return [];
|
|
16664
|
-
const relayUrls2 = await getBootstrapRelayUrls(getSimulation, signerProvider, getHostRelayUrls);
|
|
16665
|
-
if (signal?.aborted) return [];
|
|
16666
|
-
const filters = [{
|
|
16667
|
-
kinds: [PAJA_CONTACT_LIST_KIND],
|
|
16668
|
-
authors: [pubkey2],
|
|
16669
|
-
limit: PAJA_CONTACT_LIST_CANDIDATE_LIMIT
|
|
16670
|
-
}];
|
|
16671
|
-
const contactQuery = contactListCandidateQueries.get(backend);
|
|
16672
|
-
const candidates = contactQuery ? await contactQuery(relayUrls2, filters, PAJA_LIVE_QUERY_WAIT_MS, signal) : await backend.query(relayUrls2, filters, PAJA_LIVE_QUERY_WAIT_MS);
|
|
16673
|
-
if (signal?.aborted) return [];
|
|
16674
|
-
return candidates.filter((event) => event.kind === PAJA_CONTACT_LIST_KIND && event.pubkey.toLowerCase() === pubkey2.toLowerCase()).sort((left, right) => right.created_at - left.created_at || left.id.localeCompare(right.id)).slice(0, PAJA_CONTACT_LIST_CANDIDATE_LIMIT);
|
|
16675
|
-
};
|
|
16676
|
-
}
|
|
16677
|
-
function latestEvent(events, kind, pubkey2) {
|
|
16678
|
-
return events.filter((event) => event.kind === kind && event.pubkey === pubkey2).sort((a, b) => b.created_at - a.created_at)[0];
|
|
16360
|
+
function cachedSize(index) {
|
|
16361
|
+
const blobBytes = Object.values(index.blobs).reduce((sum, entry) => sum + entry.size, 0);
|
|
16362
|
+
const aggregateBytes = Object.values(index.aggregates).reduce((sum, entry) => sum + entry.size, 0);
|
|
16363
|
+
return blobBytes + aggregateBytes;
|
|
16679
16364
|
}
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16685
|
-
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
|
|
16692
|
-
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
const event = latestEvent(events, PAJA_NIP65_RELAY_LIST_KIND, pubkey2);
|
|
16697
|
-
if (event) registry2.ingest(event);
|
|
16698
|
-
}
|
|
16365
|
+
var CacheStorageNappletArtifactCache = class {
|
|
16366
|
+
cacheStorage;
|
|
16367
|
+
storage;
|
|
16368
|
+
cacheName;
|
|
16369
|
+
baseUrl;
|
|
16370
|
+
now;
|
|
16371
|
+
activeAggregates = /* @__PURE__ */ new Set();
|
|
16372
|
+
onDiagnostic;
|
|
16373
|
+
unknownBudgetBytes;
|
|
16374
|
+
softCeilingBytes;
|
|
16375
|
+
hardCeilingBytes;
|
|
16376
|
+
cachePromise;
|
|
16377
|
+
constructor(options = {}) {
|
|
16378
|
+
const maybeCacheStorage = options.cacheStorage ?? globalThis.caches;
|
|
16379
|
+
if (!maybeCacheStorage) {
|
|
16380
|
+
throw new Error("Cache Storage is unavailable");
|
|
16699
16381
|
}
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
|
|
16705
|
-
|
|
16706
|
-
|
|
16707
|
-
|
|
16382
|
+
this.cacheStorage = maybeCacheStorage;
|
|
16383
|
+
this.storage = options.storage ?? globalThis.navigator?.storage;
|
|
16384
|
+
this.cacheName = options.cacheName ?? NAPPLET_ARTIFACT_CACHE_NAME;
|
|
16385
|
+
this.baseUrl = resolveBaseUrl(options.baseUrl);
|
|
16386
|
+
this.now = options.now ?? Date.now;
|
|
16387
|
+
this.onDiagnostic = options.onDiagnostic;
|
|
16388
|
+
this.unknownBudgetBytes = options.unknownBudgetBytes ?? DEFAULT_UNKNOWN_BUDGET_BYTES;
|
|
16389
|
+
this.softCeilingBytes = options.softCeilingBytes ?? DEFAULT_SOFT_CEILING_BYTES;
|
|
16390
|
+
this.hardCeilingBytes = options.hardCeilingBytes ?? DEFAULT_HARD_CEILING_BYTES;
|
|
16391
|
+
}
|
|
16392
|
+
async readBlob(sha256Hex) {
|
|
16393
|
+
try {
|
|
16394
|
+
const cache2 = await this.open();
|
|
16395
|
+
const response = await cache2.match(this.blobUrl(sha256Hex));
|
|
16396
|
+
if (!response) return void 0;
|
|
16397
|
+
const bytes = await responseBytes(response);
|
|
16398
|
+
const index = await this.readIndex(cache2);
|
|
16399
|
+
const entry = index.blobs[sha256Hex];
|
|
16400
|
+
if (entry) {
|
|
16401
|
+
entry.lastAccessed = this.now();
|
|
16402
|
+
index.updatedAt = entry.lastAccessed;
|
|
16403
|
+
await this.writeIndex(cache2, index);
|
|
16708
16404
|
}
|
|
16405
|
+
return bytes;
|
|
16406
|
+
} catch (cause) {
|
|
16407
|
+
this.diagnostic({ type: "cache-read-failed", message: `failed to read blob ${sha256Hex}`, cause });
|
|
16408
|
+
return void 0;
|
|
16709
16409
|
}
|
|
16710
|
-
|
|
16711
|
-
|
|
16712
|
-
|
|
16713
|
-
|
|
16714
|
-
|
|
16715
|
-
|
|
16716
|
-
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
isAvailable() {
|
|
16722
|
-
return backend.isAvailable();
|
|
16723
|
-
}
|
|
16724
|
-
};
|
|
16725
|
-
}
|
|
16726
|
-
|
|
16727
|
-
// src/browser-social-cache.ts
|
|
16728
|
-
var HEX_PUBKEY = /^[0-9a-f]{64}$/i;
|
|
16729
|
-
var FOLLOW_PARSE_YIELD_EVERY = 64;
|
|
16730
|
-
var PROFILE_WARM_YIELD_EVERY = 64;
|
|
16731
|
-
function createPajaSocialCache(options) {
|
|
16732
|
-
let generation = 0;
|
|
16733
|
-
let activeRefreshController;
|
|
16734
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
16735
|
-
async function loadFollows(capturedPubkey) {
|
|
16736
|
-
const normalizedPubkey = normalizePubkey(capturedPubkey);
|
|
16737
|
-
if (!normalizedPubkey) return [];
|
|
16738
|
-
const existing = snapshots.get(normalizedPubkey);
|
|
16739
|
-
if (existing) return [...existing.follows];
|
|
16740
|
-
const capturedGeneration = generation;
|
|
16741
|
-
const follows = await verifiedFollows(normalizedPubkey);
|
|
16742
|
-
const warmed = snapshots.get(normalizedPubkey);
|
|
16743
|
-
if (warmed) return [...warmed.follows];
|
|
16744
|
-
if (generation === capturedGeneration) {
|
|
16745
|
-
snapshots.set(normalizedPubkey, { follows, profiles: /* @__PURE__ */ new Map() });
|
|
16410
|
+
}
|
|
16411
|
+
async deleteBlob(sha256Hex) {
|
|
16412
|
+
try {
|
|
16413
|
+
const cache2 = await this.open();
|
|
16414
|
+
await cache2.delete(this.blobUrl(sha256Hex));
|
|
16415
|
+
const index = await this.readIndex(cache2);
|
|
16416
|
+
delete index.blobs[sha256Hex];
|
|
16417
|
+
index.updatedAt = this.now();
|
|
16418
|
+
await this.writeIndex(cache2, index);
|
|
16419
|
+
} catch (cause) {
|
|
16420
|
+
this.diagnostic({ type: "cache-delete-failed", message: `failed to delete blob ${sha256Hex}`, cause });
|
|
16746
16421
|
}
|
|
16747
|
-
return [...follows];
|
|
16748
16422
|
}
|
|
16749
|
-
async
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
|
|
16753
|
-
if (isStillCurrent && !isStillCurrent()) return [];
|
|
16754
|
-
if (!isContactCandidate(candidate, capturedPubkey)) continue;
|
|
16423
|
+
async writeVerifiedResolution(input) {
|
|
16424
|
+
try {
|
|
16425
|
+
await this.writeVerifiedResolutionOnce(input);
|
|
16426
|
+
} catch (firstCause) {
|
|
16755
16427
|
try {
|
|
16756
|
-
|
|
16757
|
-
|
|
16428
|
+
await this.prune(input.now ?? this.now());
|
|
16429
|
+
await this.writeVerifiedResolutionOnce(input);
|
|
16430
|
+
} catch (cause) {
|
|
16431
|
+
this.diagnostic({
|
|
16432
|
+
type: "cache-write-failed",
|
|
16433
|
+
message: "failed to write verified napplet artifacts after prune retry",
|
|
16434
|
+
cause: cause ?? firstCause
|
|
16435
|
+
});
|
|
16758
16436
|
}
|
|
16759
16437
|
}
|
|
16760
|
-
return contactPubkeys(selectReplacement(verified), isStillCurrent);
|
|
16761
16438
|
}
|
|
16762
|
-
async
|
|
16763
|
-
|
|
16764
|
-
|
|
16765
|
-
const
|
|
16766
|
-
const
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
16439
|
+
async writeCoordinate(input) {
|
|
16440
|
+
const cache2 = await this.open();
|
|
16441
|
+
const now2 = this.now();
|
|
16442
|
+
const index = await this.readIndex(cache2);
|
|
16443
|
+
const key = coordinateKey(input.kind, input.pubkey, input.dTag);
|
|
16444
|
+
index.coordinates[key] = {
|
|
16445
|
+
key,
|
|
16446
|
+
kind: input.kind,
|
|
16447
|
+
pubkey: input.pubkey,
|
|
16448
|
+
dTag: input.dTag,
|
|
16449
|
+
aggregateHash: input.aggregateHash,
|
|
16450
|
+
lastResolvedAt: input.lastResolvedAt ?? now2,
|
|
16451
|
+
foregroundTtlMs: input.foregroundTtlMs ?? DEFAULT_FOREGROUND_TTL_MS,
|
|
16452
|
+
backgroundTtlMs: input.backgroundTtlMs ?? DEFAULT_BACKGROUND_TTL_MS
|
|
16453
|
+
};
|
|
16454
|
+
index.updatedAt = now2;
|
|
16455
|
+
await this.writeIndex(cache2, index);
|
|
16456
|
+
}
|
|
16457
|
+
async getCoordinate(input, options = {}) {
|
|
16458
|
+
const cache2 = await this.open();
|
|
16459
|
+
const index = await this.readIndex(cache2);
|
|
16460
|
+
const coordinate = index.coordinates[coordinateKey(input.kind, input.pubkey, input.dTag)];
|
|
16461
|
+
if (!coordinate) return void 0;
|
|
16462
|
+
return { ...coordinate, fresh: isCoordinateFresh(coordinate, options) };
|
|
16463
|
+
}
|
|
16464
|
+
async touchAggregate(dTag, aggregateHash, now2 = this.now()) {
|
|
16465
|
+
const cache2 = await this.open();
|
|
16466
|
+
const index = await this.readIndex(cache2);
|
|
16467
|
+
const key = aggregateKey(dTag, aggregateHash);
|
|
16468
|
+
const aggregate = index.aggregates[key];
|
|
16469
|
+
if (!aggregate) return;
|
|
16470
|
+
aggregate.lastAccessed = now2;
|
|
16471
|
+
for (const hash of aggregate.blobHashes) {
|
|
16472
|
+
if (index.blobs[hash]) index.blobs[hash].lastAccessed = now2;
|
|
16473
|
+
}
|
|
16474
|
+
index.updatedAt = now2;
|
|
16475
|
+
await this.writeIndex(cache2, index);
|
|
16476
|
+
}
|
|
16477
|
+
markAggregateActive(dTag, aggregateHash) {
|
|
16478
|
+
this.activeAggregates.add(aggregateKey(dTag, aggregateHash));
|
|
16479
|
+
}
|
|
16480
|
+
releaseAggregateActive(dTag, aggregateHash) {
|
|
16481
|
+
this.activeAggregates.delete(aggregateKey(dTag, aggregateHash));
|
|
16482
|
+
}
|
|
16483
|
+
async prune(now2 = this.now()) {
|
|
16771
16484
|
try {
|
|
16772
|
-
const
|
|
16773
|
-
|
|
16774
|
-
|
|
16775
|
-
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
|
|
16783
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
16485
|
+
const cache2 = await this.open();
|
|
16486
|
+
const index = await this.readIndex(cache2);
|
|
16487
|
+
let size = cachedSize(index);
|
|
16488
|
+
const budget = await this.budgetBytes();
|
|
16489
|
+
const hardBudget = await this.hardBudgetBytes();
|
|
16490
|
+
const shouldPrune = size > budget || await this.originPressureHigh();
|
|
16491
|
+
if (!shouldPrune) return;
|
|
16492
|
+
this.rewriteRefCounts(index);
|
|
16493
|
+
for (const [hash, blob] of Object.entries(index.blobs)) {
|
|
16494
|
+
if (size <= budget) break;
|
|
16495
|
+
if (blob.refCount > 0) continue;
|
|
16496
|
+
await cache2.delete(this.blobUrl(hash));
|
|
16497
|
+
delete index.blobs[hash];
|
|
16498
|
+
size -= blob.size;
|
|
16499
|
+
}
|
|
16500
|
+
const currentAggregateHashes = new Set(
|
|
16501
|
+
Object.values(index.coordinates).map((coordinate) => coordinate.aggregateHash)
|
|
16502
|
+
);
|
|
16503
|
+
const aggregates = Object.values(index.aggregates).filter((entry) => !this.activeAggregates.has(entry.key)).sort((a, b) => {
|
|
16504
|
+
const currentA = currentAggregateHashes.has(a.aggregateHash) ? 1 : 0;
|
|
16505
|
+
const currentB = currentAggregateHashes.has(b.aggregateHash) ? 1 : 0;
|
|
16506
|
+
if (currentA !== currentB) return currentA - currentB;
|
|
16507
|
+
return a.lastAccessed - b.lastAccessed;
|
|
16508
|
+
});
|
|
16509
|
+
for (const aggregate of aggregates) {
|
|
16510
|
+
if (size <= budget) break;
|
|
16511
|
+
await cache2.delete(this.aggregateUrl(aggregate.key));
|
|
16512
|
+
delete index.aggregates[aggregate.key];
|
|
16513
|
+
size -= aggregate.size;
|
|
16514
|
+
this.rewriteRefCounts(index);
|
|
16515
|
+
}
|
|
16516
|
+
if (size > hardBudget) {
|
|
16517
|
+
const active = Object.values(index.aggregates).filter((entry) => this.activeAggregates.has(entry.key)).sort((a, b) => a.lastAccessed - b.lastAccessed);
|
|
16518
|
+
for (const aggregate of active) {
|
|
16519
|
+
if (size <= hardBudget) break;
|
|
16520
|
+
await cache2.delete(this.aggregateUrl(aggregate.key));
|
|
16521
|
+
delete index.aggregates[aggregate.key];
|
|
16522
|
+
size -= aggregate.size;
|
|
16791
16523
|
}
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
16524
|
+
}
|
|
16525
|
+
this.rewriteRefCounts(index);
|
|
16526
|
+
for (const [hash, blob] of Object.entries(index.blobs)) {
|
|
16527
|
+
if (blob.refCount === 0) {
|
|
16528
|
+
await cache2.delete(this.blobUrl(hash));
|
|
16529
|
+
delete index.blobs[hash];
|
|
16796
16530
|
}
|
|
16797
16531
|
}
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16532
|
+
index.updatedAt = now2;
|
|
16533
|
+
await this.writeIndex(cache2, index);
|
|
16534
|
+
} catch (cause) {
|
|
16535
|
+
this.diagnostic({ type: "cache-prune-failed", message: "failed to prune napplet artifact cache", cause });
|
|
16801
16536
|
}
|
|
16802
16537
|
}
|
|
16803
|
-
|
|
16804
|
-
return
|
|
16538
|
+
async snapshotIndex() {
|
|
16539
|
+
return this.readIndex(await this.open());
|
|
16805
16540
|
}
|
|
16806
|
-
|
|
16807
|
-
|
|
16808
|
-
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
16816
|
-
|
|
16541
|
+
async writeVerifiedResolutionOnce(input) {
|
|
16542
|
+
const cache2 = await this.open();
|
|
16543
|
+
const now2 = input.now ?? this.now();
|
|
16544
|
+
const index = await this.readIndex(cache2);
|
|
16545
|
+
const blobHashes = [];
|
|
16546
|
+
for (const pathEntry of input.manifest.paths) {
|
|
16547
|
+
const bytes = input.files.get(pathEntry.path);
|
|
16548
|
+
if (!bytes) continue;
|
|
16549
|
+
blobHashes.push(pathEntry.sha256);
|
|
16550
|
+
await cache2.put(this.blobUrl(pathEntry.sha256), new Response(responseBody(bytes)));
|
|
16551
|
+
index.blobs[pathEntry.sha256] = {
|
|
16552
|
+
size: byteLength2(bytes),
|
|
16553
|
+
lastAccessed: now2,
|
|
16554
|
+
refCount: index.blobs[pathEntry.sha256]?.refCount ?? 0
|
|
16555
|
+
};
|
|
16556
|
+
}
|
|
16557
|
+
const key = aggregateKey(input.manifest.dTag, input.manifest.aggregateHash);
|
|
16558
|
+
const record = {
|
|
16559
|
+
schemaVersion: SCHEMA_VERSION,
|
|
16560
|
+
dTag: input.manifest.dTag,
|
|
16561
|
+
aggregateHash: input.manifest.aggregateHash,
|
|
16562
|
+
manifest: {
|
|
16563
|
+
kind: input.manifest.kind,
|
|
16564
|
+
pubkey: input.manifest.pubkey,
|
|
16565
|
+
dTag: input.manifest.dTag,
|
|
16566
|
+
paths: input.manifest.paths.map((entry) => ({ path: entry.path, sha256: entry.sha256 })),
|
|
16567
|
+
requires: [...input.manifest.requires],
|
|
16568
|
+
...input.manifest.title === void 0 ? {} : { title: input.manifest.title },
|
|
16569
|
+
...input.manifest.description === void 0 ? {} : { description: input.manifest.description }
|
|
16817
16570
|
},
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16571
|
+
indexHtml: input.indexHtml,
|
|
16572
|
+
blobHashes,
|
|
16573
|
+
writtenAt: now2
|
|
16574
|
+
};
|
|
16575
|
+
const aggregateBytes = new TextEncoder().encode(JSON.stringify(record));
|
|
16576
|
+
await cache2.put(this.aggregateUrl(key), new Response(responseBody(aggregateBytes), {
|
|
16577
|
+
headers: { "content-type": "application/json" }
|
|
16578
|
+
}));
|
|
16579
|
+
const coordKey = coordinateKey(input.manifest.kind, input.manifest.pubkey, input.manifest.dTag);
|
|
16580
|
+
index.aggregates[key] = {
|
|
16581
|
+
dTag: input.manifest.dTag,
|
|
16582
|
+
aggregateHash: input.manifest.aggregateHash,
|
|
16583
|
+
key,
|
|
16584
|
+
blobHashes,
|
|
16585
|
+
size: byteLength2(aggregateBytes),
|
|
16586
|
+
lastAccessed: now2,
|
|
16587
|
+
coordinateKey: coordKey
|
|
16588
|
+
};
|
|
16589
|
+
index.coordinates[coordKey] = {
|
|
16590
|
+
key: coordKey,
|
|
16591
|
+
kind: input.manifest.kind,
|
|
16592
|
+
pubkey: input.manifest.pubkey,
|
|
16593
|
+
dTag: input.manifest.dTag,
|
|
16594
|
+
aggregateHash: input.manifest.aggregateHash,
|
|
16595
|
+
lastResolvedAt: now2,
|
|
16596
|
+
foregroundTtlMs: DEFAULT_FOREGROUND_TTL_MS,
|
|
16597
|
+
backgroundTtlMs: DEFAULT_BACKGROUND_TTL_MS
|
|
16821
16598
|
};
|
|
16599
|
+
this.rewriteRefCounts(index);
|
|
16600
|
+
index.updatedAt = now2;
|
|
16601
|
+
await this.writeIndex(cache2, index);
|
|
16602
|
+
await this.prune(now2);
|
|
16822
16603
|
}
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
16830
|
-
|
|
16831
|
-
|
|
16832
|
-
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
}
|
|
16836
|
-
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
}
|
|
16844
|
-
|
|
16845
|
-
return [...events].sort((left, right) => {
|
|
16846
|
-
const timestampOrder = right.created_at - left.created_at;
|
|
16847
|
-
if (timestampOrder !== 0) return timestampOrder;
|
|
16848
|
-
if (left.id === right.id) return 0;
|
|
16849
|
-
return left.id < right.id ? -1 : 1;
|
|
16850
|
-
})[0];
|
|
16851
|
-
}
|
|
16852
|
-
async function contactPubkeys(event, isStillCurrent) {
|
|
16853
|
-
if (!event || !Array.isArray(event.tags)) return [];
|
|
16854
|
-
const follows = /* @__PURE__ */ new Set();
|
|
16855
|
-
for (let index = 0; index < event.tags.length; index += 1) {
|
|
16856
|
-
if (isStillCurrent && !isStillCurrent()) return [];
|
|
16857
|
-
const tag = event.tags[index];
|
|
16858
|
-
if (Array.isArray(tag) && tag[0] === "p") {
|
|
16859
|
-
const pubkey2 = normalizePubkey(tag[1]);
|
|
16860
|
-
if (pubkey2) follows.add(pubkey2);
|
|
16604
|
+
async open() {
|
|
16605
|
+
this.cachePromise ??= this.cacheStorage.open(this.cacheName);
|
|
16606
|
+
return this.cachePromise;
|
|
16607
|
+
}
|
|
16608
|
+
indexUrl() {
|
|
16609
|
+
return new URL("__kehto_napplet_cache/index.json", this.baseUrl).toString();
|
|
16610
|
+
}
|
|
16611
|
+
blobUrl(hash) {
|
|
16612
|
+
return new URL(`__kehto_napplet_cache/blob/${encodeKeyPart(hash)}`, this.baseUrl).toString();
|
|
16613
|
+
}
|
|
16614
|
+
aggregateUrl(key) {
|
|
16615
|
+
return new URL(`__kehto_napplet_cache/aggregate/${encodeKeyPart(key)}.json`, this.baseUrl).toString();
|
|
16616
|
+
}
|
|
16617
|
+
async readIndex(cache2) {
|
|
16618
|
+
const response = await cache2.match(this.indexUrl());
|
|
16619
|
+
if (!response) return emptyIndex(this.now());
|
|
16620
|
+
try {
|
|
16621
|
+
const index = await responseJson(response);
|
|
16622
|
+
if (index.schemaVersion !== SCHEMA_VERSION) return emptyIndex(this.now());
|
|
16623
|
+
return index;
|
|
16624
|
+
} catch {
|
|
16625
|
+
return emptyIndex(this.now());
|
|
16861
16626
|
}
|
|
16862
|
-
|
|
16863
|
-
|
|
16864
|
-
|
|
16627
|
+
}
|
|
16628
|
+
async writeIndex(cache2, index) {
|
|
16629
|
+
await cache2.put(this.indexUrl(), new Response(JSON.stringify(index), {
|
|
16630
|
+
headers: { "content-type": "application/json" }
|
|
16631
|
+
}));
|
|
16632
|
+
}
|
|
16633
|
+
rewriteRefCounts(index) {
|
|
16634
|
+
const counts = refCounts(index);
|
|
16635
|
+
for (const [hash, blob] of Object.entries(index.blobs)) {
|
|
16636
|
+
blob.refCount = counts[hash] ?? 0;
|
|
16865
16637
|
}
|
|
16866
16638
|
}
|
|
16867
|
-
|
|
16868
|
-
|
|
16869
|
-
|
|
16870
|
-
|
|
16871
|
-
for (const entry of result2.events) {
|
|
16872
|
-
if (entry.event.kind !== 0 || normalizePubkey(entry.event.pubkey) !== author) continue;
|
|
16873
|
-
if (!newest || isNewerProfile(entry, newest)) newest = entry;
|
|
16639
|
+
async budgetBytes() {
|
|
16640
|
+
const estimate = await this.estimateStorage();
|
|
16641
|
+
if (!estimate?.quota) return this.unknownBudgetBytes;
|
|
16642
|
+
return Math.min(this.softCeilingBytes, Math.floor(estimate.quota * 0.1));
|
|
16874
16643
|
}
|
|
16875
|
-
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
return candidate.event.created_at > existing.event.created_at;
|
|
16644
|
+
async hardBudgetBytes() {
|
|
16645
|
+
const estimate = await this.estimateStorage();
|
|
16646
|
+
if (!estimate?.quota) return this.unknownBudgetBytes;
|
|
16647
|
+
return Math.min(this.hardCeilingBytes, Math.floor(estimate.quota * 0.2));
|
|
16880
16648
|
}
|
|
16881
|
-
|
|
16882
|
-
|
|
16883
|
-
|
|
16884
|
-
|
|
16885
|
-
}
|
|
16886
|
-
function matchingCachedProfiles(profiles, filters) {
|
|
16887
|
-
const readyProfiles = profiles ? [...profiles.values()] : [];
|
|
16888
|
-
if (filters.length === 0) return readyProfiles;
|
|
16889
|
-
const matched = /* @__PURE__ */ new Map();
|
|
16890
|
-
for (const filter of filters) {
|
|
16891
|
-
const limit = typeof filter.limit === "number" && filter.limit >= 0 ? filter.limit : void 0;
|
|
16892
|
-
let count = 0;
|
|
16893
|
-
for (const entry of readyProfiles) {
|
|
16894
|
-
if (!matchesAnyFilter2(entry.event, [filter])) continue;
|
|
16895
|
-
if (limit !== void 0 && count >= limit) continue;
|
|
16896
|
-
count += 1;
|
|
16897
|
-
if (!matched.has(entry.event.id)) matched.set(entry.event.id, entry);
|
|
16898
|
-
}
|
|
16649
|
+
async originPressureHigh() {
|
|
16650
|
+
const estimate = await this.estimateStorage();
|
|
16651
|
+
if (!estimate?.quota || estimate.usage === void 0) return false;
|
|
16652
|
+
return estimate.usage / estimate.quota >= 0.8;
|
|
16899
16653
|
}
|
|
16900
|
-
|
|
16901
|
-
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
|
|
16654
|
+
async estimateStorage() {
|
|
16655
|
+
if (!this.storage?.estimate) return void 0;
|
|
16656
|
+
try {
|
|
16657
|
+
return this.storage.estimate();
|
|
16658
|
+
} catch (cause) {
|
|
16659
|
+
this.diagnostic({
|
|
16660
|
+
type: "storage-estimate-unavailable",
|
|
16661
|
+
message: "storage estimate failed; using unknown quota cache budget",
|
|
16662
|
+
cause
|
|
16663
|
+
});
|
|
16664
|
+
return void 0;
|
|
16665
|
+
}
|
|
16907
16666
|
}
|
|
16908
|
-
|
|
16909
|
-
|
|
16910
|
-
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
const
|
|
16916
|
-
|
|
16917
|
-
|
|
16918
|
-
|
|
16919
|
-
|
|
16920
|
-
const matching = filters.flatMap((filter, index) => matchesAnyFilter2(entry.event, [filter]) ? [index] : []);
|
|
16921
|
-
const allowed2 = matching.some((index) => {
|
|
16922
|
-
const limit = filters[index]?.limit;
|
|
16923
|
-
return typeof limit !== "number" || limit < 0 || counts[index] < limit;
|
|
16667
|
+
diagnostic(diagnostic) {
|
|
16668
|
+
this.onDiagnostic?.(diagnostic);
|
|
16669
|
+
}
|
|
16670
|
+
};
|
|
16671
|
+
async function openNappletArtifactCache(options = {}) {
|
|
16672
|
+
const cacheStorage = options.cacheStorage ?? globalThis.caches;
|
|
16673
|
+
if (!cacheStorage) return void 0;
|
|
16674
|
+
const storage = options.storage ?? globalThis.navigator?.storage;
|
|
16675
|
+
if (options.requireStorageEstimate && !storage?.estimate) {
|
|
16676
|
+
options.onDiagnostic?.({
|
|
16677
|
+
type: "storage-estimate-unavailable",
|
|
16678
|
+
message: "storage estimates are unavailable; using network-only loading"
|
|
16924
16679
|
});
|
|
16925
|
-
|
|
16926
|
-
for (const index of matching) counts[index] += 1;
|
|
16927
|
-
return true;
|
|
16928
|
-
});
|
|
16929
|
-
}
|
|
16930
|
-
|
|
16931
|
-
// src/browser-relay-policy.ts
|
|
16932
|
-
var PAJA_STORAGE_PROBE_KEY = "kehto:paja:storage-probe";
|
|
16933
|
-
function normalizeRelayUrl(value) {
|
|
16934
|
-
try {
|
|
16935
|
-
const url = new URL(value);
|
|
16936
|
-
if (url.protocol !== "wss:" && url.protocol !== "ws:" || url.username || url.password) return null;
|
|
16937
|
-
url.hash = "";
|
|
16938
|
-
return url.href;
|
|
16939
|
-
} catch {
|
|
16940
|
-
return null;
|
|
16680
|
+
return void 0;
|
|
16941
16681
|
}
|
|
16942
|
-
}
|
|
16943
|
-
function isVerifiedNostrEvent(value) {
|
|
16944
|
-
if (typeof value !== "object" || value === null) return false;
|
|
16945
16682
|
try {
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
return
|
|
16683
|
+
const cache2 = new CacheStorageNappletArtifactCache({ ...options, cacheStorage, storage });
|
|
16684
|
+
await cache2.snapshotIndex();
|
|
16685
|
+
return cache2;
|
|
16686
|
+
} catch (cause) {
|
|
16687
|
+
options.onDiagnostic?.({
|
|
16688
|
+
type: "cache-open-failed",
|
|
16689
|
+
message: "Cache Storage napplet artifact cache could not be opened",
|
|
16690
|
+
cause
|
|
16691
|
+
});
|
|
16692
|
+
return void 0;
|
|
16949
16693
|
}
|
|
16950
16694
|
}
|
|
16951
|
-
|
|
16952
|
-
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
|
|
16956
|
-
|
|
16957
|
-
|
|
16958
|
-
|
|
16959
|
-
|
|
16960
|
-
|
|
16695
|
+
var NAPPLET_KIND_SNAPSHOT = 5129;
|
|
16696
|
+
var NAPPLET_KIND_ROOT = 15129;
|
|
16697
|
+
var NAPPLET_KIND_NAMED = 35129;
|
|
16698
|
+
var NAPPLET_KINDS = [
|
|
16699
|
+
NAPPLET_KIND_SNAPSHOT,
|
|
16700
|
+
NAPPLET_KIND_ROOT,
|
|
16701
|
+
NAPPLET_KIND_NAMED
|
|
16702
|
+
];
|
|
16703
|
+
function isNappletManifestKind(kind) {
|
|
16704
|
+
return NAPPLET_KINDS.includes(kind);
|
|
16705
|
+
}
|
|
16706
|
+
var NappletResolutionError = class extends Error {
|
|
16707
|
+
code;
|
|
16708
|
+
constructor(code, message) {
|
|
16709
|
+
super(message);
|
|
16710
|
+
this.name = "NappletResolutionError";
|
|
16711
|
+
this.code = code;
|
|
16961
16712
|
}
|
|
16962
|
-
|
|
16963
|
-
|
|
16964
|
-
const
|
|
16965
|
-
|
|
16713
|
+
};
|
|
16714
|
+
function firstTagValue(tags, name) {
|
|
16715
|
+
for (const tag of tags) {
|
|
16716
|
+
if (tag[0] === name && typeof tag[1] === "string" && tag[1].length > 0) return tag[1];
|
|
16717
|
+
}
|
|
16718
|
+
return void 0;
|
|
16966
16719
|
}
|
|
16967
|
-
function
|
|
16968
|
-
const
|
|
16969
|
-
const
|
|
16970
|
-
|
|
16971
|
-
const current = { discovery: [], super: [], outbox: [] };
|
|
16972
|
-
const allRelays = [];
|
|
16973
|
-
function isTier(value) {
|
|
16974
|
-
return tierNames.includes(value);
|
|
16720
|
+
function allTagValues(tags, name) {
|
|
16721
|
+
const out = [];
|
|
16722
|
+
for (const tag of tags) {
|
|
16723
|
+
if (tag[0] === name && typeof tag[1] === "string" && tag[1].length > 0) out.push(tag[1]);
|
|
16975
16724
|
}
|
|
16976
|
-
|
|
16977
|
-
|
|
16978
|
-
|
|
16979
|
-
|
|
16980
|
-
|
|
16981
|
-
|
|
16982
|
-
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
16986
|
-
|
|
16987
|
-
|
|
16725
|
+
return out;
|
|
16726
|
+
}
|
|
16727
|
+
function archetypesFromTags(tags) {
|
|
16728
|
+
const out = [];
|
|
16729
|
+
for (const tag of tags) {
|
|
16730
|
+
if (tag[0] !== "archetype") continue;
|
|
16731
|
+
const slug = tag[1];
|
|
16732
|
+
if (typeof slug !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
|
|
16733
|
+
throw new NappletResolutionError(
|
|
16734
|
+
"invalid-manifest",
|
|
16735
|
+
"archetype slug must contain lowercase letters, numbers, and hyphens"
|
|
16736
|
+
);
|
|
16988
16737
|
}
|
|
16989
|
-
const
|
|
16990
|
-
|
|
16991
|
-
|
|
16992
|
-
const runtime = {
|
|
16993
|
-
allRelays,
|
|
16994
|
-
outboxRelays: current.outbox,
|
|
16995
|
-
addRelay(tier, url) {
|
|
16996
|
-
const normalized = normalizeRelayUrl(url);
|
|
16997
|
-
if (!isTier(tier) || !normalized) return;
|
|
16998
|
-
const entries = additions.get(tier) ?? /* @__PURE__ */ new Map();
|
|
16999
|
-
entries.set(normalized, url);
|
|
17000
|
-
additions.set(tier, entries);
|
|
17001
|
-
removals.get(tier)?.delete(normalized);
|
|
17002
|
-
refresh();
|
|
17003
|
-
},
|
|
17004
|
-
removeRelay(tier, url) {
|
|
17005
|
-
const normalized = normalizeRelayUrl(url);
|
|
17006
|
-
if (!isTier(tier) || !normalized) return;
|
|
17007
|
-
additions.get(tier)?.delete(normalized);
|
|
17008
|
-
const entries = removals.get(tier) ?? /* @__PURE__ */ new Set();
|
|
17009
|
-
entries.add(normalized);
|
|
17010
|
-
removals.set(tier, entries);
|
|
17011
|
-
refresh();
|
|
17012
|
-
},
|
|
17013
|
-
getRelayConfig() {
|
|
17014
|
-
refresh();
|
|
17015
|
-
return {
|
|
17016
|
-
discovery: [...current.discovery],
|
|
17017
|
-
super: [...current.super],
|
|
17018
|
-
outbox: [...current.outbox]
|
|
17019
|
-
};
|
|
17020
|
-
},
|
|
17021
|
-
getNip66Suggestions: () => [],
|
|
17022
|
-
getRelayUrls(tiers = tierNames) {
|
|
17023
|
-
refresh();
|
|
17024
|
-
return [...new Set(tiers.flatMap((tier) => current[tier]))];
|
|
16738
|
+
const convention = tag[2];
|
|
16739
|
+
if (typeof convention !== "string" || convention.length === 0) {
|
|
16740
|
+
throw new NappletResolutionError("invalid-manifest", "archetype convention is required");
|
|
17025
16741
|
}
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
16742
|
+
if (/^NAP-\d+$/.test(convention)) {
|
|
16743
|
+
throw new NappletResolutionError(
|
|
16744
|
+
"invalid-manifest",
|
|
16745
|
+
"numbered NAP identifier is not an archetype convention"
|
|
16746
|
+
);
|
|
16747
|
+
}
|
|
16748
|
+
if (!/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(convention)) {
|
|
16749
|
+
throw new NappletResolutionError(
|
|
16750
|
+
"invalid-manifest",
|
|
16751
|
+
"archetype convention must be a queryless napplet:<archetype>/<intent> identity"
|
|
16752
|
+
);
|
|
16753
|
+
}
|
|
16754
|
+
if (tag.length !== 3) {
|
|
16755
|
+
throw new NappletResolutionError(
|
|
16756
|
+
"invalid-manifest",
|
|
16757
|
+
"archetype tags must contain exactly slug and convention"
|
|
16758
|
+
);
|
|
16759
|
+
}
|
|
16760
|
+
out.push({ slug, convention });
|
|
16761
|
+
}
|
|
16762
|
+
return out;
|
|
17029
16763
|
}
|
|
17030
|
-
function
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
|
|
17034
|
-
|
|
17035
|
-
|
|
16764
|
+
function parseNappletManifest(event) {
|
|
16765
|
+
if (!isNappletManifestKind(event.kind)) {
|
|
16766
|
+
throw new NappletResolutionError("invalid-manifest", `not a NIP-5D napplet kind: ${event.kind}`);
|
|
16767
|
+
}
|
|
16768
|
+
const paths = pathEntriesFromTags(event.tags);
|
|
16769
|
+
if (paths.length === 0) {
|
|
16770
|
+
throw new NappletResolutionError("invalid-manifest", "manifest has no path tags");
|
|
16771
|
+
}
|
|
16772
|
+
const aggregateHash = aggregateTagValue(event.tags);
|
|
16773
|
+
if (!aggregateHash) {
|
|
16774
|
+
throw new NappletResolutionError("invalid-manifest", "manifest has no aggregate x tag");
|
|
16775
|
+
}
|
|
16776
|
+
const dTag = firstTagValue(event.tags, "d");
|
|
16777
|
+
if (event.kind !== NAPPLET_KIND_NAMED && dTag !== void 0) {
|
|
16778
|
+
throw new NappletResolutionError("invalid-manifest", "only named manifests may carry a d tag");
|
|
17036
16779
|
}
|
|
17037
16780
|
return {
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
const normalized = normalizeRelayUrl(url);
|
|
17050
|
-
return normalized ? [[normalized, url]] : [];
|
|
17051
|
-
}));
|
|
17052
|
-
const requested = normalizeRelayUrl(relayUrl2);
|
|
17053
|
-
const allowedUrl = requested && isPajaRelayAllowed(relayUrl2, () => relayConfig.getRelayUrls()) ? configured.get(requested) ?? requested : void 0;
|
|
17054
|
-
if (getSimulation().relay.mode !== "live" || !allowedUrl) {
|
|
17055
|
-
sourceWindow.postMessage({ type: "relay.closed", subId, reason: "scoped relay unavailable" }, "*");
|
|
17056
|
-
return;
|
|
17057
|
-
}
|
|
17058
|
-
const subscription = pool.subscription([allowedUrl], filters).subscribe((item) => {
|
|
17059
|
-
if (item === "EOSE") {
|
|
17060
|
-
sourceWindow.postMessage({ type: "relay.eose", subId }, "*");
|
|
17061
|
-
return;
|
|
17062
|
-
}
|
|
17063
|
-
if (!isVerifiedNostrEvent(item)) return;
|
|
17064
|
-
sourceWindow.postMessage({
|
|
17065
|
-
type: "relay.event",
|
|
17066
|
-
subId,
|
|
17067
|
-
result: { event: item, sidecar: { relayHints: [allowedUrl] } }
|
|
17068
|
-
}, "*");
|
|
17069
|
-
});
|
|
17070
|
-
scoped.set(windowId, { relayUrl: allowedUrl, unsubscribe: () => subscription.unsubscribe() });
|
|
17071
|
-
},
|
|
17072
|
-
closeScopedRelay,
|
|
17073
|
-
publishToScopedRelay: async (windowId, event) => {
|
|
17074
|
-
const active = scoped.get(windowId);
|
|
17075
|
-
if (getSimulation().relay.mode !== "live" || !active || !verifyEvent(event)) return false;
|
|
17076
|
-
try {
|
|
17077
|
-
await pool.publish([active.relayUrl], event);
|
|
17078
|
-
return true;
|
|
17079
|
-
} catch {
|
|
17080
|
-
return false;
|
|
17081
|
-
}
|
|
17082
|
-
},
|
|
17083
|
-
selectRelayTier: () => relayConfig.getRelayUrls(["discovery", "super"])
|
|
16781
|
+
kind: event.kind,
|
|
16782
|
+
pubkey: event.pubkey,
|
|
16783
|
+
dTag: dTag ?? "",
|
|
16784
|
+
paths,
|
|
16785
|
+
aggregateHash,
|
|
16786
|
+
servers: allTagValues(event.tags, "server"),
|
|
16787
|
+
requires: allTagValues(event.tags, "requires"),
|
|
16788
|
+
archetypes: archetypesFromTags(event.tags),
|
|
16789
|
+
title: firstTagValue(event.tags, "title"),
|
|
16790
|
+
description: firstTagValue(event.tags, "description"),
|
|
16791
|
+
source: firstTagValue(event.tags, "source")
|
|
17084
16792
|
};
|
|
17085
16793
|
}
|
|
17086
|
-
function
|
|
17087
|
-
if (typeof localStorage === "undefined") return false;
|
|
17088
|
-
let previous = null;
|
|
16794
|
+
function verifyManifestSignature(event) {
|
|
17089
16795
|
try {
|
|
17090
|
-
|
|
17091
|
-
localStorage.setItem(PAJA_STORAGE_PROBE_KEY, "1");
|
|
17092
|
-
const available = localStorage.getItem(PAJA_STORAGE_PROBE_KEY) === "1";
|
|
17093
|
-
if (previous === null) localStorage.removeItem(PAJA_STORAGE_PROBE_KEY);
|
|
17094
|
-
else localStorage.setItem(PAJA_STORAGE_PROBE_KEY, previous);
|
|
17095
|
-
return available;
|
|
16796
|
+
return verifyEvent(event);
|
|
17096
16797
|
} catch {
|
|
17097
|
-
try {
|
|
17098
|
-
if (previous === null) localStorage.removeItem(PAJA_STORAGE_PROBE_KEY);
|
|
17099
|
-
else localStorage.setItem(PAJA_STORAGE_PROBE_KEY, previous);
|
|
17100
|
-
} catch {
|
|
17101
|
-
}
|
|
17102
16798
|
return false;
|
|
17103
16799
|
}
|
|
17104
16800
|
}
|
|
17105
|
-
|
|
17106
|
-
|
|
17107
|
-
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
16801
|
+
function verifyBlobHash(bytes, sha256Hex) {
|
|
16802
|
+
return bytesToHex2(sha256(bytes)) === sha256Hex;
|
|
16803
|
+
}
|
|
16804
|
+
function stripTrailingSlash(url) {
|
|
16805
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
16806
|
+
}
|
|
16807
|
+
async function fetchBlob(servers, sha256Hex, fetchBytes) {
|
|
16808
|
+
for (const server of servers) {
|
|
16809
|
+
const url = `${stripTrailingSlash(server)}/${sha256Hex}`;
|
|
17112
16810
|
try {
|
|
17113
|
-
const
|
|
17114
|
-
if (
|
|
16811
|
+
const bytes = await fetchBytes(url);
|
|
16812
|
+
if (verifyBlobHash(bytes, sha256Hex)) return bytes;
|
|
17115
16813
|
} catch {
|
|
17116
16814
|
}
|
|
17117
16815
|
}
|
|
17118
|
-
|
|
16816
|
+
throw new NappletResolutionError("blob-unavailable", `no server served blob ${sha256Hex}`);
|
|
17119
16817
|
}
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
17123
|
-
|
|
17124
|
-
|
|
17125
|
-
|
|
16818
|
+
var INDEX_PATHS = ["/index.html", "index.html", "/"];
|
|
16819
|
+
function defaultDecode(bytes) {
|
|
16820
|
+
return new TextDecoder().decode(bytes);
|
|
16821
|
+
}
|
|
16822
|
+
async function resolveNapplet(options) {
|
|
16823
|
+
const { event, fetchBlob: fetchBlobBytes, textDecode = defaultDecode, cache: cache2 } = options;
|
|
16824
|
+
if (!verifyManifestSignature(event)) {
|
|
16825
|
+
throw new NappletResolutionError("invalid-signature", "manifest signature is invalid");
|
|
16826
|
+
}
|
|
16827
|
+
const manifest = parseNappletManifest(event);
|
|
16828
|
+
const recomputed = computeAggregateHash(manifest.paths);
|
|
16829
|
+
if (recomputed !== manifest.aggregateHash) {
|
|
16830
|
+
throw new NappletResolutionError(
|
|
16831
|
+
"aggregate-mismatch",
|
|
16832
|
+
`recomputed aggregate ${recomputed} != manifest ${manifest.aggregateHash}`
|
|
16833
|
+
);
|
|
16834
|
+
}
|
|
16835
|
+
const files = /* @__PURE__ */ new Map();
|
|
16836
|
+
for (const entry of manifest.paths) {
|
|
16837
|
+
let bytes = await cache2?.readBlob(entry.sha256);
|
|
16838
|
+
if (bytes && !verifyBlobHash(bytes, entry.sha256)) {
|
|
16839
|
+
await cache2?.deleteBlob(entry.sha256);
|
|
16840
|
+
bytes = void 0;
|
|
17126
16841
|
}
|
|
17127
|
-
|
|
17128
|
-
|
|
17129
|
-
|
|
17130
|
-
|
|
17131
|
-
|
|
17132
|
-
|
|
17133
|
-
])
|
|
17134
|
-
};
|
|
16842
|
+
bytes ??= await fetchBlobBytes(entry.sha256, manifest.servers);
|
|
16843
|
+
if (!verifyBlobHash(bytes, entry.sha256)) {
|
|
16844
|
+
throw new NappletResolutionError(
|
|
16845
|
+
"blob-hash-mismatch",
|
|
16846
|
+
`blob for ${entry.path} does not match hash ${entry.sha256}`
|
|
16847
|
+
);
|
|
17135
16848
|
}
|
|
17136
|
-
|
|
17137
|
-
return null;
|
|
16849
|
+
files.set(entry.path, bytes);
|
|
17138
16850
|
}
|
|
17139
|
-
|
|
16851
|
+
const indexEntry = manifest.paths.find((e) => INDEX_PATHS.includes(e.path));
|
|
16852
|
+
if (!indexEntry) {
|
|
16853
|
+
throw new NappletResolutionError("missing-index", "manifest has no /index.html entry");
|
|
16854
|
+
}
|
|
16855
|
+
const indexHtml = textDecode(files.get(indexEntry.path));
|
|
16856
|
+
await cache2?.writeVerifiedResolution({ event, manifest, files, indexHtml });
|
|
16857
|
+
return {
|
|
16858
|
+
dTag: manifest.dTag,
|
|
16859
|
+
aggregateHash: manifest.aggregateHash,
|
|
16860
|
+
files,
|
|
16861
|
+
indexHtml,
|
|
16862
|
+
manifest
|
|
16863
|
+
};
|
|
17140
16864
|
}
|
|
17141
|
-
|
|
16865
|
+
|
|
16866
|
+
// src/browser-resource.ts
|
|
16867
|
+
var PAJA_RESOURCE_MAX_BYTES = 10 * 1024 * 1024;
|
|
16868
|
+
var PAJA_RESOURCE_MAX_URLS = 100;
|
|
16869
|
+
var PAJA_RESOURCE_MAX_SERVERS = 8;
|
|
16870
|
+
var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
16871
|
+
var BLOSSOM_RESOURCE_PATTERN = /^blossom:sha256:([0-9a-f]{64})$/i;
|
|
16872
|
+
var GAME_BOY_NINTENDO_LOGO = [
|
|
16873
|
+
206,
|
|
16874
|
+
237,
|
|
16875
|
+
102,
|
|
16876
|
+
102,
|
|
16877
|
+
204,
|
|
16878
|
+
13,
|
|
16879
|
+
0,
|
|
16880
|
+
11,
|
|
16881
|
+
3,
|
|
16882
|
+
115,
|
|
16883
|
+
0,
|
|
16884
|
+
131,
|
|
16885
|
+
0,
|
|
16886
|
+
12,
|
|
16887
|
+
0,
|
|
16888
|
+
13,
|
|
16889
|
+
0,
|
|
16890
|
+
8,
|
|
16891
|
+
17,
|
|
16892
|
+
31,
|
|
16893
|
+
136,
|
|
16894
|
+
137,
|
|
16895
|
+
0,
|
|
16896
|
+
14,
|
|
16897
|
+
220,
|
|
16898
|
+
204,
|
|
16899
|
+
110,
|
|
16900
|
+
230,
|
|
16901
|
+
221,
|
|
16902
|
+
221,
|
|
16903
|
+
217,
|
|
16904
|
+
153,
|
|
16905
|
+
187,
|
|
16906
|
+
187,
|
|
16907
|
+
103,
|
|
16908
|
+
99,
|
|
16909
|
+
110,
|
|
16910
|
+
14,
|
|
16911
|
+
236,
|
|
16912
|
+
204,
|
|
16913
|
+
221,
|
|
16914
|
+
220,
|
|
16915
|
+
153,
|
|
16916
|
+
159,
|
|
16917
|
+
187,
|
|
16918
|
+
185,
|
|
16919
|
+
51,
|
|
16920
|
+
62
|
|
16921
|
+
];
|
|
16922
|
+
function createPajaResourceFetch(options = {}) {
|
|
16923
|
+
return async (value, init) => {
|
|
16924
|
+
if (init.signal.aborted) throw new DOMException("Resource request cancelled", "AbortError");
|
|
16925
|
+
const url = new URL(value);
|
|
16926
|
+
if (init.method && init.method.toUpperCase() !== "GET") {
|
|
16927
|
+
throw new ResourceServiceError("invalid-request", "NAP-RESOURCE is read-only");
|
|
16928
|
+
}
|
|
16929
|
+
if (url.protocol === "blossom:") {
|
|
16930
|
+
return fetchBlossomResource(
|
|
16931
|
+
value,
|
|
16932
|
+
init.signal,
|
|
16933
|
+
init.servers ?? [],
|
|
16934
|
+
init.windowId,
|
|
16935
|
+
options
|
|
16936
|
+
);
|
|
16937
|
+
}
|
|
16938
|
+
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
16939
|
+
return fetchNetworkResource(value, init.signal, options.fetch ?? globalThis.fetch);
|
|
16940
|
+
}
|
|
16941
|
+
if (url.protocol !== "data:") {
|
|
16942
|
+
throw new ResourceServiceError("unsupported-scheme", `Paja does not enable ${url.protocol}`);
|
|
16943
|
+
}
|
|
16944
|
+
if (value.length > encodedDataUrlLimit(PAJA_RESOURCE_MAX_BYTES)) {
|
|
16945
|
+
throw new ResourceServiceError("too-large", `resource exceeds ${PAJA_RESOURCE_MAX_BYTES} bytes`);
|
|
16946
|
+
}
|
|
16947
|
+
const decoded = await fetch(value, { signal: init.signal });
|
|
16948
|
+
const bytes = new Uint8Array(await decoded.arrayBuffer());
|
|
16949
|
+
if (bytes.byteLength > PAJA_RESOURCE_MAX_BYTES) {
|
|
16950
|
+
throw new ResourceServiceError("too-large", `resource exceeds ${PAJA_RESOURCE_MAX_BYTES} bytes`);
|
|
16951
|
+
}
|
|
16952
|
+
const mime = sniffSafeResourceMime(bytes);
|
|
16953
|
+
if (!mime) {
|
|
16954
|
+
throw new ResourceServiceError("decode-failed", "resource bytes are not an enabled safe media type");
|
|
16955
|
+
}
|
|
16956
|
+
return new Response(arrayBufferFor(bytes), { headers: { "content-type": mime } });
|
|
16957
|
+
};
|
|
16958
|
+
}
|
|
16959
|
+
function pajaResourceInfo() {
|
|
16960
|
+
const schemes = [
|
|
16961
|
+
{ scheme: "data", enabled: true },
|
|
16962
|
+
{ scheme: "https", enabled: true },
|
|
16963
|
+
{ scheme: "http", enabled: true },
|
|
16964
|
+
{ scheme: "blossom", enabled: true }
|
|
16965
|
+
];
|
|
16966
|
+
return {
|
|
16967
|
+
schemes,
|
|
16968
|
+
maxBytes: PAJA_RESOURCE_MAX_BYTES,
|
|
16969
|
+
maxUrls: PAJA_RESOURCE_MAX_URLS,
|
|
16970
|
+
maxServers: PAJA_RESOURCE_MAX_SERVERS
|
|
16971
|
+
};
|
|
16972
|
+
}
|
|
16973
|
+
async function fetchNetworkResource(value, signal, fetcher) {
|
|
16974
|
+
let response;
|
|
17142
16975
|
try {
|
|
17143
|
-
|
|
17144
|
-
|
|
17145
|
-
|
|
17146
|
-
|
|
16976
|
+
response = await fetcher(value, {
|
|
16977
|
+
method: "GET",
|
|
16978
|
+
signal,
|
|
16979
|
+
credentials: "omit",
|
|
16980
|
+
referrerPolicy: "no-referrer"
|
|
16981
|
+
});
|
|
16982
|
+
} catch (error) {
|
|
16983
|
+
if (signal.aborted || isAbortError(error)) throw error;
|
|
16984
|
+
throw new ResourceServiceError(
|
|
16985
|
+
"network-error",
|
|
16986
|
+
error instanceof Error ? error.message : String(error)
|
|
16987
|
+
);
|
|
16988
|
+
}
|
|
16989
|
+
if (response.status === 404 || response.status === 410) {
|
|
16990
|
+
throw new ResourceServiceError("not-found", "HTTP resource was not found");
|
|
16991
|
+
}
|
|
16992
|
+
if (!response.ok) {
|
|
16993
|
+
throw new ResourceServiceError("network-error", `HTTP resource returned ${response.status}`);
|
|
16994
|
+
}
|
|
16995
|
+
const bytes = await readCappedResponse(response, signal);
|
|
16996
|
+
const mime = sniffSafeResourceMime(bytes);
|
|
16997
|
+
if (!mime) {
|
|
16998
|
+
throw new ResourceServiceError("decode-failed", "resource bytes are not an enabled safe media type");
|
|
17147
16999
|
}
|
|
17000
|
+
return new Response(arrayBufferFor(bytes), { headers: { "content-type": mime } });
|
|
17148
17001
|
}
|
|
17149
|
-
function
|
|
17150
|
-
|
|
17151
|
-
|
|
17002
|
+
async function fetchBlossomResource(value, signal, requestServers, windowId, options) {
|
|
17003
|
+
const match = BLOSSOM_RESOURCE_PATTERN.exec(value);
|
|
17004
|
+
if (!match?.[1]) {
|
|
17005
|
+
throw new ResourceServiceError("invalid-request", "expected blossom:sha256:<64 hex characters>");
|
|
17006
|
+
}
|
|
17007
|
+
const expectedHash = match[1].toLowerCase();
|
|
17008
|
+
const configuredServers = await options.getBlossomServers?.({ url: value, windowId }) ?? [];
|
|
17009
|
+
if (signal.aborted) throw new DOMException("Resource request cancelled", "AbortError");
|
|
17010
|
+
const servers = resolveBlossomServers(requestServers, configuredServers);
|
|
17011
|
+
if (servers.length === 0) {
|
|
17012
|
+
throw new ResourceServiceError("blocked-by-policy", "Paja has no accepted Blossom server");
|
|
17013
|
+
}
|
|
17014
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
17015
|
+
let foundHashMismatch = false;
|
|
17016
|
+
let foundNotFound = false;
|
|
17017
|
+
let foundInconclusive = false;
|
|
17018
|
+
let lastNetworkMessage = "all accepted Blossom servers failed";
|
|
17019
|
+
for (const server of servers) {
|
|
17020
|
+
if (signal.aborted) throw new DOMException("Resource request cancelled", "AbortError");
|
|
17021
|
+
const resourceUrl = `${server}/${expectedHash}`;
|
|
17022
|
+
let response;
|
|
17023
|
+
try {
|
|
17024
|
+
response = await fetcher(resourceUrl, {
|
|
17025
|
+
method: "GET",
|
|
17026
|
+
signal,
|
|
17027
|
+
redirect: "error",
|
|
17028
|
+
cache: "no-store",
|
|
17029
|
+
credentials: "omit",
|
|
17030
|
+
referrerPolicy: "no-referrer"
|
|
17031
|
+
});
|
|
17032
|
+
} catch (error) {
|
|
17033
|
+
if (signal.aborted || isAbortError(error)) throw error;
|
|
17034
|
+
foundInconclusive = true;
|
|
17035
|
+
lastNetworkMessage = error instanceof Error ? error.message : String(error);
|
|
17036
|
+
continue;
|
|
17037
|
+
}
|
|
17038
|
+
if (response.status === 404 || response.status === 410) {
|
|
17039
|
+
foundNotFound = true;
|
|
17040
|
+
continue;
|
|
17041
|
+
}
|
|
17042
|
+
if (!response.ok) {
|
|
17043
|
+
foundInconclusive = true;
|
|
17044
|
+
lastNetworkMessage = `Blossom server returned HTTP ${response.status}`;
|
|
17045
|
+
continue;
|
|
17046
|
+
}
|
|
17047
|
+
const bytes = await readCappedResponse(response, signal);
|
|
17048
|
+
if (!verifyBlobHash(bytes, expectedHash)) {
|
|
17049
|
+
foundHashMismatch = true;
|
|
17050
|
+
continue;
|
|
17051
|
+
}
|
|
17052
|
+
const mime = sniffSafeResourceMime(bytes);
|
|
17053
|
+
if (!mime) {
|
|
17054
|
+
throw new ResourceServiceError("decode-failed", "resource bytes are not an enabled safe media type");
|
|
17055
|
+
}
|
|
17056
|
+
return new Response(arrayBufferFor(bytes), { headers: { "content-type": mime } });
|
|
17057
|
+
}
|
|
17058
|
+
if (foundHashMismatch) {
|
|
17059
|
+
throw new ResourceServiceError("decode-failed", "Blossom response did not match the requested SHA-256");
|
|
17060
|
+
}
|
|
17061
|
+
if (foundInconclusive) {
|
|
17062
|
+
throw new ResourceServiceError("network-error", lastNetworkMessage);
|
|
17063
|
+
}
|
|
17064
|
+
if (foundNotFound) {
|
|
17065
|
+
throw new ResourceServiceError("not-found", "Blossom resource was not found");
|
|
17066
|
+
}
|
|
17067
|
+
throw new ResourceServiceError("network-error", lastNetworkMessage);
|
|
17152
17068
|
}
|
|
17153
|
-
function
|
|
17154
|
-
|
|
17069
|
+
function resolveBlossomServers(requestServers, configuredServers) {
|
|
17070
|
+
const servers = [];
|
|
17071
|
+
for (const value of requestServers) {
|
|
17072
|
+
const server = normalizePublicBlossomServer(value);
|
|
17073
|
+
if (server && !servers.includes(server)) servers.push(server);
|
|
17074
|
+
if (servers.length === PAJA_RESOURCE_MAX_SERVERS) return servers;
|
|
17075
|
+
}
|
|
17076
|
+
for (const server of usableBlossomServers(configuredServers)) {
|
|
17077
|
+
if (!servers.includes(server)) servers.push(server);
|
|
17078
|
+
if (servers.length === PAJA_RESOURCE_MAX_SERVERS) break;
|
|
17079
|
+
}
|
|
17080
|
+
return servers;
|
|
17155
17081
|
}
|
|
17156
|
-
function
|
|
17082
|
+
function normalizePublicBlossomServer(value) {
|
|
17083
|
+
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
17157
17084
|
try {
|
|
17158
|
-
const
|
|
17159
|
-
if (
|
|
17160
|
-
|
|
17161
|
-
if (typeof profile.display_name === "string" && typeof profile.displayName !== "string") {
|
|
17162
|
-
profile.displayName = profile.display_name;
|
|
17163
|
-
}
|
|
17164
|
-
return profile;
|
|
17085
|
+
const url = new URL(value.trim());
|
|
17086
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/" || !isPublicHostname(url.hostname)) return null;
|
|
17087
|
+
return url.origin;
|
|
17165
17088
|
} catch {
|
|
17166
17089
|
return null;
|
|
17167
17090
|
}
|
|
17168
17091
|
}
|
|
17169
|
-
function
|
|
17170
|
-
const
|
|
17171
|
-
|
|
17172
|
-
|
|
17173
|
-
|
|
17174
|
-
}
|
|
17092
|
+
function isPublicHostname(value) {
|
|
17093
|
+
const hostname = value.replace(/^\[|\]$/gu, "").toLowerCase();
|
|
17094
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local") || hostname.endsWith(".internal") || !hostname.includes(".")) return false;
|
|
17095
|
+
if (hostname.includes(":")) {
|
|
17096
|
+
return hostname !== "::" && hostname !== "::1" && !hostname.startsWith("::ffff:") && !/^(?:fc|fd|fe[89ab]|ff)/iu.test(hostname) && !hostname.startsWith("2001:db8:");
|
|
17175
17097
|
}
|
|
17176
|
-
|
|
17177
|
-
|
|
17178
|
-
|
|
17179
|
-
|
|
17180
|
-
if (message.includes("denied")) return { ok: false, error: "user-denied" };
|
|
17181
|
-
if (message.includes("publish")) return { ok: false, error: "publish-failed" };
|
|
17182
|
-
return { ok: false, error: message };
|
|
17183
|
-
}
|
|
17184
|
-
function isSingleEmoji(value) {
|
|
17185
|
-
const graphemes = [...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(value)];
|
|
17186
|
-
return graphemes.length === 1 && /[\p{Extended_Pictographic}\p{Regional_Indicator}]/u.test(value);
|
|
17098
|
+
const octets = hostname.split(".").map(Number);
|
|
17099
|
+
if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) return true;
|
|
17100
|
+
const [first, second] = octets;
|
|
17101
|
+
return first !== 0 && first !== 10 && first !== 127 && first < 224 && !(first === 100 && second >= 64 && second <= 127) && !(first === 169 && second === 254) && !(first === 172 && second >= 16 && second <= 31) && !(first === 192 && second === 168) && !(first === 198 && (second === 18 || second === 19));
|
|
17187
17102
|
}
|
|
17188
|
-
function
|
|
17189
|
-
|
|
17190
|
-
|
|
17191
|
-
if (SHORTCODE.test(reaction) && !customEmojiHref) return null;
|
|
17192
|
-
const tags = [["e", target.id], ["p", target.pubkey], ["k", String(target.kind)]];
|
|
17193
|
-
if (customEmojiHref) {
|
|
17194
|
-
let url;
|
|
17103
|
+
function usableBlossomServers(values) {
|
|
17104
|
+
const servers = [];
|
|
17105
|
+
for (const value of values) {
|
|
17195
17106
|
try {
|
|
17196
|
-
|
|
17107
|
+
const [server] = normalizeUploadServers([value]);
|
|
17108
|
+
if (server && !servers.includes(server)) servers.push(server);
|
|
17197
17109
|
} catch {
|
|
17198
|
-
return null;
|
|
17199
17110
|
}
|
|
17200
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
17201
|
-
tags.push(["emoji", reaction.slice(1, -1), url.href]);
|
|
17202
17111
|
}
|
|
17203
|
-
return
|
|
17112
|
+
return servers;
|
|
17204
17113
|
}
|
|
17205
|
-
function
|
|
17206
|
-
const
|
|
17207
|
-
|
|
17208
|
-
|
|
17209
|
-
|
|
17210
|
-
|
|
17211
|
-
|
|
17212
|
-
|
|
17213
|
-
|
|
17214
|
-
|
|
17215
|
-
|
|
17216
|
-
|
|
17217
|
-
|
|
17218
|
-
|
|
17219
|
-
|
|
17220
|
-
|
|
17221
|
-
|
|
17222
|
-
|
|
17223
|
-
|
|
17224
|
-
|
|
17225
|
-
|
|
17226
|
-
|
|
17227
|
-
await options.relay.publish(options.getRelays(), event);
|
|
17228
|
-
return { ok: true, eventId: event.id, event };
|
|
17229
|
-
} catch (error) {
|
|
17230
|
-
return actionError(error);
|
|
17231
|
-
}
|
|
17232
|
-
};
|
|
17233
|
-
const mutateFollows = async (values, add) => {
|
|
17234
|
-
const targets = values.map(pubkey);
|
|
17235
|
-
if (targets.some((target) => target === null)) return { ok: false, error: "invalid-pubkey" };
|
|
17236
|
-
try {
|
|
17237
|
-
const active = await signer();
|
|
17238
|
-
const current = await currentContacts(active.pubkey);
|
|
17239
|
-
const targetSet = new Set(targets);
|
|
17240
|
-
const existing = new Set(contactPubkeys2(current));
|
|
17241
|
-
const changed = add ? [...targetSet].some((target) => !existing.has(target)) : [...targetSet].some((target) => existing.has(target));
|
|
17242
|
-
if (!changed) {
|
|
17243
|
-
return current ? { ok: true, eventId: current.id, event: current } : { ok: true };
|
|
17244
|
-
}
|
|
17245
|
-
const tags = (current?.tags ?? []).filter((tag) => {
|
|
17246
|
-
if (tag[0] !== "p" || typeof tag[1] !== "string") return true;
|
|
17247
|
-
return add || !targetSet.has(tag[1].toLowerCase());
|
|
17248
|
-
});
|
|
17249
|
-
if (add) {
|
|
17250
|
-
for (const target of targetSet) {
|
|
17251
|
-
if (!existing.has(target)) tags.push(["p", target]);
|
|
17252
|
-
}
|
|
17114
|
+
async function readCappedResponse(response, signal) {
|
|
17115
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
17116
|
+
if (Number.isFinite(declaredLength) && declaredLength > PAJA_RESOURCE_MAX_BYTES) {
|
|
17117
|
+
throw resourceTooLarge();
|
|
17118
|
+
}
|
|
17119
|
+
if (!response.body) {
|
|
17120
|
+
const bytes2 = new Uint8Array(await response.arrayBuffer());
|
|
17121
|
+
if (bytes2.byteLength > PAJA_RESOURCE_MAX_BYTES) throw resourceTooLarge();
|
|
17122
|
+
return bytes2;
|
|
17123
|
+
}
|
|
17124
|
+
const reader = response.body.getReader();
|
|
17125
|
+
const chunks = [];
|
|
17126
|
+
let total = 0;
|
|
17127
|
+
try {
|
|
17128
|
+
while (true) {
|
|
17129
|
+
if (signal.aborted) throw new DOMException("Resource request cancelled", "AbortError");
|
|
17130
|
+
const { done, value } = await reader.read();
|
|
17131
|
+
if (done) break;
|
|
17132
|
+
total += value.byteLength;
|
|
17133
|
+
if (total > PAJA_RESOURCE_MAX_BYTES) {
|
|
17134
|
+
await reader.cancel();
|
|
17135
|
+
throw resourceTooLarge();
|
|
17253
17136
|
}
|
|
17254
|
-
|
|
17255
|
-
kind: 3,
|
|
17256
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
17257
|
-
tags,
|
|
17258
|
-
content: current?.content ?? ""
|
|
17259
|
-
});
|
|
17260
|
-
} catch (error) {
|
|
17261
|
-
return actionError(error);
|
|
17137
|
+
chunks.push(value);
|
|
17262
17138
|
}
|
|
17263
|
-
}
|
|
17264
|
-
|
|
17265
|
-
|
|
17266
|
-
|
|
17267
|
-
|
|
17268
|
-
|
|
17269
|
-
|
|
17270
|
-
|
|
17271
|
-
|
|
17272
|
-
|
|
17273
|
-
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
|
|
17277
|
-
|
|
17278
|
-
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
17285
|
-
|
|
17286
|
-
|
|
17287
|
-
|
|
17288
|
-
|
|
17289
|
-
|
|
17290
|
-
|
|
17291
|
-
|
|
17292
|
-
|
|
17293
|
-
|
|
17294
|
-
|
|
17295
|
-
|
|
17296
|
-
|
|
17297
|
-
|
|
17298
|
-
|
|
17299
|
-
|
|
17300
|
-
|
|
17301
|
-
|
|
17302
|
-
|
|
17303
|
-
|
|
17304
|
-
|
|
17305
|
-
|
|
17306
|
-
|
|
17307
|
-
|
|
17308
|
-
|
|
17309
|
-
|
|
17310
|
-
|
|
17311
|
-
|
|
17312
|
-
|
|
17313
|
-
|
|
17314
|
-
|
|
17315
|
-
|
|
17316
|
-
|
|
17317
|
-
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17329
|
-
|
|
17330
|
-
|
|
17331
|
-
|
|
17139
|
+
} finally {
|
|
17140
|
+
reader.releaseLock();
|
|
17141
|
+
}
|
|
17142
|
+
const bytes = new Uint8Array(total);
|
|
17143
|
+
let offset = 0;
|
|
17144
|
+
for (const chunk of chunks) {
|
|
17145
|
+
bytes.set(chunk, offset);
|
|
17146
|
+
offset += chunk.byteLength;
|
|
17147
|
+
}
|
|
17148
|
+
return bytes;
|
|
17149
|
+
}
|
|
17150
|
+
function resourceTooLarge() {
|
|
17151
|
+
return new ResourceServiceError("too-large", `resource exceeds ${PAJA_RESOURCE_MAX_BYTES} bytes`);
|
|
17152
|
+
}
|
|
17153
|
+
function isAbortError(error) {
|
|
17154
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "DOMException");
|
|
17155
|
+
}
|
|
17156
|
+
function arrayBufferFor(bytes) {
|
|
17157
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
17158
|
+
}
|
|
17159
|
+
function encodedDataUrlLimit(maxBytes) {
|
|
17160
|
+
return Math.ceil(maxBytes * 4 / 3) + 1024;
|
|
17161
|
+
}
|
|
17162
|
+
function startsWith(bytes, signature, offset = 0) {
|
|
17163
|
+
return signature.every((value, index) => bytes[offset + index] === value);
|
|
17164
|
+
}
|
|
17165
|
+
function ascii(bytes, start, end) {
|
|
17166
|
+
return String.fromCharCode(...bytes.subarray(start, end));
|
|
17167
|
+
}
|
|
17168
|
+
function sniffSafeResourceMime(bytes) {
|
|
17169
|
+
if (startsWith(bytes, [137, 80, 78, 71, 13, 10, 26, 10])) return "image/png";
|
|
17170
|
+
if (startsWith(bytes, [255, 216, 255])) return "image/jpeg";
|
|
17171
|
+
if (ascii(bytes, 0, 6) === "GIF87a" || ascii(bytes, 0, 6) === "GIF89a") return "image/gif";
|
|
17172
|
+
if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 12) === "WEBP") return "image/webp";
|
|
17173
|
+
if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 12) === "WAVE") return "audio/wav";
|
|
17174
|
+
if (ascii(bytes, 0, 4) === "OggS") return "audio/ogg";
|
|
17175
|
+
if (ascii(bytes, 0, 3) === "ID3" || startsWith(bytes, [255, 251])) return "audio/mpeg";
|
|
17176
|
+
if (startsWith(bytes, [26, 69, 223, 163])) return "video/webm";
|
|
17177
|
+
if (ascii(bytes, 4, 8) === "ftyp") return "video/mp4";
|
|
17178
|
+
if (ascii(bytes, 0, 4) === "wOFF") return "font/woff";
|
|
17179
|
+
if (ascii(bytes, 0, 4) === "wOF2") return "font/woff2";
|
|
17180
|
+
if (isGameBoyRom(bytes)) return "application/vnd.nintendo.gb-rom";
|
|
17181
|
+
return sniffSafeTextMime(bytes);
|
|
17182
|
+
}
|
|
17183
|
+
function isGameBoyRom(bytes) {
|
|
17184
|
+
if (bytes.byteLength < 336 || !startsWith(bytes, GAME_BOY_NINTENDO_LOGO, 260)) return false;
|
|
17185
|
+
let checksum = 0;
|
|
17186
|
+
for (let index = 308; index <= 332; index += 1) {
|
|
17187
|
+
checksum = checksum - (bytes[index] ?? 0) - 1 & 255;
|
|
17188
|
+
}
|
|
17189
|
+
return checksum === bytes[333];
|
|
17190
|
+
}
|
|
17191
|
+
function sniffSafeTextMime(bytes) {
|
|
17192
|
+
let text;
|
|
17193
|
+
try {
|
|
17194
|
+
text = UTF8_DECODER.decode(bytes);
|
|
17195
|
+
} catch {
|
|
17196
|
+
return null;
|
|
17197
|
+
}
|
|
17198
|
+
if (/\u0000/u.test(text)) return null;
|
|
17199
|
+
const normalized = text.trimStart().toLowerCase();
|
|
17200
|
+
if (normalized.startsWith("<svg") || normalized.startsWith("<?xml") || normalized.startsWith("<!doctype html") || normalized.startsWith("<html") || normalized.startsWith("<script")) return null;
|
|
17201
|
+
if (normalized.startsWith("{") || normalized.startsWith("[")) {
|
|
17202
|
+
try {
|
|
17203
|
+
JSON.parse(text);
|
|
17204
|
+
return "application/json";
|
|
17205
|
+
} catch {
|
|
17206
|
+
return "text/plain";
|
|
17207
|
+
}
|
|
17208
|
+
}
|
|
17209
|
+
return "text/plain";
|
|
17210
|
+
}
|
|
17211
|
+
|
|
17212
|
+
// src/browser-blossom-events.ts
|
|
17213
|
+
var BLOSSOM_SERVER_LIST_KIND2 = 10063;
|
|
17214
|
+
var HEX_64 = /^[0-9a-f]{64}$/i;
|
|
17215
|
+
var BLOSSOM_REFERENCE = /^blossom:(?:sha256:)?([0-9a-f]{64})(?:\.[0-9a-z]+)?(?:\?([^#]*))?$/i;
|
|
17216
|
+
var DEFAULT_SERVER_LIST_TTL_MS = 5 * 6e4;
|
|
17217
|
+
var MAX_RESOURCES_PER_WINDOW = 256;
|
|
17218
|
+
var MAX_CONTEXTS_PER_RESOURCE = 8;
|
|
17219
|
+
function createPajaBlossomEventResolver(options) {
|
|
17220
|
+
const now2 = options.now ?? Date.now;
|
|
17221
|
+
const ttl = Math.max(0, options.serverListTtlMs ?? DEFAULT_SERVER_LIST_TTL_MS);
|
|
17222
|
+
const windows = /* @__PURE__ */ new Map();
|
|
17223
|
+
const serverLists = /* @__PURE__ */ new Map();
|
|
17224
|
+
const pendingServerLists = /* @__PURE__ */ new Map();
|
|
17225
|
+
function observe(windowId, results) {
|
|
17226
|
+
if (!windowId) return;
|
|
17227
|
+
let resources = windows.get(windowId);
|
|
17228
|
+
if (!resources) {
|
|
17229
|
+
resources = /* @__PURE__ */ new Map();
|
|
17230
|
+
windows.set(windowId, resources);
|
|
17231
|
+
}
|
|
17232
|
+
for (const result2 of results) {
|
|
17233
|
+
const publisher = normalizePubkey(result2.event.pubkey);
|
|
17234
|
+
if (!publisher) continue;
|
|
17235
|
+
for (const location of extractEventResourceLocations(result2.event)) {
|
|
17236
|
+
if (!resources.has(location.url) && resources.size >= MAX_RESOURCES_PER_WINDOW) {
|
|
17237
|
+
const oldest = resources.keys().next().value;
|
|
17238
|
+
if (oldest) resources.delete(oldest);
|
|
17332
17239
|
}
|
|
17333
|
-
|
|
17334
|
-
|
|
17335
|
-
|
|
17336
|
-
|
|
17337
|
-
|
|
17338
|
-
|
|
17339
|
-
|
|
17340
|
-
|
|
17341
|
-
|
|
17342
|
-
|
|
17343
|
-
|
|
17344
|
-
|
|
17240
|
+
const contexts = resources.get(location.url) ?? [];
|
|
17241
|
+
const next = {
|
|
17242
|
+
...location,
|
|
17243
|
+
eventId: result2.event.id,
|
|
17244
|
+
publisher,
|
|
17245
|
+
createdAt: result2.event.created_at
|
|
17246
|
+
};
|
|
17247
|
+
const merged = [next, ...contexts.filter((context) => context.eventId !== next.eventId)].sort(compareResourceContext).slice(0, MAX_CONTEXTS_PER_RESOURCE);
|
|
17248
|
+
resources.set(location.url, merged);
|
|
17249
|
+
}
|
|
17250
|
+
}
|
|
17251
|
+
}
|
|
17252
|
+
function decorate(router, windowId) {
|
|
17253
|
+
const getEvent = router.getEvent ? async (eventId, eventOptions) => {
|
|
17254
|
+
const result2 = await router.getEvent(eventId, eventOptions);
|
|
17255
|
+
if (result2.result) observe(windowId, [result2.result]);
|
|
17256
|
+
return result2;
|
|
17257
|
+
} : void 0;
|
|
17258
|
+
return {
|
|
17259
|
+
...getEvent ? { getEvent } : {},
|
|
17260
|
+
async query(filters, queryOptions) {
|
|
17261
|
+
const result2 = await router.query(filters, queryOptions);
|
|
17262
|
+
observe(windowId, result2.events);
|
|
17263
|
+
return result2;
|
|
17264
|
+
},
|
|
17265
|
+
subscribe(filters, subscribeOptions, sink) {
|
|
17266
|
+
const observingSink = {
|
|
17267
|
+
event(result2) {
|
|
17268
|
+
observe(windowId, [result2]);
|
|
17269
|
+
sink.event(result2);
|
|
17270
|
+
},
|
|
17271
|
+
closed: (reason) => sink.closed(reason)
|
|
17272
|
+
};
|
|
17273
|
+
return router.subscribe(filters, subscribeOptions, observingSink);
|
|
17274
|
+
},
|
|
17275
|
+
publish: router.publish.bind(router),
|
|
17276
|
+
resolveRelays: router.resolveRelays.bind(router)
|
|
17277
|
+
};
|
|
17278
|
+
}
|
|
17279
|
+
async function getServers(url, windowId) {
|
|
17280
|
+
const canonical = parseBlossomReference(url)?.url;
|
|
17281
|
+
const contexts = canonical && windowId ? windows.get(windowId)?.get(canonical) ?? [] : [];
|
|
17282
|
+
const servers = [];
|
|
17283
|
+
for (const context of contexts) appendUnique(servers, context.servers);
|
|
17284
|
+
if (servers.length < PAJA_RESOURCE_MAX_SERVERS) {
|
|
17285
|
+
const hintedAuthors = [];
|
|
17286
|
+
for (const context of contexts) appendUnique(hintedAuthors, context.authors);
|
|
17287
|
+
const authors = [...hintedAuthors];
|
|
17288
|
+
for (const context of contexts) appendUniqueWithoutLimit(authors, [context.publisher]);
|
|
17289
|
+
const defaultAuthors = [];
|
|
17290
|
+
for (const author of options.getDefaultAuthors?.() ?? []) {
|
|
17291
|
+
const normalized = normalizePubkey(author);
|
|
17292
|
+
if (normalized) appendUnique(defaultAuthors, [normalized]);
|
|
17293
|
+
}
|
|
17294
|
+
appendUniqueWithoutLimit(authors, defaultAuthors);
|
|
17295
|
+
const lists = await Promise.all(
|
|
17296
|
+
authors.map((author) => discoverServerList(author))
|
|
17297
|
+
);
|
|
17298
|
+
for (const list of lists) appendUnique(servers, list);
|
|
17299
|
+
}
|
|
17300
|
+
appendUnique(servers, options.getConfiguredServers());
|
|
17301
|
+
return servers.slice(0, PAJA_RESOURCE_MAX_SERVERS);
|
|
17302
|
+
}
|
|
17303
|
+
async function discoverServerList(pubkey2) {
|
|
17304
|
+
const cached = serverLists.get(pubkey2);
|
|
17305
|
+
if (cached && cached.expiresAt > now2()) return [...cached.servers];
|
|
17306
|
+
const pending = pendingServerLists.get(pubkey2);
|
|
17307
|
+
if (pending) return [...await pending];
|
|
17308
|
+
const lookup = loadServerList(pubkey2);
|
|
17309
|
+
pendingServerLists.set(pubkey2, lookup);
|
|
17310
|
+
try {
|
|
17311
|
+
return [...await lookup];
|
|
17312
|
+
} finally {
|
|
17313
|
+
if (pendingServerLists.get(pubkey2) === lookup) pendingServerLists.delete(pubkey2);
|
|
17314
|
+
}
|
|
17315
|
+
}
|
|
17316
|
+
async function loadServerList(pubkey2) {
|
|
17317
|
+
let result2;
|
|
17318
|
+
try {
|
|
17319
|
+
result2 = await options.baseRouter.query(
|
|
17320
|
+
[{ kinds: [BLOSSOM_SERVER_LIST_KIND2], authors: [pubkey2], limit: 1 }],
|
|
17321
|
+
{ authors: [pubkey2], limit: 1 }
|
|
17322
|
+
);
|
|
17323
|
+
} catch {
|
|
17324
|
+
return [];
|
|
17325
|
+
}
|
|
17326
|
+
const newest = result2.events.map((entry) => entry.event).filter((event) => event.kind === BLOSSOM_SERVER_LIST_KIND2 && normalizePubkey(event.pubkey) === pubkey2).sort(compareNewestEvent)[0];
|
|
17327
|
+
if (!newest) {
|
|
17328
|
+
if (!result2.error && !result2.incomplete) {
|
|
17329
|
+
serverLists.set(pubkey2, { expiresAt: now2() + ttl, servers: [] });
|
|
17345
17330
|
}
|
|
17331
|
+
return [];
|
|
17346
17332
|
}
|
|
17333
|
+
const servers = newest.tags.flatMap((tag) => {
|
|
17334
|
+
if (tag[0] !== "server" || typeof tag[1] !== "string") return [];
|
|
17335
|
+
const server = normalizePublicBlossomServer(tag[1]);
|
|
17336
|
+
return server ? [server] : [];
|
|
17337
|
+
});
|
|
17338
|
+
const deduplicated = [];
|
|
17339
|
+
appendUnique(deduplicated, servers);
|
|
17340
|
+
serverLists.set(pubkey2, { expiresAt: now2() + ttl, servers: deduplicated });
|
|
17341
|
+
return deduplicated;
|
|
17342
|
+
}
|
|
17343
|
+
return {
|
|
17344
|
+
observe,
|
|
17345
|
+
decorate,
|
|
17346
|
+
getServers,
|
|
17347
|
+
clearWindow: (windowId) => windows.delete(windowId)
|
|
17347
17348
|
};
|
|
17348
17349
|
}
|
|
17349
|
-
|
|
17350
|
-
|
|
17351
|
-
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
|
|
17355
|
-
|
|
17356
|
-
|
|
17357
|
-
|
|
17358
|
-
|
|
17359
|
-
|
|
17360
|
-
|
|
17361
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
17365
|
-
|
|
17366
|
-
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
|
|
17384
|
-
|
|
17350
|
+
function extractEventResourceLocations(event) {
|
|
17351
|
+
const locations = /* @__PURE__ */ new Map();
|
|
17352
|
+
const globalServers = event.tags.flatMap((tag) => {
|
|
17353
|
+
if (tag[0] !== "server" || typeof tag[1] !== "string") return [];
|
|
17354
|
+
const server = normalizeEventServer(tag[1]);
|
|
17355
|
+
return server ? [server] : [];
|
|
17356
|
+
});
|
|
17357
|
+
const add = (value, servers = [], authors = []) => {
|
|
17358
|
+
if (!value) return;
|
|
17359
|
+
const parsed = parseBlossomReference(value);
|
|
17360
|
+
if (!parsed) return;
|
|
17361
|
+
const existing = locations.get(parsed.url) ?? {
|
|
17362
|
+
url: parsed.url,
|
|
17363
|
+
servers: [],
|
|
17364
|
+
authors: []
|
|
17365
|
+
};
|
|
17366
|
+
appendUnique(existing.servers, [
|
|
17367
|
+
...parsed.servers,
|
|
17368
|
+
...servers.flatMap((server) => {
|
|
17369
|
+
const normalized = normalizeEventServer(server);
|
|
17370
|
+
return normalized ? [normalized] : [];
|
|
17371
|
+
}),
|
|
17372
|
+
...globalServers
|
|
17373
|
+
]);
|
|
17374
|
+
appendUnique(existing.authors, [
|
|
17375
|
+
...parsed.authors,
|
|
17376
|
+
...authors.flatMap((author) => {
|
|
17377
|
+
const normalized = normalizePubkey(author);
|
|
17378
|
+
return normalized ? [normalized] : [];
|
|
17379
|
+
})
|
|
17380
|
+
]);
|
|
17381
|
+
locations.set(parsed.url, existing);
|
|
17382
|
+
};
|
|
17383
|
+
for (const tag of event.tags) {
|
|
17384
|
+
if ((tag[0] === "resource" || tag[0] === "r") && typeof tag[1] === "string") {
|
|
17385
|
+
add(tag[1]);
|
|
17386
|
+
} else if (tag[0] === "blossom" && typeof tag[1] === "string") {
|
|
17387
|
+
add(HEX_64.test(tag[1]) ? `blossom:sha256:${tag[1]}` : tag[1]);
|
|
17388
|
+
}
|
|
17389
|
+
}
|
|
17390
|
+
const content = parseEventContent(event.content);
|
|
17391
|
+
if (content) {
|
|
17392
|
+
const contentServers = stringList2(content.servers);
|
|
17393
|
+
for (const key of ["sources", "resources"]) {
|
|
17394
|
+
const entries = content[key];
|
|
17395
|
+
if (!Array.isArray(entries)) continue;
|
|
17396
|
+
for (const entry of entries) {
|
|
17397
|
+
if (typeof entry === "string") {
|
|
17398
|
+
add(entry, contentServers);
|
|
17399
|
+
continue;
|
|
17400
|
+
}
|
|
17401
|
+
if (!isRecord5(entry)) continue;
|
|
17402
|
+
const value = stringValue(entry.uri) ?? stringValue(entry.url) ?? stringValue(entry.resource) ?? (isBlossomRecord(entry) && isHash(entry.sha256) ? `blossom:sha256:${String(entry.sha256)}` : void 0);
|
|
17403
|
+
add(value, [...contentServers, ...stringList2(entry.servers)], stringList2(entry.authors));
|
|
17404
|
+
}
|
|
17405
|
+
}
|
|
17385
17406
|
}
|
|
17386
|
-
];
|
|
17387
|
-
function publicSupport() {
|
|
17388
|
-
return SUPPORTED_LISTS.map(({ tagByItemType: _tagByItemType, ...support }) => ({
|
|
17389
|
-
...support,
|
|
17390
|
-
supportedItemTypes: support.supportedItemTypes ? [...support.supportedItemTypes] : void 0
|
|
17391
|
-
}));
|
|
17407
|
+
return [...locations.values()];
|
|
17392
17408
|
}
|
|
17393
|
-
function
|
|
17394
|
-
|
|
17409
|
+
function parseBlossomReference(value) {
|
|
17410
|
+
const match = BLOSSOM_REFERENCE.exec(value.trim());
|
|
17411
|
+
if (!match?.[1]) return null;
|
|
17412
|
+
const params = new URLSearchParams(match[2] ?? "");
|
|
17413
|
+
const servers = params.getAll("xs").flatMap((server) => {
|
|
17414
|
+
const normalized = normalizeEventServer(server);
|
|
17415
|
+
return normalized ? [normalized] : [];
|
|
17416
|
+
});
|
|
17417
|
+
const authors = params.getAll("as").flatMap((author) => {
|
|
17418
|
+
const normalized = normalizePubkey(author);
|
|
17419
|
+
return normalized ? [normalized] : [];
|
|
17420
|
+
});
|
|
17421
|
+
return {
|
|
17422
|
+
url: `blossom:sha256:${match[1].toLowerCase()}`,
|
|
17423
|
+
servers,
|
|
17424
|
+
authors
|
|
17425
|
+
};
|
|
17395
17426
|
}
|
|
17396
|
-
function
|
|
17397
|
-
const
|
|
17398
|
-
|
|
17399
|
-
const
|
|
17400
|
-
if (
|
|
17401
|
-
|
|
17402
|
-
if (candidates.length === 0) return failure2("unsupported-list", "list kind or type is not supported");
|
|
17403
|
-
if (candidates.length > 1) return failure2("ambiguous-list", "list type maps to multiple supported kinds");
|
|
17404
|
-
const support = candidates[0];
|
|
17405
|
-
const identifier = typeof input.identifier === "string" && input.identifier.length > 0 ? input.identifier : void 0;
|
|
17406
|
-
if (support.addressable && !identifier) return failure2("missing-identifier", "addressable lists require an identifier");
|
|
17407
|
-
if (!support.addressable && identifier) return failure2("invalid-list-ref", "replaceable list does not accept an identifier");
|
|
17408
|
-
return { support, ...identifier ? { identifier } : {} };
|
|
17427
|
+
function normalizeEventServer(value) {
|
|
17428
|
+
const normalized = normalizePublicBlossomServer(value);
|
|
17429
|
+
if (normalized) return normalized;
|
|
17430
|
+
const trimmed = value.trim();
|
|
17431
|
+
if (!/^[a-z0-9.-]+(?::\d+)?$/i.test(trimmed)) return null;
|
|
17432
|
+
return normalizePublicBlossomServer(`https://${trimmed}`);
|
|
17409
17433
|
}
|
|
17410
|
-
function
|
|
17434
|
+
function parseEventContent(value) {
|
|
17435
|
+
if (!value.startsWith("{") || value.length > 256 * 1024) return null;
|
|
17411
17436
|
try {
|
|
17412
|
-
const parsed =
|
|
17413
|
-
return parsed
|
|
17437
|
+
const parsed = JSON.parse(value);
|
|
17438
|
+
return isRecord5(parsed) ? parsed : null;
|
|
17414
17439
|
} catch {
|
|
17415
17440
|
return null;
|
|
17416
17441
|
}
|
|
17417
17442
|
}
|
|
17418
|
-
function
|
|
17419
|
-
if (
|
|
17420
|
-
|
|
17421
|
-
|
|
17422
|
-
|
|
17423
|
-
if (
|
|
17424
|
-
|
|
17425
|
-
|
|
17443
|
+
function compareResourceContext(left, right) {
|
|
17444
|
+
if (left.createdAt !== right.createdAt) return right.createdAt - left.createdAt;
|
|
17445
|
+
return left.eventId.localeCompare(right.eventId);
|
|
17446
|
+
}
|
|
17447
|
+
function compareNewestEvent(left, right) {
|
|
17448
|
+
if (left.created_at !== right.created_at) return right.created_at - left.created_at;
|
|
17449
|
+
return left.id.localeCompare(right.id);
|
|
17450
|
+
}
|
|
17451
|
+
function normalizePubkey(value) {
|
|
17452
|
+
return typeof value === "string" && HEX_64.test(value) ? value.toLowerCase() : null;
|
|
17453
|
+
}
|
|
17454
|
+
function appendUnique(target, values) {
|
|
17455
|
+
for (const value of values) {
|
|
17456
|
+
if (!target.includes(value)) target.push(value);
|
|
17457
|
+
if (target.length >= PAJA_RESOURCE_MAX_SERVERS) return;
|
|
17426
17458
|
}
|
|
17427
|
-
|
|
17428
|
-
|
|
17459
|
+
}
|
|
17460
|
+
function appendUniqueWithoutLimit(target, values) {
|
|
17461
|
+
for (const value of values) {
|
|
17462
|
+
if (!target.includes(value)) target.push(value);
|
|
17429
17463
|
}
|
|
17430
|
-
|
|
17431
|
-
|
|
17432
|
-
|
|
17433
|
-
|
|
17434
|
-
|
|
17435
|
-
|
|
17464
|
+
}
|
|
17465
|
+
function stringList2(value) {
|
|
17466
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
17467
|
+
}
|
|
17468
|
+
function stringValue(value) {
|
|
17469
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
17470
|
+
}
|
|
17471
|
+
function isHash(value) {
|
|
17472
|
+
return typeof value === "string" && HEX_64.test(value);
|
|
17473
|
+
}
|
|
17474
|
+
function isBlossomRecord(value) {
|
|
17475
|
+
return value.type === "blossom" || value.kind === "blossom";
|
|
17476
|
+
}
|
|
17477
|
+
function isRecord5(value) {
|
|
17478
|
+
return typeof value === "object" && value !== null;
|
|
17479
|
+
}
|
|
17480
|
+
|
|
17481
|
+
// ../nip/dist/chunk-PSENX3MS.js
|
|
17482
|
+
function parseNip65RelayList(event) {
|
|
17483
|
+
return event.tags.flatMap((tag) => {
|
|
17484
|
+
if (tag[0] !== "r" || typeof tag[1] !== "string" || tag[1].length === 0) return [];
|
|
17485
|
+
const marker = tag[2];
|
|
17486
|
+
const read = marker !== "write";
|
|
17487
|
+
const write = marker !== "read";
|
|
17488
|
+
return [{ url: tag[1], read, write }];
|
|
17489
|
+
});
|
|
17490
|
+
}
|
|
17491
|
+
function selectWriteRelays(entries) {
|
|
17492
|
+
return dedupe(entries.filter((e) => e.write).map((e) => e.url));
|
|
17493
|
+
}
|
|
17494
|
+
function selectReadRelays(entries) {
|
|
17495
|
+
return dedupe(entries.filter((e) => e.read).map((e) => e.url));
|
|
17496
|
+
}
|
|
17497
|
+
function createNip65Registry() {
|
|
17498
|
+
const lists = /* @__PURE__ */ new Map();
|
|
17499
|
+
function setRelayList(pubkey2, entries) {
|
|
17500
|
+
lists.set(pubkey2, entries.map((e) => ({ ...e })));
|
|
17501
|
+
}
|
|
17502
|
+
function ingest(event) {
|
|
17503
|
+
const entries = parseNip65RelayList(event);
|
|
17504
|
+
setRelayList(event.pubkey, entries);
|
|
17505
|
+
return entries;
|
|
17506
|
+
}
|
|
17507
|
+
function getRelayList(pubkey2) {
|
|
17508
|
+
const entries = lists.get(pubkey2);
|
|
17509
|
+
return entries ? entries.map((e) => ({ ...e })) : void 0;
|
|
17510
|
+
}
|
|
17511
|
+
function resolve(pubkeys, want) {
|
|
17512
|
+
const urls = [];
|
|
17513
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17514
|
+
for (const pubkey2 of pubkeys) {
|
|
17515
|
+
const entries = lists.get(pubkey2);
|
|
17516
|
+
if (!entries) continue;
|
|
17517
|
+
for (const entry of entries) {
|
|
17518
|
+
if (entry[want] && !seen.has(entry.url)) {
|
|
17519
|
+
seen.add(entry.url);
|
|
17520
|
+
urls.push(entry.url);
|
|
17521
|
+
}
|
|
17522
|
+
}
|
|
17436
17523
|
}
|
|
17437
|
-
return
|
|
17524
|
+
return urls;
|
|
17525
|
+
}
|
|
17526
|
+
return {
|
|
17527
|
+
ingest,
|
|
17528
|
+
setRelayList,
|
|
17529
|
+
getRelayList,
|
|
17530
|
+
has: (pubkey2) => lists.has(pubkey2),
|
|
17531
|
+
resolveOutboxRelays: (pubkeys) => resolve(pubkeys, "write"),
|
|
17532
|
+
resolveReadRelays: (pubkeys) => resolve(pubkeys, "read"),
|
|
17533
|
+
delete: (pubkey2) => lists.delete(pubkey2),
|
|
17534
|
+
clear: () => lists.clear()
|
|
17535
|
+
};
|
|
17536
|
+
}
|
|
17537
|
+
function dedupe(urls) {
|
|
17538
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17539
|
+
const out = [];
|
|
17540
|
+
for (const url of urls) {
|
|
17541
|
+
if (!seen.has(url)) {
|
|
17542
|
+
seen.add(url);
|
|
17543
|
+
out.push(url);
|
|
17544
|
+
}
|
|
17545
|
+
}
|
|
17546
|
+
return out;
|
|
17547
|
+
}
|
|
17548
|
+
|
|
17549
|
+
// src/browser-relay-runtime.ts
|
|
17550
|
+
var PAJA_NIP65_RELAY_LIST_KIND = 10002;
|
|
17551
|
+
var PAJA_CONTACT_LIST_KIND = 3;
|
|
17552
|
+
var PAJA_WEBRTC_SIGNAL_KIND = 25050;
|
|
17553
|
+
var NIP17_GIFT_WRAP_KIND2 = 1059;
|
|
17554
|
+
var PAJA_CONTACT_LIST_CANDIDATE_LIMIT = 64;
|
|
17555
|
+
var PAJA_LIVE_QUERY_WAIT_MS = 4e3;
|
|
17556
|
+
var contactListCandidateQueries = /* @__PURE__ */ new WeakMap();
|
|
17557
|
+
function matchesFilter2(event, filter) {
|
|
17558
|
+
const ids = filter.ids;
|
|
17559
|
+
if (ids && !ids.some((id) => event.id === id || event.id.startsWith(id))) return false;
|
|
17560
|
+
const authors = filter.authors;
|
|
17561
|
+
if (authors && !authors.some((author) => event.pubkey === author || event.pubkey.startsWith(author))) return false;
|
|
17562
|
+
const kinds = filter.kinds;
|
|
17563
|
+
if (kinds && !kinds.includes(event.kind)) return false;
|
|
17564
|
+
if (typeof filter.since === "number" && event.created_at < filter.since) return false;
|
|
17565
|
+
if (typeof filter.until === "number" && event.created_at > filter.until) return false;
|
|
17566
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
17567
|
+
if (!key.startsWith("#") || !Array.isArray(value)) continue;
|
|
17568
|
+
const tagName = key.slice(1);
|
|
17569
|
+
const allowed2 = value.filter((item) => typeof item === "string");
|
|
17570
|
+
if (allowed2.length === 0) continue;
|
|
17571
|
+
if (!event.tags.some((tag) => tag[0] === tagName && typeof tag[1] === "string" && allowed2.includes(tag[1]))) return false;
|
|
17438
17572
|
}
|
|
17439
|
-
const tag = [tagName, value];
|
|
17440
|
-
if (item.relay) tag.push(item.relay);
|
|
17441
|
-
if (item.label) tag.push(item.label);
|
|
17442
|
-
return tag;
|
|
17443
|
-
}
|
|
17444
|
-
function isFailure(value) {
|
|
17445
|
-
return !Array.isArray(value);
|
|
17446
|
-
}
|
|
17447
|
-
function sameItem(tag, expected) {
|
|
17448
|
-
if (tag[0] !== expected[0] || tag[1] !== expected[1]) return false;
|
|
17449
|
-
if (expected.length > 2 && tag[2] !== expected[2]) return false;
|
|
17450
|
-
if (expected.length > 3 && tag[3] !== expected[3]) return false;
|
|
17451
17573
|
return true;
|
|
17452
17574
|
}
|
|
17453
|
-
function
|
|
17454
|
-
|
|
17455
|
-
if (tag[2] === "write") return /* @__PURE__ */ new Set(["write"]);
|
|
17456
|
-
return /* @__PURE__ */ new Set(["read", "write"]);
|
|
17457
|
-
}
|
|
17458
|
-
function relayTag(url, coverage) {
|
|
17459
|
-
if (coverage.size === 0) return null;
|
|
17460
|
-
if (coverage.size === 2) return ["r", url];
|
|
17461
|
-
return ["r", url, coverage.has("read") ? "read" : "write"];
|
|
17462
|
-
}
|
|
17463
|
-
function sameRelay(tag, url) {
|
|
17464
|
-
return tag[0] === "r" && typeof tag[1] === "string" && relayUrl(tag[1]) === url;
|
|
17575
|
+
function matchesAnyFilter2(event, filters) {
|
|
17576
|
+
return filters.length === 0 || filters.some((filter) => matchesFilter2(event, filter));
|
|
17465
17577
|
}
|
|
17466
|
-
function
|
|
17467
|
-
|
|
17468
|
-
const matching = tags.filter((tag) => sameRelay(tag, url));
|
|
17469
|
-
const coverage = /* @__PURE__ */ new Set();
|
|
17470
|
-
for (const tag of matching) for (const direction of relayCoverage(tag)) coverage.add(direction);
|
|
17471
|
-
const requested = relayCoverage(expected);
|
|
17472
|
-
const before = coverage.size;
|
|
17473
|
-
for (const direction of requested) coverage.add(direction);
|
|
17474
|
-
if (coverage.size === before) return { tags, changed: false };
|
|
17475
|
-
const retained = tags.filter((tag) => !sameRelay(tag, url));
|
|
17476
|
-
retained.push(relayTag(url, coverage));
|
|
17477
|
-
return { tags: retained, changed: true };
|
|
17578
|
+
function normalizedFilters(filters) {
|
|
17579
|
+
return Array.isArray(filters) ? filters : [filters];
|
|
17478
17580
|
}
|
|
17479
|
-
function
|
|
17480
|
-
const
|
|
17481
|
-
|
|
17482
|
-
|
|
17483
|
-
const retained = tags.filter((tag) => !sameRelay(tag, url));
|
|
17484
|
-
if (expected.length === 2) return { tags: retained, changed: true };
|
|
17485
|
-
const coverage = /* @__PURE__ */ new Set();
|
|
17486
|
-
for (const tag of matching) for (const direction of relayCoverage(tag)) coverage.add(direction);
|
|
17487
|
-
coverage.delete(expected[2]);
|
|
17488
|
-
const rewritten = relayTag(url, coverage);
|
|
17489
|
-
if (rewritten) retained.push(rewritten);
|
|
17490
|
-
return { tags: retained, changed: true };
|
|
17581
|
+
function applyFilterLimit(events, filter) {
|
|
17582
|
+
const limit = typeof filter.limit === "number" && filter.limit >= 0 ? filter.limit : void 0;
|
|
17583
|
+
if (limit === void 0) return events;
|
|
17584
|
+
return [...events].sort((a, b) => b.created_at - a.created_at).slice(0, limit);
|
|
17491
17585
|
}
|
|
17492
|
-
function
|
|
17493
|
-
const
|
|
17494
|
-
|
|
17495
|
-
|
|
17496
|
-
|
|
17497
|
-
|
|
17498
|
-
|
|
17586
|
+
function collectMemoryEvents(events, filters) {
|
|
17587
|
+
const out = /* @__PURE__ */ new Map();
|
|
17588
|
+
const activeFilters = filters.length > 0 ? filters : [{}];
|
|
17589
|
+
for (const filter of activeFilters) {
|
|
17590
|
+
for (const event of applyFilterLimit(events.filter((candidate) => matchesFilter2(candidate, filter)), filter)) {
|
|
17591
|
+
out.set(event.id, event);
|
|
17592
|
+
}
|
|
17593
|
+
}
|
|
17594
|
+
return [...out.values()].sort((a, b) => b.created_at - a.created_at);
|
|
17499
17595
|
}
|
|
17500
|
-
function
|
|
17596
|
+
function subscribeLive(pool, relayUrls2, filters, next) {
|
|
17597
|
+
const activeFilters = filters.length > 0 ? filters : [{}];
|
|
17598
|
+
const requests = relayUrls2.flatMap((url) => activeFilters.map((filter) => ({ url, filter })));
|
|
17599
|
+
if (requests.length === 0) {
|
|
17600
|
+
queueMicrotask(() => next("EOSE"));
|
|
17601
|
+
return { unsubscribe() {
|
|
17602
|
+
} };
|
|
17603
|
+
}
|
|
17604
|
+
const sub = pool.subscribeMap(requests, {
|
|
17605
|
+
label: "kehto-paja-runtime",
|
|
17606
|
+
maxWait: PAJA_LIVE_QUERY_WAIT_MS,
|
|
17607
|
+
onevent: (event) => next(event),
|
|
17608
|
+
oneose: () => next("EOSE")
|
|
17609
|
+
});
|
|
17501
17610
|
return {
|
|
17502
|
-
|
|
17503
|
-
|
|
17504
|
-
|
|
17505
|
-
...resolved.identifier ? { ["#d"]: [resolved.identifier] } : {}
|
|
17611
|
+
unsubscribe() {
|
|
17612
|
+
void sub.close("paja unsubscribe");
|
|
17613
|
+
}
|
|
17506
17614
|
};
|
|
17507
17615
|
}
|
|
17508
|
-
function
|
|
17509
|
-
|
|
17616
|
+
function queryLiveBounded(pool, relayUrls2, filter, maxWaitMs, limit, signal) {
|
|
17617
|
+
if (limit === 0 || signal?.aborted) return Promise.resolve([]);
|
|
17618
|
+
return new Promise((resolve) => {
|
|
17619
|
+
const events = [];
|
|
17620
|
+
let settled = false;
|
|
17621
|
+
let closeRequested = false;
|
|
17622
|
+
let closeReason = "paja query limit reached";
|
|
17623
|
+
let subscription;
|
|
17624
|
+
let abort;
|
|
17625
|
+
const finish = () => {
|
|
17626
|
+
if (settled) return;
|
|
17627
|
+
settled = true;
|
|
17628
|
+
if (abort) signal?.removeEventListener("abort", abort);
|
|
17629
|
+
resolve(events);
|
|
17630
|
+
};
|
|
17631
|
+
const close = (reason = closeReason) => {
|
|
17632
|
+
closeRequested = true;
|
|
17633
|
+
closeReason = reason;
|
|
17634
|
+
if (!subscription) return;
|
|
17635
|
+
void Promise.resolve(subscription.close(reason)).catch(() => {
|
|
17636
|
+
});
|
|
17637
|
+
finish();
|
|
17638
|
+
};
|
|
17639
|
+
abort = () => close("paja query aborted");
|
|
17640
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
17641
|
+
subscription = pool.subscribeEose(relayUrls2, filter, {
|
|
17642
|
+
label: "kehto-paja-runtime",
|
|
17643
|
+
maxWait: maxWaitMs,
|
|
17644
|
+
onevent(event) {
|
|
17645
|
+
if (events.length >= limit) return;
|
|
17646
|
+
events.push(event);
|
|
17647
|
+
if (events.length === limit) close();
|
|
17648
|
+
},
|
|
17649
|
+
onclose: finish
|
|
17650
|
+
});
|
|
17651
|
+
if (closeRequested) close();
|
|
17652
|
+
});
|
|
17510
17653
|
}
|
|
17511
|
-
function
|
|
17512
|
-
const
|
|
17513
|
-
|
|
17514
|
-
|
|
17515
|
-
|
|
17516
|
-
|
|
17517
|
-
|
|
17518
|
-
|
|
17519
|
-
|
|
17520
|
-
|
|
17521
|
-
|
|
17522
|
-
|
|
17523
|
-
|
|
17524
|
-
|
|
17525
|
-
|
|
17526
|
-
|
|
17527
|
-
|
|
17528
|
-
|
|
17529
|
-
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
17533
|
-
|
|
17534
|
-
|
|
17535
|
-
|
|
17536
|
-
|
|
17537
|
-
|
|
17538
|
-
|
|
17539
|
-
|
|
17540
|
-
PAJA_LIVE_QUERY_WAIT_MS
|
|
17541
|
-
);
|
|
17542
|
-
current = newestList(events, resolved, active.pubkey);
|
|
17543
|
-
} catch {
|
|
17544
|
-
return failure2("list-unavailable", "current list could not be loaded");
|
|
17545
|
-
}
|
|
17546
|
-
if (!current && !(operation === "add" && mutationOptions?.create)) {
|
|
17547
|
-
return failure2("list-not-found", "list does not exist and creation was not requested");
|
|
17548
|
-
}
|
|
17549
|
-
if (operation === "remove" && current?.content && items.some((item) => item.visibility !== "public")) {
|
|
17550
|
-
return failure2("private-items-unsupported", "cannot safely remove private items without NIP-44 support");
|
|
17551
|
-
}
|
|
17552
|
-
let tags = current ? current.tags.map((tag) => [...tag]) : metadataTags(resolved, mutationOptions);
|
|
17553
|
-
let changed = 0;
|
|
17554
|
-
for (const expected of itemTags) {
|
|
17555
|
-
if (resolved.support.kind === 10002) {
|
|
17556
|
-
const result2 = operation === "add" ? addRelayItem(tags, expected) : removeRelayItem(tags, expected);
|
|
17557
|
-
tags = result2.tags;
|
|
17558
|
-
if (result2.changed) changed += 1;
|
|
17559
|
-
continue;
|
|
17560
|
-
}
|
|
17561
|
-
if (operation === "add") {
|
|
17562
|
-
if (!tags.some((tag) => sameItem(tag, expected))) {
|
|
17563
|
-
tags.push(expected);
|
|
17564
|
-
changed += 1;
|
|
17565
|
-
}
|
|
17566
|
-
} else {
|
|
17567
|
-
const before = tags.length;
|
|
17568
|
-
tags = tags.filter((tag) => !sameItem(tag, expected));
|
|
17569
|
-
if (tags.length < before) changed += 1;
|
|
17570
|
-
}
|
|
17571
|
-
}
|
|
17572
|
-
const skipped = items.length - changed;
|
|
17573
|
-
if (changed === 0) {
|
|
17574
|
-
return {
|
|
17575
|
-
ok: true,
|
|
17576
|
-
...current ? { eventId: current.id, event: { ...current } } : {},
|
|
17577
|
-
[operation === "add" ? "added" : "removed"]: 0,
|
|
17578
|
-
skipped
|
|
17579
|
-
};
|
|
17580
|
-
}
|
|
17654
|
+
async function queryLive(pool, relayUrls2, filters, maxWaitMs = PAJA_LIVE_QUERY_WAIT_MS, collectUntilLimit = false, signal) {
|
|
17655
|
+
const activeFilters = filters.length > 0 ? filters : [{}];
|
|
17656
|
+
const batches = await Promise.all(activeFilters.map((filter) => {
|
|
17657
|
+
const limit = typeof filter.limit === "number" && filter.limit >= 0 ? filter.limit : void 0;
|
|
17658
|
+
if (collectUntilLimit && limit !== void 0) return queryLiveBounded(pool, relayUrls2, filter, maxWaitMs, limit, signal);
|
|
17659
|
+
return pool.querySync(relayUrls2, filter, {
|
|
17660
|
+
label: "kehto-paja-runtime",
|
|
17661
|
+
maxWait: maxWaitMs
|
|
17662
|
+
});
|
|
17663
|
+
}));
|
|
17664
|
+
const out = /* @__PURE__ */ new Map();
|
|
17665
|
+
for (const event of batches.flat()) out.set(event.id, event);
|
|
17666
|
+
return [...out.values()].sort((a, b) => b.created_at - a.created_at);
|
|
17667
|
+
}
|
|
17668
|
+
async function publishLive(pool, relayUrls2, event) {
|
|
17669
|
+
const results = await Promise.allSettled(pool.publish(relayUrls2, event));
|
|
17670
|
+
return Object.fromEntries(relayUrls2.map((relayUrl2, index) => [relayUrl2, results[index]?.status === "fulfilled"]));
|
|
17671
|
+
}
|
|
17672
|
+
async function countLive(pool, relayUrls2, filters) {
|
|
17673
|
+
let failure3 = new Error("count unavailable");
|
|
17674
|
+
for (const relayUrl2 of relayUrls2) {
|
|
17675
|
+
const controller = new AbortController();
|
|
17676
|
+
let timeout;
|
|
17677
|
+
const timedOut = new Promise((_resolve, reject2) => {
|
|
17678
|
+
timeout = setTimeout(() => {
|
|
17679
|
+
controller.abort();
|
|
17680
|
+
reject2(new Error("count timeout"));
|
|
17681
|
+
}, PAJA_LIVE_QUERY_WAIT_MS);
|
|
17682
|
+
});
|
|
17581
17683
|
try {
|
|
17582
|
-
const
|
|
17583
|
-
|
|
17584
|
-
|
|
17585
|
-
|
|
17586
|
-
|
|
17587
|
-
|
|
17588
|
-
|
|
17589
|
-
|
|
17590
|
-
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
eventId: event.id,
|
|
17596
|
-
event: { ...event },
|
|
17597
|
-
[operation === "add" ? "added" : "removed"]: changed,
|
|
17598
|
-
skipped
|
|
17599
|
-
};
|
|
17684
|
+
const relay = await Promise.race([
|
|
17685
|
+
pool.ensureRelay(relayUrl2, {
|
|
17686
|
+
connectionTimeout: PAJA_LIVE_QUERY_WAIT_MS,
|
|
17687
|
+
abort: controller.signal
|
|
17688
|
+
}),
|
|
17689
|
+
timedOut
|
|
17690
|
+
]);
|
|
17691
|
+
const count = await Promise.race([
|
|
17692
|
+
relay.count(filters, { id: null }),
|
|
17693
|
+
timedOut
|
|
17694
|
+
]);
|
|
17695
|
+
if (!Number.isSafeInteger(count) || count < 0) throw new Error("invalid count");
|
|
17696
|
+
return { count, relay: relayUrl2 };
|
|
17600
17697
|
} catch (error) {
|
|
17601
|
-
|
|
17602
|
-
|
|
17698
|
+
failure3 = error;
|
|
17699
|
+
} finally {
|
|
17700
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
17603
17701
|
}
|
|
17702
|
+
}
|
|
17703
|
+
throw failure3;
|
|
17704
|
+
}
|
|
17705
|
+
function createPajaRelayBackend(getSimulation, confirmRequest, livePool = new SimplePool()) {
|
|
17706
|
+
livePool.trackRelays = true;
|
|
17707
|
+
const fixtureEvents = getSimulation().relay.fixtures.flatMap(toNostrEvent);
|
|
17708
|
+
const acceptedMemoryEvents = [];
|
|
17709
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
17710
|
+
const isAvailable = () => getSimulation().relay.mode !== "disabled" && getPajaRelayUrls(getSimulation()).length > 0;
|
|
17711
|
+
const cachedEvents = () => getSimulation().relay.mode === "memory" ? [...fixtureEvents, ...acceptedMemoryEvents] : [];
|
|
17712
|
+
const query = async (relayUrls2, filters, maxWaitMs) => {
|
|
17713
|
+
const cached = collectMemoryEvents(cachedEvents(), filters);
|
|
17714
|
+
if (getSimulation().relay.mode !== "live" || relayUrls2.length === 0) return cached;
|
|
17715
|
+
const liveEvents = await queryLive(livePool, relayUrls2, filters, maxWaitMs);
|
|
17716
|
+
const out = /* @__PURE__ */ new Map();
|
|
17717
|
+
for (const event of [...cached, ...liveEvents]) out.set(event.id, event);
|
|
17718
|
+
return [...out.values()].sort((a, b) => b.created_at - a.created_at);
|
|
17604
17719
|
};
|
|
17605
|
-
|
|
17606
|
-
|
|
17607
|
-
|
|
17608
|
-
|
|
17720
|
+
const queryContactListCandidates = async (relayUrls2, filters, maxWaitMs, signal) => {
|
|
17721
|
+
const cached = collectMemoryEvents(cachedEvents(), filters);
|
|
17722
|
+
if (signal?.aborted || getSimulation().relay.mode !== "live" || relayUrls2.length === 0) return cached;
|
|
17723
|
+
const liveEvents = await queryLive(livePool, relayUrls2, filters, maxWaitMs, true, signal);
|
|
17724
|
+
const out = /* @__PURE__ */ new Map();
|
|
17725
|
+
for (const event of [...cached, ...liveEvents]) out.set(event.id, event);
|
|
17726
|
+
return [...out.values()].sort((a, b) => b.created_at - a.created_at);
|
|
17609
17727
|
};
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
`);
|
|
17616
|
-
lines.sort();
|
|
17617
|
-
return bytesToHex2(sha256(new TextEncoder().encode(lines.join(""))));
|
|
17618
|
-
}
|
|
17619
|
-
function pathEntriesFromTags(tags) {
|
|
17620
|
-
const entries = [];
|
|
17621
|
-
for (const tag of tags) {
|
|
17622
|
-
if (tag[0] === "path" && typeof tag[1] === "string" && typeof tag[2] === "string" && tag[1].length > 0 && tag[2].length > 0) {
|
|
17623
|
-
entries.push({ path: tag[1], sha256: tag[2] });
|
|
17728
|
+
function retainPublishedEvent(event, mode) {
|
|
17729
|
+
if (mode !== "memory") return;
|
|
17730
|
+
acceptedMemoryEvents.push(event);
|
|
17731
|
+
for (const subscriber of subscribers) {
|
|
17732
|
+
if (matchesAnyFilter2(event, subscriber.filters)) subscriber.next(event);
|
|
17624
17733
|
}
|
|
17625
17734
|
}
|
|
17626
|
-
|
|
17627
|
-
|
|
17628
|
-
|
|
17629
|
-
|
|
17630
|
-
|
|
17631
|
-
|
|
17735
|
+
async function attemptPublish(relayUrls2, event) {
|
|
17736
|
+
const simulation = getSimulation();
|
|
17737
|
+
const rejected = Object.fromEntries(relayUrls2.map((url) => [url, false]));
|
|
17738
|
+
if (simulation.relay.mode === "disabled") {
|
|
17739
|
+
return { outcomes: rejected, error: "relay unavailable" };
|
|
17740
|
+
}
|
|
17741
|
+
if (!await confirmRequest({ action: "publish", event })) {
|
|
17742
|
+
return { outcomes: rejected, error: "publish denied" };
|
|
17632
17743
|
}
|
|
17744
|
+
const outcomes = simulation.relay.mode === "live" ? await publishLive(livePool, relayUrls2, event) : Object.fromEntries(relayUrls2.map((url) => [url, true]));
|
|
17745
|
+
if (!Object.values(outcomes).some(Boolean)) {
|
|
17746
|
+
return { outcomes, error: "publish failed" };
|
|
17747
|
+
}
|
|
17748
|
+
retainPublishedEvent(event, simulation.relay.mode);
|
|
17749
|
+
return { outcomes };
|
|
17633
17750
|
}
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17751
|
+
const backend = {
|
|
17752
|
+
subscription(relayUrls2, filtersInput) {
|
|
17753
|
+
return {
|
|
17754
|
+
subscribe(next) {
|
|
17755
|
+
const filters = normalizedFilters(filtersInput);
|
|
17756
|
+
const subscriber = {
|
|
17757
|
+
filters,
|
|
17758
|
+
next: (item) => next(item)
|
|
17759
|
+
};
|
|
17760
|
+
subscribers.add(subscriber);
|
|
17761
|
+
for (const event of collectMemoryEvents(cachedEvents(), filters)) next(event);
|
|
17762
|
+
const liveSub = getSimulation().relay.mode === "live" ? subscribeLive(livePool, relayUrls2, filters, subscriber.next) : null;
|
|
17763
|
+
queueMicrotask(() => {
|
|
17764
|
+
if (subscribers.has(subscriber) && !liveSub) next("EOSE");
|
|
17765
|
+
});
|
|
17766
|
+
return {
|
|
17767
|
+
unsubscribe() {
|
|
17768
|
+
subscribers.delete(subscriber);
|
|
17769
|
+
liveSub?.unsubscribe();
|
|
17770
|
+
}
|
|
17771
|
+
};
|
|
17772
|
+
}
|
|
17773
|
+
};
|
|
17774
|
+
},
|
|
17775
|
+
async publish(relayUrls2, event) {
|
|
17776
|
+
const attempt = await attemptPublish(relayUrls2, event);
|
|
17777
|
+
if (attempt.error) throw new Error(attempt.error);
|
|
17778
|
+
},
|
|
17779
|
+
request(relayUrls2, filtersInput) {
|
|
17780
|
+
return {
|
|
17781
|
+
subscribe(observer) {
|
|
17782
|
+
const filters = normalizedFilters(filtersInput);
|
|
17783
|
+
let active = true;
|
|
17784
|
+
query(relayUrls2, filters).then((matched) => {
|
|
17785
|
+
if (!active) return;
|
|
17786
|
+
for (const event of matched) observer.next(event);
|
|
17787
|
+
observer.complete();
|
|
17788
|
+
}).catch(() => {
|
|
17789
|
+
if (active) observer.error();
|
|
17790
|
+
});
|
|
17791
|
+
return { unsubscribe() {
|
|
17792
|
+
active = false;
|
|
17793
|
+
} };
|
|
17794
|
+
}
|
|
17795
|
+
};
|
|
17796
|
+
},
|
|
17797
|
+
async count(relayUrls2, filters) {
|
|
17798
|
+
if (getSimulation().relay.mode !== "live") throw new Error("count unavailable");
|
|
17799
|
+
return (await countLive(livePool, relayUrls2, normalizedFilters(filters))).count;
|
|
17800
|
+
},
|
|
17801
|
+
async countWithRelay(relayUrls2, filters) {
|
|
17802
|
+
if (getSimulation().relay.mode !== "live") throw new Error("count unavailable");
|
|
17803
|
+
return countLive(livePool, relayUrls2, normalizedFilters(filters));
|
|
17804
|
+
},
|
|
17805
|
+
observedRelayUrls(eventId) {
|
|
17806
|
+
return [...livePool.seenOn?.get(eventId) ?? []].map((relay) => relay.url);
|
|
17807
|
+
},
|
|
17808
|
+
query,
|
|
17809
|
+
async publishToRelays(relayUrls2, event) {
|
|
17810
|
+
return (await attemptPublish(relayUrls2, event)).outcomes;
|
|
17811
|
+
},
|
|
17812
|
+
async publishWebrtcSignal(relayUrls2, event) {
|
|
17813
|
+
if (getSimulation().relay.mode !== "live" || relayUrls2.length === 0 || event.kind !== PAJA_WEBRTC_SIGNAL_KIND || !verifyEvent(event)) {
|
|
17814
|
+
throw new Error("signaling unavailable");
|
|
17815
|
+
}
|
|
17816
|
+
const outcomes = await publishLive(livePool, relayUrls2, event);
|
|
17817
|
+
if (!Object.values(outcomes).some(Boolean)) throw new Error("signaling unavailable");
|
|
17818
|
+
retainPublishedEvent(event, "live");
|
|
17819
|
+
},
|
|
17820
|
+
async publishDmGiftWrap(relayUrls2, event) {
|
|
17821
|
+
if (getSimulation().relay.mode !== "live" || relayUrls2.length === 0 || event.kind !== NIP17_GIFT_WRAP_KIND2 || !verifyEvent(event)) {
|
|
17822
|
+
throw new Error("DM relay publication failed");
|
|
17823
|
+
}
|
|
17824
|
+
const outcomes = await publishLive(livePool, relayUrls2, event);
|
|
17825
|
+
if (!Object.values(outcomes).some(Boolean)) throw new Error("DM relay publication failed");
|
|
17826
|
+
retainPublishedEvent(event, "live");
|
|
17827
|
+
},
|
|
17828
|
+
isAvailable,
|
|
17829
|
+
close() {
|
|
17830
|
+
livePool.destroy();
|
|
17831
|
+
}
|
|
17654
17832
|
};
|
|
17833
|
+
contactListCandidateQueries.set(backend, queryContactListCandidates);
|
|
17834
|
+
return backend;
|
|
17655
17835
|
}
|
|
17656
|
-
function
|
|
17657
|
-
|
|
17658
|
-
|
|
17659
|
-
|
|
17660
|
-
return
|
|
17661
|
-
}
|
|
17662
|
-
function isCoordinateFresh(coordinate, options = {}) {
|
|
17663
|
-
const now2 = options.now ?? Date.now();
|
|
17664
|
-
const ttl = options.background ? coordinate.backgroundTtlMs : coordinate.foregroundTtlMs;
|
|
17665
|
-
return ttl === Number.POSITIVE_INFINITY || now2 - coordinate.lastResolvedAt <= ttl;
|
|
17666
|
-
}
|
|
17667
|
-
function byteLength2(bytes) {
|
|
17668
|
-
return bytes.byteLength;
|
|
17669
|
-
}
|
|
17670
|
-
function encodeKeyPart(value) {
|
|
17671
|
-
return encodeURIComponent(value).replaceAll("%2F", "%252F");
|
|
17672
|
-
}
|
|
17673
|
-
function cloneBytes(bytes) {
|
|
17674
|
-
return new Uint8Array(bytes);
|
|
17675
|
-
}
|
|
17676
|
-
function responseBody(bytes) {
|
|
17677
|
-
return cloneBytes(bytes).buffer;
|
|
17836
|
+
function toNostrEvent(value) {
|
|
17837
|
+
if (typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.pubkey === "string" && typeof value.kind === "number" && Array.isArray(value.tags) && typeof value.content === "string" && typeof value.sig === "string") {
|
|
17838
|
+
return [value];
|
|
17839
|
+
}
|
|
17840
|
+
return [];
|
|
17678
17841
|
}
|
|
17679
|
-
function
|
|
17680
|
-
return
|
|
17842
|
+
function getPajaRelayUrls(simulation) {
|
|
17843
|
+
return simulation.relay.mode === "disabled" ? [] : [...simulation.relay.urls];
|
|
17681
17844
|
}
|
|
17682
|
-
function
|
|
17683
|
-
|
|
17684
|
-
const
|
|
17685
|
-
|
|
17686
|
-
|
|
17845
|
+
function dedupeRelayUrls(urls) {
|
|
17846
|
+
const out = /* @__PURE__ */ new Set();
|
|
17847
|
+
for (const url of urls) {
|
|
17848
|
+
const trimmed = url.trim();
|
|
17849
|
+
if (trimmed.length > 0) out.add(trimmed);
|
|
17850
|
+
}
|
|
17851
|
+
return [...out];
|
|
17687
17852
|
}
|
|
17688
|
-
async function
|
|
17689
|
-
|
|
17853
|
+
async function getSignerRelayUrls(signerProvider, direction) {
|
|
17854
|
+
const signer = signerProvider?.getSigner();
|
|
17855
|
+
if (!signer?.getRelays) return [];
|
|
17856
|
+
try {
|
|
17857
|
+
const relays = await signer.getRelays();
|
|
17858
|
+
return dedupeRelayUrls(Object.entries(relays).flatMap(([url, permissions]) => {
|
|
17859
|
+
if (direction === "read" && permissions.read) return [url];
|
|
17860
|
+
if (direction === "write" && permissions.write) return [url];
|
|
17861
|
+
return [];
|
|
17862
|
+
}));
|
|
17863
|
+
} catch {
|
|
17864
|
+
return [];
|
|
17865
|
+
}
|
|
17690
17866
|
}
|
|
17691
|
-
async function
|
|
17692
|
-
return
|
|
17867
|
+
async function getBootstrapRelayUrls(getSimulation, signerProvider, getHostRelayUrls) {
|
|
17868
|
+
return dedupeRelayUrls([
|
|
17869
|
+
...getHostRelayUrls?.() ?? getPajaRelayUrls(getSimulation()),
|
|
17870
|
+
...await getSignerRelayUrls(signerProvider, "read")
|
|
17871
|
+
]);
|
|
17693
17872
|
}
|
|
17694
|
-
function
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
|
|
17699
|
-
|
|
17873
|
+
function createPajaContactListLoader(backend, getSimulation, signerProvider, getHostRelayUrls) {
|
|
17874
|
+
return async (pubkey2, signal) => {
|
|
17875
|
+
if (!/^[0-9a-fA-F]{64}$/.test(pubkey2) || signal?.aborted) return [];
|
|
17876
|
+
const relayUrls2 = await getBootstrapRelayUrls(getSimulation, signerProvider, getHostRelayUrls);
|
|
17877
|
+
if (signal?.aborted) return [];
|
|
17878
|
+
const filters = [{
|
|
17879
|
+
kinds: [PAJA_CONTACT_LIST_KIND],
|
|
17880
|
+
authors: [pubkey2],
|
|
17881
|
+
limit: PAJA_CONTACT_LIST_CANDIDATE_LIMIT
|
|
17882
|
+
}];
|
|
17883
|
+
const contactQuery = contactListCandidateQueries.get(backend);
|
|
17884
|
+
const candidates = contactQuery ? await contactQuery(relayUrls2, filters, PAJA_LIVE_QUERY_WAIT_MS, signal) : await backend.query(relayUrls2, filters, PAJA_LIVE_QUERY_WAIT_MS);
|
|
17885
|
+
if (signal?.aborted) return [];
|
|
17886
|
+
return candidates.filter((event) => event.kind === PAJA_CONTACT_LIST_KIND && event.pubkey.toLowerCase() === pubkey2.toLowerCase()).sort((left, right) => right.created_at - left.created_at || left.id.localeCompare(right.id)).slice(0, PAJA_CONTACT_LIST_CANDIDATE_LIMIT);
|
|
17887
|
+
};
|
|
17700
17888
|
}
|
|
17701
|
-
function
|
|
17702
|
-
|
|
17703
|
-
const aggregateBytes = Object.values(index.aggregates).reduce((sum, entry) => sum + entry.size, 0);
|
|
17704
|
-
return blobBytes + aggregateBytes;
|
|
17889
|
+
function latestEvent(events, kind, pubkey2) {
|
|
17890
|
+
return events.filter((event) => event.kind === kind && event.pubkey === pubkey2).sort((a, b) => b.created_at - a.created_at)[0];
|
|
17705
17891
|
}
|
|
17706
|
-
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
|
|
17718
|
-
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
|
|
17892
|
+
function createPajaRelayListLoader(backend, getSimulation, signerProvider, getHostRelayUrls) {
|
|
17893
|
+
const registry2 = createNip65Registry();
|
|
17894
|
+
return async (pubkeys) => {
|
|
17895
|
+
const uniquePubkeys = [...new Set(pubkeys.filter((pubkey2) => /^[0-9a-fA-F]{64}$/.test(pubkey2)))];
|
|
17896
|
+
const missing = uniquePubkeys.filter((pubkey2) => !registry2.has(pubkey2));
|
|
17897
|
+
if (missing.length > 0) {
|
|
17898
|
+
const events = await backend.query(await getBootstrapRelayUrls(
|
|
17899
|
+
getSimulation,
|
|
17900
|
+
signerProvider,
|
|
17901
|
+
getHostRelayUrls
|
|
17902
|
+
), [{
|
|
17903
|
+
kinds: [PAJA_NIP65_RELAY_LIST_KIND],
|
|
17904
|
+
authors: missing,
|
|
17905
|
+
limit: Math.max(missing.length * 2, 10)
|
|
17906
|
+
}], PAJA_LIVE_QUERY_WAIT_MS);
|
|
17907
|
+
for (const pubkey2 of missing) {
|
|
17908
|
+
const event = latestEvent(events, PAJA_NIP65_RELAY_LIST_KIND, pubkey2);
|
|
17909
|
+
if (event) registry2.ingest(event);
|
|
17910
|
+
}
|
|
17722
17911
|
}
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
|
|
17730
|
-
|
|
17731
|
-
this.hardCeilingBytes = options.hardCeilingBytes ?? DEFAULT_HARD_CEILING_BYTES;
|
|
17732
|
-
}
|
|
17733
|
-
async readBlob(sha256Hex) {
|
|
17734
|
-
try {
|
|
17735
|
-
const cache2 = await this.open();
|
|
17736
|
-
const response = await cache2.match(this.blobUrl(sha256Hex));
|
|
17737
|
-
if (!response) return void 0;
|
|
17738
|
-
const bytes = await responseBytes(response);
|
|
17739
|
-
const index = await this.readIndex(cache2);
|
|
17740
|
-
const entry = index.blobs[sha256Hex];
|
|
17741
|
-
if (entry) {
|
|
17742
|
-
entry.lastAccessed = this.now();
|
|
17743
|
-
index.updatedAt = entry.lastAccessed;
|
|
17744
|
-
await this.writeIndex(cache2, index);
|
|
17912
|
+
const out = /* @__PURE__ */ new Map();
|
|
17913
|
+
for (const pubkey2 of uniquePubkeys) {
|
|
17914
|
+
const entries = registry2.getRelayList(pubkey2);
|
|
17915
|
+
if (entries) {
|
|
17916
|
+
out.set(pubkey2, {
|
|
17917
|
+
read: selectReadRelays(entries),
|
|
17918
|
+
write: selectWriteRelays(entries)
|
|
17919
|
+
});
|
|
17745
17920
|
}
|
|
17746
|
-
return bytes;
|
|
17747
|
-
} catch (cause) {
|
|
17748
|
-
this.diagnostic({ type: "cache-read-failed", message: `failed to read blob ${sha256Hex}`, cause });
|
|
17749
|
-
return void 0;
|
|
17750
17921
|
}
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
|
|
17755
|
-
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
|
|
17760
|
-
|
|
17761
|
-
|
|
17922
|
+
return out;
|
|
17923
|
+
};
|
|
17924
|
+
}
|
|
17925
|
+
function createPajaOutboxRelayPool(backend) {
|
|
17926
|
+
return {
|
|
17927
|
+
subscribe(filters, relayUrls2, callback) {
|
|
17928
|
+
return backend.subscription(relayUrls2, filters).subscribe((item) => callback(item));
|
|
17929
|
+
},
|
|
17930
|
+
publish(event, relayUrls2) {
|
|
17931
|
+
return backend.publishToRelays(relayUrls2, event);
|
|
17932
|
+
},
|
|
17933
|
+
isAvailable() {
|
|
17934
|
+
return backend.isAvailable();
|
|
17935
|
+
}
|
|
17936
|
+
};
|
|
17937
|
+
}
|
|
17938
|
+
|
|
17939
|
+
// src/browser-social-cache.ts
|
|
17940
|
+
var HEX_PUBKEY = /^[0-9a-f]{64}$/i;
|
|
17941
|
+
var FOLLOW_PARSE_YIELD_EVERY = 64;
|
|
17942
|
+
var PROFILE_WARM_YIELD_EVERY = 64;
|
|
17943
|
+
function createPajaSocialCache(options) {
|
|
17944
|
+
let generation = 0;
|
|
17945
|
+
let activeRefreshController;
|
|
17946
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
17947
|
+
async function loadFollows(capturedPubkey) {
|
|
17948
|
+
const normalizedPubkey = normalizePubkey2(capturedPubkey);
|
|
17949
|
+
if (!normalizedPubkey) return [];
|
|
17950
|
+
const existing = snapshots.get(normalizedPubkey);
|
|
17951
|
+
if (existing) return [...existing.follows];
|
|
17952
|
+
const capturedGeneration = generation;
|
|
17953
|
+
const follows = await verifiedFollows(normalizedPubkey);
|
|
17954
|
+
const warmed = snapshots.get(normalizedPubkey);
|
|
17955
|
+
if (warmed) return [...warmed.follows];
|
|
17956
|
+
if (generation === capturedGeneration) {
|
|
17957
|
+
snapshots.set(normalizedPubkey, { follows, profiles: /* @__PURE__ */ new Map() });
|
|
17762
17958
|
}
|
|
17959
|
+
return [...follows];
|
|
17763
17960
|
}
|
|
17764
|
-
async
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
|
|
17961
|
+
async function verifiedFollows(capturedPubkey, isStillCurrent, signal) {
|
|
17962
|
+
const candidates = await options.loadContactList(capturedPubkey, signal);
|
|
17963
|
+
const verified = [];
|
|
17964
|
+
for (const candidate of candidates) {
|
|
17965
|
+
if (isStillCurrent && !isStillCurrent()) return [];
|
|
17966
|
+
if (!isContactCandidate(candidate, capturedPubkey)) continue;
|
|
17768
17967
|
try {
|
|
17769
|
-
await
|
|
17770
|
-
|
|
17771
|
-
} catch (cause) {
|
|
17772
|
-
this.diagnostic({
|
|
17773
|
-
type: "cache-write-failed",
|
|
17774
|
-
message: "failed to write verified napplet artifacts after prune retry",
|
|
17775
|
-
cause: cause ?? firstCause
|
|
17776
|
-
});
|
|
17968
|
+
if (await options.verifyEvent(candidate)) verified.push(candidate);
|
|
17969
|
+
} catch {
|
|
17777
17970
|
}
|
|
17778
17971
|
}
|
|
17972
|
+
return contactPubkeys(selectReplacement(verified), isStillCurrent);
|
|
17779
17973
|
}
|
|
17780
|
-
async
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
const
|
|
17784
|
-
const
|
|
17785
|
-
|
|
17786
|
-
|
|
17787
|
-
|
|
17788
|
-
|
|
17789
|
-
dTag: input.dTag,
|
|
17790
|
-
aggregateHash: input.aggregateHash,
|
|
17791
|
-
lastResolvedAt: input.lastResolvedAt ?? now2,
|
|
17792
|
-
foregroundTtlMs: input.foregroundTtlMs ?? DEFAULT_FOREGROUND_TTL_MS,
|
|
17793
|
-
backgroundTtlMs: input.backgroundTtlMs ?? DEFAULT_BACKGROUND_TTL_MS
|
|
17794
|
-
};
|
|
17795
|
-
index.updatedAt = now2;
|
|
17796
|
-
await this.writeIndex(cache2, index);
|
|
17797
|
-
}
|
|
17798
|
-
async getCoordinate(input, options = {}) {
|
|
17799
|
-
const cache2 = await this.open();
|
|
17800
|
-
const index = await this.readIndex(cache2);
|
|
17801
|
-
const coordinate = index.coordinates[coordinateKey(input.kind, input.pubkey, input.dTag)];
|
|
17802
|
-
if (!coordinate) return void 0;
|
|
17803
|
-
return { ...coordinate, fresh: isCoordinateFresh(coordinate, options) };
|
|
17804
|
-
}
|
|
17805
|
-
async touchAggregate(dTag, aggregateHash, now2 = this.now()) {
|
|
17806
|
-
const cache2 = await this.open();
|
|
17807
|
-
const index = await this.readIndex(cache2);
|
|
17808
|
-
const key = aggregateKey(dTag, aggregateHash);
|
|
17809
|
-
const aggregate = index.aggregates[key];
|
|
17810
|
-
if (!aggregate) return;
|
|
17811
|
-
aggregate.lastAccessed = now2;
|
|
17812
|
-
for (const hash of aggregate.blobHashes) {
|
|
17813
|
-
if (index.blobs[hash]) index.blobs[hash].lastAccessed = now2;
|
|
17814
|
-
}
|
|
17815
|
-
index.updatedAt = now2;
|
|
17816
|
-
await this.writeIndex(cache2, index);
|
|
17817
|
-
}
|
|
17818
|
-
markAggregateActive(dTag, aggregateHash) {
|
|
17819
|
-
this.activeAggregates.add(aggregateKey(dTag, aggregateHash));
|
|
17820
|
-
}
|
|
17821
|
-
releaseAggregateActive(dTag, aggregateHash) {
|
|
17822
|
-
this.activeAggregates.delete(aggregateKey(dTag, aggregateHash));
|
|
17823
|
-
}
|
|
17824
|
-
async prune(now2 = this.now()) {
|
|
17974
|
+
async function refreshActiveIdentity() {
|
|
17975
|
+
activeRefreshController?.abort();
|
|
17976
|
+
activeRefreshController = void 0;
|
|
17977
|
+
const currentGeneration = ++generation;
|
|
17978
|
+
const capturedPubkey = normalizePubkey2(options.getActivePubkey() ?? "");
|
|
17979
|
+
if (!capturedPubkey) return;
|
|
17980
|
+
const controller = new AbortController();
|
|
17981
|
+
activeRefreshController = controller;
|
|
17982
|
+
const isStillCurrent = () => !controller.signal.aborted && isCurrent(capturedPubkey, currentGeneration);
|
|
17825
17983
|
try {
|
|
17826
|
-
const
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
17836
|
-
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
}
|
|
17841
|
-
const currentAggregateHashes = new Set(
|
|
17842
|
-
Object.values(index.coordinates).map((coordinate) => coordinate.aggregateHash)
|
|
17843
|
-
);
|
|
17844
|
-
const aggregates = Object.values(index.aggregates).filter((entry) => !this.activeAggregates.has(entry.key)).sort((a, b) => {
|
|
17845
|
-
const currentA = currentAggregateHashes.has(a.aggregateHash) ? 1 : 0;
|
|
17846
|
-
const currentB = currentAggregateHashes.has(b.aggregateHash) ? 1 : 0;
|
|
17847
|
-
if (currentA !== currentB) return currentA - currentB;
|
|
17848
|
-
return a.lastAccessed - b.lastAccessed;
|
|
17849
|
-
});
|
|
17850
|
-
for (const aggregate of aggregates) {
|
|
17851
|
-
if (size <= budget) break;
|
|
17852
|
-
await cache2.delete(this.aggregateUrl(aggregate.key));
|
|
17853
|
-
delete index.aggregates[aggregate.key];
|
|
17854
|
-
size -= aggregate.size;
|
|
17855
|
-
this.rewriteRefCounts(index);
|
|
17856
|
-
}
|
|
17857
|
-
if (size > hardBudget) {
|
|
17858
|
-
const active = Object.values(index.aggregates).filter((entry) => this.activeAggregates.has(entry.key)).sort((a, b) => a.lastAccessed - b.lastAccessed);
|
|
17859
|
-
for (const aggregate of active) {
|
|
17860
|
-
if (size <= hardBudget) break;
|
|
17861
|
-
await cache2.delete(this.aggregateUrl(aggregate.key));
|
|
17862
|
-
delete index.aggregates[aggregate.key];
|
|
17863
|
-
size -= aggregate.size;
|
|
17984
|
+
const follows = await verifiedFollows(capturedPubkey, isStillCurrent, controller.signal);
|
|
17985
|
+
if (!isStillCurrent()) return;
|
|
17986
|
+
const snapshot = { follows, profiles: /* @__PURE__ */ new Map() };
|
|
17987
|
+
snapshots.set(capturedPubkey, snapshot);
|
|
17988
|
+
for (let index = 0; index < follows.length; index += 1) {
|
|
17989
|
+
if (!isStillCurrent()) return;
|
|
17990
|
+
const author = follows[index];
|
|
17991
|
+
let warmed;
|
|
17992
|
+
try {
|
|
17993
|
+
warmed = await options.baseRouter.query(
|
|
17994
|
+
[{ kinds: [0], authors: [author], limit: 1 }],
|
|
17995
|
+
{ authors: [author], limit: 1 }
|
|
17996
|
+
);
|
|
17997
|
+
} catch {
|
|
17864
17998
|
}
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17999
|
+
if (!isStillCurrent()) return;
|
|
18000
|
+
if (warmed) {
|
|
18001
|
+
const profile = profileResult(warmed, author);
|
|
18002
|
+
if (profile) snapshot.profiles.set(author, profile);
|
|
18003
|
+
}
|
|
18004
|
+
if ((index + 1) % PROFILE_WARM_YIELD_EVERY === 0 && index + 1 < follows.length) {
|
|
18005
|
+
if (!isStillCurrent()) return;
|
|
18006
|
+
await yieldToEventLoop();
|
|
18007
|
+
if (!isStillCurrent()) return;
|
|
17871
18008
|
}
|
|
17872
18009
|
}
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
this.diagnostic({ type: "cache-prune-failed", message: "failed to prune napplet artifact cache", cause });
|
|
18010
|
+
} catch {
|
|
18011
|
+
} finally {
|
|
18012
|
+
if (activeRefreshController === controller) activeRefreshController = void 0;
|
|
17877
18013
|
}
|
|
17878
18014
|
}
|
|
17879
|
-
|
|
17880
|
-
return
|
|
18015
|
+
function isCurrent(capturedPubkey, currentGeneration) {
|
|
18016
|
+
return generation === currentGeneration && normalizePubkey2(options.getActivePubkey() ?? "") === capturedPubkey;
|
|
17881
18017
|
}
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
17890
|
-
|
|
17891
|
-
|
|
17892
|
-
|
|
17893
|
-
size: byteLength2(bytes),
|
|
17894
|
-
lastAccessed: now2,
|
|
17895
|
-
refCount: index.blobs[pathEntry.sha256]?.refCount ?? 0
|
|
17896
|
-
};
|
|
17897
|
-
}
|
|
17898
|
-
const key = aggregateKey(input.manifest.dTag, input.manifest.aggregateHash);
|
|
17899
|
-
const record = {
|
|
17900
|
-
schemaVersion: SCHEMA_VERSION,
|
|
17901
|
-
dTag: input.manifest.dTag,
|
|
17902
|
-
aggregateHash: input.manifest.aggregateHash,
|
|
17903
|
-
manifest: {
|
|
17904
|
-
kind: input.manifest.kind,
|
|
17905
|
-
pubkey: input.manifest.pubkey,
|
|
17906
|
-
dTag: input.manifest.dTag,
|
|
17907
|
-
paths: input.manifest.paths.map((entry) => ({ path: entry.path, sha256: entry.sha256 })),
|
|
17908
|
-
requires: [...input.manifest.requires],
|
|
17909
|
-
...input.manifest.title === void 0 ? {} : { title: input.manifest.title },
|
|
17910
|
-
...input.manifest.description === void 0 ? {} : { description: input.manifest.description }
|
|
18018
|
+
function decorate(router, canReadIdentity) {
|
|
18019
|
+
return {
|
|
18020
|
+
...router.getEvent ? { getEvent: router.getEvent.bind(router) } : {},
|
|
18021
|
+
async query(filters, queryOptions) {
|
|
18022
|
+
const canAugmentAtStart = canReadIdentity?.() ?? true;
|
|
18023
|
+
const activePubkey = normalizePubkey2(options.getActivePubkey() ?? "");
|
|
18024
|
+
const snapshot = canAugmentAtStart && activePubkey ? snapshots.get(activePubkey) : void 0;
|
|
18025
|
+
const base = await router.query(filters, queryOptions);
|
|
18026
|
+
if (!canAugmentAtStart || !(canReadIdentity?.() ?? true)) return base;
|
|
18027
|
+
const cached = matchingCachedProfiles(snapshot?.profiles, filters);
|
|
18028
|
+
return mergeResult(base, cached, filters, queryOptions);
|
|
17911
18029
|
},
|
|
17912
|
-
|
|
17913
|
-
|
|
17914
|
-
|
|
17915
|
-
};
|
|
17916
|
-
const aggregateBytes = new TextEncoder().encode(JSON.stringify(record));
|
|
17917
|
-
await cache2.put(this.aggregateUrl(key), new Response(responseBody(aggregateBytes), {
|
|
17918
|
-
headers: { "content-type": "application/json" }
|
|
17919
|
-
}));
|
|
17920
|
-
const coordKey = coordinateKey(input.manifest.kind, input.manifest.pubkey, input.manifest.dTag);
|
|
17921
|
-
index.aggregates[key] = {
|
|
17922
|
-
dTag: input.manifest.dTag,
|
|
17923
|
-
aggregateHash: input.manifest.aggregateHash,
|
|
17924
|
-
key,
|
|
17925
|
-
blobHashes,
|
|
17926
|
-
size: byteLength2(aggregateBytes),
|
|
17927
|
-
lastAccessed: now2,
|
|
17928
|
-
coordinateKey: coordKey
|
|
17929
|
-
};
|
|
17930
|
-
index.coordinates[coordKey] = {
|
|
17931
|
-
key: coordKey,
|
|
17932
|
-
kind: input.manifest.kind,
|
|
17933
|
-
pubkey: input.manifest.pubkey,
|
|
17934
|
-
dTag: input.manifest.dTag,
|
|
17935
|
-
aggregateHash: input.manifest.aggregateHash,
|
|
17936
|
-
lastResolvedAt: now2,
|
|
17937
|
-
foregroundTtlMs: DEFAULT_FOREGROUND_TTL_MS,
|
|
17938
|
-
backgroundTtlMs: DEFAULT_BACKGROUND_TTL_MS
|
|
18030
|
+
subscribe: router.subscribe.bind(router),
|
|
18031
|
+
publish: router.publish.bind(router),
|
|
18032
|
+
resolveRelays: router.resolveRelays.bind(router)
|
|
17939
18033
|
};
|
|
17940
|
-
this.rewriteRefCounts(index);
|
|
17941
|
-
index.updatedAt = now2;
|
|
17942
|
-
await this.writeIndex(cache2, index);
|
|
17943
|
-
await this.prune(now2);
|
|
17944
|
-
}
|
|
17945
|
-
async open() {
|
|
17946
|
-
this.cachePromise ??= this.cacheStorage.open(this.cacheName);
|
|
17947
|
-
return this.cachePromise;
|
|
17948
|
-
}
|
|
17949
|
-
indexUrl() {
|
|
17950
|
-
return new URL("__kehto_napplet_cache/index.json", this.baseUrl).toString();
|
|
17951
|
-
}
|
|
17952
|
-
blobUrl(hash) {
|
|
17953
|
-
return new URL(`__kehto_napplet_cache/blob/${encodeKeyPart(hash)}`, this.baseUrl).toString();
|
|
17954
|
-
}
|
|
17955
|
-
aggregateUrl(key) {
|
|
17956
|
-
return new URL(`__kehto_napplet_cache/aggregate/${encodeKeyPart(key)}.json`, this.baseUrl).toString();
|
|
17957
18034
|
}
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
|
|
17965
|
-
|
|
17966
|
-
|
|
18035
|
+
const unsubscribe = options.subscribeSignerChange?.(() => {
|
|
18036
|
+
void refreshActiveIdentity();
|
|
18037
|
+
});
|
|
18038
|
+
return {
|
|
18039
|
+
getFollows: loadFollows,
|
|
18040
|
+
refreshActiveIdentity,
|
|
18041
|
+
decorate,
|
|
18042
|
+
dispose: () => {
|
|
18043
|
+
activeRefreshController?.abort();
|
|
18044
|
+
unsubscribe?.();
|
|
17967
18045
|
}
|
|
17968
|
-
}
|
|
17969
|
-
|
|
17970
|
-
|
|
17971
|
-
|
|
17972
|
-
|
|
17973
|
-
|
|
17974
|
-
|
|
17975
|
-
|
|
17976
|
-
|
|
17977
|
-
|
|
18046
|
+
};
|
|
18047
|
+
}
|
|
18048
|
+
function normalizePubkey2(value) {
|
|
18049
|
+
return typeof value === "string" && HEX_PUBKEY.test(value) ? value.toLowerCase() : null;
|
|
18050
|
+
}
|
|
18051
|
+
function isContactCandidate(candidate, capturedPubkey) {
|
|
18052
|
+
if (typeof candidate !== "object" || candidate === null) return false;
|
|
18053
|
+
const event = candidate;
|
|
18054
|
+
return event.kind === 3 && normalizePubkey2(event.pubkey) === capturedPubkey;
|
|
18055
|
+
}
|
|
18056
|
+
function selectReplacement(events) {
|
|
18057
|
+
return [...events].sort((left, right) => {
|
|
18058
|
+
const timestampOrder = right.created_at - left.created_at;
|
|
18059
|
+
if (timestampOrder !== 0) return timestampOrder;
|
|
18060
|
+
if (left.id === right.id) return 0;
|
|
18061
|
+
return left.id < right.id ? -1 : 1;
|
|
18062
|
+
})[0];
|
|
18063
|
+
}
|
|
18064
|
+
async function contactPubkeys(event, isStillCurrent) {
|
|
18065
|
+
if (!event || !Array.isArray(event.tags)) return [];
|
|
18066
|
+
const follows = /* @__PURE__ */ new Set();
|
|
18067
|
+
for (let index = 0; index < event.tags.length; index += 1) {
|
|
18068
|
+
if (isStillCurrent && !isStillCurrent()) return [];
|
|
18069
|
+
const tag = event.tags[index];
|
|
18070
|
+
if (Array.isArray(tag) && tag[0] === "p") {
|
|
18071
|
+
const pubkey2 = normalizePubkey2(tag[1]);
|
|
18072
|
+
if (pubkey2) follows.add(pubkey2);
|
|
18073
|
+
}
|
|
18074
|
+
if ((index + 1) % FOLLOW_PARSE_YIELD_EVERY === 0 && index + 1 < event.tags.length) {
|
|
18075
|
+
await yieldToEventLoop();
|
|
18076
|
+
if (isStillCurrent && !isStillCurrent()) return [];
|
|
17978
18077
|
}
|
|
17979
18078
|
}
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
|
|
17987
|
-
if (!estimate?.quota) return this.unknownBudgetBytes;
|
|
17988
|
-
return Math.min(this.hardCeilingBytes, Math.floor(estimate.quota * 0.2));
|
|
18079
|
+
return [...follows];
|
|
18080
|
+
}
|
|
18081
|
+
function profileResult(result2, author) {
|
|
18082
|
+
let newest;
|
|
18083
|
+
for (const entry of result2.events) {
|
|
18084
|
+
if (entry.event.kind !== 0 || normalizePubkey2(entry.event.pubkey) !== author) continue;
|
|
18085
|
+
if (!newest || isNewerProfile(entry, newest)) newest = entry;
|
|
17989
18086
|
}
|
|
17990
|
-
|
|
17991
|
-
|
|
17992
|
-
|
|
17993
|
-
|
|
18087
|
+
return newest;
|
|
18088
|
+
}
|
|
18089
|
+
function isNewerProfile(candidate, existing) {
|
|
18090
|
+
if (candidate.event.created_at !== existing.event.created_at) {
|
|
18091
|
+
return candidate.event.created_at > existing.event.created_at;
|
|
17994
18092
|
}
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
|
|
17998
|
-
|
|
17999
|
-
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
18003
|
-
|
|
18004
|
-
|
|
18005
|
-
|
|
18093
|
+
return candidate.event.id < existing.event.id;
|
|
18094
|
+
}
|
|
18095
|
+
function yieldToEventLoop() {
|
|
18096
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
18097
|
+
}
|
|
18098
|
+
function matchingCachedProfiles(profiles, filters) {
|
|
18099
|
+
const readyProfiles = profiles ? [...profiles.values()] : [];
|
|
18100
|
+
if (filters.length === 0) return readyProfiles;
|
|
18101
|
+
const matched = /* @__PURE__ */ new Map();
|
|
18102
|
+
for (const filter of filters) {
|
|
18103
|
+
const limit = typeof filter.limit === "number" && filter.limit >= 0 ? filter.limit : void 0;
|
|
18104
|
+
let count = 0;
|
|
18105
|
+
for (const entry of readyProfiles) {
|
|
18106
|
+
if (!matchesAnyFilter2(entry.event, [filter])) continue;
|
|
18107
|
+
if (limit !== void 0 && count >= limit) continue;
|
|
18108
|
+
count += 1;
|
|
18109
|
+
if (!matched.has(entry.event.id)) matched.set(entry.event.id, entry);
|
|
18006
18110
|
}
|
|
18007
18111
|
}
|
|
18008
|
-
|
|
18009
|
-
|
|
18112
|
+
return [...matched.values()];
|
|
18113
|
+
}
|
|
18114
|
+
function mergeResult(base, cached, filters, queryOptions) {
|
|
18115
|
+
const events = /* @__PURE__ */ new Map();
|
|
18116
|
+
for (const entry of base.events) events.set(entry.event.id, entry);
|
|
18117
|
+
for (const entry of cached) {
|
|
18118
|
+
if (!events.has(entry.event.id)) events.set(entry.event.id, entry);
|
|
18010
18119
|
}
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18014
|
-
|
|
18015
|
-
|
|
18016
|
-
|
|
18017
|
-
|
|
18018
|
-
|
|
18019
|
-
|
|
18120
|
+
return {
|
|
18121
|
+
events: applyFinalLimits([...events.values()], filters, queryOptions),
|
|
18122
|
+
...base.incomplete === void 0 ? {} : { incomplete: base.incomplete },
|
|
18123
|
+
...base.error === void 0 ? {} : { error: base.error }
|
|
18124
|
+
};
|
|
18125
|
+
}
|
|
18126
|
+
function applyFinalLimits(entries, filters, queryOptions) {
|
|
18127
|
+
const aggregateLimit = typeof queryOptions?.limit === "number" && queryOptions.limit >= 0 ? queryOptions.limit : void 0;
|
|
18128
|
+
const aggregateCapped = aggregateLimit === void 0 ? entries : entries.slice(0, aggregateLimit);
|
|
18129
|
+
if (filters.length === 0) return aggregateCapped;
|
|
18130
|
+
const counts = filters.map(() => 0);
|
|
18131
|
+
return aggregateCapped.filter((entry) => {
|
|
18132
|
+
const matching = filters.flatMap((filter, index) => matchesAnyFilter2(entry.event, [filter]) ? [index] : []);
|
|
18133
|
+
const allowed2 = matching.some((index) => {
|
|
18134
|
+
const limit = filters[index]?.limit;
|
|
18135
|
+
return typeof limit !== "number" || limit < 0 || counts[index] < limit;
|
|
18020
18136
|
});
|
|
18021
|
-
return
|
|
18022
|
-
|
|
18137
|
+
if (!allowed2) return false;
|
|
18138
|
+
for (const index of matching) counts[index] += 1;
|
|
18139
|
+
return true;
|
|
18140
|
+
});
|
|
18141
|
+
}
|
|
18142
|
+
|
|
18143
|
+
// src/browser-relay-policy.ts
|
|
18144
|
+
var PAJA_STORAGE_PROBE_KEY = "kehto:paja:storage-probe";
|
|
18145
|
+
function normalizeRelayUrl(value) {
|
|
18023
18146
|
try {
|
|
18024
|
-
const
|
|
18025
|
-
|
|
18026
|
-
|
|
18027
|
-
|
|
18028
|
-
|
|
18029
|
-
|
|
18030
|
-
message: "Cache Storage napplet artifact cache could not be opened",
|
|
18031
|
-
cause
|
|
18032
|
-
});
|
|
18033
|
-
return void 0;
|
|
18147
|
+
const url = new URL(value);
|
|
18148
|
+
if (url.protocol !== "wss:" && url.protocol !== "ws:" || url.username || url.password) return null;
|
|
18149
|
+
url.hash = "";
|
|
18150
|
+
return url.href;
|
|
18151
|
+
} catch {
|
|
18152
|
+
return null;
|
|
18034
18153
|
}
|
|
18035
18154
|
}
|
|
18036
|
-
|
|
18037
|
-
|
|
18038
|
-
|
|
18039
|
-
|
|
18040
|
-
|
|
18041
|
-
|
|
18042
|
-
NAPPLET_KIND_NAMED
|
|
18043
|
-
];
|
|
18044
|
-
function isNappletManifestKind(kind) {
|
|
18045
|
-
return NAPPLET_KINDS.includes(kind);
|
|
18046
|
-
}
|
|
18047
|
-
var NappletResolutionError = class extends Error {
|
|
18048
|
-
code;
|
|
18049
|
-
constructor(code, message) {
|
|
18050
|
-
super(message);
|
|
18051
|
-
this.name = "NappletResolutionError";
|
|
18052
|
-
this.code = code;
|
|
18053
|
-
}
|
|
18054
|
-
};
|
|
18055
|
-
function firstTagValue(tags, name) {
|
|
18056
|
-
for (const tag of tags) {
|
|
18057
|
-
if (tag[0] === name && typeof tag[1] === "string" && tag[1].length > 0) return tag[1];
|
|
18155
|
+
function isVerifiedNostrEvent(value) {
|
|
18156
|
+
if (typeof value !== "object" || value === null) return false;
|
|
18157
|
+
try {
|
|
18158
|
+
return verifyEvent(value);
|
|
18159
|
+
} catch {
|
|
18160
|
+
return false;
|
|
18058
18161
|
}
|
|
18059
|
-
return void 0;
|
|
18060
18162
|
}
|
|
18061
|
-
function
|
|
18062
|
-
const
|
|
18063
|
-
|
|
18064
|
-
|
|
18163
|
+
function isPajaRelayAllowed(value, getConfiguredRelays) {
|
|
18164
|
+
const normalized = normalizeRelayUrl(value);
|
|
18165
|
+
if (!normalized) return false;
|
|
18166
|
+
if (getConfiguredRelays().some((url2) => normalizeRelayUrl(url2) === normalized)) return true;
|
|
18167
|
+
const url = new URL(normalized);
|
|
18168
|
+
if (url.protocol !== "wss:") return false;
|
|
18169
|
+
const hostname = url.hostname.replace(/^\[|\]$/gu, "").toLowerCase();
|
|
18170
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local") || hostname.endsWith(".internal") || !hostname.includes(".")) return false;
|
|
18171
|
+
if (hostname.includes(":")) {
|
|
18172
|
+
return hostname !== "::" && hostname !== "::1" && !/^(?:fc|fd|fe[89ab])/iu.test(hostname);
|
|
18065
18173
|
}
|
|
18066
|
-
|
|
18174
|
+
const octets = hostname.split(".").map(Number);
|
|
18175
|
+
if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) return true;
|
|
18176
|
+
const [first, second] = octets;
|
|
18177
|
+
return first !== 0 && first !== 10 && first !== 127 && first < 224 && !(first === 100 && second >= 64 && second <= 127) && !(first === 169 && second === 254) && !(first === 172 && second >= 16 && second <= 31) && !(first === 192 && second === 168) && !(first === 198 && (second === 18 || second === 19));
|
|
18067
18178
|
}
|
|
18068
|
-
function
|
|
18069
|
-
const
|
|
18070
|
-
|
|
18071
|
-
|
|
18072
|
-
|
|
18073
|
-
|
|
18074
|
-
|
|
18075
|
-
|
|
18076
|
-
|
|
18077
|
-
|
|
18078
|
-
|
|
18079
|
-
|
|
18080
|
-
|
|
18081
|
-
|
|
18082
|
-
|
|
18083
|
-
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
18087
|
-
|
|
18088
|
-
|
|
18089
|
-
if (!/^napplet:[^/?#\s]+\/[^/?#\s]+$/.test(convention)) {
|
|
18090
|
-
throw new NappletResolutionError(
|
|
18091
|
-
"invalid-manifest",
|
|
18092
|
-
"archetype convention must be a queryless napplet:<archetype>/<intent> identity"
|
|
18093
|
-
);
|
|
18094
|
-
}
|
|
18095
|
-
if (tag.length !== 3) {
|
|
18096
|
-
throw new NappletResolutionError(
|
|
18097
|
-
"invalid-manifest",
|
|
18098
|
-
"archetype tags must contain exactly slug and convention"
|
|
18099
|
-
);
|
|
18179
|
+
function createPajaRelayConfig(getSimulation) {
|
|
18180
|
+
const tierNames = ["discovery", "super", "outbox"];
|
|
18181
|
+
const additions = /* @__PURE__ */ new Map();
|
|
18182
|
+
const removals = /* @__PURE__ */ new Map();
|
|
18183
|
+
const current = { discovery: [], super: [], outbox: [] };
|
|
18184
|
+
const allRelays = [];
|
|
18185
|
+
function isTier(value) {
|
|
18186
|
+
return tierNames.includes(value);
|
|
18187
|
+
}
|
|
18188
|
+
function refresh() {
|
|
18189
|
+
for (const tier of tierNames) {
|
|
18190
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18191
|
+
const values = [];
|
|
18192
|
+
const removed = removals.get(tier);
|
|
18193
|
+
for (const url of [...getPajaRelayUrls(getSimulation()), ...additions.get(tier)?.values() ?? []]) {
|
|
18194
|
+
const normalized = normalizeRelayUrl(url);
|
|
18195
|
+
if (!normalized || seen.has(normalized) || removed?.has(normalized)) continue;
|
|
18196
|
+
seen.add(normalized);
|
|
18197
|
+
values.push(url);
|
|
18198
|
+
}
|
|
18199
|
+
current[tier].splice(0, current[tier].length, ...values);
|
|
18100
18200
|
}
|
|
18101
|
-
|
|
18201
|
+
const combined = [...new Set(tierNames.flatMap((tier) => current[tier]))];
|
|
18202
|
+
allRelays.splice(0, allRelays.length, ...combined);
|
|
18102
18203
|
}
|
|
18103
|
-
|
|
18204
|
+
const runtime = {
|
|
18205
|
+
allRelays,
|
|
18206
|
+
outboxRelays: current.outbox,
|
|
18207
|
+
addRelay(tier, url) {
|
|
18208
|
+
const normalized = normalizeRelayUrl(url);
|
|
18209
|
+
if (!isTier(tier) || !normalized) return;
|
|
18210
|
+
const entries = additions.get(tier) ?? /* @__PURE__ */ new Map();
|
|
18211
|
+
entries.set(normalized, url);
|
|
18212
|
+
additions.set(tier, entries);
|
|
18213
|
+
removals.get(tier)?.delete(normalized);
|
|
18214
|
+
refresh();
|
|
18215
|
+
},
|
|
18216
|
+
removeRelay(tier, url) {
|
|
18217
|
+
const normalized = normalizeRelayUrl(url);
|
|
18218
|
+
if (!isTier(tier) || !normalized) return;
|
|
18219
|
+
additions.get(tier)?.delete(normalized);
|
|
18220
|
+
const entries = removals.get(tier) ?? /* @__PURE__ */ new Set();
|
|
18221
|
+
entries.add(normalized);
|
|
18222
|
+
removals.set(tier, entries);
|
|
18223
|
+
refresh();
|
|
18224
|
+
},
|
|
18225
|
+
getRelayConfig() {
|
|
18226
|
+
refresh();
|
|
18227
|
+
return {
|
|
18228
|
+
discovery: [...current.discovery],
|
|
18229
|
+
super: [...current.super],
|
|
18230
|
+
outbox: [...current.outbox]
|
|
18231
|
+
};
|
|
18232
|
+
},
|
|
18233
|
+
getNip66Suggestions: () => [],
|
|
18234
|
+
getRelayUrls(tiers = tierNames) {
|
|
18235
|
+
refresh();
|
|
18236
|
+
return [...new Set(tiers.flatMap((tier) => current[tier]))];
|
|
18237
|
+
}
|
|
18238
|
+
};
|
|
18239
|
+
refresh();
|
|
18240
|
+
return runtime;
|
|
18104
18241
|
}
|
|
18105
|
-
function
|
|
18106
|
-
|
|
18107
|
-
|
|
18108
|
-
|
|
18109
|
-
|
|
18110
|
-
|
|
18111
|
-
throw new NappletResolutionError("invalid-manifest", "manifest has no path tags");
|
|
18112
|
-
}
|
|
18113
|
-
const aggregateHash = aggregateTagValue(event.tags);
|
|
18114
|
-
if (!aggregateHash) {
|
|
18115
|
-
throw new NappletResolutionError("invalid-manifest", "manifest has no aggregate x tag");
|
|
18116
|
-
}
|
|
18117
|
-
const dTag = firstTagValue(event.tags, "d");
|
|
18118
|
-
if (event.kind !== NAPPLET_KIND_NAMED && dTag !== void 0) {
|
|
18119
|
-
throw new NappletResolutionError("invalid-manifest", "only named manifests may carry a d tag");
|
|
18242
|
+
function createPajaRelayHooks(pool, getSimulation, relayConfig) {
|
|
18243
|
+
const cleanups = /* @__PURE__ */ new Map();
|
|
18244
|
+
const scoped = /* @__PURE__ */ new Map();
|
|
18245
|
+
function closeScopedRelay(windowId) {
|
|
18246
|
+
scoped.get(windowId)?.unsubscribe();
|
|
18247
|
+
scoped.delete(windowId);
|
|
18120
18248
|
}
|
|
18121
18249
|
return {
|
|
18122
|
-
|
|
18123
|
-
|
|
18124
|
-
|
|
18125
|
-
|
|
18126
|
-
|
|
18127
|
-
|
|
18128
|
-
|
|
18129
|
-
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
|
|
18250
|
+
getRelayPool: () => pool,
|
|
18251
|
+
trackSubscription(subKey, cleanup) {
|
|
18252
|
+
cleanups.set(subKey, cleanup);
|
|
18253
|
+
},
|
|
18254
|
+
untrackSubscription(subKey) {
|
|
18255
|
+
cleanups.get(subKey)?.();
|
|
18256
|
+
cleanups.delete(subKey);
|
|
18257
|
+
},
|
|
18258
|
+
openScopedRelay(windowId, relayUrl2, subId, filters, sourceWindow) {
|
|
18259
|
+
closeScopedRelay(windowId);
|
|
18260
|
+
const configured = new Map(relayConfig.getRelayUrls().flatMap((url) => {
|
|
18261
|
+
const normalized = normalizeRelayUrl(url);
|
|
18262
|
+
return normalized ? [[normalized, url]] : [];
|
|
18263
|
+
}));
|
|
18264
|
+
const requested = normalizeRelayUrl(relayUrl2);
|
|
18265
|
+
const allowedUrl = requested && isPajaRelayAllowed(relayUrl2, () => relayConfig.getRelayUrls()) ? configured.get(requested) ?? requested : void 0;
|
|
18266
|
+
if (getSimulation().relay.mode !== "live" || !allowedUrl) {
|
|
18267
|
+
sourceWindow.postMessage({ type: "relay.closed", subId, reason: "scoped relay unavailable" }, "*");
|
|
18268
|
+
return;
|
|
18269
|
+
}
|
|
18270
|
+
const subscription = pool.subscription([allowedUrl], filters).subscribe((item) => {
|
|
18271
|
+
if (item === "EOSE") {
|
|
18272
|
+
sourceWindow.postMessage({ type: "relay.eose", subId }, "*");
|
|
18273
|
+
return;
|
|
18274
|
+
}
|
|
18275
|
+
if (!isVerifiedNostrEvent(item)) return;
|
|
18276
|
+
sourceWindow.postMessage({
|
|
18277
|
+
type: "relay.event",
|
|
18278
|
+
subId,
|
|
18279
|
+
result: { event: item, sidecar: { relayHints: [allowedUrl] } }
|
|
18280
|
+
}, "*");
|
|
18281
|
+
});
|
|
18282
|
+
scoped.set(windowId, { relayUrl: allowedUrl, unsubscribe: () => subscription.unsubscribe() });
|
|
18283
|
+
},
|
|
18284
|
+
closeScopedRelay,
|
|
18285
|
+
publishToScopedRelay: async (windowId, event) => {
|
|
18286
|
+
const active = scoped.get(windowId);
|
|
18287
|
+
if (getSimulation().relay.mode !== "live" || !active || !verifyEvent(event)) return false;
|
|
18288
|
+
try {
|
|
18289
|
+
await pool.publish([active.relayUrl], event);
|
|
18290
|
+
return true;
|
|
18291
|
+
} catch {
|
|
18292
|
+
return false;
|
|
18293
|
+
}
|
|
18294
|
+
},
|
|
18295
|
+
selectRelayTier: () => relayConfig.getRelayUrls(["discovery", "super"])
|
|
18133
18296
|
};
|
|
18134
18297
|
}
|
|
18135
|
-
function
|
|
18298
|
+
function hasWritableLocalStorage() {
|
|
18299
|
+
if (typeof localStorage === "undefined") return false;
|
|
18300
|
+
let previous = null;
|
|
18136
18301
|
try {
|
|
18137
|
-
|
|
18302
|
+
previous = localStorage.getItem(PAJA_STORAGE_PROBE_KEY);
|
|
18303
|
+
localStorage.setItem(PAJA_STORAGE_PROBE_KEY, "1");
|
|
18304
|
+
const available = localStorage.getItem(PAJA_STORAGE_PROBE_KEY) === "1";
|
|
18305
|
+
if (previous === null) localStorage.removeItem(PAJA_STORAGE_PROBE_KEY);
|
|
18306
|
+
else localStorage.setItem(PAJA_STORAGE_PROBE_KEY, previous);
|
|
18307
|
+
return available;
|
|
18138
18308
|
} catch {
|
|
18309
|
+
try {
|
|
18310
|
+
if (previous === null) localStorage.removeItem(PAJA_STORAGE_PROBE_KEY);
|
|
18311
|
+
else localStorage.setItem(PAJA_STORAGE_PROBE_KEY, previous);
|
|
18312
|
+
} catch {
|
|
18313
|
+
}
|
|
18139
18314
|
return false;
|
|
18140
18315
|
}
|
|
18141
18316
|
}
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
}
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
for (const server of servers) {
|
|
18150
|
-
const url = `${stripTrailingSlash(server)}/${sha256Hex}`;
|
|
18317
|
+
|
|
18318
|
+
// src/browser-common.ts
|
|
18319
|
+
var HEX_642 = /^[0-9a-f]{64}$/i;
|
|
18320
|
+
var SHORTCODE = /^:[a-zA-Z0-9_+-]+:$/;
|
|
18321
|
+
function relayUrls(values) {
|
|
18322
|
+
const urls = /* @__PURE__ */ new Set();
|
|
18323
|
+
for (const value of values) {
|
|
18151
18324
|
try {
|
|
18152
|
-
const
|
|
18153
|
-
if (
|
|
18325
|
+
const url = new URL(value);
|
|
18326
|
+
if (url.protocol === "wss:" || url.protocol === "ws:") urls.add(url.href);
|
|
18154
18327
|
} catch {
|
|
18155
18328
|
}
|
|
18156
18329
|
}
|
|
18157
|
-
|
|
18158
|
-
}
|
|
18159
|
-
var INDEX_PATHS = ["/index.html", "index.html", "/"];
|
|
18160
|
-
function defaultDecode(bytes) {
|
|
18161
|
-
return new TextDecoder().decode(bytes);
|
|
18330
|
+
return [...urls];
|
|
18162
18331
|
}
|
|
18163
|
-
|
|
18164
|
-
|
|
18165
|
-
|
|
18166
|
-
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
const recomputed = computeAggregateHash(manifest.paths);
|
|
18170
|
-
if (recomputed !== manifest.aggregateHash) {
|
|
18171
|
-
throw new NappletResolutionError(
|
|
18172
|
-
"aggregate-mismatch",
|
|
18173
|
-
`recomputed aggregate ${recomputed} != manifest ${manifest.aggregateHash}`
|
|
18174
|
-
);
|
|
18175
|
-
}
|
|
18176
|
-
const files = /* @__PURE__ */ new Map();
|
|
18177
|
-
for (const entry of manifest.paths) {
|
|
18178
|
-
let bytes = await cache2?.readBlob(entry.sha256);
|
|
18179
|
-
if (bytes && !verifyBlobHash(bytes, entry.sha256)) {
|
|
18180
|
-
await cache2?.deleteBlob(entry.sha256);
|
|
18181
|
-
bytes = void 0;
|
|
18332
|
+
function profileTarget(target, defaults) {
|
|
18333
|
+
if (HEX_642.test(target)) return { pubkey: target.toLowerCase(), relays: relayUrls(defaults) };
|
|
18334
|
+
try {
|
|
18335
|
+
const decoded = decode(target);
|
|
18336
|
+
if (decoded.type === "npub") {
|
|
18337
|
+
return { pubkey: decoded.data.toLowerCase(), relays: relayUrls(defaults) };
|
|
18182
18338
|
}
|
|
18183
|
-
|
|
18184
|
-
|
|
18185
|
-
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
|
|
18339
|
+
if (decoded.type === "nprofile") {
|
|
18340
|
+
return {
|
|
18341
|
+
pubkey: decoded.data.pubkey.toLowerCase(),
|
|
18342
|
+
relays: relayUrls([
|
|
18343
|
+
...(decoded.data.relays ?? []).filter((relay) => isPajaRelayAllowed(relay, () => defaults)),
|
|
18344
|
+
...defaults
|
|
18345
|
+
])
|
|
18346
|
+
};
|
|
18189
18347
|
}
|
|
18190
|
-
|
|
18348
|
+
} catch {
|
|
18349
|
+
return null;
|
|
18191
18350
|
}
|
|
18192
|
-
|
|
18193
|
-
|
|
18194
|
-
|
|
18351
|
+
return null;
|
|
18352
|
+
}
|
|
18353
|
+
function npub(value) {
|
|
18354
|
+
try {
|
|
18355
|
+
const decoded = decode(value);
|
|
18356
|
+
return decoded.type === "npub" ? decoded.data.toLowerCase() : null;
|
|
18357
|
+
} catch {
|
|
18358
|
+
return null;
|
|
18195
18359
|
}
|
|
18196
|
-
const indexHtml = textDecode(files.get(indexEntry.path));
|
|
18197
|
-
await cache2?.writeVerifiedResolution({ event, manifest, files, indexHtml });
|
|
18198
|
-
return {
|
|
18199
|
-
dTag: manifest.dTag,
|
|
18200
|
-
aggregateHash: manifest.aggregateHash,
|
|
18201
|
-
files,
|
|
18202
|
-
indexHtml,
|
|
18203
|
-
manifest
|
|
18204
|
-
};
|
|
18205
18360
|
}
|
|
18206
|
-
|
|
18207
|
-
|
|
18208
|
-
|
|
18209
|
-
var PAJA_RESOURCE_MAX_URLS = 100;
|
|
18210
|
-
var PAJA_RESOURCE_MAX_SERVERS = 8;
|
|
18211
|
-
var UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
18212
|
-
var BLOSSOM_RESOURCE_PATTERN = /^blossom:sha256:([0-9a-f]{64})$/i;
|
|
18213
|
-
function createPajaResourceFetch(options = {}) {
|
|
18214
|
-
return async (value, init) => {
|
|
18215
|
-
if (init.signal.aborted) throw new DOMException("Resource request cancelled", "AbortError");
|
|
18216
|
-
const url = new URL(value);
|
|
18217
|
-
if (init.method && init.method.toUpperCase() !== "GET") {
|
|
18218
|
-
throw new ResourceServiceError("invalid-request", "NAP-RESOURCE is read-only");
|
|
18219
|
-
}
|
|
18220
|
-
if (url.protocol === "blossom:") {
|
|
18221
|
-
return fetchBlossomResource(value, init.signal, init.servers ?? [], options);
|
|
18222
|
-
}
|
|
18223
|
-
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
18224
|
-
return fetchNetworkResource(value, init.signal, options.fetch ?? globalThis.fetch);
|
|
18225
|
-
}
|
|
18226
|
-
if (url.protocol !== "data:") {
|
|
18227
|
-
throw new ResourceServiceError("unsupported-scheme", `Paja does not enable ${url.protocol}`);
|
|
18228
|
-
}
|
|
18229
|
-
if (value.length > encodedDataUrlLimit(PAJA_RESOURCE_MAX_BYTES)) {
|
|
18230
|
-
throw new ResourceServiceError("too-large", `resource exceeds ${PAJA_RESOURCE_MAX_BYTES} bytes`);
|
|
18231
|
-
}
|
|
18232
|
-
const decoded = await fetch(value, { signal: init.signal });
|
|
18233
|
-
const bytes = new Uint8Array(await decoded.arrayBuffer());
|
|
18234
|
-
if (bytes.byteLength > PAJA_RESOURCE_MAX_BYTES) {
|
|
18235
|
-
throw new ResourceServiceError("too-large", `resource exceeds ${PAJA_RESOURCE_MAX_BYTES} bytes`);
|
|
18236
|
-
}
|
|
18237
|
-
const mime = sniffSafeResourceMime(bytes);
|
|
18238
|
-
if (!mime) {
|
|
18239
|
-
throw new ResourceServiceError("decode-failed", "resource bytes are not an enabled safe media type");
|
|
18240
|
-
}
|
|
18241
|
-
return new Response(arrayBufferFor(bytes), { headers: { "content-type": mime } });
|
|
18242
|
-
};
|
|
18361
|
+
function pubkey(value) {
|
|
18362
|
+
if (HEX_642.test(value)) return value.toLowerCase();
|
|
18363
|
+
return npub(value);
|
|
18243
18364
|
}
|
|
18244
|
-
function
|
|
18245
|
-
|
|
18246
|
-
{ scheme: "data", enabled: true },
|
|
18247
|
-
{ scheme: "https", enabled: true },
|
|
18248
|
-
{ scheme: "http", enabled: true },
|
|
18249
|
-
{ scheme: "blossom", enabled: true }
|
|
18250
|
-
];
|
|
18251
|
-
return {
|
|
18252
|
-
schemes,
|
|
18253
|
-
maxBytes: PAJA_RESOURCE_MAX_BYTES,
|
|
18254
|
-
maxUrls: PAJA_RESOURCE_MAX_URLS,
|
|
18255
|
-
maxServers: PAJA_RESOURCE_MAX_SERVERS
|
|
18256
|
-
};
|
|
18365
|
+
function newestVerified(events, predicate) {
|
|
18366
|
+
return events.filter((event) => predicate(event) && verifyEvent(event)).sort((left, right) => right.created_at - left.created_at || left.id.localeCompare(right.id))[0] ?? null;
|
|
18257
18367
|
}
|
|
18258
|
-
|
|
18259
|
-
let response;
|
|
18368
|
+
function parseProfile(event) {
|
|
18260
18369
|
try {
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
}
|
|
18267
|
-
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
"network-error",
|
|
18271
|
-
error instanceof Error ? error.message : String(error)
|
|
18272
|
-
);
|
|
18273
|
-
}
|
|
18274
|
-
if (response.status === 404 || response.status === 410) {
|
|
18275
|
-
throw new ResourceServiceError("not-found", "HTTP resource was not found");
|
|
18276
|
-
}
|
|
18277
|
-
if (!response.ok) {
|
|
18278
|
-
throw new ResourceServiceError("network-error", `HTTP resource returned ${response.status}`);
|
|
18279
|
-
}
|
|
18280
|
-
const bytes = await readCappedResponse(response, signal);
|
|
18281
|
-
const mime = sniffSafeResourceMime(bytes);
|
|
18282
|
-
if (!mime) {
|
|
18283
|
-
throw new ResourceServiceError("decode-failed", "resource bytes are not an enabled safe media type");
|
|
18370
|
+
const parsed = JSON.parse(event.content);
|
|
18371
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
18372
|
+
const profile = { ...parsed };
|
|
18373
|
+
if (typeof profile.display_name === "string" && typeof profile.displayName !== "string") {
|
|
18374
|
+
profile.displayName = profile.display_name;
|
|
18375
|
+
}
|
|
18376
|
+
return profile;
|
|
18377
|
+
} catch {
|
|
18378
|
+
return null;
|
|
18284
18379
|
}
|
|
18285
|
-
return new Response(arrayBufferFor(bytes), { headers: { "content-type": mime } });
|
|
18286
18380
|
}
|
|
18287
|
-
|
|
18288
|
-
const
|
|
18289
|
-
|
|
18290
|
-
|
|
18381
|
+
function contactPubkeys2(event) {
|
|
18382
|
+
const follows = /* @__PURE__ */ new Set();
|
|
18383
|
+
for (const tag of event?.tags ?? []) {
|
|
18384
|
+
if (tag[0] === "p" && typeof tag[1] === "string" && HEX_642.test(tag[1])) {
|
|
18385
|
+
follows.add(tag[1].toLowerCase());
|
|
18386
|
+
}
|
|
18291
18387
|
}
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
);
|
|
18297
|
-
if (
|
|
18298
|
-
|
|
18388
|
+
return [...follows];
|
|
18389
|
+
}
|
|
18390
|
+
function actionError(error) {
|
|
18391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18392
|
+
if (message.includes("denied")) return { ok: false, error: "user-denied" };
|
|
18393
|
+
if (message.includes("publish")) return { ok: false, error: "publish-failed" };
|
|
18394
|
+
return { ok: false, error: message };
|
|
18395
|
+
}
|
|
18396
|
+
function isSingleEmoji(value) {
|
|
18397
|
+
const graphemes = [...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(value)];
|
|
18398
|
+
return graphemes.length === 1 && /[\p{Extended_Pictographic}\p{Regional_Indicator}]/u.test(value);
|
|
18399
|
+
}
|
|
18400
|
+
function reactionTags(target, reaction, customEmojiHref) {
|
|
18401
|
+
if (reaction !== "+" && reaction !== "-" && !isSingleEmoji(reaction) && !SHORTCODE.test(reaction)) return null;
|
|
18402
|
+
if (customEmojiHref && !SHORTCODE.test(reaction)) return null;
|
|
18403
|
+
if (SHORTCODE.test(reaction) && !customEmojiHref) return null;
|
|
18404
|
+
const tags = [["e", target.id], ["p", target.pubkey], ["k", String(target.kind)]];
|
|
18405
|
+
if (customEmojiHref) {
|
|
18406
|
+
let url;
|
|
18407
|
+
try {
|
|
18408
|
+
url = new URL(customEmojiHref);
|
|
18409
|
+
} catch {
|
|
18410
|
+
return null;
|
|
18411
|
+
}
|
|
18412
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
18413
|
+
tags.push(["emoji", reaction.slice(1, -1), url.href]);
|
|
18299
18414
|
}
|
|
18300
|
-
|
|
18301
|
-
|
|
18302
|
-
|
|
18303
|
-
|
|
18304
|
-
|
|
18305
|
-
|
|
18306
|
-
if (
|
|
18307
|
-
const
|
|
18308
|
-
|
|
18415
|
+
return tags;
|
|
18416
|
+
}
|
|
18417
|
+
function createPajaCommonBackend(options) {
|
|
18418
|
+
const query = (filters, relays = options.getRelays()) => options.relay.query(relays, filters, PAJA_LIVE_QUERY_WAIT_MS);
|
|
18419
|
+
const identity = async () => {
|
|
18420
|
+
const active = options.getSigner();
|
|
18421
|
+
if (!active?.getPublicKey) throw new Error("not-signed-in");
|
|
18422
|
+
const activePubkey = (await active.getPublicKey()).toLowerCase();
|
|
18423
|
+
if (!HEX_642.test(activePubkey)) throw new Error("not-signed-in");
|
|
18424
|
+
return { signer: active, pubkey: activePubkey };
|
|
18425
|
+
};
|
|
18426
|
+
const signer = async () => {
|
|
18427
|
+
const active = await identity();
|
|
18428
|
+
if (!active.signer.signEvent) throw new Error("not-signed-in");
|
|
18429
|
+
return active;
|
|
18430
|
+
};
|
|
18431
|
+
const currentContacts = async (author) => {
|
|
18432
|
+
const events = await query([{ kinds: [3], authors: [author], limit: 16 }]);
|
|
18433
|
+
return newestVerified(events, (event) => event.kind === 3 && event.pubkey.toLowerCase() === author);
|
|
18434
|
+
};
|
|
18435
|
+
const publish = async (active, author, template) => {
|
|
18436
|
+
try {
|
|
18437
|
+
const event = await active.signEvent(template);
|
|
18438
|
+
if (event.pubkey.toLowerCase() !== author || !verifyEvent(event)) return { ok: false, error: "invalid-signature" };
|
|
18439
|
+
await options.relay.publish(options.getRelays(), event);
|
|
18440
|
+
return { ok: true, eventId: event.id, event };
|
|
18441
|
+
} catch (error) {
|
|
18442
|
+
return actionError(error);
|
|
18443
|
+
}
|
|
18444
|
+
};
|
|
18445
|
+
const mutateFollows = async (values, add) => {
|
|
18446
|
+
const targets = values.map(pubkey);
|
|
18447
|
+
if (targets.some((target) => target === null)) return { ok: false, error: "invalid-pubkey" };
|
|
18309
18448
|
try {
|
|
18310
|
-
|
|
18311
|
-
|
|
18312
|
-
|
|
18313
|
-
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
|
|
18449
|
+
const active = await signer();
|
|
18450
|
+
const current = await currentContacts(active.pubkey);
|
|
18451
|
+
const targetSet = new Set(targets);
|
|
18452
|
+
const existing = new Set(contactPubkeys2(current));
|
|
18453
|
+
const changed = add ? [...targetSet].some((target) => !existing.has(target)) : [...targetSet].some((target) => existing.has(target));
|
|
18454
|
+
if (!changed) {
|
|
18455
|
+
return current ? { ok: true, eventId: current.id, event: current } : { ok: true };
|
|
18456
|
+
}
|
|
18457
|
+
const tags = (current?.tags ?? []).filter((tag) => {
|
|
18458
|
+
if (tag[0] !== "p" || typeof tag[1] !== "string") return true;
|
|
18459
|
+
return add || !targetSet.has(tag[1].toLowerCase());
|
|
18460
|
+
});
|
|
18461
|
+
if (add) {
|
|
18462
|
+
for (const target of targetSet) {
|
|
18463
|
+
if (!existing.has(target)) tags.push(["p", target]);
|
|
18464
|
+
}
|
|
18465
|
+
}
|
|
18466
|
+
return publish(active.signer, active.pubkey, {
|
|
18467
|
+
kind: 3,
|
|
18468
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
18469
|
+
tags,
|
|
18470
|
+
content: current?.content ?? ""
|
|
18317
18471
|
});
|
|
18318
18472
|
} catch (error) {
|
|
18319
|
-
|
|
18320
|
-
foundInconclusive = true;
|
|
18321
|
-
lastNetworkMessage = error instanceof Error ? error.message : String(error);
|
|
18322
|
-
continue;
|
|
18323
|
-
}
|
|
18324
|
-
if (response.status === 404 || response.status === 410) {
|
|
18325
|
-
foundNotFound = true;
|
|
18326
|
-
continue;
|
|
18327
|
-
}
|
|
18328
|
-
if (!response.ok) {
|
|
18329
|
-
foundInconclusive = true;
|
|
18330
|
-
lastNetworkMessage = `Blossom server returned HTTP ${response.status}`;
|
|
18331
|
-
continue;
|
|
18332
|
-
}
|
|
18333
|
-
const bytes = await readCappedResponse(response, signal);
|
|
18334
|
-
if (!verifyBlobHash(bytes, expectedHash)) {
|
|
18335
|
-
foundHashMismatch = true;
|
|
18336
|
-
continue;
|
|
18473
|
+
return actionError(error);
|
|
18337
18474
|
}
|
|
18338
|
-
|
|
18339
|
-
|
|
18340
|
-
|
|
18475
|
+
};
|
|
18476
|
+
return {
|
|
18477
|
+
async getProfile(target) {
|
|
18478
|
+
const resolved = profileTarget(target, options.getRelays());
|
|
18479
|
+
if (!resolved) return { ok: false, pubkey: "", error: "invalid-profile-target" };
|
|
18480
|
+
try {
|
|
18481
|
+
const events = await query([{ kinds: [0], authors: [resolved.pubkey], limit: 16 }], resolved.relays);
|
|
18482
|
+
const event = newestVerified(events, (candidate) => candidate.kind === 0 && candidate.pubkey.toLowerCase() === resolved.pubkey);
|
|
18483
|
+
if (!event) return { ok: true, pubkey: resolved.pubkey, profile: null };
|
|
18484
|
+
return {
|
|
18485
|
+
ok: true,
|
|
18486
|
+
pubkey: resolved.pubkey,
|
|
18487
|
+
profile: parseProfile(event),
|
|
18488
|
+
result: { event, sidecar: { relayHints: resolved.relays } }
|
|
18489
|
+
};
|
|
18490
|
+
} catch {
|
|
18491
|
+
return { ok: false, pubkey: resolved.pubkey, error: "relay-timeout" };
|
|
18492
|
+
}
|
|
18493
|
+
},
|
|
18494
|
+
async follows() {
|
|
18495
|
+
try {
|
|
18496
|
+
const active = await identity();
|
|
18497
|
+
return { ok: true, pubkeys: contactPubkeys2(await currentContacts(active.pubkey)) };
|
|
18498
|
+
} catch (error) {
|
|
18499
|
+
const failure3 = actionError(error);
|
|
18500
|
+
return { ok: false, pubkeys: [], error: failure3.error };
|
|
18501
|
+
}
|
|
18502
|
+
},
|
|
18503
|
+
follow: (pubkeys) => mutateFollows(pubkeys, true),
|
|
18504
|
+
unfollow: (pubkeys) => mutateFollows(pubkeys, false),
|
|
18505
|
+
async react(targetEventId, reaction, customEmojiHref) {
|
|
18506
|
+
if (!HEX_642.test(targetEventId)) return { ok: false, error: "invalid-target" };
|
|
18507
|
+
try {
|
|
18508
|
+
const active = await signer();
|
|
18509
|
+
const events = await query([{ ids: [targetEventId], limit: 8 }]);
|
|
18510
|
+
const target = newestVerified(events, (event) => event.id === targetEventId.toLowerCase());
|
|
18511
|
+
if (!target) return { ok: false, error: "invalid-target" };
|
|
18512
|
+
const tags = reactionTags(target, reaction, customEmojiHref);
|
|
18513
|
+
if (!tags) return { ok: false, error: "invalid-reaction" };
|
|
18514
|
+
return publish(active.signer, active.pubkey, {
|
|
18515
|
+
kind: 7,
|
|
18516
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
18517
|
+
tags,
|
|
18518
|
+
content: reaction
|
|
18519
|
+
});
|
|
18520
|
+
} catch (error) {
|
|
18521
|
+
return actionError(error);
|
|
18522
|
+
}
|
|
18523
|
+
},
|
|
18524
|
+
async report(target, reason, text) {
|
|
18525
|
+
try {
|
|
18526
|
+
const active = await signer();
|
|
18527
|
+
if (target.type === "pubkey") {
|
|
18528
|
+
const reportedPubkey = pubkey(target.pubkey);
|
|
18529
|
+
if (!reportedPubkey) return { ok: false, error: "invalid-target" };
|
|
18530
|
+
return publish(active.signer, active.pubkey, {
|
|
18531
|
+
kind: 1984,
|
|
18532
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
18533
|
+
tags: [["p", reportedPubkey, target.relay ?? "", reason]],
|
|
18534
|
+
content: text
|
|
18535
|
+
});
|
|
18536
|
+
}
|
|
18537
|
+
if (!HEX_642.test(target.id)) return { ok: false, error: "invalid-target" };
|
|
18538
|
+
const explicitAuthor = target.pubkey ? pubkey(target.pubkey) : void 0;
|
|
18539
|
+
if (target.pubkey && !explicitAuthor) return { ok: false, error: "invalid-target" };
|
|
18540
|
+
let author = explicitAuthor ?? null;
|
|
18541
|
+
if (!author) {
|
|
18542
|
+
const events = await query([{ ids: [target.id], limit: 8 }]);
|
|
18543
|
+
author = newestVerified(events, (event) => event.id === target.id.toLowerCase())?.pubkey ?? null;
|
|
18544
|
+
}
|
|
18545
|
+
if (!author) return { ok: false, error: "author-unresolved" };
|
|
18546
|
+
return publish(active.signer, active.pubkey, {
|
|
18547
|
+
kind: 1984,
|
|
18548
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
18549
|
+
tags: [
|
|
18550
|
+
["e", target.id.toLowerCase(), target.relay ?? "", reason],
|
|
18551
|
+
["p", author.toLowerCase(), "", reason]
|
|
18552
|
+
],
|
|
18553
|
+
content: text
|
|
18554
|
+
});
|
|
18555
|
+
} catch (error) {
|
|
18556
|
+
return actionError(error);
|
|
18557
|
+
}
|
|
18341
18558
|
}
|
|
18342
|
-
|
|
18343
|
-
}
|
|
18344
|
-
if (foundHashMismatch) {
|
|
18345
|
-
throw new ResourceServiceError("decode-failed", "Blossom response did not match the requested SHA-256");
|
|
18346
|
-
}
|
|
18347
|
-
if (foundInconclusive) {
|
|
18348
|
-
throw new ResourceServiceError("network-error", lastNetworkMessage);
|
|
18349
|
-
}
|
|
18350
|
-
if (foundNotFound) {
|
|
18351
|
-
throw new ResourceServiceError("not-found", "Blossom resource was not found");
|
|
18352
|
-
}
|
|
18353
|
-
throw new ResourceServiceError("network-error", lastNetworkMessage);
|
|
18559
|
+
};
|
|
18354
18560
|
}
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18363
|
-
|
|
18364
|
-
|
|
18561
|
+
|
|
18562
|
+
// src/browser-lists.ts
|
|
18563
|
+
var HEX_643 = /^[0-9a-f]{64}$/i;
|
|
18564
|
+
var ADDRESS = /^\d+:[0-9a-f]{64}:.+$/i;
|
|
18565
|
+
var SUPPORTED_LISTS = [
|
|
18566
|
+
{
|
|
18567
|
+
kind: 1e4,
|
|
18568
|
+
type: "mute-list",
|
|
18569
|
+
addressable: false,
|
|
18570
|
+
supportedItemTypes: ["pubkey", "event", "hashtag", "word"],
|
|
18571
|
+
privateItems: false,
|
|
18572
|
+
tagByItemType: { pubkey: "p", event: "e", hashtag: "t", word: "word" }
|
|
18573
|
+
},
|
|
18574
|
+
{
|
|
18575
|
+
kind: 10002,
|
|
18576
|
+
type: "relay-list-metadata",
|
|
18577
|
+
addressable: false,
|
|
18578
|
+
supportedItemTypes: ["relay"],
|
|
18579
|
+
privateItems: false,
|
|
18580
|
+
tagByItemType: { relay: "r" }
|
|
18581
|
+
},
|
|
18582
|
+
{
|
|
18583
|
+
kind: 10003,
|
|
18584
|
+
type: "bookmarks",
|
|
18585
|
+
addressable: false,
|
|
18586
|
+
supportedItemTypes: ["event", "address"],
|
|
18587
|
+
privateItems: false,
|
|
18588
|
+
tagByItemType: { event: "e", address: "a" }
|
|
18589
|
+
},
|
|
18590
|
+
{
|
|
18591
|
+
kind: 3e4,
|
|
18592
|
+
type: "follow-sets",
|
|
18593
|
+
addressable: true,
|
|
18594
|
+
supportedItemTypes: ["pubkey"],
|
|
18595
|
+
privateItems: false,
|
|
18596
|
+
tagByItemType: { pubkey: "p" }
|
|
18365
18597
|
}
|
|
18366
|
-
|
|
18598
|
+
];
|
|
18599
|
+
function publicSupport() {
|
|
18600
|
+
return SUPPORTED_LISTS.map(({ tagByItemType: _tagByItemType, ...support }) => ({
|
|
18601
|
+
...support,
|
|
18602
|
+
supportedItemTypes: support.supportedItemTypes ? [...support.supportedItemTypes] : void 0
|
|
18603
|
+
}));
|
|
18367
18604
|
}
|
|
18368
|
-
function
|
|
18369
|
-
|
|
18605
|
+
function failure2(error, reason) {
|
|
18606
|
+
return { ok: false, error, reason, supported: publicSupport() };
|
|
18607
|
+
}
|
|
18608
|
+
function resolveList(ref) {
|
|
18609
|
+
const input = ref;
|
|
18610
|
+
const hasKind = typeof input.kind === "number";
|
|
18611
|
+
const hasType = typeof input.type === "string" && input.type.length > 0;
|
|
18612
|
+
if (hasKind === hasType) return failure2("invalid-list-ref", "exactly one list kind or type is required");
|
|
18613
|
+
const candidates = hasKind ? SUPPORTED_LISTS.filter((support2) => support2.kind === input.kind) : SUPPORTED_LISTS.filter((support2) => support2.type === input.type);
|
|
18614
|
+
if (candidates.length === 0) return failure2("unsupported-list", "list kind or type is not supported");
|
|
18615
|
+
if (candidates.length > 1) return failure2("ambiguous-list", "list type maps to multiple supported kinds");
|
|
18616
|
+
const support = candidates[0];
|
|
18617
|
+
const identifier = typeof input.identifier === "string" && input.identifier.length > 0 ? input.identifier : void 0;
|
|
18618
|
+
if (support.addressable && !identifier) return failure2("missing-identifier", "addressable lists require an identifier");
|
|
18619
|
+
if (!support.addressable && identifier) return failure2("invalid-list-ref", "replaceable list does not accept an identifier");
|
|
18620
|
+
return { support, ...identifier ? { identifier } : {} };
|
|
18621
|
+
}
|
|
18622
|
+
function relayUrl(value) {
|
|
18370
18623
|
try {
|
|
18371
|
-
const
|
|
18372
|
-
|
|
18373
|
-
return url.origin;
|
|
18624
|
+
const parsed = new URL(value);
|
|
18625
|
+
return parsed.protocol === "ws:" || parsed.protocol === "wss:" ? parsed.href : null;
|
|
18374
18626
|
} catch {
|
|
18375
18627
|
return null;
|
|
18376
18628
|
}
|
|
18377
18629
|
}
|
|
18378
|
-
function
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
if (
|
|
18382
|
-
|
|
18383
|
-
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
const [first, second] = octets;
|
|
18387
|
-
return first !== 0 && first !== 10 && first !== 127 && first < 224 && !(first === 100 && second >= 64 && second <= 127) && !(first === 169 && second === 254) && !(first === 172 && second >= 16 && second <= 31) && !(first === 192 && second === 168) && !(first === 198 && (second === 18 || second === 19));
|
|
18388
|
-
}
|
|
18389
|
-
function usableBlossomServers(values) {
|
|
18390
|
-
const servers = [];
|
|
18391
|
-
for (const value of values) {
|
|
18392
|
-
try {
|
|
18393
|
-
const [server] = normalizeUploadServers([value]);
|
|
18394
|
-
if (server && !servers.includes(server)) servers.push(server);
|
|
18395
|
-
} catch {
|
|
18396
|
-
}
|
|
18397
|
-
}
|
|
18398
|
-
return servers;
|
|
18399
|
-
}
|
|
18400
|
-
async function readCappedResponse(response, signal) {
|
|
18401
|
-
const declaredLength = Number(response.headers.get("content-length"));
|
|
18402
|
-
if (Number.isFinite(declaredLength) && declaredLength > PAJA_RESOURCE_MAX_BYTES) {
|
|
18403
|
-
throw resourceTooLarge();
|
|
18630
|
+
function itemTag(support, item) {
|
|
18631
|
+
if (item.visibility === "private") return failure2("private-items-unsupported", "private list items require NIP-44 support");
|
|
18632
|
+
const tagName = support.tagByItemType[item.itemType];
|
|
18633
|
+
if (!tagName) return failure2("unsupported-item", `${item.itemType} is not supported by ${support.type}`);
|
|
18634
|
+
const value = item.value.trim();
|
|
18635
|
+
if (!value) return failure2("invalid-item", "list item value is empty");
|
|
18636
|
+
if ((item.itemType === "pubkey" || item.itemType === "event") && !HEX_643.test(value)) {
|
|
18637
|
+
return failure2("invalid-item", `${item.itemType} values must be 32-byte hex`);
|
|
18404
18638
|
}
|
|
18405
|
-
if (!
|
|
18406
|
-
|
|
18407
|
-
if (bytes2.byteLength > PAJA_RESOURCE_MAX_BYTES) throw resourceTooLarge();
|
|
18408
|
-
return bytes2;
|
|
18639
|
+
if (item.itemType === "address" && !ADDRESS.test(value)) {
|
|
18640
|
+
return failure2("invalid-item", "address values must use kind:pubkey:identifier");
|
|
18409
18641
|
}
|
|
18410
|
-
|
|
18411
|
-
|
|
18412
|
-
|
|
18413
|
-
|
|
18414
|
-
|
|
18415
|
-
|
|
18416
|
-
const { done, value } = await reader.read();
|
|
18417
|
-
if (done) break;
|
|
18418
|
-
total += value.byteLength;
|
|
18419
|
-
if (total > PAJA_RESOURCE_MAX_BYTES) {
|
|
18420
|
-
await reader.cancel();
|
|
18421
|
-
throw resourceTooLarge();
|
|
18422
|
-
}
|
|
18423
|
-
chunks.push(value);
|
|
18642
|
+
if (support.kind === 10002) {
|
|
18643
|
+
const url = relayUrl(value);
|
|
18644
|
+
if (!url) return failure2("invalid-item", "relay list items require ws or wss URLs");
|
|
18645
|
+
const marker = item.marker ?? "read-write";
|
|
18646
|
+
if (marker !== "read" && marker !== "write" && marker !== "read-write") {
|
|
18647
|
+
return failure2("invalid-item", "relay marker must be read, write, or read-write");
|
|
18424
18648
|
}
|
|
18425
|
-
|
|
18426
|
-
reader.releaseLock();
|
|
18427
|
-
}
|
|
18428
|
-
const bytes = new Uint8Array(total);
|
|
18429
|
-
let offset = 0;
|
|
18430
|
-
for (const chunk of chunks) {
|
|
18431
|
-
bytes.set(chunk, offset);
|
|
18432
|
-
offset += chunk.byteLength;
|
|
18649
|
+
return marker === "read-write" ? ["r", url] : ["r", url, marker];
|
|
18433
18650
|
}
|
|
18434
|
-
|
|
18651
|
+
const tag = [tagName, value];
|
|
18652
|
+
if (item.relay) tag.push(item.relay);
|
|
18653
|
+
if (item.label) tag.push(item.label);
|
|
18654
|
+
return tag;
|
|
18435
18655
|
}
|
|
18436
|
-
function
|
|
18437
|
-
return
|
|
18656
|
+
function isFailure(value) {
|
|
18657
|
+
return !Array.isArray(value);
|
|
18438
18658
|
}
|
|
18439
|
-
function
|
|
18440
|
-
|
|
18659
|
+
function sameItem(tag, expected) {
|
|
18660
|
+
if (tag[0] !== expected[0] || tag[1] !== expected[1]) return false;
|
|
18661
|
+
if (expected.length > 2 && tag[2] !== expected[2]) return false;
|
|
18662
|
+
if (expected.length > 3 && tag[3] !== expected[3]) return false;
|
|
18663
|
+
return true;
|
|
18441
18664
|
}
|
|
18442
|
-
function
|
|
18443
|
-
|
|
18665
|
+
function relayCoverage(tag) {
|
|
18666
|
+
if (tag[2] === "read") return /* @__PURE__ */ new Set(["read"]);
|
|
18667
|
+
if (tag[2] === "write") return /* @__PURE__ */ new Set(["write"]);
|
|
18668
|
+
return /* @__PURE__ */ new Set(["read", "write"]);
|
|
18444
18669
|
}
|
|
18445
|
-
function
|
|
18446
|
-
|
|
18670
|
+
function relayTag(url, coverage) {
|
|
18671
|
+
if (coverage.size === 0) return null;
|
|
18672
|
+
if (coverage.size === 2) return ["r", url];
|
|
18673
|
+
return ["r", url, coverage.has("read") ? "read" : "write"];
|
|
18447
18674
|
}
|
|
18448
|
-
function
|
|
18449
|
-
return
|
|
18675
|
+
function sameRelay(tag, url) {
|
|
18676
|
+
return tag[0] === "r" && typeof tag[1] === "string" && relayUrl(tag[1]) === url;
|
|
18450
18677
|
}
|
|
18451
|
-
function
|
|
18452
|
-
|
|
18678
|
+
function addRelayItem(tags, expected) {
|
|
18679
|
+
const url = expected[1];
|
|
18680
|
+
const matching = tags.filter((tag) => sameRelay(tag, url));
|
|
18681
|
+
const coverage = /* @__PURE__ */ new Set();
|
|
18682
|
+
for (const tag of matching) for (const direction of relayCoverage(tag)) coverage.add(direction);
|
|
18683
|
+
const requested = relayCoverage(expected);
|
|
18684
|
+
const before = coverage.size;
|
|
18685
|
+
for (const direction of requested) coverage.add(direction);
|
|
18686
|
+
if (coverage.size === before) return { tags, changed: false };
|
|
18687
|
+
const retained = tags.filter((tag) => !sameRelay(tag, url));
|
|
18688
|
+
retained.push(relayTag(url, coverage));
|
|
18689
|
+
return { tags: retained, changed: true };
|
|
18453
18690
|
}
|
|
18454
|
-
function
|
|
18455
|
-
|
|
18456
|
-
|
|
18457
|
-
if (
|
|
18458
|
-
|
|
18459
|
-
if (
|
|
18460
|
-
|
|
18461
|
-
|
|
18462
|
-
|
|
18463
|
-
|
|
18464
|
-
if (
|
|
18465
|
-
|
|
18466
|
-
return sniffSafeTextMime(bytes);
|
|
18691
|
+
function removeRelayItem(tags, expected) {
|
|
18692
|
+
const url = expected[1];
|
|
18693
|
+
const matching = tags.filter((tag) => sameRelay(tag, url));
|
|
18694
|
+
if (matching.length === 0) return { tags, changed: false };
|
|
18695
|
+
const retained = tags.filter((tag) => !sameRelay(tag, url));
|
|
18696
|
+
if (expected.length === 2) return { tags: retained, changed: true };
|
|
18697
|
+
const coverage = /* @__PURE__ */ new Set();
|
|
18698
|
+
for (const tag of matching) for (const direction of relayCoverage(tag)) coverage.add(direction);
|
|
18699
|
+
coverage.delete(expected[2]);
|
|
18700
|
+
const rewritten = relayTag(url, coverage);
|
|
18701
|
+
if (rewritten) retained.push(rewritten);
|
|
18702
|
+
return { tags: retained, changed: true };
|
|
18467
18703
|
}
|
|
18468
|
-
function
|
|
18469
|
-
|
|
18470
|
-
|
|
18471
|
-
|
|
18472
|
-
|
|
18473
|
-
|
|
18474
|
-
|
|
18475
|
-
|
|
18476
|
-
|
|
18477
|
-
|
|
18478
|
-
|
|
18704
|
+
function metadataTags(resolved, options) {
|
|
18705
|
+
const tags = [];
|
|
18706
|
+
if (resolved.identifier) tags.push(["d", resolved.identifier]);
|
|
18707
|
+
if (options?.title) tags.push(["title", options.title]);
|
|
18708
|
+
if (options?.description) tags.push(["description", options.description]);
|
|
18709
|
+
if (options?.image) tags.push(["image", options.image]);
|
|
18710
|
+
return tags;
|
|
18711
|
+
}
|
|
18712
|
+
function listFilter(resolved, author) {
|
|
18713
|
+
return {
|
|
18714
|
+
kinds: [resolved.support.kind],
|
|
18715
|
+
authors: [author],
|
|
18716
|
+
limit: 16,
|
|
18717
|
+
...resolved.identifier ? { ["#d"]: [resolved.identifier] } : {}
|
|
18718
|
+
};
|
|
18719
|
+
}
|
|
18720
|
+
function newestList(events, resolved, author) {
|
|
18721
|
+
return events.filter((event) => event.kind === resolved.support.kind && event.pubkey.toLowerCase() === author && (!resolved.identifier || event.tags.some((tag) => tag[0] === "d" && tag[1] === resolved.identifier)) && verifyEvent(event)).sort((left, right) => right.created_at - left.created_at || left.id.localeCompare(right.id))[0] ?? null;
|
|
18722
|
+
}
|
|
18723
|
+
function createPajaListsBackend(options) {
|
|
18724
|
+
const activeSigner = async () => {
|
|
18725
|
+
const signer = options.getSigner();
|
|
18726
|
+
if (!signer?.getPublicKey || !signer.signEvent) throw new Error("not-signed-in");
|
|
18727
|
+
const pubkey2 = (await signer.getPublicKey()).toLowerCase();
|
|
18728
|
+
if (!HEX_643.test(pubkey2)) throw new Error("not-signed-in");
|
|
18729
|
+
return { signer, pubkey: pubkey2 };
|
|
18730
|
+
};
|
|
18731
|
+
const mutation = async (operation, ref, items, mutationOptions) => {
|
|
18732
|
+
const resolved = resolveList(ref);
|
|
18733
|
+
if ("ok" in resolved) return resolved;
|
|
18734
|
+
if (items.length === 0) return { ok: true, [operation === "add" ? "added" : "removed"]: 0, skipped: 0 };
|
|
18735
|
+
const itemTags = [];
|
|
18736
|
+
for (const item of items) {
|
|
18737
|
+
const tag = itemTag(resolved.support, item);
|
|
18738
|
+
if (isFailure(tag)) return tag;
|
|
18739
|
+
itemTags.push(tag);
|
|
18740
|
+
}
|
|
18741
|
+
let active;
|
|
18479
18742
|
try {
|
|
18480
|
-
|
|
18481
|
-
return "application/json";
|
|
18743
|
+
active = await activeSigner();
|
|
18482
18744
|
} catch {
|
|
18483
|
-
return "
|
|
18745
|
+
return failure2("not-signed-in", "list mutations require an active signer");
|
|
18484
18746
|
}
|
|
18485
|
-
|
|
18486
|
-
|
|
18747
|
+
let current;
|
|
18748
|
+
try {
|
|
18749
|
+
const events = await options.relay.query(
|
|
18750
|
+
options.getRelays(),
|
|
18751
|
+
[listFilter(resolved, active.pubkey)],
|
|
18752
|
+
PAJA_LIVE_QUERY_WAIT_MS
|
|
18753
|
+
);
|
|
18754
|
+
current = newestList(events, resolved, active.pubkey);
|
|
18755
|
+
} catch {
|
|
18756
|
+
return failure2("list-unavailable", "current list could not be loaded");
|
|
18757
|
+
}
|
|
18758
|
+
if (!current && !(operation === "add" && mutationOptions?.create)) {
|
|
18759
|
+
return failure2("list-not-found", "list does not exist and creation was not requested");
|
|
18760
|
+
}
|
|
18761
|
+
if (operation === "remove" && current?.content && items.some((item) => item.visibility !== "public")) {
|
|
18762
|
+
return failure2("private-items-unsupported", "cannot safely remove private items without NIP-44 support");
|
|
18763
|
+
}
|
|
18764
|
+
let tags = current ? current.tags.map((tag) => [...tag]) : metadataTags(resolved, mutationOptions);
|
|
18765
|
+
let changed = 0;
|
|
18766
|
+
for (const expected of itemTags) {
|
|
18767
|
+
if (resolved.support.kind === 10002) {
|
|
18768
|
+
const result2 = operation === "add" ? addRelayItem(tags, expected) : removeRelayItem(tags, expected);
|
|
18769
|
+
tags = result2.tags;
|
|
18770
|
+
if (result2.changed) changed += 1;
|
|
18771
|
+
continue;
|
|
18772
|
+
}
|
|
18773
|
+
if (operation === "add") {
|
|
18774
|
+
if (!tags.some((tag) => sameItem(tag, expected))) {
|
|
18775
|
+
tags.push(expected);
|
|
18776
|
+
changed += 1;
|
|
18777
|
+
}
|
|
18778
|
+
} else {
|
|
18779
|
+
const before = tags.length;
|
|
18780
|
+
tags = tags.filter((tag) => !sameItem(tag, expected));
|
|
18781
|
+
if (tags.length < before) changed += 1;
|
|
18782
|
+
}
|
|
18783
|
+
}
|
|
18784
|
+
const skipped = items.length - changed;
|
|
18785
|
+
if (changed === 0) {
|
|
18786
|
+
return {
|
|
18787
|
+
ok: true,
|
|
18788
|
+
...current ? { eventId: current.id, event: { ...current } } : {},
|
|
18789
|
+
[operation === "add" ? "added" : "removed"]: 0,
|
|
18790
|
+
skipped
|
|
18791
|
+
};
|
|
18792
|
+
}
|
|
18793
|
+
try {
|
|
18794
|
+
const template = {
|
|
18795
|
+
kind: resolved.support.kind,
|
|
18796
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
18797
|
+
tags,
|
|
18798
|
+
content: current?.content ?? ""
|
|
18799
|
+
};
|
|
18800
|
+
const event = await active.signer.signEvent(template);
|
|
18801
|
+
if (event.pubkey.toLowerCase() !== active.pubkey || !verifyEvent(event)) {
|
|
18802
|
+
return failure2("publish-failed", "signer returned an invalid list event");
|
|
18803
|
+
}
|
|
18804
|
+
await options.relay.publish(options.getRelays(), event);
|
|
18805
|
+
return {
|
|
18806
|
+
ok: true,
|
|
18807
|
+
eventId: event.id,
|
|
18808
|
+
event: { ...event },
|
|
18809
|
+
[operation === "add" ? "added" : "removed"]: changed,
|
|
18810
|
+
skipped
|
|
18811
|
+
};
|
|
18812
|
+
} catch (error) {
|
|
18813
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18814
|
+
return failure2(message.includes("denied") ? "user-denied" : "publish-failed", message);
|
|
18815
|
+
}
|
|
18816
|
+
};
|
|
18817
|
+
return {
|
|
18818
|
+
supported: publicSupport,
|
|
18819
|
+
add: (list, items, mutationOptions) => mutation("add", list, items, mutationOptions),
|
|
18820
|
+
remove: (list, items, mutationOptions) => mutation("remove", list, items, mutationOptions)
|
|
18821
|
+
};
|
|
18487
18822
|
}
|
|
18488
18823
|
|
|
18489
18824
|
// src/browser-webrtc.ts
|
|
@@ -18520,7 +18855,7 @@ var BrowserWebrtcController = class {
|
|
|
18520
18855
|
};
|
|
18521
18856
|
refreshAvailability() {
|
|
18522
18857
|
const signer = this.options.getSigner();
|
|
18523
|
-
const nextPubkey = isSignalingSigner(signer) ?
|
|
18858
|
+
const nextPubkey = isSignalingSigner(signer) ? normalizePubkey3(this.options.getPubkey()) : null;
|
|
18524
18859
|
if (this.activePubkey !== nextPubkey) {
|
|
18525
18860
|
this.dispose();
|
|
18526
18861
|
this.activePubkey = nextPubkey;
|
|
@@ -18540,8 +18875,8 @@ var BrowserWebrtcController = class {
|
|
|
18540
18875
|
throw new Error("session limit reached");
|
|
18541
18876
|
}
|
|
18542
18877
|
const signer = this.requireSigner();
|
|
18543
|
-
const localPubkey =
|
|
18544
|
-
if (!localPubkey || localPubkey !==
|
|
18878
|
+
const localPubkey = normalizePubkey3(await signer.getPublicKey());
|
|
18879
|
+
if (!localPubkey || localPubkey !== normalizePubkey3(this.options.getPubkey())) throw new Error("signaling unavailable");
|
|
18545
18880
|
const identity = this.options.getIdentity(context.windowId);
|
|
18546
18881
|
const allowed2 = await this.options.confirm({
|
|
18547
18882
|
action: "webrtc",
|
|
@@ -18554,7 +18889,7 @@ var BrowserWebrtcController = class {
|
|
|
18554
18889
|
const id = this.crypto.randomUUID();
|
|
18555
18890
|
const channel = request.channel ?? "default";
|
|
18556
18891
|
const roomHash = request.scope.type === "room" ? await hashRoom(this.crypto, request.scope.room) : void 0;
|
|
18557
|
-
const allowedPeers = request.scope.type === "direct" ? /* @__PURE__ */ new Set([
|
|
18892
|
+
const allowedPeers = request.scope.type === "direct" ? /* @__PURE__ */ new Set([normalizePubkey3(request.scope.pubkey)]) : request.scope.peers && request.scope.peers.length > 0 ? new Set(request.scope.peers.map((peer) => normalizePubkey3(peer))) : null;
|
|
18558
18893
|
const session = {
|
|
18559
18894
|
id,
|
|
18560
18895
|
windowId: context.windowId,
|
|
@@ -18731,7 +19066,7 @@ var BrowserWebrtcController = class {
|
|
|
18731
19066
|
async publishPresence(session, type, recipient) {
|
|
18732
19067
|
const tags = [["type", type]];
|
|
18733
19068
|
if (recipient) tags.push(["p", recipient]);
|
|
18734
|
-
else if (session.request.scope.type === "direct") tags.push(["p",
|
|
19069
|
+
else if (session.request.scope.type === "direct") tags.push(["p", normalizePubkey3(session.request.scope.pubkey)]);
|
|
18735
19070
|
else if (session.allowedPeers) {
|
|
18736
19071
|
for (const peer of session.allowedPeers) tags.push(["p", peer]);
|
|
18737
19072
|
}
|
|
@@ -18788,7 +19123,7 @@ var BrowserWebrtcController = class {
|
|
|
18788
19123
|
}
|
|
18789
19124
|
requireSessionSigner(session) {
|
|
18790
19125
|
const signer = this.requireSigner();
|
|
18791
|
-
if (
|
|
19126
|
+
if (normalizePubkey3(this.options.getPubkey()) !== session.localPubkey) throw new Error("signaling unavailable");
|
|
18792
19127
|
return signer;
|
|
18793
19128
|
}
|
|
18794
19129
|
removePeer(session, pubkey2, notify) {
|
|
@@ -18834,12 +19169,12 @@ function validateOpenRequest(request) {
|
|
|
18834
19169
|
throw new Error("unsupported scope");
|
|
18835
19170
|
}
|
|
18836
19171
|
if (request.scope.type === "direct") {
|
|
18837
|
-
if (!
|
|
19172
|
+
if (!normalizePubkey3(request.scope.pubkey)) throw new Error("unsupported scope");
|
|
18838
19173
|
} else if (request.scope.type === "room") {
|
|
18839
19174
|
if (typeof request.scope.room !== "string" || request.scope.room.length < 8 || request.scope.room.length > 128) {
|
|
18840
19175
|
throw new Error("unsupported scope");
|
|
18841
19176
|
}
|
|
18842
|
-
if (request.scope.peers && (request.scope.peers.length > PAJA_WEBRTC_MAX_PEERS || request.scope.peers.some((peer) => !
|
|
19177
|
+
if (request.scope.peers && (request.scope.peers.length > PAJA_WEBRTC_MAX_PEERS || request.scope.peers.some((peer) => !normalizePubkey3(peer)))) throw new Error("unsupported scope");
|
|
18843
19178
|
} else {
|
|
18844
19179
|
throw new Error("unsupported scope");
|
|
18845
19180
|
}
|
|
@@ -18853,17 +19188,17 @@ function signalFilters(session) {
|
|
|
18853
19188
|
return session.roomHash ? [{ ...base, "#r": [session.roomHash] }] : [{ ...base, "#p": [session.localPubkey] }];
|
|
18854
19189
|
}
|
|
18855
19190
|
function scopeAllowsPeer(session, pubkey2) {
|
|
18856
|
-
return
|
|
19191
|
+
return normalizePubkey3(pubkey2) !== null && pubkey2 !== session.localPubkey && (session.allowedPeers === null || session.allowedPeers.has(pubkey2));
|
|
18857
19192
|
}
|
|
18858
19193
|
function isSignalPayload(value, session) {
|
|
18859
|
-
if (!
|
|
18860
|
-
if (value.channel !== session.channel || value.protocol !== session.protocol || !
|
|
19194
|
+
if (!isRecord6(value) || value.version !== 1 || typeof value.signalId !== "string") return false;
|
|
19195
|
+
if (value.channel !== session.channel || value.protocol !== session.protocol || !isRecord6(value.description)) return false;
|
|
18861
19196
|
return (value.description.type === "offer" || value.description.type === "answer") && typeof value.description.sdp === "string";
|
|
18862
19197
|
}
|
|
18863
19198
|
function isSignalingSigner(signer) {
|
|
18864
19199
|
return typeof signer?.getPublicKey === "function" && typeof signer.signEvent === "function" && typeof signer.nip44?.encrypt === "function" && typeof signer.nip44.decrypt === "function";
|
|
18865
19200
|
}
|
|
18866
|
-
function
|
|
19201
|
+
function normalizePubkey3(value) {
|
|
18867
19202
|
return typeof value === "string" && /^[0-9a-f]{64}$/iu.test(value) ? value.toLowerCase() : null;
|
|
18868
19203
|
}
|
|
18869
19204
|
function describeScope(request) {
|
|
@@ -18890,7 +19225,7 @@ function tagValue2(tags, name) {
|
|
|
18890
19225
|
function tagValues(tags, name) {
|
|
18891
19226
|
return tags.filter((tag) => tag[0] === name && typeof tag[1] === "string").map((tag) => tag[1]);
|
|
18892
19227
|
}
|
|
18893
|
-
function
|
|
19228
|
+
function isRecord6(value) {
|
|
18894
19229
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18895
19230
|
}
|
|
18896
19231
|
function defaultPeerConnectionFactory() {
|
|
@@ -19685,7 +20020,7 @@ function createRuntimeSigner(getSimulation, confirmRequest, signerProvider, sour
|
|
|
19685
20020
|
}
|
|
19686
20021
|
return signer;
|
|
19687
20022
|
}
|
|
19688
|
-
function createDevServices(backend, getSimulation, relayConfig, onThemeService, onThemeBroadcast, confirmRequest,
|
|
20023
|
+
function createDevServices(backend, getSimulation, relayConfig, onThemeService, onThemeBroadcast, confirmRequest, getConfiguredBlossomServers, uploadRuntime, signerProvider, intentHost, getIdentity, userActivation, notifyOptions, configOptions) {
|
|
19689
20024
|
const theme = createThemeService({
|
|
19690
20025
|
initialTheme: createDevTheme(getSimulation().theme.mode, getSimulation().theme.values),
|
|
19691
20026
|
onBroadcast: onThemeBroadcast
|
|
@@ -19696,6 +20031,11 @@ function createDevServices(backend, getSimulation, relayConfig, onThemeService,
|
|
|
19696
20031
|
const getWriteRelays = () => relayConfig.getRelayUrls(["outbox"]);
|
|
19697
20032
|
const getAllRelays = () => relayConfig.getRelayUrls();
|
|
19698
20033
|
const baseOutboxRouter = createOutboxRouter(backend, getSimulation, relayConfig, confirmRequest, signerProvider);
|
|
20034
|
+
const blossomEventResolver = createPajaBlossomEventResolver({
|
|
20035
|
+
baseRouter: baseOutboxRouter,
|
|
20036
|
+
getDefaultAuthors: () => [getRuntimePubkey(getSimulation, signerProvider)],
|
|
20037
|
+
getConfiguredServers: getConfiguredBlossomServers
|
|
20038
|
+
});
|
|
19699
20039
|
const socialCache = createPajaSocialCache({
|
|
19700
20040
|
baseRouter: baseOutboxRouter,
|
|
19701
20041
|
loadContactList: createPajaContactListLoader(backend, getSimulation, signerProvider, getReadRelays),
|
|
@@ -19714,11 +20054,20 @@ function createDevServices(backend, getSimulation, relayConfig, onThemeService,
|
|
|
19714
20054
|
getSigner: () => createRuntimeSigner(getSimulation, confirmRequest, signerProvider)
|
|
19715
20055
|
});
|
|
19716
20056
|
void socialCache.refreshActiveIdentity();
|
|
20057
|
+
const resourceService = createResourceService({
|
|
20058
|
+
fetch: createPajaResourceFetch({
|
|
20059
|
+
getBlossomServers: ({ url, windowId }) => blossomEventResolver.getServers(url, windowId)
|
|
20060
|
+
}),
|
|
20061
|
+
resourceInfo: () => pajaResourceInfo()
|
|
20062
|
+
});
|
|
19717
20063
|
const services = {
|
|
19718
|
-
resource:
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
20064
|
+
resource: {
|
|
20065
|
+
...resourceService,
|
|
20066
|
+
onWindowDestroyed(windowId) {
|
|
20067
|
+
resourceService.onWindowDestroyed?.(windowId);
|
|
20068
|
+
blossomEventResolver.clearWindow(windowId);
|
|
20069
|
+
}
|
|
20070
|
+
}
|
|
19722
20071
|
};
|
|
19723
20072
|
if (getSimulation().capabilities.domains.keys && typeof document !== "undefined") {
|
|
19724
20073
|
services.keys = createKeysService({ listenerTarget: document });
|
|
@@ -19742,9 +20091,13 @@ function createDevServices(backend, getSimulation, relayConfig, onThemeService,
|
|
|
19742
20091
|
});
|
|
19743
20092
|
services.outbox = createOutboxService({
|
|
19744
20093
|
router: baseOutboxRouter,
|
|
19745
|
-
|
|
19746
|
-
|
|
19747
|
-
(
|
|
20094
|
+
getReadRouter: (windowId) => blossomEventResolver.decorate(baseOutboxRouter, windowId),
|
|
20095
|
+
getQueryRouter: (windowId, context) => blossomEventResolver.decorate(
|
|
20096
|
+
socialCache.decorate(
|
|
20097
|
+
baseOutboxRouter,
|
|
20098
|
+
() => context?.hasCapability(windowId, "identity:read") ?? false
|
|
20099
|
+
),
|
|
20100
|
+
windowId
|
|
19748
20101
|
)
|
|
19749
20102
|
});
|
|
19750
20103
|
}
|
|
@@ -19914,7 +20267,7 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
|
|
|
19914
20267
|
getNappletIdentity: resolveIdentity,
|
|
19915
20268
|
subscribeSignerChange: signerProvider?.subscribe?.bind(signerProvider)
|
|
19916
20269
|
}) : void 0;
|
|
19917
|
-
const
|
|
20270
|
+
const getConfiguredBlossomServers = () => [
|
|
19918
20271
|
...config.target?.pointer?.blossomServers ?? [],
|
|
19919
20272
|
...uploadRuntime?.getServers() ?? getSimulation().upload.servers
|
|
19920
20273
|
];
|
|
@@ -19927,7 +20280,7 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
|
|
|
19927
20280
|
onThemeService,
|
|
19928
20281
|
onThemeBroadcast,
|
|
19929
20282
|
confirmRequest,
|
|
19930
|
-
|
|
20283
|
+
getConfiguredBlossomServers,
|
|
19931
20284
|
uploadRuntime,
|
|
19932
20285
|
signerProvider,
|
|
19933
20286
|
intentHost,
|
|
@@ -20456,14 +20809,14 @@ function createPajaPostMessageProxy(realWin, state, windowId) {
|
|
|
20456
20809
|
return proxy;
|
|
20457
20810
|
}
|
|
20458
20811
|
function redactPajaMessageForLog(message) {
|
|
20459
|
-
if (!
|
|
20812
|
+
if (!isRecord7(message) || message.type !== "config.values") return message;
|
|
20460
20813
|
return {
|
|
20461
20814
|
type: "config.values",
|
|
20462
20815
|
...typeof message.id === "string" ? { id: message.id } : {},
|
|
20463
20816
|
values: "[redacted by host]"
|
|
20464
20817
|
};
|
|
20465
20818
|
}
|
|
20466
|
-
function
|
|
20819
|
+
function isRecord7(value) {
|
|
20467
20820
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20468
20821
|
}
|
|
20469
20822
|
function installPajaOriginRegistryProxy(originRegistry2, stateRef) {
|
|
@@ -22234,7 +22587,7 @@ function createPajaConfigController(options) {
|
|
|
22234
22587
|
const raw = storage.getItem(storageKey(options.getIdentity(windowId)));
|
|
22235
22588
|
if (!raw) return cloneValues(options.getInitialValues?.(windowId) ?? {});
|
|
22236
22589
|
const value = JSON.parse(raw);
|
|
22237
|
-
return
|
|
22590
|
+
return isRecord8(value) ? value : {};
|
|
22238
22591
|
} catch {
|
|
22239
22592
|
return {};
|
|
22240
22593
|
}
|
|
@@ -22297,9 +22650,9 @@ function cloneValues(values) {
|
|
|
22297
22650
|
}
|
|
22298
22651
|
}
|
|
22299
22652
|
function renderObjectFields(document2, parent, schema, values, path, bindings, selectedSection) {
|
|
22300
|
-
const properties =
|
|
22653
|
+
const properties = isRecord8(schema.properties) ? schema.properties : {};
|
|
22301
22654
|
const required = new Set(Array.isArray(schema.required) ? schema.required.filter((key) => typeof key === "string") : []);
|
|
22302
|
-
const ordered = Object.entries(properties).filter((entry) =>
|
|
22655
|
+
const ordered = Object.entries(properties).filter((entry) => isRecord8(entry[1])).filter(([, child2]) => !selectedSection || child2["x-napplet-section"] === selectedSection || child2.type === "object").sort(([leftKey, left], [rightKey, right]) => compareSchemaFields(leftKey, left, rightKey, right));
|
|
22303
22656
|
for (const [key, rawChild] of ordered) {
|
|
22304
22657
|
const child2 = rawChild;
|
|
22305
22658
|
const childPath = [...path, key];
|
|
@@ -22397,7 +22750,7 @@ function assignPath(target, path, value) {
|
|
|
22397
22750
|
let cursor = target;
|
|
22398
22751
|
for (const segment of path.slice(0, -1)) {
|
|
22399
22752
|
const child2 = cursor[segment];
|
|
22400
|
-
if (!
|
|
22753
|
+
if (!isRecord8(child2)) cursor[segment] = {};
|
|
22401
22754
|
cursor = cursor[segment];
|
|
22402
22755
|
}
|
|
22403
22756
|
const final = path.at(-1);
|
|
@@ -22406,7 +22759,7 @@ function assignPath(target, path, value) {
|
|
|
22406
22759
|
function readPath(values, path) {
|
|
22407
22760
|
let cursor = values;
|
|
22408
22761
|
for (const segment of path) {
|
|
22409
|
-
if (!
|
|
22762
|
+
if (!isRecord8(cursor)) return void 0;
|
|
22410
22763
|
cursor = cursor[segment];
|
|
22411
22764
|
}
|
|
22412
22765
|
return cursor;
|
|
@@ -22448,7 +22801,7 @@ function readGlobalStorage() {
|
|
|
22448
22801
|
return null;
|
|
22449
22802
|
}
|
|
22450
22803
|
}
|
|
22451
|
-
function
|
|
22804
|
+
function isRecord8(value) {
|
|
22452
22805
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22453
22806
|
}
|
|
22454
22807
|
|