@kehto/paja 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15656,6 +15656,115 @@ function nappletNamespacePrelude(domains) {
15656
15656
  configurable: false
15657
15657
  });
15658
15658
  }
15659
+ function insertNappletHead(html, content) {
15660
+ const opening = /^([\t\n\f\r ]*(?:<!doctype[\t\n\f\r ]+html[\t\n\f\r ]*>[\t\n\f\r ]*)?(?:<html>[\t\n\f\r ]*)?)(<head>)?/i.exec(html);
15661
+ const prefix = opening[0];
15662
+ const insertion = opening[2] ? content : `<head>${content}</head>`;
15663
+ return `${prefix}${insertion}${html.slice(prefix.length)}`;
15664
+ }
15665
+ var DEFAULT_DIRECTIVES = {
15666
+ "default-src": ["'none'"],
15667
+ "script-src": ["'unsafe-inline'", "'wasm-unsafe-eval'"],
15668
+ "style-src": ["'unsafe-inline'"],
15669
+ "img-src": ["data:", "blob:"],
15670
+ "font-src": ["data:"],
15671
+ "connect-src": ["'none'"],
15672
+ "worker-src": ["'none'"],
15673
+ "child-src": ["'none'"],
15674
+ "frame-src": ["'none'"],
15675
+ "media-src": ["'none'"],
15676
+ "object-src": ["'none'"],
15677
+ "manifest-src": ["'none'"],
15678
+ "base-uri": ["'none'"],
15679
+ "form-action": ["'none'"]
15680
+ };
15681
+ var OPTIONAL_DIRECTIVES = /* @__PURE__ */ new Set([
15682
+ "script-src-elem",
15683
+ "script-src-attr",
15684
+ "style-src-elem",
15685
+ "style-src-attr"
15686
+ ]);
15687
+ var KEYWORDS = /* @__PURE__ */ new Set([
15688
+ "'none'",
15689
+ "'self'",
15690
+ "'unsafe-inline'",
15691
+ "'wasm-unsafe-eval'",
15692
+ "'unsafe-hashes'",
15693
+ "'strict-dynamic'",
15694
+ "'report-sample'"
15695
+ ]);
15696
+ function connectionOrigin(source) {
15697
+ let url;
15698
+ try {
15699
+ url = new URL(source);
15700
+ } catch {
15701
+ throw new Error(`CSP connection grant must be an exact HTTP(S)/WS(S) origin: ${source}`);
15702
+ }
15703
+ if (!/^(https?|wss?):$/.test(url.protocol) || url.username || url.password || url.pathname !== "/" && url.pathname !== "" || url.search || url.hash || /[\s*\\;,"<>'`]/.test(source + url.origin) || !source.startsWith(`${url.protocol}//`)) {
15704
+ throw new Error(`CSP connection grant must be an exact HTTP(S)/WS(S) origin: ${source}`);
15705
+ }
15706
+ return url.origin;
15707
+ }
15708
+ function validateSources(directive, sources2) {
15709
+ if (!Array.isArray(sources2) || sources2.length === 0) {
15710
+ throw new Error(`CSP ${directive} needs a nonempty source list; use 'none' to deny it`);
15711
+ }
15712
+ const result2 = [...new Set(sources2)];
15713
+ for (const source of result2) {
15714
+ if (typeof source !== "string" || !/^[\x21-\x7e]+$/.test(source) || /[;,"<>\\]/.test(source)) {
15715
+ throw new Error(`Invalid CSP source in ${directive}`);
15716
+ }
15717
+ if (source.toLowerCase() === "'unsafe-eval'") {
15718
+ throw new Error("NIP-5D CSP forbids 'unsafe-eval'; use 'wasm-unsafe-eval' for WebAssembly");
15719
+ }
15720
+ if (source.includes("'") && !KEYWORDS.has(source) && !/^'(?:nonce-|sha(?:256|384|512)-)[A-Za-z0-9+/_-]+=*'$/.test(source)) {
15721
+ throw new Error(`Invalid CSP keyword in ${directive}: ${source}`);
15722
+ }
15723
+ }
15724
+ if (result2.includes("'none'") && result2.length !== 1) {
15725
+ throw new Error(`CSP ${directive} cannot combine 'none' with other sources`);
15726
+ }
15727
+ return result2;
15728
+ }
15729
+ function requireBootstrapScripts(directive, sources2) {
15730
+ if (!sources2.includes("'unsafe-inline'") || sources2.some(
15731
+ (source) => /^'(?:nonce-|sha(?:256|384|512)-|strict-dynamic')/.test(source)
15732
+ )) {
15733
+ throw new Error(`CSP ${directive} must allow the shell's inline namespace bootstrap`);
15734
+ }
15735
+ }
15736
+ function buildNappletCsp(options = {}) {
15737
+ const grants = [...new Set((options.connectOrigins ?? []).map(connectionOrigin))].sort();
15738
+ const policy = {
15739
+ ...DEFAULT_DIRECTIVES,
15740
+ "connect-src": grants.length ? grants : ["'none'"]
15741
+ };
15742
+ for (const [directive, sources2] of Object.entries(options.directives ?? {})) {
15743
+ if (!Object.hasOwn(DEFAULT_DIRECTIVES, directive) && !OPTIONAL_DIRECTIVES.has(directive)) {
15744
+ throw new Error(`Unsupported meta CSP directive: ${directive}`);
15745
+ }
15746
+ policy[directive] = validateSources(directive, sources2);
15747
+ }
15748
+ const connect = policy["connect-src"];
15749
+ if (!(connect.length === 1 && connect[0] === "'none'")) {
15750
+ policy["connect-src"] = [...new Set(connect.map((source) => {
15751
+ const origin = connectionOrigin(source);
15752
+ if (!grants.includes(origin)) throw new Error(`CSP connection origin was not granted: ${origin}`);
15753
+ return origin;
15754
+ }))].sort();
15755
+ }
15756
+ requireBootstrapScripts("script-src", policy["script-src"]);
15757
+ if (policy["script-src-elem"]) requireBootstrapScripts("script-src-elem", policy["script-src-elem"]);
15758
+ return Object.entries(policy).map(([name, sources2]) => `${name} ${sources2.join(" ")}`).join("; ");
15759
+ }
15760
+ function renderNappletCspMeta(options = {}) {
15761
+ const policy = buildNappletCsp(options).replace(/&/g, "&amp;").replace(/"/g, "&quot;");
15762
+ return `<meta http-equiv="Content-Security-Policy" content="${policy}">`;
15763
+ }
15764
+ function prepareNappletSrcdoc(html, options) {
15765
+ const meta = renderNappletCspMeta(options.csp);
15766
+ return insertNappletHead(html, meta + renderNappletNamespacePrelude(options));
15767
+ }
15659
15768
 
15660
15769
  // src/browser-console-panel.ts
15661
15770
  var PAJA_CONSOLE_PANEL_STORAGE_KEY = "kehto:paja:console:v1";
@@ -21333,265 +21442,6 @@ async function sha256Hex2(bytes) {
21333
21442
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
21334
21443
  }
21335
21444
 
21336
- // src/runtime-resolver.ts
21337
- var PAJA_NAPPLET_MANIFEST_KINDS = NAPPLET_KINDS;
21338
- function decodePajaPointer(value) {
21339
- const trimmed = value.trim();
21340
- const decoded = decode(trimmed);
21341
- if (decoded.type === "naddr") {
21342
- return {
21343
- type: "naddr",
21344
- value: trimmed,
21345
- identifier: decoded.data.identifier,
21346
- pubkey: decoded.data.pubkey,
21347
- kind: decoded.data.kind,
21348
- relays: [...decoded.data.relays ?? []]
21349
- };
21350
- }
21351
- if (decoded.type === "nevent") {
21352
- return {
21353
- type: "nevent",
21354
- value: trimmed,
21355
- id: decoded.data.id,
21356
- ...decoded.data.author ? { author: decoded.data.author } : {},
21357
- ...decoded.data.kind ? { kind: decoded.data.kind } : {},
21358
- relays: [...decoded.data.relays ?? []]
21359
- };
21360
- }
21361
- throw new Error(`Expected naddr or nevent pointer, got ${decoded.type}.`);
21362
- }
21363
- async function resolvePajaPointer(value, options = {}) {
21364
- const pointer = decodePajaPointer(value);
21365
- const relays = uniqueRelayUrls([...pointer.relays, ...options.relays ?? []]);
21366
- if (relays.length === 0) {
21367
- throw new Error("Pointer has no relay hints or enabled configured live relays.");
21368
- }
21369
- const pool = options.pool ?? new SimplePool();
21370
- const ownsPool = options.pool === void 0;
21371
- try {
21372
- const event = await resolvePointerEvent(pointer, pool, relays, normalizeMaxWaitMs(options.maxWaitMs));
21373
- const blossomServers = resolvePointerBlossomServers(event, options.blossomServers ?? []);
21374
- const resolved = await resolveNapplet({
21375
- event,
21376
- cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
21377
- fetchBlob: (sha256Hex3, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex3, async (url) => {
21378
- const response = await (options.fetcher ?? fetch)(url);
21379
- if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
21380
- return new Uint8Array(await response.arrayBuffer());
21381
- })
21382
- });
21383
- return {
21384
- pointer,
21385
- event,
21386
- relays,
21387
- blossomServers,
21388
- dTag: resolved.dTag,
21389
- aggregateHash: resolved.aggregateHash,
21390
- indexHtml: resolved.indexHtml,
21391
- manifest: resolved.manifest
21392
- };
21393
- } finally {
21394
- if (ownsPool) {
21395
- pool.close?.(relays);
21396
- pool.destroy?.();
21397
- }
21398
- }
21399
- }
21400
- function resolvePointerBlossomServers(event, configured) {
21401
- const servers = [];
21402
- for (const value of event.tags.flatMap(
21403
- (tag) => tag[0] === "server" && tag[1] ? [tag[1]] : []
21404
- )) {
21405
- const normalized = normalizePublicBlossomServer(value);
21406
- if (normalized && !servers.includes(normalized)) servers.push(normalized);
21407
- }
21408
- for (const value of configured) {
21409
- const server = normalizePublicBlossomServer(value) ?? value;
21410
- if (!servers.includes(server)) servers.push(server);
21411
- }
21412
- return servers;
21413
- }
21414
- function injectPajaRuntimeCsp(html, origins) {
21415
- const grantedOrigins = [...new Set(origins)].sort();
21416
- const connectSrc = grantedOrigins.length > 0 ? `connect-src ${grantedOrigins.join(" ")}` : "connect-src 'none'";
21417
- const value = [
21418
- "default-src 'none'",
21419
- "script-src 'unsafe-inline' 'wasm-unsafe-eval'",
21420
- "style-src 'unsafe-inline'",
21421
- "img-src data: blob:",
21422
- "font-src data:",
21423
- connectSrc,
21424
- "worker-src 'none'",
21425
- "child-src 'none'",
21426
- "frame-src 'none'",
21427
- "media-src 'none'",
21428
- "object-src 'none'",
21429
- "manifest-src 'none'",
21430
- "prefetch-src 'none'",
21431
- "base-uri 'none'",
21432
- "form-action 'none'",
21433
- "frame-ancestors 'self'"
21434
- ].join("; ");
21435
- const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(value)}">`;
21436
- if (/<head[^>]*>/i.test(html)) {
21437
- return html.replace(/<head[^>]*>/i, (open) => `${open}${meta}`);
21438
- }
21439
- if (/<html[^>]*>/i.test(html)) {
21440
- return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${meta}</head>`);
21441
- }
21442
- return `${meta}${html}`;
21443
- }
21444
- async function resolvePointerEvent(pointer, pool, relays, maxWaitMs) {
21445
- const filter = pointer.type === "naddr" ? addressableFilter(pointer) : eventFilter(pointer);
21446
- const result2 = await queryPointerRelays(pool, relays, filter, maxWaitMs);
21447
- const event = result2.events.filter((candidate) => matchesPointer(pointer, candidate)).sort((a, b) => b.created_at - a.created_at)[0];
21448
- if (!event) {
21449
- if (result2.timedOut) {
21450
- throw new Error(
21451
- `Relay resolution timed out after ${maxWaitMs}ms for ${pointer.type} pointer across ${relays.length} relays before complete EOSE.`
21452
- );
21453
- }
21454
- const failures = relayFailureReasons(result2.reasons);
21455
- if (result2.error || failures.length > 0) {
21456
- const detail = result2.error instanceof Error ? result2.error.message : result2.error === void 0 ? failures.join("; ") : String(result2.error);
21457
- throw new Error(`Relay query failed for ${pointer.type} pointer before complete EOSE: ${detail}`);
21458
- }
21459
- throw new Error(
21460
- `All ${relays.length} relays reached EOSE without a matching ${pointer.type} napplet manifest event.`
21461
- );
21462
- }
21463
- if (!isNappletManifestKind(event.kind)) {
21464
- throw new Error(`Pointer resolved kind ${event.kind}; expected ${formatNappletManifestKinds()}.`);
21465
- }
21466
- return event;
21467
- }
21468
- function queryPointerRelays(pool, relays, filter, maxWaitMs) {
21469
- if (pool.subscribeManyEose) {
21470
- return queryPointerRelaySubscription(pool, relays, filter, maxWaitMs);
21471
- }
21472
- return queryPointerRelaySync(pool, relays, filter, maxWaitMs);
21473
- }
21474
- function queryPointerRelaySubscription(pool, relays, filter, maxWaitMs) {
21475
- return new Promise((resolve) => {
21476
- const events = [];
21477
- let settled = false;
21478
- let subscription;
21479
- const finish = (result2) => {
21480
- if (settled) return;
21481
- settled = true;
21482
- clearTimeout(deadline);
21483
- resolve({ events, ...result2 });
21484
- };
21485
- const deadline = setTimeout(() => {
21486
- finish({ reasons: [], timedOut: true });
21487
- void subscription?.close("Paja pointer relay deadline exceeded");
21488
- }, maxWaitMs);
21489
- try {
21490
- subscription = pool.subscribeManyEose([...relays], filter, {
21491
- maxWait: maxWaitMs,
21492
- label: "kehto-paja-runtime",
21493
- onevent(event) {
21494
- events.push(event);
21495
- },
21496
- onclose(reasons) {
21497
- finish({ reasons, timedOut: false });
21498
- }
21499
- });
21500
- } catch (error) {
21501
- finish({ reasons: [], timedOut: false, error });
21502
- }
21503
- });
21504
- }
21505
- function queryPointerRelaySync(pool, relays, filter, maxWaitMs) {
21506
- return new Promise((resolve) => {
21507
- let settled = false;
21508
- const finish = (result2) => {
21509
- if (settled) return;
21510
- settled = true;
21511
- clearTimeout(deadline);
21512
- resolve(result2);
21513
- };
21514
- const deadline = setTimeout(() => {
21515
- finish({ events: [], reasons: [], timedOut: true });
21516
- }, maxWaitMs);
21517
- void pool.querySync([...relays], filter, {
21518
- maxWait: maxWaitMs,
21519
- label: "kehto-paja-runtime"
21520
- }).then(
21521
- (events) => finish({ events, reasons: [], timedOut: false }),
21522
- (error) => finish({ events: [], reasons: [], timedOut: false, error })
21523
- );
21524
- });
21525
- }
21526
- function relayFailureReasons(reasons) {
21527
- return [...new Set(reasons.map((reason) => reason.trim()).filter((reason) => reason.length > 0 && reason !== "closed automatically on eose").map((reason) => reason.slice(0, 160)))].slice(0, 3);
21528
- }
21529
- function normalizeMaxWaitMs(value) {
21530
- if (value === void 0) return 5e3;
21531
- if (!Number.isFinite(value) || value <= 0) {
21532
- throw new Error("Pointer relay deadline must be a positive number of milliseconds.");
21533
- }
21534
- return Math.floor(value);
21535
- }
21536
- function addressableFilter(pointer) {
21537
- if (pointer.kind !== NAPPLET_KIND_NAMED) {
21538
- throw new Error(`naddr kind ${pointer.kind} is not a named NIP-5D napplet manifest kind.`);
21539
- }
21540
- return {
21541
- kinds: [NAPPLET_KIND_NAMED],
21542
- authors: [pointer.pubkey],
21543
- "#d": [pointer.identifier],
21544
- limit: 1
21545
- };
21546
- }
21547
- function eventFilter(pointer) {
21548
- return {
21549
- ids: [pointer.id],
21550
- ...pointer.kind ? { kinds: [pointer.kind] } : {},
21551
- ...pointer.author ? { authors: [pointer.author] } : {},
21552
- limit: 1
21553
- };
21554
- }
21555
- function matchesPointer(pointer, event) {
21556
- if (pointer.type === "nevent") {
21557
- if (event.id !== pointer.id) return false;
21558
- if (pointer.author && event.pubkey !== pointer.author) return false;
21559
- if (pointer.kind && event.kind !== pointer.kind) return false;
21560
- return true;
21561
- }
21562
- return event.kind === pointer.kind && event.pubkey === pointer.pubkey && event.tags.some((tag) => tag[0] === "d" && tag[1] === pointer.identifier);
21563
- }
21564
- function uniqueRelayUrls(values) {
21565
- const out = [];
21566
- const seen = /* @__PURE__ */ new Set();
21567
- for (const value of values) {
21568
- const url = normalizeRelayUrl2(value);
21569
- if (!url || seen.has(url)) continue;
21570
- seen.add(url);
21571
- out.push(url);
21572
- }
21573
- return out;
21574
- }
21575
- function normalizeRelayUrl2(value) {
21576
- const raw = value.trim();
21577
- if (!raw) return void 0;
21578
- try {
21579
- const url = new URL(raw);
21580
- if (url.protocol !== "wss:" && url.protocol !== "ws:") return void 0;
21581
- url.hash = "";
21582
- url.search = "";
21583
- return url.href.replace(/\/$/, "");
21584
- } catch {
21585
- return void 0;
21586
- }
21587
- }
21588
- function escapeAttribute(value) {
21589
- return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
21590
- }
21591
- function formatNappletManifestKinds() {
21592
- return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
21593
- }
21594
-
21595
21445
  // src/browser-target-frame.ts
21596
21446
  function resolvePajaFrameEnvironment(hooks, identity) {
21597
21447
  if (typeof identity.dTag !== "string" || !identity.aggregateHash) {
@@ -21634,16 +21484,20 @@ async function navigateFrame(frame, config, generation, adapter, resolvedTarget,
21634
21484
  }
21635
21485
  }
21636
21486
  if (isCurrent && !isCurrent()) return null;
21487
+ const srcdoc = prepareNappletSrcdoc(resolvedTarget.indexHtml, {
21488
+ domains,
21489
+ csp: {
21490
+ ...config.csp,
21491
+ connectOrigins: [
21492
+ ...connectOrigins([...resolvedTarget.relays, ...resolvedTarget.blossomServers]),
21493
+ ...config.csp?.connectOrigins ?? []
21494
+ ]
21495
+ }
21496
+ });
21637
21497
  const registeredWindowId2 = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
21638
21498
  onRegistered?.(registeredWindowId2);
21639
21499
  frame.removeAttribute("src");
21640
- frame.srcdoc = injectNappletNamespacePrelude(
21641
- injectPajaRuntimeCsp(
21642
- resolvedTarget.indexHtml,
21643
- connectOrigins([...resolvedTarget.relays, ...resolvedTarget.blossomServers])
21644
- ),
21645
- { domains }
21646
- );
21500
+ frame.srcdoc = srcdoc;
21647
21501
  return registeredWindowId2;
21648
21502
  }
21649
21503
  const html = await fetchTargetHtml();
@@ -21666,11 +21520,7 @@ function connectOrigins(urls) {
21666
21520
  for (const value of urls) {
21667
21521
  try {
21668
21522
  const url = new URL(value);
21669
- if (url.protocol === "wss:" || url.protocol === "ws:") {
21670
- out.add(value.replace(/\/$/, ""));
21671
- } else {
21672
- out.add(url.origin);
21673
- }
21523
+ if (["https:", "http:", "wss:", "ws:"].includes(url.protocol)) out.add(url.origin);
21674
21524
  } catch {
21675
21525
  }
21676
21526
  }
@@ -21689,7 +21539,7 @@ async function fetchTargetHtml() {
21689
21539
  return response.text();
21690
21540
  }
21691
21541
  function injectBaseHref(html, targetUrl) {
21692
- const base = `<base href="${escapeAttribute2(targetUrl)}">`;
21542
+ const base = `<base href="${escapeAttribute(targetUrl)}">`;
21693
21543
  if (/<head[^>]*>/i.test(html)) {
21694
21544
  return html.replace(/<head[^>]*>/i, (open) => `${open}${base}`);
21695
21545
  }
@@ -21698,7 +21548,7 @@ function injectBaseHref(html, targetUrl) {
21698
21548
  }
21699
21549
  return `${base}${html}`;
21700
21550
  }
21701
- function escapeAttribute2(value) {
21551
+ function escapeAttribute(value) {
21702
21552
  return escapeHtml(value).replaceAll('"', "&quot;");
21703
21553
  }
21704
21554
  function escapeHtml(value) {
@@ -23271,6 +23121,232 @@ function installLocalFileControls(load) {
23271
23121
  function disposeNothing() {
23272
23122
  }
23273
23123
 
23124
+ // src/runtime-resolver.ts
23125
+ var PAJA_NAPPLET_MANIFEST_KINDS = NAPPLET_KINDS;
23126
+ function decodePajaPointer(value) {
23127
+ const trimmed = value.trim();
23128
+ const decoded = decode(trimmed);
23129
+ if (decoded.type === "naddr") {
23130
+ return {
23131
+ type: "naddr",
23132
+ value: trimmed,
23133
+ identifier: decoded.data.identifier,
23134
+ pubkey: decoded.data.pubkey,
23135
+ kind: decoded.data.kind,
23136
+ relays: [...decoded.data.relays ?? []]
23137
+ };
23138
+ }
23139
+ if (decoded.type === "nevent") {
23140
+ return {
23141
+ type: "nevent",
23142
+ value: trimmed,
23143
+ id: decoded.data.id,
23144
+ ...decoded.data.author ? { author: decoded.data.author } : {},
23145
+ ...decoded.data.kind ? { kind: decoded.data.kind } : {},
23146
+ relays: [...decoded.data.relays ?? []]
23147
+ };
23148
+ }
23149
+ throw new Error(`Expected naddr or nevent pointer, got ${decoded.type}.`);
23150
+ }
23151
+ async function resolvePajaPointer(value, options = {}) {
23152
+ const pointer = decodePajaPointer(value);
23153
+ const relays = uniqueRelayUrls([...pointer.relays, ...options.relays ?? []]);
23154
+ if (relays.length === 0) {
23155
+ throw new Error("Pointer has no relay hints or enabled configured live relays.");
23156
+ }
23157
+ const pool = options.pool ?? new SimplePool();
23158
+ const ownsPool = options.pool === void 0;
23159
+ try {
23160
+ const event = await resolvePointerEvent(pointer, pool, relays, normalizeMaxWaitMs(options.maxWaitMs));
23161
+ const blossomServers = resolvePointerBlossomServers(event, options.blossomServers ?? []);
23162
+ const resolved = await resolveNapplet({
23163
+ event,
23164
+ cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
23165
+ fetchBlob: (sha256Hex3, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex3, async (url) => {
23166
+ const response = await (options.fetcher ?? fetch)(url);
23167
+ if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
23168
+ return new Uint8Array(await response.arrayBuffer());
23169
+ })
23170
+ });
23171
+ return {
23172
+ pointer,
23173
+ event,
23174
+ relays,
23175
+ blossomServers,
23176
+ dTag: resolved.dTag,
23177
+ aggregateHash: resolved.aggregateHash,
23178
+ indexHtml: resolved.indexHtml,
23179
+ manifest: resolved.manifest
23180
+ };
23181
+ } finally {
23182
+ if (ownsPool) {
23183
+ pool.close?.(relays);
23184
+ pool.destroy?.();
23185
+ }
23186
+ }
23187
+ }
23188
+ function resolvePointerBlossomServers(event, configured) {
23189
+ const servers = [];
23190
+ for (const value of event.tags.flatMap(
23191
+ (tag) => tag[0] === "server" && tag[1] ? [tag[1]] : []
23192
+ )) {
23193
+ const normalized = normalizePublicBlossomServer(value);
23194
+ if (normalized && !servers.includes(normalized)) servers.push(normalized);
23195
+ }
23196
+ for (const value of configured) {
23197
+ const server = normalizePublicBlossomServer(value) ?? value;
23198
+ if (!servers.includes(server)) servers.push(server);
23199
+ }
23200
+ return servers;
23201
+ }
23202
+ async function resolvePointerEvent(pointer, pool, relays, maxWaitMs) {
23203
+ const filter = pointer.type === "naddr" ? addressableFilter(pointer) : eventFilter(pointer);
23204
+ const result2 = await queryPointerRelays(pool, relays, filter, maxWaitMs);
23205
+ const event = result2.events.filter((candidate) => matchesPointer(pointer, candidate)).sort((a, b) => b.created_at - a.created_at)[0];
23206
+ if (!event) {
23207
+ if (result2.timedOut) {
23208
+ throw new Error(
23209
+ `Relay resolution timed out after ${maxWaitMs}ms for ${pointer.type} pointer across ${relays.length} relays before complete EOSE.`
23210
+ );
23211
+ }
23212
+ const failures = relayFailureReasons(result2.reasons);
23213
+ if (result2.error || failures.length > 0) {
23214
+ const detail = result2.error instanceof Error ? result2.error.message : result2.error === void 0 ? failures.join("; ") : String(result2.error);
23215
+ throw new Error(`Relay query failed for ${pointer.type} pointer before complete EOSE: ${detail}`);
23216
+ }
23217
+ throw new Error(
23218
+ `All ${relays.length} relays reached EOSE without a matching ${pointer.type} napplet manifest event.`
23219
+ );
23220
+ }
23221
+ if (!isNappletManifestKind(event.kind)) {
23222
+ throw new Error(`Pointer resolved kind ${event.kind}; expected ${formatNappletManifestKinds()}.`);
23223
+ }
23224
+ return event;
23225
+ }
23226
+ function queryPointerRelays(pool, relays, filter, maxWaitMs) {
23227
+ if (pool.subscribeManyEose) {
23228
+ return queryPointerRelaySubscription(pool, relays, filter, maxWaitMs);
23229
+ }
23230
+ return queryPointerRelaySync(pool, relays, filter, maxWaitMs);
23231
+ }
23232
+ function queryPointerRelaySubscription(pool, relays, filter, maxWaitMs) {
23233
+ return new Promise((resolve) => {
23234
+ const events = [];
23235
+ let settled = false;
23236
+ let subscription;
23237
+ const finish = (result2) => {
23238
+ if (settled) return;
23239
+ settled = true;
23240
+ clearTimeout(deadline);
23241
+ resolve({ events, ...result2 });
23242
+ };
23243
+ const deadline = setTimeout(() => {
23244
+ finish({ reasons: [], timedOut: true });
23245
+ void subscription?.close("Paja pointer relay deadline exceeded");
23246
+ }, maxWaitMs);
23247
+ try {
23248
+ subscription = pool.subscribeManyEose([...relays], filter, {
23249
+ maxWait: maxWaitMs,
23250
+ label: "kehto-paja-runtime",
23251
+ onevent(event) {
23252
+ events.push(event);
23253
+ },
23254
+ onclose(reasons) {
23255
+ finish({ reasons, timedOut: false });
23256
+ }
23257
+ });
23258
+ } catch (error) {
23259
+ finish({ reasons: [], timedOut: false, error });
23260
+ }
23261
+ });
23262
+ }
23263
+ function queryPointerRelaySync(pool, relays, filter, maxWaitMs) {
23264
+ return new Promise((resolve) => {
23265
+ let settled = false;
23266
+ const finish = (result2) => {
23267
+ if (settled) return;
23268
+ settled = true;
23269
+ clearTimeout(deadline);
23270
+ resolve(result2);
23271
+ };
23272
+ const deadline = setTimeout(() => {
23273
+ finish({ events: [], reasons: [], timedOut: true });
23274
+ }, maxWaitMs);
23275
+ void pool.querySync([...relays], filter, {
23276
+ maxWait: maxWaitMs,
23277
+ label: "kehto-paja-runtime"
23278
+ }).then(
23279
+ (events) => finish({ events, reasons: [], timedOut: false }),
23280
+ (error) => finish({ events: [], reasons: [], timedOut: false, error })
23281
+ );
23282
+ });
23283
+ }
23284
+ function relayFailureReasons(reasons) {
23285
+ return [...new Set(reasons.map((reason) => reason.trim()).filter((reason) => reason.length > 0 && reason !== "closed automatically on eose").map((reason) => reason.slice(0, 160)))].slice(0, 3);
23286
+ }
23287
+ function normalizeMaxWaitMs(value) {
23288
+ if (value === void 0) return 5e3;
23289
+ if (!Number.isFinite(value) || value <= 0) {
23290
+ throw new Error("Pointer relay deadline must be a positive number of milliseconds.");
23291
+ }
23292
+ return Math.floor(value);
23293
+ }
23294
+ function addressableFilter(pointer) {
23295
+ if (pointer.kind !== NAPPLET_KIND_NAMED) {
23296
+ throw new Error(`naddr kind ${pointer.kind} is not a named NIP-5D napplet manifest kind.`);
23297
+ }
23298
+ return {
23299
+ kinds: [NAPPLET_KIND_NAMED],
23300
+ authors: [pointer.pubkey],
23301
+ "#d": [pointer.identifier],
23302
+ limit: 1
23303
+ };
23304
+ }
23305
+ function eventFilter(pointer) {
23306
+ return {
23307
+ ids: [pointer.id],
23308
+ ...pointer.kind ? { kinds: [pointer.kind] } : {},
23309
+ ...pointer.author ? { authors: [pointer.author] } : {},
23310
+ limit: 1
23311
+ };
23312
+ }
23313
+ function matchesPointer(pointer, event) {
23314
+ if (pointer.type === "nevent") {
23315
+ if (event.id !== pointer.id) return false;
23316
+ if (pointer.author && event.pubkey !== pointer.author) return false;
23317
+ if (pointer.kind && event.kind !== pointer.kind) return false;
23318
+ return true;
23319
+ }
23320
+ return event.kind === pointer.kind && event.pubkey === pointer.pubkey && event.tags.some((tag) => tag[0] === "d" && tag[1] === pointer.identifier);
23321
+ }
23322
+ function uniqueRelayUrls(values) {
23323
+ const out = [];
23324
+ const seen = /* @__PURE__ */ new Set();
23325
+ for (const value of values) {
23326
+ const url = normalizeRelayUrl2(value);
23327
+ if (!url || seen.has(url)) continue;
23328
+ seen.add(url);
23329
+ out.push(url);
23330
+ }
23331
+ return out;
23332
+ }
23333
+ function normalizeRelayUrl2(value) {
23334
+ const raw = value.trim();
23335
+ if (!raw) return void 0;
23336
+ try {
23337
+ const url = new URL(raw);
23338
+ if (url.protocol !== "wss:" && url.protocol !== "ws:") return void 0;
23339
+ url.hash = "";
23340
+ url.search = "";
23341
+ return url.href.replace(/\/$/, "");
23342
+ } catch {
23343
+ return void 0;
23344
+ }
23345
+ }
23346
+ function formatNappletManifestKinds() {
23347
+ return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
23348
+ }
23349
+
23274
23350
  // src/browser-intent-host.ts
23275
23351
  function createPajaIntentTargetOptions(getState, getContext, effects = {}) {
23276
23352
  return {