@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.
- package/README.md +25 -10
- package/dist/browser-host.js +349 -273
- package/dist/browser-host.js.map +1 -1
- package/dist/{chunk-YX6C2UU7.js → chunk-GY5ET3PC.js} +4 -32
- package/dist/chunk-GY5ET3PC.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/{server-Ia5n1Bqv.d.ts → server-5j5uoy2Z.d.ts} +5 -0
- package/package.json +5 -5
- package/dist/chunk-YX6C2UU7.js.map +0 -1
package/dist/browser-host.js
CHANGED
|
@@ -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, "&").replace(/"/g, """);
|
|
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("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
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 =
|
|
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 (
|
|
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="${
|
|
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
|
|
21551
|
+
function escapeAttribute(value) {
|
|
21702
21552
|
return escapeHtml(value).replaceAll('"', """);
|
|
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 {
|