@kehto/paja 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2447,11 +2447,8 @@ function createFirewallGate(config) {
2447
2447
  if (decision === "prompt") fireConsent(windowId, napplet);
2448
2448
  return "drop";
2449
2449
  }
2450
- const id = envelope.id ?? "";
2451
- const type = denialResponseType(envelope);
2452
- if (type) {
2453
- hooks.sendToNapplet(windowId, { type, id, error: `firewall: ${reason}` });
2454
- }
2450
+ const denial = createDenialResponse(envelope, `firewall: ${reason}`);
2451
+ if (denial) hooks.sendToNapplet(windowId, denial);
2455
2452
  hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
2456
2453
  if (decision === "prompt") {
2457
2454
  fireConsent(windowId, napplet);
@@ -2473,6 +2470,19 @@ function denialResponseType(envelope) {
2473
2470
  if (envelope.type.startsWith("inc.")) return null;
2474
2471
  return `${envelope.type}.error`;
2475
2472
  }
2473
+ function createDenialResponse(envelope, error) {
2474
+ const id = envelope.id ?? "";
2475
+ if (envelope.type === "outbox.query") {
2476
+ return {
2477
+ type: "outbox.query.result",
2478
+ id,
2479
+ events: [],
2480
+ error
2481
+ };
2482
+ }
2483
+ const type = denialResponseType(envelope);
2484
+ return type ? { type, id, error } : null;
2485
+ }
2476
2486
  function createMessageHandler(hooks, sessionRegistry2, enforceNap, dispatchNapEnvelope, firewallGate) {
2477
2487
  return (windowId, msg) => {
2478
2488
  if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
@@ -2497,12 +2507,9 @@ function createMessageHandler(hooks, sessionRegistry2, enforceNap, dispatchNapEn
2497
2507
  hooks.sendToNapplet(windowId, canonicalResult);
2498
2508
  return;
2499
2509
  }
2500
- const id = envelope.id ?? "";
2501
2510
  const error = formatDenialReason(result2.capability);
2502
- const type = denialResponseType(envelope);
2503
- if (type) {
2504
- hooks.sendToNapplet(windowId, { type, id, error });
2505
- }
2511
+ const denial = createDenialResponse(envelope, error);
2512
+ if (denial) hooks.sendToNapplet(windowId, denial);
2506
2513
  return;
2507
2514
  }
2508
2515
  }
@@ -19935,97 +19942,442 @@ function forwardPajaHotkey(event) {
19935
19942
  }));
19936
19943
  }
19937
19944
 
