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