19938
- // src/browser-devtools.ts
19939
- var PAJA_LOG_LIMIT = 500;
19940
- var proxyToReal = /* @__PURE__ */ new WeakMap();
19941
- var realToProxy = /* @__PURE__ */ new WeakMap();
19942
- var proxyContexts = /* @__PURE__ */ new WeakMap();
19943
- function isNappletMessage(value) {
19944
- return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string";
19945
- }
19946
- function describeMessage(raw) {
19947
- if (isNappletMessage(raw)) return raw.type;
19948
- if (Array.isArray(raw) && typeof raw[0] === "string") return raw[0];
19949
- return "unknown";
19945
+ // src/runtime-resolver.ts
19946
+ var PAJA_NAPPLET_MANIFEST_KINDS = NAPPLET_KINDS;
19947
+ function decodePajaPointer(value) {
19948
+ const trimmed = value.trim();
19949
+ const decoded = decode(trimmed);
19950
+ if (decoded.type === "naddr") {
19951
+ return {
19952
+ type: "naddr",
19953
+ value: trimmed,
19954
+ identifier: decoded.data.identifier,
19955
+ pubkey: decoded.data.pubkey,
19956
+ kind: decoded.data.kind,
19957
+ relays: [...decoded.data.relays ?? []]
19958
+ };
19959
+ }
19960
+ if (decoded.type === "nevent") {
19961
+ return {
19962
+ type: "nevent",
19963
+ value: trimmed,
19964
+ id: decoded.data.id,
19965
+ ...decoded.data.author ? { author: decoded.data.author } : {},
19966
+ ...decoded.data.kind ? { kind: decoded.data.kind } : {},
19967
+ relays: [...decoded.data.relays ?? []]
19968
+ };
19969
+ }
19970
+ throw new Error(`Expected naddr or nevent pointer, got ${decoded.type}.`);
19950
19971
  }
19951
- function previewMessage(raw) {
19972
+ async function resolvePajaPointer(value, options = {}) {
19973
+ const pointer = decodePajaPointer(value);
19974
+ const relays = uniqueRelayUrls([...pointer.relays, ...options.relays ?? []]);
19975
+ if (relays.length === 0) {
19976
+ throw new Error("Pointer has no relay hints or enabled configured live relays.");
19977
+ }
19978
+ const pool = options.pool ?? new SimplePool();
19979
+ const ownsPool = options.pool === void 0;
19952
19980
  try {
19953
- return JSON.stringify(raw);
19954
- } catch (error) {
19955
- return `unserializable:${error instanceof Error ? error.message : String(error)}`;
19981
+ const event = await resolvePointerEvent(pointer, pool, relays, normalizeMaxWaitMs(options.maxWaitMs));
19982
+ const blossomServers = [...options.blossomServers ?? []];
19983
+ const resolved = await resolveNapplet({
19984
+ event,
19985
+ cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
19986
+ fetchBlob: (sha256Hex, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex, async (url) => {
19987
+ const response = await (options.fetcher ?? fetch)(url);
19988
+ if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
19989
+ return new Uint8Array(await response.arrayBuffer());
19990
+ })
19991
+ });
19992
+ return {
19993
+ pointer,
19994
+ event,
19995
+ relays,
19996
+ blossomServers,
19997
+ dTag: resolved.dTag,
19998
+ aggregateHash: resolved.aggregateHash,
19999
+ indexHtml: resolved.indexHtml,
20000
+ manifest: resolved.manifest
20001
+ };
20002
+ } finally {
20003
+ if (ownsPool) {
20004
+ pool.close?.(relays);
20005
+ pool.destroy?.();
20006
+ }
19956
20007
  }
19957
20008
  }
19958
- function compactMessageDetail(raw) {
19959
- const detail = {};
19960
- for (const [key, value] of Object.entries(raw)) {
19961
- if (key !== "type" && key !== "id" && value !== void 0) detail[key] = value;
20009
+ function injectPajaRuntimeCsp(html, origins) {
20010
+ const grantedOrigins = [...new Set(origins)].sort();
20011
+ const connectSrc = grantedOrigins.length > 0 ? `connect-src ${grantedOrigins.join(" ")}` : "connect-src 'none'";
20012
+ const value = [
20013
+ "default-src 'none'",
20014
+ "script-src 'unsafe-inline' 'wasm-unsafe-eval'",
20015
+ "style-src 'unsafe-inline'",
20016
+ "img-src data: blob:",
20017
+ "font-src data:",
20018
+ connectSrc,
20019
+ "worker-src 'none'",
20020
+ "child-src 'none'",
20021
+ "frame-src 'none'",
20022
+ "media-src 'none'",
20023
+ "object-src 'none'",
20024
+ "manifest-src 'none'",
20025
+ "prefetch-src 'none'",
20026
+ "base-uri 'none'",
20027
+ "form-action 'none'",
20028
+ "frame-ancestors 'self'"
20029
+ ].join("; ");
20030
+ const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(value)}">`;
20031
+ if (/<head[^>]*>/i.test(html)) {
20032
+ return html.replace(/<head[^>]*>/i, (open) => `${open}${meta}`);
19962
20033
  }
19963
- if (Object.keys(detail).length === 0) return "";
19964
- try {
19965
- return JSON.stringify(detail);
19966
- } catch {
19967
- return "";
20034
+ if (/<html[^>]*>/i.test(html)) {
20035
+ return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${meta}</head>`);
19968
20036
  }
20037
+ return `${meta}${html}`;
19969
20038
  }
19970
- function describeMessageDetail(raw) {
19971
- if (!isNappletMessage(raw)) return "";
19972
- const value = raw;
19973
- for (const key of ["error", "message", "reason"]) {
19974
- const detail = value[key];
19975
- if (typeof detail === "string" && detail.length > 0) return detail;
20039
+ async function resolvePointerEvent(pointer, pool, relays, maxWaitMs) {
20040
+ const filter = pointer.type === "naddr" ? addressableFilter(pointer) : eventFilter(pointer);
20041
+ const result2 = await queryPointerRelays(pool, relays, filter, maxWaitMs);
20042
+ const event = result2.events.filter((candidate) => matchesPointer(pointer, candidate)).sort((a, b) => b.created_at - a.created_at)[0];
20043
+ if (!event) {
20044
+ if (result2.timedOut) {
20045
+ throw new Error(
20046
+ `Relay resolution timed out after ${maxWaitMs}ms for ${pointer.type} pointer across ${relays.length} relays before complete EOSE.`
20047
+ );
20048
+ }
20049
+ const failures = relayFailureReasons(result2.reasons);
20050
+ if (result2.error || failures.length > 0) {
20051
+ const detail = result2.error instanceof Error ? result2.error.message : result2.error === void 0 ? failures.join("; ") : String(result2.error);
20052
+ throw new Error(`Relay query failed for ${pointer.type} pointer before complete EOSE: ${detail}`);
20053
+ }
20054
+ throw new Error(
20055
+ `All ${relays.length} relays reached EOSE without a matching ${pointer.type} napplet manifest event.`
20056
+ );
19976
20057
  }
19977
- return raw.type.endsWith(".error") ? compactMessageDetail(value) : "";
20058
+ if (!isNappletManifestKind(event.kind)) {
20059
+ throw new Error(`Pointer resolved kind ${event.kind}; expected ${formatNappletManifestKinds()}.`);
20060
+ }
20061
+ return event;
19978
20062
  }
19979
- function domainEnabled(simulation, domain) {
19980
- return simulation.capabilities.domains[domain] === true;
20063
+ function queryPointerRelays(pool, relays, filter, maxWaitMs) {
20064
+ if (pool.subscribeManyEose) {
20065
+ return queryPointerRelaySubscription(pool, relays, filter, maxWaitMs);
20066
+ }
20067
+ return queryPointerRelaySync(pool, relays, filter, maxWaitMs);
19981
20068
  }
19982
- function getTargetIdentity(config) {
19983
- return {
19984
- pubkey: "",
19985
- dTag: config.window.dTag,
19986
- aggregateHash: config.window.aggregateHash
19987
- };
20069
+ function queryPointerRelaySubscription(pool, relays, filter, maxWaitMs) {
20070
+ return new Promise((resolve) => {
20071
+ const events = [];
20072
+ let settled = false;
20073
+ let subscription;
20074
+ const finish = (result2) => {
20075
+ if (settled) return;
20076
+ settled = true;
20077
+ clearTimeout(deadline);
20078
+ resolve({ events, ...result2 });
20079
+ };
20080
+ const deadline = setTimeout(() => {
20081
+ finish({ reasons: [], timedOut: true });
20082
+ void subscription?.close("Paja pointer relay deadline exceeded");
20083
+ }, maxWaitMs);
20084
+ try {
20085
+ subscription = pool.subscribeManyEose([...relays], filter, {
20086
+ maxWait: maxWaitMs,
20087
+ label: "kehto-paja-runtime",
20088
+ onevent(event) {
20089
+ events.push(event);
20090
+ },
20091
+ onclose(reasons) {
20092
+ finish({ reasons, timedOut: false });
20093
+ }
20094
+ });
20095
+ } catch (error) {
20096
+ finish({ reasons: [], timedOut: false, error });
20097
+ }
20098
+ });
19988
20099
  }
19989
- function appendPajaMessageLog(state, direction, raw, windowId) {
19990
- if (!state) return;
19991
- state.messageLog.push({
19992
- index: state.messageLog.length,
19993
- timestamp: Date.now(),
19994
- direction,
19995
- type: describeMessage(raw),
19996
- windowId,
19997
- preview: previewMessage(raw),
19998
- detail: describeMessageDetail(raw)
20100
+ function queryPointerRelaySync(pool, relays, filter, maxWaitMs) {
20101
+ return new Promise((resolve) => {
20102
+ let settled = false;
20103
+ const finish = (result2) => {
20104
+ if (settled) return;
20105
+ settled = true;
20106
+ clearTimeout(deadline);
20107
+ resolve(result2);
20108
+ };
20109
+ const deadline = setTimeout(() => {
20110
+ finish({ events: [], reasons: [], timedOut: true });
20111
+ }, maxWaitMs);
20112
+ void pool.querySync([...relays], filter, {
20113
+ maxWait: maxWaitMs,
20114
+ label: "kehto-paja-runtime"
20115
+ }).then(
20116
+ (events) => finish({ events, reasons: [], timedOut: false }),
20117
+ (error) => finish({ events: [], reasons: [], timedOut: false, error })
20118
+ );
19999
20119
  });
20000
- if (state.messageLog.length > PAJA_LOG_LIMIT) {
20001
- state.messageLog.splice(0, state.messageLog.length - PAJA_LOG_LIMIT);
20120
+ }
20121
+ function relayFailureReasons(reasons) {
20122
+ 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);
20123
+ }
20124
+ function normalizeMaxWaitMs(value) {
20125
+ if (value === void 0) return 5e3;
20126
+ if (!Number.isFinite(value) || value <= 0) {
20127
+ throw new Error("Pointer relay deadline must be a positive number of milliseconds.");
20002
20128
  }
20003
- renderPajaMessageLog(state);
20129
+ return Math.floor(value);
20004
20130
  }
20005
- function createPajaPostMessageProxy(realWin, state, windowId) {
20006
- const existing = realToProxy.get(realWin);
20007
- if (existing) {
20008
- proxyContexts.set(existing, { state, windowId });
20009
- return existing;
20131
+ function addressableFilter(pointer) {
20132
+ if (pointer.kind !== NAPPLET_KIND_NAMED) {
20133
+ throw new Error(`naddr kind ${pointer.kind} is not a named NIP-5D napplet manifest kind.`);
20010
20134
  }
20011
- const proxy = new Proxy(realWin, {
20012
- get(target, prop) {
20013
- if (prop === "postMessage") {
20014
- return (msg, targetOrigin, transfer) => {
20015
- const context = proxyContexts.get(proxy);
20016
- appendPajaMessageLog(
20017
- context?.state ?? state,
20018
- "shell->napplet",
20019
- redactPajaMessageForLog(msg),
20020
- context?.windowId ?? windowId
20021
- );
20022
- return target.postMessage(msg, targetOrigin, transfer);
20023
- };
20024
- }
20025
- try {
20026
- const val = Reflect.get(target, prop, target);
20027
- return typeof val === "function" ? val.bind(target) : val;
20028
- } catch {
20135
+ return {
20136
+ kinds: [NAPPLET_KIND_NAMED],
20137
+ authors: [pointer.pubkey],
20138
+ "#d": [pointer.identifier],
20139
+ limit: 1
20140
+ };
20141
+ }
20142
+ function eventFilter(pointer) {
20143
+ return {
20144
+ ids: [pointer.id],
20145
+ ...pointer.kind ? { kinds: [pointer.kind] } : {},
20146
+ ...pointer.author ? { authors: [pointer.author] } : {},
20147
+ limit: 1
20148
+ };
20149
+ }
20150
+ function matchesPointer(pointer, event) {
20151
+ if (pointer.type === "nevent") {
20152
+ if (event.id !== pointer.id) return false;
20153
+ if (pointer.author && event.pubkey !== pointer.author) return false;
20154
+ if (pointer.kind && event.kind !== pointer.kind) return false;
20155
+ return true;
20156
+ }
20157
+ return event.kind === pointer.kind && event.pubkey === pointer.pubkey && event.tags.some((tag) => tag[0] === "d" && tag[1] === pointer.identifier);
20158
+ }
20159
+ function uniqueRelayUrls(values) {
20160
+ const out = [];
20161
+ const seen = /* @__PURE__ */ new Set();
20162
+ for (const value of values) {
20163
+ const url = normalizeRelayUrl2(value);
20164
+ if (!url || seen.has(url)) continue;
20165
+ seen.add(url);
20166
+ out.push(url);
20167
+ }
20168
+ return out;
20169
+ }
20170
+ function normalizeRelayUrl2(value) {
20171
+ const raw = value.trim();
20172
+ if (!raw) return void 0;
20173
+ try {
20174
+ const url = new URL(raw);
20175
+ if (url.protocol !== "wss:" && url.protocol !== "ws:") return void 0;
20176
+ url.hash = "";
20177
+ url.search = "";
20178
+ return url.href.replace(/\/$/, "");
20179
+ } catch {
20180
+ return void 0;
20181
+ }
20182
+ }
20183
+ function escapeAttribute(value) {
20184
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
20185
+ }
20186
+ function formatNappletManifestKinds() {
20187
+ return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
20188
+ }
20189
+
20190
+ // src/browser-target-frame.ts
20191
+ function resolvePajaFrameEnvironment(hooks, identity) {
20192
+ if (!identity.dTag || !identity.aggregateHash) {
20193
+ throw new Error("Paja frame identity requires a dTag and aggregate hash");
20194
+ }
20195
+ return resolveShellEnvironment(hooks, identity);
20196
+ }
20197
+ function getTargetIdentity(config, resolvedTarget) {
20198
+ return {
20199
+ pubkey: "",
20200
+ dTag: resolvedTarget?.dTag ?? config.window.dTag,
20201
+ aggregateHash: resolvedTarget?.aggregateHash ?? config.window.aggregateHash
20202
+ };
20203
+ }
20204
+ function getTargetOriginIdentity(config, resolvedTarget) {
20205
+ const target = getTargetIdentity(config, resolvedTarget);
20206
+ return Object.freeze({ dTag: target.dTag, aggregateHash: target.aggregateHash });
20207
+ }
20208
+ function registerFrameForGeneration(frame, config, generation, identity, environment, windowId = `${config.window.id}:${generation}`) {
20209
+ const win = frame.contentWindow;
20210
+ if (!win) return null;
20211
+ originRegistry.register(win, windowId, identity);
20212
+ originRegistry.setEnvironment(win, environment);
20213
+ return windowId;
20214
+ }
20215
+ async function navigateFrame(frame, config, generation, adapter, resolvedTarget, windowId, isCurrent, onRegistered) {
20216
+ const identity = getTargetOriginIdentity(config, resolvedTarget);
20217
+ const environment = resolvePajaFrameEnvironment(adapter, identity);
20218
+ const domains = environment.capabilities.domains;
20219
+ if (config.target.mode === "runtime-pointer") {
20220
+ if (!resolvedTarget) {
20221
+ frame.removeAttribute("src");
20222
+ frame.srcdoc = "<!doctype html><html><body></body></html>";
20223
+ return null;
20224
+ }
20225
+ if (isCurrent && !isCurrent()) return null;
20226
+ const registeredWindowId2 = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
20227
+ onRegistered?.(registeredWindowId2);
20228
+ frame.removeAttribute("src");
20229
+ frame.srcdoc = injectNappletNamespacePrelude(
20230
+ injectPajaRuntimeCsp(
20231
+ resolvedTarget.indexHtml,
20232
+ connectOrigins([...resolvedTarget.relays, ...resolvedTarget.blossomServers])
20233
+ ),
20234
+ { domains }
20235
+ );
20236
+ return registeredWindowId2;
20237
+ }
20238
+ const html = await fetchTargetHtml();
20239
+ if (isCurrent && !isCurrent()) return null;
20240
+ const registeredWindowId = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
20241
+ onRegistered?.(registeredWindowId);
20242
+ frame.removeAttribute("src");
20243
+ frame.srcdoc = injectNappletNamespacePrelude(
20244
+ injectBaseHref(html, config.target.url),
20245
+ { domains }
20246
+ );
20247
+ return registeredWindowId;
20248
+ }
20249
+ function renderTargetErrorHtml(error) {
20250
+ const message = error instanceof Error ? error.message : String(error);
20251
+ return `<!doctype html><html><body><pre>${escapeHtml(message)}</pre></body></html>`;
20252
+ }
20253
+ function connectOrigins(urls) {
20254
+ const out = /* @__PURE__ */ new Set();
20255
+ for (const value of urls) {
20256
+ try {
20257
+ const url = new URL(value);
20258
+ if (url.protocol === "wss:" || url.protocol === "ws:") {
20259
+ out.add(value.replace(/\/$/, ""));
20260
+ } else {
20261
+ out.add(url.origin);
20262
+ }
20263
+ } catch {
20264
+ }
20265
+ }
20266
+ return [...out];
20267
+ }
20268
+ async function fetchTargetHtml() {
20269
+ const response = await fetch(new URL("./__kehto/target.html", window.location.href), {
20270
+ cache: "no-store",
20271
+ headers: {
20272
+ accept: "text/html, application/xhtml+xml;q=0.9, */*;q=0.8"
20273
+ }
20274
+ });
20275
+ if (!response.ok) {
20276
+ throw new Error(`Paja target fetch failed: ${response.status} ${response.statusText}`);
20277
+ }
20278
+ return response.text();
20279
+ }
20280
+ function injectBaseHref(html, targetUrl) {
20281
+ const base = `<base href="${escapeAttribute2(targetUrl)}">`;
20282
+ if (/<head[^>]*>/i.test(html)) {
20283
+ return html.replace(/<head[^>]*>/i, (open) => `${open}${base}`);
20284
+ }
20285
+ if (/<html[^>]*>/i.test(html)) {
20286
+ return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${base}</head>`);
20287
+ }
20288
+ return `${base}${html}`;
20289
+ }
20290
+ function escapeAttribute2(value) {
20291
+ return escapeHtml(value).replaceAll('"', "&quot;");
20292
+ }
20293
+ function escapeHtml(value) {
20294
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
20295
+ }
20296
+
20297
+ // src/browser-devtools.ts
20298
+ var PAJA_LOG_LIMIT = 500;
20299
+ var proxyToReal = /* @__PURE__ */ new WeakMap();
20300
+ var realToProxy = /* @__PURE__ */ new WeakMap();
20301
+ var proxyContexts = /* @__PURE__ */ new WeakMap();
20302
+ function isNappletMessage(value) {
20303
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string";
20304
+ }
20305
+ function describeMessage(raw) {
20306
+ if (isNappletMessage(raw)) return raw.type;
20307
+ if (Array.isArray(raw) && typeof raw[0] === "string") return raw[0];
20308
+ return "unknown";
20309
+ }
20310
+ function previewMessage(raw) {
20311
+ try {
20312
+ return JSON.stringify(raw);
20313
+ } catch (error) {
20314
+ return `unserializable:${error instanceof Error ? error.message : String(error)}`;
20315
+ }
20316
+ }
20317
+ function compactMessageDetail(raw) {
20318
+ const detail = {};
20319
+ for (const [key, value] of Object.entries(raw)) {
20320
+ if (key !== "type" && key !== "id" && value !== void 0) detail[key] = value;
20321
+ }
20322
+ if (Object.keys(detail).length === 0) return "";
20323
+ try {
20324
+ return JSON.stringify(detail);
20325
+ } catch {
20326
+ return "";
20327
+ }
20328
+ }
20329
+ function describeMessageDetail(raw) {
20330
+ if (!isNappletMessage(raw)) return "";
20331
+ const value = raw;
20332
+ for (const key of ["error", "message", "reason"]) {
20333
+ const detail = value[key];
20334
+ if (typeof detail === "string" && detail.length > 0) return detail;
20335
+ }
20336
+ return raw.type.endsWith(".error") ? compactMessageDetail(value) : "";
20337
+ }
20338
+ function domainEnabled(simulation, domain) {
20339
+ return simulation.capabilities.domains[domain] === true;
20340
+ }
20341
+ function appendPajaMessageLog(state, direction, raw, windowId) {
20342
+ if (!state) return;
20343
+ state.messageLog.push({
20344
+ index: state.messageLog.length,
20345
+ timestamp: Date.now(),
20346
+ direction,
20347
+ type: describeMessage(raw),
20348
+ windowId,
20349
+ preview: previewMessage(raw),
20350
+ detail: describeMessageDetail(raw)
20351
+ });
20352
+ if (state.messageLog.length > PAJA_LOG_LIMIT) {
20353
+ state.messageLog.splice(0, state.messageLog.length - PAJA_LOG_LIMIT);
20354
+ }
20355
+ renderPajaMessageLog(state);
20356
+ }
20357
+ function createPajaPostMessageProxy(realWin, state, windowId) {
20358
+ const existing = realToProxy.get(realWin);
20359
+ if (existing) {
20360
+ proxyContexts.set(existing, { state, windowId });
20361
+ return existing;
20362
+ }
20363
+ const proxy = new Proxy(realWin, {
20364
+ get(target, prop) {
20365
+ if (prop === "postMessage") {
20366
+ return (msg, targetOrigin, transfer) => {
20367
+ const context = proxyContexts.get(proxy);
20368
+ appendPajaMessageLog(
20369
+ context?.state ?? state,
20370
+ "shell->napplet",
20371
+ redactPajaMessageForLog(msg),
20372
+ context?.windowId ?? windowId
20373
+ );
20374
+ return target.postMessage(msg, targetOrigin, transfer);
20375
+ };
20376
+ }
20377
+ try {
20378
+ const val = Reflect.get(target, prop, target);
20379
+ return typeof val === "function" ? val.bind(target) : val;
20380
+ } catch {
20029
20381
  return void 0;
20030
20382
  }
20031
20383
  }
@@ -20134,7 +20486,7 @@ function renderInterfaceControls(state) {
20134
20486
  function renderAclControls(state, bridge) {
20135
20487
  const container = document.getElementById("acl-controls");
20136
20488
  if (!container) return;
20137
- const identity = getTargetIdentity(state.config);
20489
+ const identity = getTargetIdentity(state.config, state.resolvedTarget);
20138
20490
  container.replaceChildren(...ALL_CAPABILITIES.map((capability) => {
20139
20491
  const allowed2 = bridge?.runtime.aclState.check(
20140
20492
  identity.pubkey,
@@ -21391,251 +21743,6 @@ function runtimeTabWindowId(config, tab) {
21391
21743
  return `${config.window.id}:${tab.id}:${tab.generation}`;
21392
21744
  }
21393
21745
 
21394
- // src/runtime-resolver.ts
21395
- var PAJA_NAPPLET_MANIFEST_KINDS = NAPPLET_KINDS;
21396
- function decodePajaPointer(value) {
21397
- const trimmed = value.trim();
21398
- const decoded = decode(trimmed);
21399
- if (decoded.type === "naddr") {
21400
- return {
21401
- type: "naddr",
21402
- value: trimmed,
21403
- identifier: decoded.data.identifier,
21404
- pubkey: decoded.data.pubkey,
21405
- kind: decoded.data.kind,
21406
- relays: [...decoded.data.relays ?? []]
21407
- };
21408
- }
21409
- if (decoded.type === "nevent") {
21410
- return {
21411
- type: "nevent",
21412
- value: trimmed,
21413
- id: decoded.data.id,
21414
- ...decoded.data.author ? { author: decoded.data.author } : {},
21415
- ...decoded.data.kind ? { kind: decoded.data.kind } : {},
21416
- relays: [...decoded.data.relays ?? []]
21417
- };
21418
- }
21419
- throw new Error(`Expected naddr or nevent pointer, got ${decoded.type}.`);
21420
- }
21421
- async function resolvePajaPointer(value, options = {}) {
21422
- const pointer = decodePajaPointer(value);
21423
- const relays = uniqueRelayUrls([...pointer.relays, ...options.relays ?? []]);
21424
- if (relays.length === 0) {
21425
- throw new Error("Pointer has no relay hints or enabled configured live relays.");
21426
- }
21427
- const pool = options.pool ?? new SimplePool();
21428
- const ownsPool = options.pool === void 0;
21429
- try {
21430
- const event = await resolvePointerEvent(pointer, pool, relays, normalizeMaxWaitMs(options.maxWaitMs));
21431
- const blossomServers = [...options.blossomServers ?? []];
21432
- const resolved = await resolveNapplet({
21433
- event,
21434
- cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
21435
- fetchBlob: (sha256Hex, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex, async (url) => {
21436
- const response = await (options.fetcher ?? fetch)(url);
21437
- if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
21438
- return new Uint8Array(await response.arrayBuffer());
21439
- })
21440
- });
21441
- return {
21442
- pointer,
21443
- event,
21444
- relays,
21445
- blossomServers,
21446
- dTag: resolved.dTag,
21447
- aggregateHash: resolved.aggregateHash,
21448
- indexHtml: resolved.indexHtml,
21449
- manifest: resolved.manifest
21450
- };
21451
- } finally {
21452
- if (ownsPool) {
21453
- pool.close?.(relays);
21454
- pool.destroy?.();
21455
- }
21456
- }
21457
- }
21458
- function injectPajaRuntimeCsp(html, origins) {
21459
- const grantedOrigins = [...new Set(origins)].sort();
21460
- const connectSrc = grantedOrigins.length > 0 ? `connect-src ${grantedOrigins.join(" ")}` : "connect-src 'none'";
21461
- const value = [
21462
- "default-src 'none'",
21463
- "script-src 'unsafe-inline'",
21464
- "style-src 'unsafe-inline'",
21465
- "img-src data: blob:",
21466
- "font-src data:",
21467
- connectSrc,
21468
- "worker-src 'none'",
21469
- "child-src 'none'",
21470
- "frame-src 'none'",
21471
- "media-src 'none'",
21472
- "object-src 'none'",
21473
- "manifest-src 'none'",
21474
- "prefetch-src 'none'",
21475
- "base-uri 'none'",
21476
- "form-action 'none'",
21477
- "frame-ancestors 'self'"
21478
- ].join("; ");
21479
- const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(value)}">`;
21480
- if (/<head[^>]*>/i.test(html)) {
21481
- return html.replace(/<head[^>]*>/i, (open) => `${open}${meta}`);
21482
- }
21483
- if (/<html[^>]*>/i.test(html)) {
21484
- return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${meta}</head>`);
21485
- }
21486
- return `${meta}${html}`;
21487
- }
21488
- async function resolvePointerEvent(pointer, pool, relays, maxWaitMs) {
21489
- const filter = pointer.type === "naddr" ? addressableFilter(pointer) : eventFilter(pointer);
21490
- const result2 = await queryPointerRelays(pool, relays, filter, maxWaitMs);
21491
- const event = result2.events.filter((candidate) => matchesPointer(pointer, candidate)).sort((a, b) => b.created_at - a.created_at)[0];
21492
- if (!event) {
21493
- if (result2.timedOut) {
21494
- throw new Error(
21495
- `Relay resolution timed out after ${maxWaitMs}ms for ${pointer.type} pointer across ${relays.length} relays before complete EOSE.`
21496
- );
21497
- }
21498
- const failures = relayFailureReasons(result2.reasons);
21499
- if (result2.error || failures.length > 0) {
21500
- const detail = result2.error instanceof Error ? result2.error.message : result2.error === void 0 ? failures.join("; ") : String(result2.error);
21501
- throw new Error(`Relay query failed for ${pointer.type} pointer before complete EOSE: ${detail}`);
21502
- }
21503
- throw new Error(
21504
- `All ${relays.length} relays reached EOSE without a matching ${pointer.type} napplet manifest event.`
21505
- );
21506
- }
21507
- if (!isNappletManifestKind(event.kind)) {
21508
- throw new Error(`Pointer resolved kind ${event.kind}; expected ${formatNappletManifestKinds()}.`);
21509
- }
21510
- return event;
21511
- }
21512
- function queryPointerRelays(pool, relays, filter, maxWaitMs) {
21513
- if (pool.subscribeManyEose) {
21514
- return queryPointerRelaySubscription(pool, relays, filter, maxWaitMs);
21515
- }
21516
- return queryPointerRelaySync(pool, relays, filter, maxWaitMs);
21517
- }
21518
- function queryPointerRelaySubscription(pool, relays, filter, maxWaitMs) {
21519
- return new Promise((resolve) => {
21520
- const events = [];
21521
- let settled = false;
21522
- let subscription;
21523
- const finish = (result2) => {
21524
- if (settled) return;
21525
- settled = true;
21526
- clearTimeout(deadline);
21527
- resolve({ events, ...result2 });
21528
- };
21529
- const deadline = setTimeout(() => {
21530
- finish({ reasons: [], timedOut: true });
21531
- void subscription?.close("Paja pointer relay deadline exceeded");
21532
- }, maxWaitMs);
21533
- try {
21534
- subscription = pool.subscribeManyEose([...relays], filter, {
21535
- maxWait: maxWaitMs,
21536
- label: "kehto-paja-runtime",
21537
- onevent(event) {
21538
- events.push(event);
21539
- },
21540
- onclose(reasons) {
21541
- finish({ reasons, timedOut: false });
21542
- }
21543
- });
21544
- } catch (error) {
21545
- finish({ reasons: [], timedOut: false, error });
21546
- }
21547
- });
21548
- }
21549
- function queryPointerRelaySync(pool, relays, filter, maxWaitMs) {
21550
- return new Promise((resolve) => {
21551
- let settled = false;
21552
- const finish = (result2) => {
21553
- if (settled) return;
21554
- settled = true;
21555
- clearTimeout(deadline);
21556
- resolve(result2);
21557
- };
21558
- const deadline = setTimeout(() => {
21559
- finish({ events: [], reasons: [], timedOut: true });
21560
- }, maxWaitMs);
21561
- void pool.querySync([...relays], filter, {
21562
- maxWait: maxWaitMs,
21563
- label: "kehto-paja-runtime"
21564
- }).then(
21565
- (events) => finish({ events, reasons: [], timedOut: false }),
21566
- (error) => finish({ events: [], reasons: [], timedOut: false, error })
21567
- );
21568
- });
21569
- }
21570
- function relayFailureReasons(reasons) {
21571
- 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);
21572
- }
21573
- function normalizeMaxWaitMs(value) {
21574
- if (value === void 0) return 5e3;
21575
- if (!Number.isFinite(value) || value <= 0) {
21576
- throw new Error("Pointer relay deadline must be a positive number of milliseconds.");
21577
- }
21578
- return Math.floor(value);
21579
- }
21580
- function addressableFilter(pointer) {
21581
- if (pointer.kind !== NAPPLET_KIND_NAMED) {
21582
- throw new Error(`naddr kind ${pointer.kind} is not a named NIP-5D napplet manifest kind.`);
21583
- }
21584
- return {
21585
- kinds: [NAPPLET_KIND_NAMED],
21586
- authors: [pointer.pubkey],
21587
- "#d": [pointer.identifier],
21588
- limit: 1
21589
- };
21590
- }
21591
- function eventFilter(pointer) {
21592
- return {
21593
- ids: [pointer.id],
21594
- ...pointer.kind ? { kinds: [pointer.kind] } : {},
21595
- ...pointer.author ? { authors: [pointer.author] } : {},
21596
- limit: 1
21597
- };
21598
- }
21599
- function matchesPointer(pointer, event) {
21600
- if (pointer.type === "nevent") {
21601
- if (event.id !== pointer.id) return false;
21602
- if (pointer.author && event.pubkey !== pointer.author) return false;
21603
- if (pointer.kind && event.kind !== pointer.kind) return false;
21604
- return true;
21605
- }
21606
- return event.kind === pointer.kind && event.pubkey === pointer.pubkey && event.tags.some((tag) => tag[0] === "d" && tag[1] === pointer.identifier);
21607
- }
21608
- function uniqueRelayUrls(values) {
21609
- const out = [];
21610
- const seen = /* @__PURE__ */ new Set();
21611
- for (const value of values) {
21612
- const url = normalizeRelayUrl2(value);
21613
- if (!url || seen.has(url)) continue;
21614
- seen.add(url);
21615
- out.push(url);
21616
- }
21617
- return out;
21618
- }
21619
- function normalizeRelayUrl2(value) {
21620
- const raw = value.trim();
21621
- if (!raw) return void 0;
21622
- try {
21623
- const url = new URL(raw);
21624
- if (url.protocol !== "wss:" && url.protocol !== "ws:") return void 0;
21625
- url.hash = "";
21626
- url.search = "";
21627
- return url.href.replace(/\/$/, "");
21628
- } catch {
21629
- return void 0;
21630
- }
21631
- }
21632
- function escapeAttribute(value) {
21633
- return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
21634
- }
21635
- function formatNappletManifestKinds() {
21636
- return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
21637
- }
21638
-
21639
21746
  // src/browser-intent-host.ts
21640
21747
  function createPajaIntentTargetOptions(getState, getContext, effects = {}) {
21641
21748
  return {
@@ -21815,113 +21922,6 @@ function createPajaThemeBroadcastLink() {
21815
21922
  };
21816
21923
  }
21817
21924
 
21818
- // src/browser-target-frame.ts
21819
- function resolvePajaFrameEnvironment(hooks, identity) {
21820
- if (!identity.dTag || !identity.aggregateHash) {
21821
- throw new Error("Paja frame identity requires a dTag and aggregate hash");
21822
- }
21823
- return resolveShellEnvironment(hooks, identity);
21824
- }
21825
- function getTargetIdentity2(config, resolvedTarget) {
21826
- return {
21827
- pubkey: "",
21828
- dTag: resolvedTarget?.dTag ?? config.window.dTag,
21829
- aggregateHash: resolvedTarget?.aggregateHash ?? config.window.aggregateHash
21830
- };
21831
- }
21832
- function getTargetOriginIdentity(config, resolvedTarget) {
21833
- const target = getTargetIdentity2(config, resolvedTarget);
21834
- return Object.freeze({ dTag: target.dTag, aggregateHash: target.aggregateHash });
21835
- }
21836
- function registerFrameForGeneration(frame, config, generation, identity, environment, windowId = `${config.window.id}:${generation}`) {
21837
- const win = frame.contentWindow;
21838
- if (!win) return null;
21839
- originRegistry.register(win, windowId, identity);
21840
- originRegistry.setEnvironment(win, environment);
21841
- return windowId;
21842
- }
21843
- async function navigateFrame(frame, config, generation, adapter, resolvedTarget, windowId, isCurrent, onRegistered) {
21844
- const identity = getTargetOriginIdentity(config, resolvedTarget);
21845
- const environment = resolvePajaFrameEnvironment(adapter, identity);
21846
- const domains = environment.capabilities.domains;
21847
- if (config.target.mode === "runtime-pointer") {
21848
- if (!resolvedTarget) {
21849
- frame.removeAttribute("src");
21850
- frame.srcdoc = "<!doctype html><html><body></body></html>";
21851
- return null;
21852
- }
21853
- if (isCurrent && !isCurrent()) return null;
21854
- const registeredWindowId2 = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
21855
- onRegistered?.(registeredWindowId2);
21856
- frame.removeAttribute("src");
21857
- frame.srcdoc = injectNappletNamespacePrelude(
21858
- injectPajaRuntimeCsp(
21859
- resolvedTarget.indexHtml,
21860
- connectOrigins([...resolvedTarget.relays, ...resolvedTarget.blossomServers])
21861
- ),
21862
- { domains }
21863
- );
21864
- return registeredWindowId2;
21865
- }
21866
- const html = await fetchTargetHtml();
21867
- if (isCurrent && !isCurrent()) return null;
21868
- const registeredWindowId = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
21869
- onRegistered?.(registeredWindowId);
21870
- frame.removeAttribute("src");
21871
- frame.srcdoc = injectNappletNamespacePrelude(
21872
- injectBaseHref(html, config.target.url),
21873
- { domains }
21874
- );
21875
- return registeredWindowId;
21876
- }
21877
- function renderTargetErrorHtml(error) {
21878
- const message = error instanceof Error ? error.message : String(error);
21879
- return `<!doctype html><html><body><pre>${escapeHtml(message)}</pre></body></html>`;
21880
- }
21881
- function connectOrigins(urls) {
21882
- const out = /* @__PURE__ */ new Set();
21883
- for (const value of urls) {
21884
- try {
21885
- const url = new URL(value);
21886
- if (url.protocol === "wss:" || url.protocol === "ws:") {
21887
- out.add(value.replace(/\/$/, ""));
21888
- } else {
21889
- out.add(url.origin);
21890
- }
21891
- } catch {
21892
- }
21893
- }
21894
- return [...out];
21895
- }
21896
- async function fetchTargetHtml() {
21897
- const response = await fetch(new URL("./__kehto/target.html", window.location.href), {
21898
- cache: "no-store",
21899
- headers: {
21900
- accept: "text/html, application/xhtml+xml;q=0.9, */*;q=0.8"
21901
- }
21902
- });
21903
- if (!response.ok) {
21904
- throw new Error(`Paja target fetch failed: ${response.status} ${response.statusText}`);
21905
- }
21906
- return response.text();
21907
- }
21908
- function injectBaseHref(html, targetUrl) {
21909
- const base = `<base href="${escapeAttribute2(targetUrl)}">`;
21910
- if (/<head[^>]*>/i.test(html)) {
21911
- return html.replace(/<head[^>]*>/i, (open) => `${open}${base}`);
21912
- }
21913
- if (/<html[^>]*>/i.test(html)) {
21914
- return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${base}</head>`);
21915
- }
21916
- return `${base}${html}`;
21917
- }
21918
- function escapeAttribute2(value) {
21919
- return escapeHtml(value).replaceAll('"', "&quot;");
21920
- }
21921
- function escapeHtml(value) {
21922
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
21923
- }
21924
-
21925
21925
  // src/browser-target-diagnostics.ts
21926
21926
  async function reportTargetCorsDiagnostic(state) {
21927
21927
  let diagnostic;
@@ -22710,7 +22710,7 @@ function createPajaBrowserState(context) {
22710
22710
  setRuntimeDomainEnabled(this, context, domain, enabled);
22711
22711
  },
22712
22712
  setAclCapability(capability, enabled) {
22713
- const identity = getTargetIdentity2(config, this.resolvedTarget);
22713
+ const identity = getTargetIdentity(config, this.resolvedTarget);
22714
22714
  if (enabled) bridge.runtime.aclState.grant(identity.pubkey, identity.dTag, identity.aggregateHash, capability);
22715
22715
  else bridge.runtime.aclState.revoke(identity.pubkey, identity.dTag, identity.aggregateHash, capability);
22716
22716
  bridge.runtime.aclState.persist();
@@ -22784,7 +22784,7 @@ async function installPajaHost() {
22784
22784
  );
22785
22785
  const getWindowIdentity = (windowId) => {
22786
22786
  const tabTarget = stateRef?.tabs.find((tab) => tab.windowId === windowId)?.resolvedTarget;
22787
- return getTargetIdentity2(config, tabTarget ?? stateRef?.resolvedTarget);
22787
+ return getTargetIdentity(config, tabTarget ?? stateRef?.resolvedTarget);
22788
22788
  };
22789
22789
  const notifyController = createPajaNotifyController({
22790
22790
  confirm: confirmationController.confirm,