@kehto/paja 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1329,7 +1329,7 @@ function handleRelayPublish(context, windowId, m) {
1329
1329
  sendRelayPublishResult(hooks, windowId, id, false, void 0, "invalid event template");
1330
1330
  return;
1331
1331
  }
1332
- const signer = hooks.auth.getSigner();
1332
+ const signer = hooks.auth.getSigner(windowId);
1333
1333
  const signEvent = signer?.signEvent?.bind(signer);
1334
1334
  if (!signEvent) {
1335
1335
  sendRelayPublishResult(hooks, windowId, id, false, void 0, "no signer configured");
@@ -1440,7 +1440,7 @@ function handleRelayPublishEncrypted(context, windowId, msg) {
1440
1440
  replyPe(false, { error: `unsupported encryption scheme: ${encryption}` });
1441
1441
  return;
1442
1442
  }
1443
- const peSigner = hooks.auth.getSigner();
1443
+ const peSigner = hooks.auth.getSigner(windowId);
1444
1444
  if (!peSigner) {
1445
1445
  replyPe(false, { error: "no signer configured" });
1446
1446
  return;
@@ -1449,12 +1449,9 @@ function handleRelayPublishEncrypted(context, windowId, msg) {
1449
1449
  replyPe(false, { error: "invalid event template" });
1450
1450
  return;
1451
1451
  }
1452
- publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe);
1452
+ publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, peSigner, replyPe);
1453
1453
  }
1454
- function publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, replyPe) {
1455
- const { hooks } = context;
1456
- const peSigner = hooks.auth.getSigner();
1457
- if (!peSigner) return;
1454
+ function publishEncrypted(context, windowId, id, recipient, encryption, eventTemplate, peSigner, replyPe) {
1458
1455
  (async () => {
1459
1456
  try {
1460
1457
  const plaintext = String(eventTemplate.content ?? "");
@@ -1697,7 +1694,7 @@ function createIdentityHandler(context) {
1697
1694
  return;
1698
1695
  }
1699
1696
  const action = msg.type.slice("identity.".length);
1700
- const signer = hooks.auth.getSigner();
1697
+ const signer = hooks.auth.getSigner(windowId);
1701
1698
  const fallback = createCanonicalDomainResult(msg);
1702
1699
  if (!fallback) return;
1703
1700
  const sendResult = (payload = {}) => {
@@ -2450,11 +2447,8 @@ function createFirewallGate(config) {
2450
2447
  if (decision === "prompt") fireConsent(windowId, napplet);
2451
2448
  return "drop";
2452
2449
  }
2453
- const id = envelope.id ?? "";
2454
- const type = denialResponseType(envelope);
2455
- if (type) {
2456
- hooks.sendToNapplet(windowId, { type, id, error: `firewall: ${reason}` });
2457
- }
2450
+ const denial = createDenialResponse(envelope, `firewall: ${reason}`);
2451
+ if (denial) hooks.sendToNapplet(windowId, denial);
2458
2452
  hooks.onFirewallEvent?.({ windowId, napplet, opClass, decision, action, ruleId, reason, message: envelope });
2459
2453
  if (decision === "prompt") {
2460
2454
  fireConsent(windowId, napplet);
@@ -2476,6 +2470,19 @@ function denialResponseType(envelope) {
2476
2470
  if (envelope.type.startsWith("inc.")) return null;
2477
2471
  return `${envelope.type}.error`;
2478
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
+ }
2479
2486
  function createMessageHandler(hooks, sessionRegistry2, enforceNap, dispatchNapEnvelope, firewallGate) {
2480
2487
  return (windowId, msg) => {
2481
2488
  if (typeof msg !== "object" || msg === null || !("type" in msg)) return;
@@ -2500,12 +2507,9 @@ function createMessageHandler(hooks, sessionRegistry2, enforceNap, dispatchNapEn
2500
2507
  hooks.sendToNapplet(windowId, canonicalResult);
2501
2508
  return;
2502
2509
  }
2503
- const id = envelope.id ?? "";
2504
2510
  const error = formatDenialReason(result2.capability);
2505
- const type = denialResponseType(envelope);
2506
- if (type) {
2507
- hooks.sendToNapplet(windowId, { type, id, error });
2508
- }
2511
+ const denial = createDenialResponse(envelope, error);
2512
+ if (denial) hooks.sendToNapplet(windowId, denial);
2509
2513
  return;
2510
2514
  }
2511
2515
  }
@@ -2835,8 +2839,8 @@ function createAuthAdapter(shellHooks) {
2835
2839
  getUserPubkey() {
2836
2840
  return shellHooks.auth.getUserPubkey();
2837
2841
  },
2838
- getSigner() {
2839
- return shellHooks.auth.getSigner();
2842
+ getSigner(windowId) {
2843
+ return shellHooks.auth.getSigner(windowId);
2840
2844
  }
2841
2845
  };
2842
2846
  }
@@ -11087,11 +11091,20 @@ function requestKey(windowId, requestId) {
11087
11091
  return `${windowId}\0${requestId}`;
11088
11092
  }
11089
11093
  function assertResourceOptions(options) {
11090
- if (typeof options?.fetch !== "function" || typeof options?.isOriginGranted !== "function" || typeof options?.getConnectGrants !== "function" || typeof options?.resolveIdentity !== "function") {
11094
+ if (typeof options?.fetch !== "function") {
11091
11095
  throw new Error(
11092
- "[RESOURCE-01 / H-03] createResourceService requires {fetch, isOriginGranted, getConnectGrants, resolveIdentity} \u2014 all four options are required from day one. The grants source (getConnectGrants) MUST be wired at construction time to prevent unguarded fetch proxying."
11096
+ "[RESOURCE-01] createResourceService requires a runtime-provided fetch resolver."
11093
11097
  );
11094
11098
  }
11099
+ const hasOriginPolicy = options.isOriginGranted !== void 0 || options.getConnectGrants !== void 0;
11100
+ if (hasOriginPolicy && (typeof options.isOriginGranted !== "function" || typeof options.getConnectGrants !== "function" || typeof options.resolveIdentity !== "function")) {
11101
+ throw new Error(
11102
+ "[RESOURCE-01] optional origin policy requires {isOriginGranted, getConnectGrants, resolveIdentity} together."
11103
+ );
11104
+ }
11105
+ if (options.resolveIdentity !== void 0 && typeof options.resolveIdentity !== "function") {
11106
+ throw new Error("[RESOURCE-01] resolveIdentity must be a function when provided.");
11107
+ }
11095
11108
  }
11096
11109
  function trackRequest(state, requestId, windowId, controller) {
11097
11110
  const key = requestKey(windowId, requestId);
@@ -11147,7 +11160,7 @@ function normalizeResourceInfo(info) {
11147
11160
  }
11148
11161
  async function resolveResourceInfo(options, windowId) {
11149
11162
  const configured = options.resourceInfo ?? DEFAULT_RESOURCE_INFO;
11150
- const info = typeof configured === "function" ? await configured({ windowId, identity: options.resolveIdentity(windowId) }) : configured;
11163
+ const info = typeof configured === "function" ? await configured({ windowId, identity: options.resolveIdentity?.(windowId) ?? null }) : configured;
11151
11164
  return normalizeResourceInfo(info);
11152
11165
  }
11153
11166
  async function handleInfo(options, windowId, requestId, send) {
@@ -11181,22 +11194,29 @@ function classifyResourceFailure(error) {
11181
11194
  message: error instanceof Error ? error.message : String(error)
11182
11195
  };
11183
11196
  }
11184
- async function fetchResourceItem(options, identity, info, url, signal) {
11197
+ async function fetchResourceItem(options, windowId, info, url, signal) {
11185
11198
  const parsedUrl = parseResourceUrl(url);
11186
11199
  if (!parsedUrl) return resourceInvalidRequest(url, `invalid URL: ${url}`);
11187
- const scheme = parsedUrl.protocol.slice(0, -1).toLowerCase();
11188
- if (!info.schemes.some((item) => item.enabled && item.scheme.toLowerCase() === scheme)) {
11189
- return { ok: false, url, error: "unsupported-scheme", message: `scheme ${scheme} is not enabled` };
11190
- }
11191
- const origin = parsedUrl.origin;
11192
- const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);
11193
- if (!options.isOriginGranted(origin, grants)) {
11194
- return {
11195
- ok: false,
11196
- url,
11197
- error: "blocked-by-policy",
11198
- message: `origin ${origin} not granted`
11199
- };
11200
+ if (options.isOriginGranted && options.getConnectGrants) {
11201
+ const identity = options.resolveIdentity?.(windowId) ?? null;
11202
+ if (!identity) {
11203
+ return {
11204
+ ok: false,
11205
+ url,
11206
+ error: "blocked-by-policy",
11207
+ message: "napplet identity not resolvable for runtime origin policy"
11208
+ };
11209
+ }
11210
+ const origin = parsedUrl.origin;
11211
+ const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);
11212
+ if (!options.isOriginGranted(origin, grants)) {
11213
+ return {
11214
+ ok: false,
11215
+ url,
11216
+ error: "blocked-by-policy",
11217
+ message: `origin ${origin} not granted by runtime policy`
11218
+ };
11219
+ }
11200
11220
  }
11201
11221
  try {
11202
11222
  const response = await options.fetch(url, {
@@ -11226,16 +11246,11 @@ async function fetchResourceItem(options, identity, info, url, signal) {
11226
11246
  }
11227
11247
  async function handleBytes(options, state, windowId, msg, send) {
11228
11248
  const { requestId, url } = msg;
11229
- const identity = options.resolveIdentity(windowId);
11230
- if (!identity) {
11231
- sendResourceError(send, requestId, "napplet identity not resolvable", "blocked-by-policy");
11232
- return;
11233
- }
11234
11249
  const controller = new AbortController();
11235
11250
  trackRequest(state, requestId, windowId, controller);
11236
11251
  try {
11237
11252
  const info = await resolveResourceInfo(options, windowId);
11238
- const item = await fetchResourceItem(options, identity, info, url, controller.signal);
11253
+ const item = await fetchResourceItem(options, windowId, info, url, controller.signal);
11239
11254
  if (controller.signal.aborted) return;
11240
11255
  if (!item.ok) {
11241
11256
  sendResourceError(send, requestId, item.message, item.error);
@@ -11262,11 +11277,6 @@ async function handleBytesMany(options, state, windowId, msg, send) {
11262
11277
  sendBytesManyError(send, requestId, "resource.bytesMany requires a non-empty urls array", "invalid-request");
11263
11278
  return;
11264
11279
  }
11265
- const identity = options.resolveIdentity(windowId);
11266
- if (!identity) {
11267
- sendBytesManyError(send, requestId, "napplet identity not resolvable", "blocked-by-policy");
11268
- return;
11269
- }
11270
11280
  let info;
11271
11281
  try {
11272
11282
  info = await resolveResourceInfo(options, windowId);
@@ -11284,7 +11294,7 @@ async function handleBytesMany(options, state, windowId, msg, send) {
11284
11294
  try {
11285
11295
  const items = [];
11286
11296
  for (const url of urls) {
11287
- const item = await fetchResourceItem(options, identity, info, url, controller.signal);
11297
+ const item = await fetchResourceItem(options, windowId, info, url, controller.signal);
11288
11298
  if (controller.signal.aborted) return;
11289
11299
  if (item.ok) {
11290
11300
  items.push({
@@ -11343,7 +11353,7 @@ function createResourceService(options) {
11343
11353
  const descriptor = {
11344
11354
  name: "resource",
11345
11355
  version: RESOURCE_SERVICE_VERSION,
11346
- description: "NAP-RESOURCE reference service \u2014 shell-proxied authenticated fetch (RESOURCE-01..06)"
11356
+ description: "NAP-RESOURCE service \u2014 runtime-resolved byte transport (RESOURCE-01..06)"
11347
11357
  };
11348
11358
  const handler = {
11349
11359
  descriptor,
@@ -18186,6 +18196,9 @@ function createPajaResourceFetch(options = {}) {
18186
18196
  if (url.protocol === "blossom:") {
18187
18197
  return fetchBlossomResource(value, init.signal, options);
18188
18198
  }
18199
+ if (url.protocol === "http:" || url.protocol === "https:") {
18200
+ return fetchNetworkResource(value, init.signal, options.fetch ?? globalThis.fetch);
18201
+ }
18189
18202
  if (url.protocol !== "data:") {
18190
18203
  throw new ResourceServiceError("unsupported-scheme", `Paja does not enable ${url.protocol}`);
18191
18204
  }
@@ -18205,7 +18218,11 @@ function createPajaResourceFetch(options = {}) {
18205
18218
  };
18206
18219
  }
18207
18220
  function pajaResourceInfo(blossomServers = []) {
18208
- const schemes = [{ scheme: "data", enabled: true }];
18221
+ const schemes = [
18222
+ { scheme: "data", enabled: true },
18223
+ { scheme: "https", enabled: true },
18224
+ { scheme: "http", enabled: true }
18225
+ ];
18209
18226
  if (usableBlossomServers(blossomServers).length > 0) {
18210
18227
  schemes.push({ scheme: "blossom", enabled: true });
18211
18228
  }
@@ -18215,6 +18232,35 @@ function pajaResourceInfo(blossomServers = []) {
18215
18232
  maxUrls: PAJA_RESOURCE_MAX_URLS
18216
18233
  };
18217
18234
  }
18235
+ async function fetchNetworkResource(value, signal, fetcher) {
18236
+ let response;
18237
+ try {
18238
+ response = await fetcher(value, {
18239
+ method: "GET",
18240
+ signal,
18241
+ credentials: "omit",
18242
+ referrerPolicy: "no-referrer"
18243
+ });
18244
+ } catch (error) {
18245
+ if (signal.aborted || isAbortError(error)) throw error;
18246
+ throw new ResourceServiceError(
18247
+ "network-error",
18248
+ error instanceof Error ? error.message : String(error)
18249
+ );
18250
+ }
18251
+ if (response.status === 404 || response.status === 410) {
18252
+ throw new ResourceServiceError("not-found", "HTTP resource was not found");
18253
+ }
18254
+ if (!response.ok) {
18255
+ throw new ResourceServiceError("network-error", `HTTP resource returned ${response.status}`);
18256
+ }
18257
+ const bytes = await readCappedResponse(response, signal);
18258
+ const mime = sniffSafeResourceMime(bytes);
18259
+ if (!mime) {
18260
+ throw new ResourceServiceError("decode-failed", "resource bytes are not an enabled safe media type");
18261
+ }
18262
+ return new Response(arrayBufferFor(bytes), { headers: { "content-type": mime } });
18263
+ }
18218
18264
  async function fetchBlossomResource(value, signal, options) {
18219
18265
  const match = BLOSSOM_RESOURCE_PATTERN.exec(value);
18220
18266
  if (!match?.[1]) {
@@ -19421,12 +19467,17 @@ async function createPajaBrowserFsBackend(options) {
19421
19467
  // src/browser-dev-runtime.ts
19422
19468
  var DEV_SIGNER_SECRET_KEY = generateSecretKey();
19423
19469
  var PAJA_DEV_SIGNER_PUBKEY = getPublicKey(DEV_SIGNER_SECRET_KEY);
19424
- function createPajaDevSigner(getSimulation, confirm) {
19470
+ function createPajaDevSigner(getSimulation, confirm, source) {
19425
19471
  return {
19426
19472
  getPublicKey: () => PAJA_DEV_SIGNER_PUBKEY,
19427
19473
  getRelays: () => Object.fromEntries(getPajaRelayUrls(getSimulation()).map((relay) => [relay, { read: true, write: true }])),
19428
19474
  async signEvent(event) {
19429
- if (!await confirm({ action: "sign", event })) {
19475
+ const signerContext = source ? { ...source, signerPubkey: PAJA_DEV_SIGNER_PUBKEY } : void 0;
19476
+ if (!await confirm({
19477
+ action: "sign",
19478
+ event,
19479
+ ...signerContext ? { signerContext } : {}
19480
+ })) {
19430
19481
  throw new Error("Paja signing request denied");
19431
19482
  }
19432
19483
  const template = { ...event };
@@ -19549,10 +19600,12 @@ function createDevTheme(mode, values) {
19549
19600
  function getRuntimePubkey(getSimulation, signerProvider) {
19550
19601
  return signerProvider?.getPubkey() || (signerProvider?.getMethod() === "dev" ? PAJA_DEV_SIGNER_PUBKEY : "") || getSimulation().identity.pubkey;
19551
19602
  }
19552
- function createRuntimeSigner(getSimulation, confirmRequest, signerProvider) {
19553
- const signer = signerProvider?.getSigner();
19603
+ function createRuntimeSigner(getSimulation, confirmRequest, signerProvider, source) {
19604
+ const signer = signerProvider?.getSigner(source);
19554
19605
  if (!signer) {
19555
- if (signerProvider?.getMethod() === "dev") return createPajaDevSigner(getSimulation, confirmRequest);
19606
+ if (signerProvider?.getMethod() === "dev") {
19607
+ return createPajaDevSigner(getSimulation, confirmRequest, source);
19608
+ }
19556
19609
  const fixedPubkey = getSimulation().identity.pubkey;
19557
19610
  if (fixedPubkey) {
19558
19611
  return {
@@ -19596,9 +19649,6 @@ function createDevServices(backend, getSimulation, relayConfig, onThemeService,
19596
19649
  const services = {
19597
19650
  resource: createResourceService({
19598
19651
  fetch: createPajaResourceFetch({ getBlossomServers }),
19599
- isOriginGranted: (origin, grants) => grants.includes(origin),
19600
- getConnectGrants: () => ["null"],
19601
- resolveIdentity: (windowId) => getIdentity?.(windowId) ?? null,
19602
19652
  resourceInfo: () => pajaResourceInfo(getBlossomServers())
19603
19653
  })
19604
19654
  };
@@ -19772,6 +19822,18 @@ function createDevServices(backend, getSimulation, relayConfig, onThemeService,
19772
19822
  return { services, refreshAvailability };
19773
19823
  }
19774
19824
  function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadcast, confirmRequest, signerProvider, getIdentity, onEnvironmentChanged, intentHost, userActivation, notifyOptions, configOptions) {
19825
+ const resolveIdentity = (windowId) => getIdentity?.(windowId) ?? {
19826
+ dTag: config.window.dTag,
19827
+ aggregateHash: config.window.aggregateHash
19828
+ };
19829
+ const signerSource = (windowId) => {
19830
+ const napplet = resolveIdentity(windowId);
19831
+ return {
19832
+ windowId,
19833
+ napplet,
19834
+ runtimeScope: config.target?.mode === "iframe-url" ? `target-url:${config.target.url}` : `artifact:${napplet.aggregateHash}`
19835
+ };
19836
+ };
19775
19837
  const relayBackend = createPajaRelayBackend(getSimulation, confirmRequest);
19776
19838
  const relayConfig = createPajaRelayConfig(getSimulation);
19777
19839
  const uploadRuntime = getSimulation().upload.mode === "blossom" ? createPajaUploadRuntime({
@@ -19781,10 +19843,7 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
19781
19843
  queryDiscovery: (relayUrls2, filters) => relayBackend.query(relayUrls2, filters),
19782
19844
  getRelayUrls: () => relayConfig.getRelayUrls(["discovery", "super"]),
19783
19845
  confirmRequest,
19784
- getNappletIdentity: (windowId) => getIdentity?.(windowId) ?? {
19785
- dTag: config.window.dTag,
19786
- aggregateHash: config.window.aggregateHash
19787
- },
19846
+ getNappletIdentity: resolveIdentity,
19788
19847
  subscribeSignerChange: signerProvider?.subscribe?.bind(signerProvider)
19789
19848
  }) : void 0;
19790
19849
  const getBlossomServers = () => [
@@ -19804,20 +19863,14 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
19804
19863
  uploadRuntime,
19805
19864
  signerProvider,
19806
19865
  intentHost,
19807
- getIdentity ?? (() => ({
19808
- dTag: config.window.dTag,
19809
- aggregateHash: config.window.aggregateHash
19810
- })),
19866
+ resolveIdentity,
19811
19867
  userActivation,
19812
19868
  notifyOptions,
19813
19869
  configOptions
19814
19870
  );
19815
19871
  const services = serviceBundle.services;
19816
19872
  const ready = createPajaBrowserFsBackend({
19817
- getIdentity: (windowId) => getIdentity?.(windowId) ?? {
19818
- dTag: config.window.dTag,
19819
- aggregateHash: config.window.aggregateHash
19820
- },
19873
+ getIdentity: resolveIdentity,
19821
19874
  userActivation
19822
19875
  }).then((fsBackend) => {
19823
19876
  if (!fsBackend) return;
@@ -19839,7 +19892,12 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
19839
19892
  windowManager: { createWindow: () => null },
19840
19893
  auth: {
19841
19894
  getUserPubkey: () => getRuntimePubkey(getSimulation, signerProvider),
19842
- getSigner: () => createRuntimeSigner(getSimulation, confirmRequest, signerProvider)
19895
+ getSigner: (windowId) => createRuntimeSigner(
19896
+ getSimulation,
19897
+ confirmRequest,
19898
+ signerProvider,
19899
+ windowId ? signerSource(windowId) : void 0
19900
+ )
19843
19901
  },
19844
19902
  services,
19845
19903
  get capabilities() {
@@ -19884,621 +19942,1214 @@ function forwardPajaHotkey(event) {
19884
19942
  }));
19885
19943
  }
19886
19944
 
19887
- // src/browser-devtools.ts
19888
- var PAJA_LOG_LIMIT = 500;
19889
- var proxyToReal = /* @__PURE__ */ new WeakMap();
19890
- var realToProxy = /* @__PURE__ */ new WeakMap();
19891
- var proxyContexts = /* @__PURE__ */ new WeakMap();
19892
- function isNappletMessage(value) {
19893
- return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string";
19894
- }
19895
- function describeMessage(raw) {
19896
- if (isNappletMessage(raw)) return raw.type;
19897
- if (Array.isArray(raw) && typeof raw[0] === "string") return raw[0];
19898
- return "unknown";
19899
- }
19900
- function previewMessage(raw) {
19901
- try {
19902
- return JSON.stringify(raw);
19903
- } catch (error) {
19904
- return `unserializable:${error instanceof Error ? error.message : String(error)}`;
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
+ };
19905
19969
  }
19970
+ throw new Error(`Expected naddr or nevent pointer, got ${decoded.type}.`);
19906
19971
  }
19907
- function compactMessageDetail(raw) {
19908
- const detail = {};
19909
- for (const [key, value] of Object.entries(raw)) {
19910
- if (key !== "type" && key !== "id" && value !== void 0) detail[key] = value;
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.");
19911
19977
  }
19912
- if (Object.keys(detail).length === 0) return "";
19978
+ const pool = options.pool ?? new SimplePool();
19979
+ const ownsPool = options.pool === void 0;
19913
19980
  try {
19914
- return JSON.stringify(detail);
19915
- } catch {
19916
- return "";
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
+ }
19917
20007
  }
19918
20008
  }
19919
- function describeMessageDetail(raw) {
19920
- if (!isNappletMessage(raw)) return "";
19921
- const value = raw;
19922
- for (const key of ["error", "message", "reason"]) {
19923
- const detail = value[key];
19924
- if (typeof detail === "string" && detail.length > 0) return detail;
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'",
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}`);
19925
20033
  }
19926
- return raw.type.endsWith(".error") ? compactMessageDetail(value) : "";
19927
- }
19928
- function domainEnabled(simulation, domain) {
19929
- return simulation.capabilities.domains[domain] === true;
19930
- }
19931
- function getTargetIdentity(config) {
19932
- return {
19933
- pubkey: "",
19934
- dTag: config.window.dTag,
19935
- aggregateHash: config.window.aggregateHash
19936
- };
19937
- }
19938
- function appendPajaMessageLog(state, direction, raw, windowId) {
19939
- if (!state) return;
19940
- state.messageLog.push({
19941
- index: state.messageLog.length,
19942
- timestamp: Date.now(),
19943
- direction,
19944
- type: describeMessage(raw),
19945
- windowId,
19946
- preview: previewMessage(raw),
19947
- detail: describeMessageDetail(raw)
19948
- });
19949
- if (state.messageLog.length > PAJA_LOG_LIMIT) {
19950
- state.messageLog.splice(0, state.messageLog.length - PAJA_LOG_LIMIT);
20034
+ if (/<html[^>]*>/i.test(html)) {
20035
+ return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${meta}</head>`);
19951
20036
  }
19952
- renderPajaMessageLog(state);
20037
+ return `${meta}${html}`;
19953
20038
  }
19954
- function createPajaPostMessageProxy(realWin, state, windowId) {
19955
- const existing = realToProxy.get(realWin);
19956
- if (existing) {
19957
- proxyContexts.set(existing, { state, windowId });
19958
- return existing;
19959
- }
19960
- const proxy = new Proxy(realWin, {
19961
- get(target, prop) {
19962
- if (prop === "postMessage") {
19963
- return (msg, targetOrigin, transfer) => {
19964
- const context = proxyContexts.get(proxy);
19965
- appendPajaMessageLog(
19966
- context?.state ?? state,
19967
- "shell->napplet",
19968
- redactPajaMessageForLog(msg),
19969
- context?.windowId ?? windowId
19970
- );
19971
- return target.postMessage(msg, targetOrigin, transfer);
19972
- };
19973
- }
19974
- try {
19975
- const val = Reflect.get(target, prop, target);
19976
- return typeof val === "function" ? val.bind(target) : val;
19977
- } catch {
19978
- return void 0;
19979
- }
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
+ );
19980
20048
  }
19981
- });
19982
- proxyToReal.set(proxy, realWin);
19983
- realToProxy.set(realWin, proxy);
19984
- proxyContexts.set(proxy, { state, windowId });
19985
- return proxy;
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
+ );
20057
+ }
20058
+ if (!isNappletManifestKind(event.kind)) {
20059
+ throw new Error(`Pointer resolved kind ${event.kind}; expected ${formatNappletManifestKinds()}.`);
20060
+ }
20061
+ return event;
19986
20062
  }
19987
- function redactPajaMessageForLog(message) {
19988
- if (!isRecord6(message) || message.type !== "config.values") return message;
19989
- return {
19990
- type: "config.values",
19991
- ...typeof message.id === "string" ? { id: message.id } : {},
19992
- values: "[redacted by host]"
19993
- };
19994
- }
19995
- function isRecord6(value) {
19996
- return typeof value === "object" && value !== null && !Array.isArray(value);
19997
- }
19998
- function installPajaOriginRegistryProxy(originRegistry2, stateRef) {
19999
- const originalGetIframeWindow = originRegistry2.getIframeWindow.bind(originRegistry2);
20000
- originRegistry2.getIframeWindow = (windowId) => {
20001
- const win = originalGetIframeWindow(windowId);
20002
- if (!win) return null;
20003
- return createPajaPostMessageProxy(win, stateRef(), windowId);
20004
- };
20005
- const originalGetWindowId = originRegistry2.getWindowId.bind(originRegistry2);
20006
- originRegistry2.getWindowId = (win) => {
20007
- const result2 = originalGetWindowId(win);
20008
- if (result2) return result2;
20009
- const real = proxyToReal.get(win);
20010
- return real ? originalGetWindowId(real) : void 0;
20011
- };
20012
- const originalGetIdentity = originRegistry2.getIdentity.bind(originRegistry2);
20013
- originRegistry2.getIdentity = (win) => {
20014
- const real = proxyToReal.get(win);
20015
- return originalGetIdentity(win) ?? (real ? originalGetIdentity(real) : void 0);
20016
- };
20017
- const originalGetRegistrationId = originRegistry2.getRegistrationId.bind(originRegistry2);
20018
- originRegistry2.getRegistrationId = (win) => {
20019
- const real = proxyToReal.get(win);
20020
- return originalGetRegistrationId(win) ?? (real ? originalGetRegistrationId(real) : void 0);
20021
- };
20022
- const originalGetEnvironment = originRegistry2.getEnvironment.bind(originRegistry2);
20023
- originRegistry2.getEnvironment = (win) => {
20024
- const real = proxyToReal.get(win);
20025
- return originalGetEnvironment(win) ?? (real ? originalGetEnvironment(real) : void 0);
20026
- };
20027
- }
20028
- function renderPajaDevtools(state, options) {
20029
- renderInterfaceControls(state);
20030
- renderAclControls(state, options.bridge);
20031
- renderSignerStatus(state, options.devSignerPubkey);
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);
20032
20068
  }
20033
- function renderPajaMessageLog(state) {
20034
- const container = document.getElementById("message-log");
20035
- if (!container) return;
20036
- const filter = state.messageFilter.trim().toLowerCase();
20037
- const rows = state.messageLog.filter((entry) => filter.length === 0 || `${entry.direction} ${entry.type} ${entry.detail} ${entry.preview}`.toLowerCase().includes(filter)).slice(-120).map((entry) => {
20038
- const row = document.createElement("div");
20039
- row.className = "log-row";
20040
- row.dataset.messageType = entry.type;
20041
- if (entry.type.endsWith(".error")) row.dataset.error = "true";
20042
- const dir = document.createElement("div");
20043
- dir.className = "log-dir";
20044
- dir.textContent = entry.direction;
20045
- const body = document.createElement("div");
20046
- body.className = "log-body";
20047
- const type = document.createElement("div");
20048
- type.className = "log-type";
20049
- type.title = entry.preview;
20050
- type.textContent = entry.type;
20051
- body.append(type);
20052
- if (entry.detail) {
20053
- const detail = document.createElement("div");
20054
- detail.className = "log-detail";
20055
- detail.textContent = entry.detail;
20056
- detail.title = entry.preview;
20057
- body.append(detail);
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 });
20058
20097
  }
20059
- row.append(dir, body);
20060
- return row;
20061
20098
  });
20062
- container.replaceChildren(...rows);
20063
- container.scrollTop = container.scrollHeight;
20064
20099
  }
20065
- function renderInterfaceControls(state) {
20066
- const container = document.getElementById("interface-toggles");
20067
- if (!container) return;
20068
- container.replaceChildren(...PAJA_SIMULATION_DOMAINS.map((domain) => {
20069
- const enabled = domainEnabled(state.simulation, domain);
20070
- const button2 = document.createElement("button");
20071
- button2.type = "button";
20072
- button2.className = "toggle";
20073
- button2.dataset.interfaceDomain = domain;
20074
- button2.dataset.enabled = String(enabled);
20075
- button2.textContent = domain;
20076
- button2.title = `${domain} injection ${enabled ? "enabled" : "disabled"}`;
20077
- button2.addEventListener("click", () => {
20078
- state.setDomainEnabled(domain, !domainEnabled(state.simulation, domain));
20079
- });
20080
- return button2;
20081
- }));
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
+ );
20119
+ });
20082
20120
  }
20083
- function renderAclControls(state, bridge) {
20084
- const container = document.getElementById("acl-controls");
20085
- if (!container) return;
20086
- const identity = getTargetIdentity(state.config);
20087
- container.replaceChildren(...ALL_CAPABILITIES.map((capability) => {
20088
- const allowed2 = bridge?.runtime.aclState.check(
20089
- identity.pubkey,
20090
- identity.dTag,
20091
- identity.aggregateHash,
20092
- capability
20093
- ) ?? true;
20094
- const button2 = document.createElement("button");
20095
- button2.type = "button";
20096
- button2.className = "toggle";
20097
- button2.dataset.aclCapability = capability;
20098
- button2.dataset.enabled = String(allowed2);
20099
- button2.textContent = capability;
20100
- button2.title = `${capability} ${allowed2 ? "allowed" : "blocked"}`;
20101
- button2.addEventListener("click", () => state.setAclCapability(capability, !allowed2));
20102
- return button2;
20103
- }));
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);
20104
20123
  }
20105
- function renderSignerStatus(state, signerPubkey) {
20106
- const el = document.getElementById("signer-status");
20107
- const controls = document.getElementById("signer-controls");
20108
- if (!el || !controls) return;
20109
- const fallbackPubkey = state.simulation.identity.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
20110
- const pubkey2 = state.simulation.identity.pubkey || state.signer.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
20111
- const methodLabel = state.signer.method === "nip07" ? "NIP-07" : state.signer.method === "nip46" ? "bunker" : state.signer.method === "dev" ? "dev" : "none";
20112
- const relay = state.signer.relay ? ` \xB7 relay ${state.signer.relay}` : "";
20113
- const prefix = state.signer.status === "disconnected" ? "no signer connected" : state.signer.status === "error" ? `${methodLabel} error: ${state.signer.error ?? "unknown error"} \xB7 fallback pubkey ${fallbackPubkey}` : `${methodLabel} ${state.signer.status} \xB7 pubkey ${pubkey2 || "(none)"}${relay}`;
20114
- el.textContent = `${prefix} \xB7 every sign/publish request prompts`;
20115
- const previousInput = controls.querySelector("#signer-bunker-uri")?.value ?? "";
20116
- const devButton = document.createElement("button");
20117
- devButton.type = "button";
20118
- devButton.id = "signer-dev";
20119
- devButton.dataset.active = String(state.signer.method === "dev");
20120
- devButton.textContent = "Dev";
20121
- devButton.addEventListener("click", () => state.useDevSigner());
20122
- const nip07Button = document.createElement("button");
20123
- nip07Button.type = "button";
20124
- nip07Button.id = "signer-nip07";
20125
- nip07Button.dataset.active = String(state.signer.method === "nip07");
20126
- nip07Button.textContent = "NIP-07";
20127
- nip07Button.addEventListener("click", () => void state.connectNip07());
20128
- const bunkerInput = document.createElement("input");
20129
- bunkerInput.id = "signer-bunker-uri";
20130
- bunkerInput.type = "text";
20131
- bunkerInput.autocomplete = "off";
20132
- bunkerInput.placeholder = "bunker://...";
20133
- bunkerInput.setAttribute("aria-label", "Bunker connection URI");
20134
- bunkerInput.value = previousInput;
20135
- const bunkerButton = document.createElement("button");
20136
- bunkerButton.type = "button";
20137
- bunkerButton.id = "signer-bunker";
20138
- bunkerButton.dataset.active = String(state.signer.method === "nip46");
20139
- bunkerButton.textContent = "Bunker";
20140
- bunkerButton.addEventListener("click", () => void state.connectBunker(bunkerInput.value));
20141
- controls.replaceChildren(devButton, nip07Button, bunkerInput, bunkerButton);
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.");
20128
+ }
20129
+ return Math.floor(value);
20142
20130
  }
20143
-
20144
- // src/browser-nip46-client.ts
20145
- function parseBunkerUri(uri) {
20146
- if (!uri || typeof uri !== "string") return null;
20147
- let rest;
20148
- if (uri.startsWith("bunker://")) {
20149
- rest = uri.slice("bunker://".length);
20150
- } else if (uri.startsWith("nostrconnect://")) {
20151
- rest = uri.slice("nostrconnect://".length);
20152
- } else {
20153
- return null;
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.`);
20154
20134
  }
20155
- const questionMark = rest.indexOf("?");
20156
- const pubkey2 = questionMark !== -1 ? rest.slice(0, questionMark) : rest;
20157
- const queryString = questionMark !== -1 ? rest.slice(questionMark + 1) : "";
20158
- if (!pubkey2 || pubkey2.length !== 64 || !/^[0-9a-f]+$/u.test(pubkey2)) return null;
20159
- const params = new URLSearchParams(queryString);
20160
- const relay = params.get("relay");
20161
- if (!relay) return null;
20162
- return { pubkey: pubkey2, relay, secret: params.get("secret") ?? void 0 };
20135
+ return {
20136
+ kinds: [NAPPLET_KIND_NAMED],
20137
+ authors: [pointer.pubkey],
20138
+ "#d": [pointer.identifier],
20139
+ limit: 1
20140
+ };
20163
20141
  }
20164
- function createNip46ClientState(options) {
20165
- const localSecretKey = normalizeLocalSecretKey(options.localSecretKey ?? generateSecretKey());
20166
- const localPubkey = getPublicKey(localSecretKey);
20142
+ function eventFilter(pointer) {
20167
20143
  return {
20168
- relayUrl: options.relayUrl,
20169
- bunkerPubkey: options.bunkerPubkey,
20170
- timeout: options.timeout ?? 3e4,
20171
- localSecretKey,
20172
- localPubkey,
20173
- pending: /* @__PURE__ */ new Map(),
20174
- subId: `paja-nip46-${localPubkey.substring(0, 8)}`,
20175
- ws: null,
20176
- authorizedPubkey: null
20144
+ ids: [pointer.id],
20145
+ ...pointer.kind ? { kinds: [pointer.kind] } : {},
20146
+ ...pointer.author ? { authors: [pointer.author] } : {},
20147
+ limit: 1
20177
20148
  };
20178
20149
  }
20179
- function normalizeLocalSecretKey(secretKey) {
20180
- if (secretKey.length !== 32) {
20181
- throw new Error("NIP-46 local secret key must be 32 bytes");
20182
- }
20183
- return new Uint8Array(secretKey);
20184
- }
20185
- function cleanupNip46State(state) {
20186
- for (const entry of state.pending.values()) {
20187
- clearTimeout(entry.timer);
20188
- entry.reject(new Error("NIP-46 client closed"));
20189
- }
20190
- state.pending.clear();
20191
- if (state.ws && state.ws.readyState !== WebSocket.CLOSED) {
20192
- state.ws.close();
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;
20193
20156
  }
20194
- state.ws = null;
20157
+ return event.kind === pointer.kind && event.pubkey === pointer.pubkey && event.tags.some((tag) => tag[0] === "d" && tag[1] === pointer.identifier);
20195
20158
  }
20196
- async function sendRequestWithId(state, correlationId, method, params) {
20197
- if (!state.ws || state.ws.readyState !== WebSocket.OPEN) {
20198
- throw new Error("NIP-46 WebSocket not open");
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);
20199
20167
  }
20200
- const payload = JSON.stringify({ id: correlationId, method, params });
20201
- const encryptedContent = encrypt(payload, getConversationKey(state.localSecretKey, state.bunkerPubkey));
20202
- const signedEvent = finalizeEvent({
20203
- kind: 24133,
20204
- created_at: Math.floor(Date.now() / 1e3),
20205
- tags: [["p", state.bunkerPubkey]],
20206
- content: encryptedContent
20207
- }, state.localSecretKey);
20208
- return new Promise((resolve, reject2) => {
20209
- const timer = setTimeout(() => {
20210
- state.pending.delete(correlationId);
20211
- reject2(new Error(`NIP-46 request timed out: ${method}`));
20212
- }, state.timeout);
20213
- state.pending.set(correlationId, { resolve, reject: reject2, timer });
20214
- state.ws.send(JSON.stringify(["EVENT", signedEvent]));
20215
- });
20168
+ return out;
20216
20169
  }
20217
- async function handleRelayMessage(state, data) {
20218
- let msg;
20219
- try {
20220
- msg = JSON.parse(data);
20221
- } catch {
20222
- return;
20223
- }
20224
- if (!Array.isArray(msg) || msg[0] !== "EVENT") return;
20225
- const event = msg[2];
20226
- if (!event || event.kind !== 24133 || typeof event.content !== "string") return;
20227
- let response;
20170
+ function normalizeRelayUrl2(value) {
20171
+ const raw = value.trim();
20172
+ if (!raw) return void 0;
20228
20173
  try {
20229
- response = JSON.parse(decrypt(event.content, getConversationKey(state.localSecretKey, state.bunkerPubkey)));
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(/\/$/, "");
20230
20179
  } catch {
20231
- return;
20232
- }
20233
- const entry = state.pending.get(response.id);
20234
- if (!entry) return;
20235
- clearTimeout(entry.timer);
20236
- state.pending.delete(response.id);
20237
- if (response.error) {
20238
- entry.reject(new Error(response.error));
20239
- } else {
20240
- entry.resolve(response.result);
20241
- }
20242
- }
20243
- function rejectPendingOnClose(state) {
20244
- for (const entry of state.pending.values()) {
20245
- clearTimeout(entry.timer);
20246
- entry.reject(new Error("NIP-46 WebSocket closed unexpectedly"));
20180
+ return void 0;
20247
20181
  }
20248
- state.pending.clear();
20249
20182
  }
20250
- function openWebSocket(state) {
20251
- return new Promise((resolve, reject2) => {
20252
- state.ws = new WebSocket(state.relayUrl);
20253
- state.ws.onopen = () => {
20254
- state.ws.send(JSON.stringify([
20255
- "REQ",
20256
- state.subId,
20257
- { kinds: [24133], "#p": [state.localPubkey] }
20258
- ]));
20259
- resolve();
20260
- };
20261
- state.ws.onmessage = (event) => {
20262
- handleRelayMessage(state, event.data).catch(() => {
20263
- });
20264
- };
20265
- state.ws.onerror = () => reject2(new Error(`NIP-46 WebSocket connection failed: ${state.relayUrl}`));
20266
- state.ws.onclose = () => rejectPendingOnClose(state);
20267
- });
20183
+ function escapeAttribute(value) {
20184
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
20268
20185
  }
20269
- function createNipEncryptionMethods(state, prefix) {
20270
- return {
20271
- encrypt: async (pubkey2, plaintext) => sendRequestWithId(state, crypto.randomUUID(), `${prefix}_encrypt`, [pubkey2, plaintext]),
20272
- decrypt: async (pubkey2, ciphertext) => sendRequestWithId(state, crypto.randomUUID(), `${prefix}_decrypt`, [pubkey2, ciphertext])
20273
- };
20186
+ function formatNappletManifestKinds() {
20187
+ return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
20274
20188
  }
20275
- async function connectNip46State(state, secret) {
20276
- await openWebSocket(state);
20277
- const result2 = await sendRequestWithId(state, crypto.randomUUID(), "connect", [state.bunkerPubkey, secret ?? ""]);
20278
- if (typeof result2 === "string") {
20279
- state.authorizedPubkey = result2;
20280
- } else if (result2 && typeof result2 === "object" && "pubkey" in result2) {
20281
- state.authorizedPubkey = result2.pubkey;
20282
- } else {
20283
- const pkResult = await sendRequestWithId(state, crypto.randomUUID(), "get_public_key", []);
20284
- state.authorizedPubkey = pkResult;
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");
20285
20194
  }
20286
- return state.authorizedPubkey;
20195
+ return resolveShellEnvironment(hooks, identity);
20287
20196
  }
20288
- function createNip46Signer(state) {
20197
+ function getTargetIdentity(config, resolvedTarget) {
20289
20198
  return {
20290
- getPublicKey: async () => {
20291
- if (state.authorizedPubkey) return state.authorizedPubkey;
20292
- return sendRequestWithId(state, crypto.randomUUID(), "get_public_key", []);
20293
- },
20294
- signEvent: async (event) => {
20295
- const result2 = await sendRequestWithId(state, crypto.randomUUID(), "sign_event", [JSON.stringify(event)]);
20296
- if (typeof result2 === "string") {
20297
- return JSON.parse(result2);
20298
- }
20299
- return result2;
20300
- },
20301
- nip04: createNipEncryptionMethods(state, "nip04"),
20302
- nip44: createNipEncryptionMethods(state, "nip44")
20199
+ pubkey: "",
20200
+ dTag: resolvedTarget?.dTag ?? config.window.dTag,
20201
+ aggregateHash: resolvedTarget?.aggregateHash ?? config.window.aggregateHash
20303
20202
  };
20304
20203
  }
20305
- function createNip46Client(options) {
20306
- const state = createNip46ClientState(options);
20307
- return {
20308
- connect: () => connectNip46State(state, options.secret),
20309
- getSigner: () => createNip46Signer(state),
20310
- close: () => cleanupNip46State(state)
20311
- };
20204
+ function getTargetOriginIdentity(config, resolvedTarget) {
20205
+ const target = getTargetIdentity(config, resolvedTarget);
20206
+ return Object.freeze({ dTag: target.dTag, aggregateHash: target.aggregateHash });
20312
20207
  }
20313
-
20314
- // src/browser-signers.ts
20315
- function cloneState(state) {
20316
- return { ...state };
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;
20317
20214
  }
20318
- function toErrorMessage6(error) {
20319
- return error instanceof Error ? error.message : String(error);
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;
20320
20248
  }
20321
- function readNip07Signer() {
20322
- const candidate = globalThis.nostr;
20323
- if (!candidate || typeof candidate !== "object") return null;
20324
- const signer = candidate;
20325
- if (typeof signer.getPublicKey !== "function" || typeof signer.signEvent !== "function") return null;
20326
- return signer;
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>`;
20327
20252
  }
20328
- function createConfirmedSigner(signer, confirmRequest) {
20329
- return {
20330
- ...signer,
20331
- async signEvent(event) {
20332
- if (typeof signer.signEvent !== "function") {
20333
- throw new Error("Signer does not support event signing");
20334
- }
20335
- if (!await confirmRequest({ action: "sign", event })) {
20336
- throw new Error("Paja signing request denied");
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);
20337
20262
  }
20338
- return signer.signEvent(event);
20263
+ } catch {
20339
20264
  }
20340
- };
20265
+ }
20266
+ return [...out];
20341
20267
  }
20342
- function createNip07Adapter(signer) {
20343
- return {
20344
- getPublicKey: signer.getPublicKey?.bind(signer),
20345
- signEvent: signer.signEvent?.bind(signer),
20346
- getRelays: signer.getRelays?.bind(signer),
20347
- nip04: signer.nip04,
20348
- nip44: signer.nip44
20349
- };
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();
20350
20279
  }
20351
- function createPajaSignerController(options) {
20352
- let state = {
20353
- method: "none",
20354
- status: "disconnected",
20355
- pubkey: null,
20356
- relay: null,
20357
- error: null
20358
- };
20359
- let activeSigner = null;
20360
- let activeClient = null;
20361
- const listeners = /* @__PURE__ */ new Set();
20362
- const update = (next) => {
20363
- state = { ...state, ...next };
20364
- for (const listener of listeners) listener();
20365
- options.onChange(cloneState(state));
20366
- };
20367
- const closeClient = () => {
20368
- activeClient?.close();
20369
- activeClient = null;
20370
- };
20371
- return {
20372
- getState: () => cloneState(state),
20373
- getSigner: () => activeSigner,
20374
- getMethod: () => state.method,
20375
- getPubkey: () => state.pubkey,
20376
- subscribe(listener) {
20377
- listeners.add(listener);
20378
- return () => listeners.delete(listener);
20379
- },
20380
- useDevSigner() {
20381
- closeClient();
20382
- activeSigner = null;
20383
- update({
20384
- method: "dev",
20385
- status: "connected",
20386
- pubkey: null,
20387
- relay: null,
20388
- error: null
20389
- });
20390
- },
20391
- async connectNip07() {
20392
- closeClient();
20393
- activeSigner = null;
20394
- update({ method: "nip07", status: "connecting", pubkey: null, relay: null, error: null });
20395
- try {
20396
- const signer = readNip07Signer();
20397
- if (!signer) throw new Error("NIP-07 signer not found");
20398
- const pubkey2 = await signer.getPublicKey();
20399
- activeSigner = createConfirmedSigner(createNip07Adapter(signer), options.confirmRequest);
20400
- update({ method: "nip07", status: "connected", pubkey: pubkey2, relay: null, error: null });
20401
- } catch (error) {
20402
- activeSigner = null;
20403
- update({ method: "nip07", status: "error", pubkey: null, relay: null, error: toErrorMessage6(error) });
20404
- }
20405
- },
20406
- async connectBunker(uri) {
20407
- closeClient();
20408
- activeSigner = null;
20409
- const parsed = parseBunkerUri(uri.trim());
20410
- if (!parsed) {
20411
- update({
20412
- method: "nip46",
20413
- status: "error",
20414
- pubkey: null,
20415
- relay: null,
20416
- error: "Enter a bunker:// or nostrconnect:// URI with a relay parameter"
20417
- });
20418
- return;
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
+ };
20419
20376
  }
20420
- update({ method: "nip46", status: "connecting", pubkey: null, relay: parsed.relay, error: null });
20421
20377
  try {
20422
- const client = createNip46Client({
20423
- relayUrl: parsed.relay,
20424
- bunkerPubkey: parsed.pubkey,
20425
- secret: parsed.secret
20426
- });
20427
- const pubkey2 = await client.connect();
20428
- activeClient = client;
20429
- activeSigner = createConfirmedSigner(client.getSigner(), options.confirmRequest);
20430
- update({ method: "nip46", status: "connected", pubkey: pubkey2, relay: parsed.relay, error: null });
20431
- } catch (error) {
20432
- closeClient();
20433
- activeSigner = null;
20434
- update({ method: "nip46", status: "error", pubkey: null, relay: parsed.relay, error: toErrorMessage6(error) });
20378
+ const val = Reflect.get(target, prop, target);
20379
+ return typeof val === "function" ? val.bind(target) : val;
20380
+ } catch {
20381
+ return void 0;
20435
20382
  }
20436
20383
  }
20437
- };
20384
+ });
20385
+ proxyToReal.set(proxy, realWin);
20386
+ realToProxy.set(realWin, proxy);
20387
+ proxyContexts.set(proxy, { state, windowId });
20388
+ return proxy;
20438
20389
  }
20439
-
20440
- // src/browser-host-signer.ts
20441
- function confirmationDetails(...lines) {
20442
- return lines.filter((line) => line !== void 0).join("\n");
20390
+ function redactPajaMessageForLog(message) {
20391
+ if (!isRecord6(message) || message.type !== "config.values") return message;
20392
+ return {
20393
+ type: "config.values",
20394
+ ...typeof message.id === "string" ? { id: message.id } : {},
20395
+ values: "[redacted by host]"
20396
+ };
20443
20397
  }
20444
- function confirmationCopy(title, summary, approveLabel, ...details) {
20445
- return { title, summary, details: confirmationDetails(...details), approveLabel };
20398
+ function isRecord6(value) {
20399
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20446
20400
  }
20447
- function createPajaConfirmationController(getState) {
20448
- const dialog = document.getElementById("paja-confirmation-dialog");
20449
- const title = document.getElementById("paja-confirmation-title");
20450
- const summary = document.getElementById("paja-confirmation-summary");
20451
- const details = document.getElementById("paja-confirmation-details");
20452
- const approve = document.getElementById("paja-confirmation-approve");
20453
- const deny = document.getElementById("paja-confirmation-deny");
20454
- const ready = dialog instanceof HTMLDialogElement && title instanceof HTMLElement && summary instanceof HTMLElement && details instanceof HTMLElement && approve instanceof HTMLButtonElement && deny instanceof HTMLButtonElement;
20455
- const queue = [];
20456
- let active = null;
20457
- let previousFocus = null;
20458
- let disposed = false;
20459
- const pump = () => {
20460
- if (!ready || disposed || active) return;
20461
- active = queue.shift() ?? null;
20462
- if (!active) return;
20463
- const copy = describeConfirmation(active.request);
20464
- title.textContent = copy.title;
20465
- summary.textContent = copy.summary;
20466
- details.textContent = copy.details;
20467
- approve.textContent = copy.approveLabel;
20468
- previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
20469
- dialog.showModal();
20470
- deny.focus();
20401
+ function installPajaOriginRegistryProxy(originRegistry2, stateRef) {
20402
+ const originalGetIframeWindow = originRegistry2.getIframeWindow.bind(originRegistry2);
20403
+ originRegistry2.getIframeWindow = (windowId) => {
20404
+ const win = originalGetIframeWindow(windowId);
20405
+ if (!win) return null;
20406
+ return createPajaPostMessageProxy(win, stateRef(), windowId);
20471
20407
  };
20472
- const settle6 = (allowed2) => {
20473
- const current = active;
20474
- if (!current) return;
20475
- active = null;
20476
- if (ready && dialog.open) dialog.close();
20477
- recordPajaConfirmation(getState(), current.request, allowed2);
20478
- if (allowed2) current.approve();
20479
- else current.deny();
20480
- previousFocus?.focus();
20481
- previousFocus = null;
20482
- queueMicrotask(pump);
20408
+ const originalGetWindowId = originRegistry2.getWindowId.bind(originRegistry2);
20409
+ originRegistry2.getWindowId = (win) => {
20410
+ const result2 = originalGetWindowId(win);
20411
+ if (result2) return result2;
20412
+ const real = proxyToReal.get(win);
20413
+ return real ? originalGetWindowId(real) : void 0;
20483
20414
  };
20484
- const onApprove = () => settle6(true);
20485
- const onDeny = () => settle6(false);
20486
- const onCancel = (event) => {
20487
- event.preventDefault();
20488
- settle6(false);
20415
+ const originalGetIdentity = originRegistry2.getIdentity.bind(originRegistry2);
20416
+ originRegistry2.getIdentity = (win) => {
20417
+ const real = proxyToReal.get(win);
20418
+ return originalGetIdentity(win) ?? (real ? originalGetIdentity(real) : void 0);
20489
20419
  };
20490
- if (ready) {
20491
- approve.addEventListener("click", onApprove);
20492
- deny.addEventListener("click", onDeny);
20493
- dialog.addEventListener("cancel", onCancel);
20494
- }
20495
- return {
20496
- confirm(request) {
20497
- if (!ready || disposed) {
20498
- recordPajaConfirmation(getState(), request, false);
20499
- return false;
20500
- }
20501
- return new Promise((resolve) => {
20420
+ const originalGetRegistrationId = originRegistry2.getRegistrationId.bind(originRegistry2);
20421
+ originRegistry2.getRegistrationId = (win) => {
20422
+ const real = proxyToReal.get(win);
20423
+ return originalGetRegistrationId(win) ?? (real ? originalGetRegistrationId(real) : void 0);
20424
+ };
20425
+ const originalGetEnvironment = originRegistry2.getEnvironment.bind(originRegistry2);
20426
+ originRegistry2.getEnvironment = (win) => {
20427
+ const real = proxyToReal.get(win);
20428
+ return originalGetEnvironment(win) ?? (real ? originalGetEnvironment(real) : void 0);
20429
+ };
20430
+ }
20431
+ function renderPajaDevtools(state, options) {
20432
+ renderInterfaceControls(state);
20433
+ renderAclControls(state, options.bridge);
20434
+ renderSignerStatus(state, options.devSignerPubkey);
20435
+ }
20436
+ function renderPajaMessageLog(state) {
20437
+ const container = document.getElementById("message-log");
20438
+ if (!container) return;
20439
+ const filter = state.messageFilter.trim().toLowerCase();
20440
+ const rows = state.messageLog.filter((entry) => filter.length === 0 || `${entry.direction} ${entry.type} ${entry.detail} ${entry.preview}`.toLowerCase().includes(filter)).slice(-120).map((entry) => {
20441
+ const row = document.createElement("div");
20442
+ row.className = "log-row";
20443
+ row.dataset.messageType = entry.type;
20444
+ if (entry.type.endsWith(".error")) row.dataset.error = "true";
20445
+ const dir = document.createElement("div");
20446
+ dir.className = "log-dir";
20447
+ dir.textContent = entry.direction;
20448
+ const body = document.createElement("div");
20449
+ body.className = "log-body";
20450
+ const type = document.createElement("div");
20451
+ type.className = "log-type";
20452
+ type.title = entry.preview;
20453
+ type.textContent = entry.type;
20454
+ body.append(type);
20455
+ if (entry.detail) {
20456
+ const detail = document.createElement("div");
20457
+ detail.className = "log-detail";
20458
+ detail.textContent = entry.detail;
20459
+ detail.title = entry.preview;
20460
+ body.append(detail);
20461
+ }
20462
+ row.append(dir, body);
20463
+ return row;
20464
+ });
20465
+ container.replaceChildren(...rows);
20466
+ container.scrollTop = container.scrollHeight;
20467
+ }
20468
+ function renderInterfaceControls(state) {
20469
+ const container = document.getElementById("interface-toggles");
20470
+ if (!container) return;
20471
+ container.replaceChildren(...PAJA_SIMULATION_DOMAINS.map((domain) => {
20472
+ const enabled = domainEnabled(state.simulation, domain);
20473
+ const button2 = document.createElement("button");
20474
+ button2.type = "button";
20475
+ button2.className = "toggle";
20476
+ button2.dataset.interfaceDomain = domain;
20477
+ button2.dataset.enabled = String(enabled);
20478
+ button2.textContent = domain;
20479
+ button2.title = `${domain} injection ${enabled ? "enabled" : "disabled"}`;
20480
+ button2.addEventListener("click", () => {
20481
+ state.setDomainEnabled(domain, !domainEnabled(state.simulation, domain));
20482
+ });
20483
+ return button2;
20484
+ }));
20485
+ }
20486
+ function renderAclControls(state, bridge) {
20487
+ const container = document.getElementById("acl-controls");
20488
+ if (!container) return;
20489
+ const identity = getTargetIdentity(state.config, state.resolvedTarget);
20490
+ container.replaceChildren(...ALL_CAPABILITIES.map((capability) => {
20491
+ const allowed2 = bridge?.runtime.aclState.check(
20492
+ identity.pubkey,
20493
+ identity.dTag,
20494
+ identity.aggregateHash,
20495
+ capability
20496
+ ) ?? true;
20497
+ const button2 = document.createElement("button");
20498
+ button2.type = "button";
20499
+ button2.className = "toggle";
20500
+ button2.dataset.aclCapability = capability;
20501
+ button2.dataset.enabled = String(allowed2);
20502
+ button2.textContent = capability;
20503
+ button2.title = `${capability} ${allowed2 ? "allowed" : "blocked"}`;
20504
+ button2.addEventListener("click", () => state.setAclCapability(capability, !allowed2));
20505
+ return button2;
20506
+ }));
20507
+ }
20508
+ function renderSignerStatus(state, signerPubkey) {
20509
+ const el = document.getElementById("signer-status");
20510
+ const controls = document.getElementById("signer-controls");
20511
+ const clearConsent = document.getElementById("signer-consent-clear");
20512
+ if (!el || !controls || !(clearConsent instanceof HTMLButtonElement)) return;
20513
+ const fallbackPubkey = state.simulation.identity.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
20514
+ const pubkey2 = state.simulation.identity.pubkey || state.signer.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
20515
+ const methodLabel = state.signer.method === "nip07" ? "NIP-07" : state.signer.method === "nip46" ? "bunker" : state.signer.method === "dev" ? "dev" : "none";
20516
+ const relay = state.signer.relay ? ` \xB7 relay ${state.signer.relay}` : "";
20517
+ const prefix = state.signer.status === "disconnected" ? "no signer connected" : state.signer.status === "error" ? `${methodLabel} error: ${state.signer.error ?? "unknown error"} \xB7 fallback pubkey ${fallbackPubkey}` : `${methodLabel} ${state.signer.status} \xB7 pubkey ${pubkey2 || "(none)"}${relay}`;
20518
+ const consentSummary = state.signerConsentCount === 0 ? "signing asks every time" : `${state.signerConsentCount} remembered signing approval${state.signerConsentCount === 1 ? "" : "s"}`;
20519
+ el.textContent = `${prefix} \xB7 ${consentSummary} \xB7 publishing asks every time`;
20520
+ clearConsent.hidden = state.signerConsentCount === 0;
20521
+ clearConsent.textContent = `Forget ${state.signerConsentCount} remembered approval${state.signerConsentCount === 1 ? "" : "s"}`;
20522
+ clearConsent.onclick = () => state.clearSignerConsent();
20523
+ const previousInput = controls.querySelector("#signer-bunker-uri")?.value ?? "";
20524
+ const devButton = document.createElement("button");
20525
+ devButton.type = "button";
20526
+ devButton.id = "signer-dev";
20527
+ devButton.dataset.active = String(state.signer.method === "dev");
20528
+ devButton.textContent = "Dev";
20529
+ devButton.addEventListener("click", () => state.useDevSigner());
20530
+ const nip07Button = document.createElement("button");
20531
+ nip07Button.type = "button";
20532
+ nip07Button.id = "signer-nip07";
20533
+ nip07Button.dataset.active = String(state.signer.method === "nip07");
20534
+ nip07Button.textContent = "NIP-07";
20535
+ nip07Button.addEventListener("click", () => void state.connectNip07());
20536
+ const bunkerInput = document.createElement("input");
20537
+ bunkerInput.id = "signer-bunker-uri";
20538
+ bunkerInput.type = "text";
20539
+ bunkerInput.autocomplete = "off";
20540
+ bunkerInput.placeholder = "bunker://...";
20541
+ bunkerInput.setAttribute("aria-label", "Bunker connection URI");
20542
+ bunkerInput.value = previousInput;
20543
+ const bunkerButton = document.createElement("button");
20544
+ bunkerButton.type = "button";
20545
+ bunkerButton.id = "signer-bunker";
20546
+ bunkerButton.dataset.active = String(state.signer.method === "nip46");
20547
+ bunkerButton.textContent = "Bunker";
20548
+ bunkerButton.addEventListener("click", () => void state.connectBunker(bunkerInput.value));
20549
+ controls.replaceChildren(devButton, nip07Button, bunkerInput, bunkerButton);
20550
+ }
20551
+
20552
+ // src/browser-nip46-client.ts
20553
+ function parseBunkerUri(uri) {
20554
+ if (!uri || typeof uri !== "string") return null;
20555
+ let rest;
20556
+ if (uri.startsWith("bunker://")) {
20557
+ rest = uri.slice("bunker://".length);
20558
+ } else if (uri.startsWith("nostrconnect://")) {
20559
+ rest = uri.slice("nostrconnect://".length);
20560
+ } else {
20561
+ return null;
20562
+ }
20563
+ const questionMark = rest.indexOf("?");
20564
+ const pubkey2 = questionMark !== -1 ? rest.slice(0, questionMark) : rest;
20565
+ const queryString = questionMark !== -1 ? rest.slice(questionMark + 1) : "";
20566
+ if (!pubkey2 || pubkey2.length !== 64 || !/^[0-9a-f]+$/u.test(pubkey2)) return null;
20567
+ const params = new URLSearchParams(queryString);
20568
+ const relay = params.get("relay");
20569
+ if (!relay) return null;
20570
+ return { pubkey: pubkey2, relay, secret: params.get("secret") ?? void 0 };
20571
+ }
20572
+ function createNip46ClientState(options) {
20573
+ const localSecretKey = normalizeLocalSecretKey(options.localSecretKey ?? generateSecretKey());
20574
+ const localPubkey = getPublicKey(localSecretKey);
20575
+ return {
20576
+ relayUrl: options.relayUrl,
20577
+ bunkerPubkey: options.bunkerPubkey,
20578
+ timeout: options.timeout ?? 3e4,
20579
+ localSecretKey,
20580
+ localPubkey,
20581
+ pending: /* @__PURE__ */ new Map(),
20582
+ subId: `paja-nip46-${localPubkey.substring(0, 8)}`,
20583
+ ws: null,
20584
+ authorizedPubkey: null
20585
+ };
20586
+ }
20587
+ function normalizeLocalSecretKey(secretKey) {
20588
+ if (secretKey.length !== 32) {
20589
+ throw new Error("NIP-46 local secret key must be 32 bytes");
20590
+ }
20591
+ return new Uint8Array(secretKey);
20592
+ }
20593
+ function cleanupNip46State(state) {
20594
+ for (const entry of state.pending.values()) {
20595
+ clearTimeout(entry.timer);
20596
+ entry.reject(new Error("NIP-46 client closed"));
20597
+ }
20598
+ state.pending.clear();
20599
+ if (state.ws && state.ws.readyState !== WebSocket.CLOSED) {
20600
+ state.ws.close();
20601
+ }
20602
+ state.ws = null;
20603
+ }
20604
+ async function sendRequestWithId(state, correlationId, method, params) {
20605
+ if (!state.ws || state.ws.readyState !== WebSocket.OPEN) {
20606
+ throw new Error("NIP-46 WebSocket not open");
20607
+ }
20608
+ const payload = JSON.stringify({ id: correlationId, method, params });
20609
+ const encryptedContent = encrypt(payload, getConversationKey(state.localSecretKey, state.bunkerPubkey));
20610
+ const signedEvent = finalizeEvent({
20611
+ kind: 24133,
20612
+ created_at: Math.floor(Date.now() / 1e3),
20613
+ tags: [["p", state.bunkerPubkey]],
20614
+ content: encryptedContent
20615
+ }, state.localSecretKey);
20616
+ return new Promise((resolve, reject2) => {
20617
+ const timer = setTimeout(() => {
20618
+ state.pending.delete(correlationId);
20619
+ reject2(new Error(`NIP-46 request timed out: ${method}`));
20620
+ }, state.timeout);
20621
+ state.pending.set(correlationId, { resolve, reject: reject2, timer });
20622
+ state.ws.send(JSON.stringify(["EVENT", signedEvent]));
20623
+ });
20624
+ }
20625
+ async function handleRelayMessage(state, data) {
20626
+ let msg;
20627
+ try {
20628
+ msg = JSON.parse(data);
20629
+ } catch {
20630
+ return;
20631
+ }
20632
+ if (!Array.isArray(msg) || msg[0] !== "EVENT") return;
20633
+ const event = msg[2];
20634
+ if (!event || event.kind !== 24133 || typeof event.content !== "string") return;
20635
+ let response;
20636
+ try {
20637
+ response = JSON.parse(decrypt(event.content, getConversationKey(state.localSecretKey, state.bunkerPubkey)));
20638
+ } catch {
20639
+ return;
20640
+ }
20641
+ const entry = state.pending.get(response.id);
20642
+ if (!entry) return;
20643
+ clearTimeout(entry.timer);
20644
+ state.pending.delete(response.id);
20645
+ if (response.error) {
20646
+ entry.reject(new Error(response.error));
20647
+ } else {
20648
+ entry.resolve(response.result);
20649
+ }
20650
+ }
20651
+ function rejectPendingOnClose(state) {
20652
+ for (const entry of state.pending.values()) {
20653
+ clearTimeout(entry.timer);
20654
+ entry.reject(new Error("NIP-46 WebSocket closed unexpectedly"));
20655
+ }
20656
+ state.pending.clear();
20657
+ }
20658
+ function openWebSocket(state) {
20659
+ return new Promise((resolve, reject2) => {
20660
+ state.ws = new WebSocket(state.relayUrl);
20661
+ state.ws.onopen = () => {
20662
+ state.ws.send(JSON.stringify([
20663
+ "REQ",
20664
+ state.subId,
20665
+ { kinds: [24133], "#p": [state.localPubkey] }
20666
+ ]));
20667
+ resolve();
20668
+ };
20669
+ state.ws.onmessage = (event) => {
20670
+ handleRelayMessage(state, event.data).catch(() => {
20671
+ });
20672
+ };
20673
+ state.ws.onerror = () => reject2(new Error(`NIP-46 WebSocket connection failed: ${state.relayUrl}`));
20674
+ state.ws.onclose = () => rejectPendingOnClose(state);
20675
+ });
20676
+ }
20677
+ function createNipEncryptionMethods(state, prefix) {
20678
+ return {
20679
+ encrypt: async (pubkey2, plaintext) => sendRequestWithId(state, crypto.randomUUID(), `${prefix}_encrypt`, [pubkey2, plaintext]),
20680
+ decrypt: async (pubkey2, ciphertext) => sendRequestWithId(state, crypto.randomUUID(), `${prefix}_decrypt`, [pubkey2, ciphertext])
20681
+ };
20682
+ }
20683
+ async function connectNip46State(state, secret) {
20684
+ await openWebSocket(state);
20685
+ const result2 = await sendRequestWithId(state, crypto.randomUUID(), "connect", [state.bunkerPubkey, secret ?? ""]);
20686
+ if (typeof result2 === "string") {
20687
+ state.authorizedPubkey = result2;
20688
+ } else if (result2 && typeof result2 === "object" && "pubkey" in result2) {
20689
+ state.authorizedPubkey = result2.pubkey;
20690
+ } else {
20691
+ const pkResult = await sendRequestWithId(state, crypto.randomUUID(), "get_public_key", []);
20692
+ state.authorizedPubkey = pkResult;
20693
+ }
20694
+ return state.authorizedPubkey;
20695
+ }
20696
+ function createNip46Signer(state) {
20697
+ return {
20698
+ getPublicKey: async () => {
20699
+ if (state.authorizedPubkey) return state.authorizedPubkey;
20700
+ return sendRequestWithId(state, crypto.randomUUID(), "get_public_key", []);
20701
+ },
20702
+ signEvent: async (event) => {
20703
+ const result2 = await sendRequestWithId(state, crypto.randomUUID(), "sign_event", [JSON.stringify(event)]);
20704
+ if (typeof result2 === "string") {
20705
+ return JSON.parse(result2);
20706
+ }
20707
+ return result2;
20708
+ },
20709
+ nip04: createNipEncryptionMethods(state, "nip04"),
20710
+ nip44: createNipEncryptionMethods(state, "nip44")
20711
+ };
20712
+ }
20713
+ function createNip46Client(options) {
20714
+ const state = createNip46ClientState(options);
20715
+ return {
20716
+ connect: () => connectNip46State(state, options.secret),
20717
+ getSigner: () => createNip46Signer(state),
20718
+ close: () => cleanupNip46State(state)
20719
+ };
20720
+ }
20721
+
20722
+ // src/browser-signers.ts
20723
+ function cloneState(state) {
20724
+ return { ...state };
20725
+ }
20726
+ function toErrorMessage6(error) {
20727
+ return error instanceof Error ? error.message : String(error);
20728
+ }
20729
+ function readNip07Signer() {
20730
+ const candidate = globalThis.nostr;
20731
+ if (!candidate || typeof candidate !== "object") return null;
20732
+ const signer = candidate;
20733
+ if (typeof signer.getPublicKey !== "function" || typeof signer.signEvent !== "function") return null;
20734
+ return signer;
20735
+ }
20736
+ function createConfirmedSigner(signer, confirmRequest, source) {
20737
+ return {
20738
+ ...signer,
20739
+ async signEvent(event) {
20740
+ if (typeof signer.signEvent !== "function") {
20741
+ throw new Error("Signer does not support event signing");
20742
+ }
20743
+ const signerContext = await resolveSignerContext(signer, source);
20744
+ if (!await confirmRequest({
20745
+ action: "sign",
20746
+ event,
20747
+ ...signerContext ? { signerContext } : {}
20748
+ })) {
20749
+ throw new Error("Paja signing request denied");
20750
+ }
20751
+ return signer.signEvent(event);
20752
+ }
20753
+ };
20754
+ }
20755
+ async function resolveSignerContext(signer, source) {
20756
+ if (!source || typeof signer.getPublicKey !== "function") return void 0;
20757
+ try {
20758
+ const signerPubkey = await signer.getPublicKey();
20759
+ if (!/^[0-9a-f]{64}$/i.test(signerPubkey)) return void 0;
20760
+ return { ...source, signerPubkey };
20761
+ } catch {
20762
+ return void 0;
20763
+ }
20764
+ }
20765
+ function createNip07Adapter(signer) {
20766
+ return {
20767
+ getPublicKey: signer.getPublicKey?.bind(signer),
20768
+ signEvent: signer.signEvent?.bind(signer),
20769
+ getRelays: signer.getRelays?.bind(signer),
20770
+ nip04: signer.nip04,
20771
+ nip44: signer.nip44
20772
+ };
20773
+ }
20774
+ function createPajaSignerController(options) {
20775
+ let state = {
20776
+ method: "none",
20777
+ status: "disconnected",
20778
+ pubkey: null,
20779
+ relay: null,
20780
+ error: null
20781
+ };
20782
+ let activeSigner = null;
20783
+ let activeClient = null;
20784
+ const listeners = /* @__PURE__ */ new Set();
20785
+ const update = (next) => {
20786
+ state = { ...state, ...next };
20787
+ for (const listener of listeners) listener();
20788
+ options.onChange(cloneState(state));
20789
+ };
20790
+ const closeClient = () => {
20791
+ activeClient?.close();
20792
+ activeClient = null;
20793
+ };
20794
+ return {
20795
+ getState: () => cloneState(state),
20796
+ getSigner(source) {
20797
+ if (!activeSigner) return null;
20798
+ return createConfirmedSigner(activeSigner, options.confirmRequest, source);
20799
+ },
20800
+ getMethod: () => state.method,
20801
+ getPubkey: () => state.pubkey,
20802
+ subscribe(listener) {
20803
+ listeners.add(listener);
20804
+ return () => listeners.delete(listener);
20805
+ },
20806
+ useDevSigner() {
20807
+ closeClient();
20808
+ activeSigner = null;
20809
+ update({
20810
+ method: "dev",
20811
+ status: "connected",
20812
+ pubkey: null,
20813
+ relay: null,
20814
+ error: null
20815
+ });
20816
+ },
20817
+ async connectNip07() {
20818
+ closeClient();
20819
+ activeSigner = null;
20820
+ update({ method: "nip07", status: "connecting", pubkey: null, relay: null, error: null });
20821
+ try {
20822
+ const signer = readNip07Signer();
20823
+ if (!signer) throw new Error("NIP-07 signer not found");
20824
+ const pubkey2 = await signer.getPublicKey();
20825
+ activeSigner = createNip07Adapter(signer);
20826
+ update({ method: "nip07", status: "connected", pubkey: pubkey2, relay: null, error: null });
20827
+ } catch (error) {
20828
+ activeSigner = null;
20829
+ update({ method: "nip07", status: "error", pubkey: null, relay: null, error: toErrorMessage6(error) });
20830
+ }
20831
+ },
20832
+ async connectBunker(uri) {
20833
+ closeClient();
20834
+ activeSigner = null;
20835
+ const parsed = parseBunkerUri(uri.trim());
20836
+ if (!parsed) {
20837
+ update({
20838
+ method: "nip46",
20839
+ status: "error",
20840
+ pubkey: null,
20841
+ relay: null,
20842
+ error: "Enter a bunker:// or nostrconnect:// URI with a relay parameter"
20843
+ });
20844
+ return;
20845
+ }
20846
+ update({ method: "nip46", status: "connecting", pubkey: null, relay: parsed.relay, error: null });
20847
+ try {
20848
+ const client = createNip46Client({
20849
+ relayUrl: parsed.relay,
20850
+ bunkerPubkey: parsed.pubkey,
20851
+ secret: parsed.secret
20852
+ });
20853
+ const pubkey2 = await client.connect();
20854
+ activeClient = client;
20855
+ activeSigner = client.getSigner();
20856
+ update({ method: "nip46", status: "connected", pubkey: pubkey2, relay: parsed.relay, error: null });
20857
+ } catch (error) {
20858
+ closeClient();
20859
+ activeSigner = null;
20860
+ update({ method: "nip46", status: "error", pubkey: null, relay: parsed.relay, error: toErrorMessage6(error) });
20861
+ }
20862
+ }
20863
+ };
20864
+ }
20865
+
20866
+ // src/browser-signer-consent.ts
20867
+ var PAJA_SIGNER_CONSENT_STORAGE_KEY = "kehto:paja:signer-consent:v1";
20868
+ var PAJA_SIGNER_CONSENT_VERSION = 1;
20869
+ var MAX_CONSENT_SUBJECTS = 128;
20870
+ var MAX_KINDS_PER_SUBJECT = 128;
20871
+ var MAX_IDENTITY_FIELD_LENGTH = 512;
20872
+ function createPajaSignerConsentStore(storage = readPajaSignerConsentStorage()) {
20873
+ let grants = readConsentDocument(storage).grants;
20874
+ const persist = () => {
20875
+ if (!storage) return;
20876
+ const document2 = {
20877
+ version: PAJA_SIGNER_CONSENT_VERSION,
20878
+ grants
20879
+ };
20880
+ try {
20881
+ storage.setItem(PAJA_SIGNER_CONSENT_STORAGE_KEY, JSON.stringify(document2));
20882
+ } catch {
20883
+ }
20884
+ };
20885
+ const recordFor = (context) => {
20886
+ const subject = normalizeContext(context);
20887
+ if (!subject) return null;
20888
+ return grants.find((grant2) => sameSubject(grant2, subject)) ?? null;
20889
+ };
20890
+ const ensureRecord = (context) => {
20891
+ const subject = normalizeContext(context);
20892
+ if (!subject) return null;
20893
+ const existing = grants.find((grant2) => sameSubject(grant2, subject));
20894
+ if (existing) return existing;
20895
+ if (grants.length >= MAX_CONSENT_SUBJECTS) return null;
20896
+ const created = { ...subject, trusted: false, kinds: [] };
20897
+ grants = [...grants, created];
20898
+ return created;
20899
+ };
20900
+ return {
20901
+ match(context, kind) {
20902
+ const normalizedKind = normalizeKind(kind);
20903
+ if (normalizedKind === null) return null;
20904
+ const record = recordFor(context);
20905
+ if (!record) return null;
20906
+ if (record.trusted) return "napplet";
20907
+ return record.kinds.includes(normalizedKind) ? "kind" : null;
20908
+ },
20909
+ rememberKind(context, kind) {
20910
+ const normalizedKind = normalizeKind(kind);
20911
+ if (normalizedKind === null) return false;
20912
+ const record = ensureRecord(context);
20913
+ if (!record) return false;
20914
+ if (record.trusted || record.kinds.includes(normalizedKind)) return true;
20915
+ if (record.kinds.length >= MAX_KINDS_PER_SUBJECT) return false;
20916
+ record.kinds = [...record.kinds, normalizedKind].sort((left, right) => left - right);
20917
+ persist();
20918
+ return true;
20919
+ },
20920
+ trustNapplet(context) {
20921
+ const record = ensureRecord(context);
20922
+ if (!record) return false;
20923
+ if (!record.trusted || record.kinds.length > 0) {
20924
+ record.trusted = true;
20925
+ record.kinds = [];
20926
+ persist();
20927
+ }
20928
+ return true;
20929
+ },
20930
+ clear() {
20931
+ const previous = grants;
20932
+ grants = [];
20933
+ if (!storage) return true;
20934
+ try {
20935
+ storage.removeItem(PAJA_SIGNER_CONSENT_STORAGE_KEY);
20936
+ return true;
20937
+ } catch {
20938
+ try {
20939
+ const empty = {
20940
+ version: PAJA_SIGNER_CONSENT_VERSION,
20941
+ grants: []
20942
+ };
20943
+ storage.setItem(PAJA_SIGNER_CONSENT_STORAGE_KEY, JSON.stringify(empty));
20944
+ return true;
20945
+ } catch {
20946
+ grants = previous;
20947
+ return false;
20948
+ }
20949
+ }
20950
+ },
20951
+ count() {
20952
+ return grants.reduce((total, grant2) => total + (grant2.trusted ? 1 : grant2.kinds.length), 0);
20953
+ }
20954
+ };
20955
+ }
20956
+ function isPajaSignerConsentContext(context) {
20957
+ return normalizeContext(context) !== null;
20958
+ }
20959
+ function readPajaSignerConsentStorage() {
20960
+ if (typeof window === "undefined") return null;
20961
+ try {
20962
+ return window.localStorage;
20963
+ } catch {
20964
+ return null;
20965
+ }
20966
+ }
20967
+ function readConsentDocument(storage) {
20968
+ const empty = () => ({ version: PAJA_SIGNER_CONSENT_VERSION, grants: [] });
20969
+ if (!storage) return empty();
20970
+ try {
20971
+ const raw = storage.getItem(PAJA_SIGNER_CONSENT_STORAGE_KEY);
20972
+ if (!raw) return empty();
20973
+ const parsed = JSON.parse(raw);
20974
+ if (parsed.version !== PAJA_SIGNER_CONSENT_VERSION || !Array.isArray(parsed.grants)) return empty();
20975
+ const grants = [];
20976
+ for (const candidate of parsed.grants.slice(0, MAX_CONSENT_SUBJECTS)) {
20977
+ const record = normalizeRecord(candidate);
20978
+ if (!record || grants.some((existing) => sameSubject(existing, record))) continue;
20979
+ grants.push(record);
20980
+ }
20981
+ return { version: PAJA_SIGNER_CONSENT_VERSION, grants };
20982
+ } catch {
20983
+ return empty();
20984
+ }
20985
+ }
20986
+ function normalizeRecord(value) {
20987
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
20988
+ const candidate = value;
20989
+ const subject = normalizeSubject(
20990
+ candidate.signerPubkey,
20991
+ candidate.dTag,
20992
+ candidate.aggregateHash,
20993
+ candidate.runtimeScope
20994
+ );
20995
+ if (!subject || typeof candidate.trusted !== "boolean" || !Array.isArray(candidate.kinds)) return null;
20996
+ const kinds = [...new Set(candidate.kinds.map((kind) => normalizeKind(kind)).filter((kind) => kind !== null))].slice(0, MAX_KINDS_PER_SUBJECT).sort((left, right) => left - right);
20997
+ return {
20998
+ ...subject,
20999
+ trusted: candidate.trusted,
21000
+ kinds: candidate.trusted ? [] : kinds
21001
+ };
21002
+ }
21003
+ function normalizeContext(context) {
21004
+ return normalizeSubject(
21005
+ context.signerPubkey,
21006
+ context.napplet.dTag,
21007
+ context.napplet.aggregateHash,
21008
+ context.runtimeScope
21009
+ );
21010
+ }
21011
+ function normalizeSubject(signerPubkey, dTag, aggregateHash, runtimeScope) {
21012
+ if (typeof signerPubkey !== "string" || !/^[0-9a-f]{64}$/i.test(signerPubkey)) return null;
21013
+ if (!validIdentityField(dTag) || !validIdentityField(aggregateHash) || !validIdentityField(runtimeScope)) return null;
21014
+ return {
21015
+ signerPubkey: signerPubkey.toLowerCase(),
21016
+ dTag,
21017
+ aggregateHash,
21018
+ runtimeScope
21019
+ };
21020
+ }
21021
+ function validIdentityField(value) {
21022
+ return typeof value === "string" && value.length > 0 && value.length <= MAX_IDENTITY_FIELD_LENGTH;
21023
+ }
21024
+ function normalizeKind(value) {
21025
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 65535 ? value : null;
21026
+ }
21027
+ function sameSubject(left, right) {
21028
+ return left.signerPubkey === right.signerPubkey && left.dTag === right.dTag && left.aggregateHash === right.aggregateHash && left.runtimeScope === right.runtimeScope;
21029
+ }
21030
+
21031
+ // src/browser-host-signer.ts
21032
+ function confirmationDetails(...lines) {
21033
+ return lines.filter((line) => line !== void 0).join("\n");
21034
+ }
21035
+ function confirmationCopy(title, summary, approveLabel, ...details) {
21036
+ return { title, summary, details: confirmationDetails(...details), approveLabel };
21037
+ }
21038
+ function createPajaConfirmationController(getState, options = {}) {
21039
+ const dialog = document.getElementById("paja-confirmation-dialog");
21040
+ const title = document.getElementById("paja-confirmation-title");
21041
+ const summary = document.getElementById("paja-confirmation-summary");
21042
+ const details = document.getElementById("paja-confirmation-details");
21043
+ const approve = document.getElementById("paja-confirmation-approve");
21044
+ const deny = document.getElementById("paja-confirmation-deny");
21045
+ const signerConsent = document.getElementById("paja-signer-consent");
21046
+ const signerConsentOnce = document.getElementById("paja-signer-consent-once");
21047
+ const signerConsentKind = document.getElementById("paja-signer-consent-kind");
21048
+ const signerConsentNapplet = document.getElementById("paja-signer-consent-napplet");
21049
+ const signerConsentKindValue = document.getElementById("paja-signer-consent-kind-value");
21050
+ const signerConsentNappletValue = document.getElementById("paja-signer-consent-napplet-value");
21051
+ const ready = dialog instanceof HTMLDialogElement && title instanceof HTMLElement && summary instanceof HTMLElement && details instanceof HTMLElement && approve instanceof HTMLButtonElement && deny instanceof HTMLButtonElement;
21052
+ const signerConsentReady = signerConsent instanceof HTMLFieldSetElement && signerConsentOnce instanceof HTMLInputElement && signerConsentKind instanceof HTMLInputElement && signerConsentNapplet instanceof HTMLInputElement && signerConsentKindValue instanceof HTMLElement && signerConsentNappletValue instanceof HTMLElement;
21053
+ const consentStore = createPajaSignerConsentStore(
21054
+ options.storage === void 0 ? void 0 : options.storage
21055
+ );
21056
+ const queue = [];
21057
+ let active = null;
21058
+ let previousFocus = null;
21059
+ let disposed = false;
21060
+ const reportSignerConsentChange = () => {
21061
+ options.onSignerConsentChange?.(consentStore.count());
21062
+ };
21063
+ const configureSignerConsent = (request) => {
21064
+ if (!signerConsentReady) return;
21065
+ signerConsent.hidden = true;
21066
+ signerConsentOnce.checked = true;
21067
+ signerConsentKind.checked = false;
21068
+ signerConsentNapplet.checked = false;
21069
+ const candidate = signerConsentCandidate(request);
21070
+ if (!candidate) return;
21071
+ signerConsent.hidden = false;
21072
+ signerConsentKindValue.textContent = String(candidate.kind);
21073
+ signerConsentNappletValue.textContent = candidate.context.napplet.dTag;
21074
+ };
21075
+ const rememberSignerChoice = (request) => {
21076
+ if (!signerConsentReady) return "once";
21077
+ const candidate = signerConsentCandidate(request);
21078
+ if (!candidate) return "once";
21079
+ if (signerConsentNapplet.checked && consentStore.trustNapplet(candidate.context)) {
21080
+ reportSignerConsentChange();
21081
+ return "trust-napplet";
21082
+ }
21083
+ if (signerConsentKind.checked && consentStore.rememberKind(candidate.context, candidate.kind)) {
21084
+ reportSignerConsentChange();
21085
+ return "remember-kind";
21086
+ }
21087
+ return "once";
21088
+ };
21089
+ const rememberedSignerChoice = (request) => {
21090
+ const candidate = signerConsentCandidate(request);
21091
+ return candidate ? consentStore.match(candidate.context, candidate.kind) : null;
21092
+ };
21093
+ const pump = () => {
21094
+ if (!ready || disposed || active) return;
21095
+ while (!active && queue.length > 0) {
21096
+ const candidate = queue.shift() ?? null;
21097
+ if (!candidate) break;
21098
+ const remembered = rememberedSignerChoice(candidate.request);
21099
+ if (!remembered) {
21100
+ active = candidate;
21101
+ break;
21102
+ }
21103
+ recordPajaConfirmation(
21104
+ getState(),
21105
+ candidate.request,
21106
+ true,
21107
+ remembered === "kind" ? "remembered-kind" : "remembered-napplet"
21108
+ );
21109
+ candidate.approve();
21110
+ }
21111
+ if (!active) return;
21112
+ const copy = describeConfirmation(active.request);
21113
+ title.textContent = copy.title;
21114
+ summary.textContent = copy.summary;
21115
+ details.textContent = copy.details;
21116
+ approve.textContent = copy.approveLabel;
21117
+ configureSignerConsent(active.request);
21118
+ previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
21119
+ dialog.showModal();
21120
+ deny.focus();
21121
+ };
21122
+ const settle6 = (allowed2) => {
21123
+ const current = active;
21124
+ if (!current) return;
21125
+ active = null;
21126
+ if (ready && dialog.open) dialog.close();
21127
+ const approvalMode = allowed2 ? rememberSignerChoice(current.request) : void 0;
21128
+ recordPajaConfirmation(getState(), current.request, allowed2, approvalMode);
21129
+ if (allowed2) current.approve();
21130
+ else current.deny();
21131
+ previousFocus?.focus();
21132
+ previousFocus = null;
21133
+ queueMicrotask(pump);
21134
+ };
21135
+ const onApprove = () => settle6(true);
21136
+ const onDeny = () => settle6(false);
21137
+ const onCancel = (event) => {
21138
+ event.preventDefault();
21139
+ settle6(false);
21140
+ };
21141
+ if (ready) {
21142
+ approve.addEventListener("click", onApprove);
21143
+ deny.addEventListener("click", onDeny);
21144
+ dialog.addEventListener("cancel", onCancel);
21145
+ }
21146
+ return {
21147
+ confirm(request) {
21148
+ if (!ready || disposed) {
21149
+ recordPajaConfirmation(getState(), request, false);
21150
+ return false;
21151
+ }
21152
+ return new Promise((resolve) => {
20502
21153
  queue.push({ request, approve: () => resolve(true), deny: () => resolve(false) });
20503
21154
  pump();
20504
21155
  });
@@ -20528,6 +21179,12 @@ function createPajaConfirmationController(getState) {
20528
21179
  });
20529
21180
  }
20530
21181
  },
21182
+ clearSignerConsent() {
21183
+ const cleared = consentStore.clear();
21184
+ reportSignerConsentChange();
21185
+ return cleared;
21186
+ },
21187
+ getSignerConsentCount: () => consentStore.count(),
20531
21188
  dispose() {
20532
21189
  if (disposed) return;
20533
21190
  disposed = true;
@@ -20544,6 +21201,13 @@ function createPajaConfirmationController(getState) {
20544
21201
  }
20545
21202
  };
20546
21203
  }
21204
+ function signerConsentCandidate(request) {
21205
+ if (request.action !== "sign" || !request.signerContext) return null;
21206
+ if (!isPajaSignerConsentContext(request.signerContext)) return null;
21207
+ const kind = request.event.kind;
21208
+ if (typeof kind !== "number" || !Number.isSafeInteger(kind) || kind < 0 || kind > 65535) return null;
21209
+ return { context: request.signerContext, kind };
21210
+ }
20547
21211
  function describeConfirmation(request) {
20548
21212
  if (request.action === "upload") {
20549
21213
  return confirmationCopy(
@@ -20584,730 +21248,499 @@ function describeConfirmation(request) {
20584
21248
  return confirmationCopy(
20585
21249
  "Allow notifications?",
20586
21250
  `${request.napplet.dTag} requests permission to show shell notifications.`,
20587
- "Allow notifications",
20588
- `Napplet: ${request.napplet.dTag} (${request.windowId})`,
20589
- `Identity: ${request.napplet.aggregateHash}`,
20590
- `Channel: ${request.channel ?? "all notifications"}`
20591
- );
20592
- }
20593
- if (request.action === "webrtc") {
20594
- return confirmationCopy(
20595
- "Start a peer connection?",
20596
- `${request.napplet.dTag} requests a WebRTC data session.`,
20597
- "Connect",
20598
- `Napplet: ${request.napplet.dTag} (${request.windowId})`,
20599
- `Scope: ${request.scope}`,
20600
- request.warning
20601
- );
20602
- }
20603
- if (request.action === "dm") {
20604
- return confirmationCopy(
20605
- "Send this encrypted message?",
20606
- "A napplet requests NIP-17 direct-message delivery.",
20607
- "Send message",
20608
- `Recipients: ${request.recipients.join(", ")}`,
20609
- `Content: ${request.content.slice(0, 240)}`,
20610
- request.warning
20611
- );
20612
- }
20613
- if (request.action === "fs") {
20614
- return confirmationCopy(
20615
- "Choose filesystem access?",
20616
- "A napplet requests a browser-mediated filesystem selection.",
20617
- "Open chooser",
20618
- `Napplet window: ${request.windowId}`,
20619
- `Selection: ${request.kind}`,
20620
- `Purpose: ${request.description}`,
20621
- "The browser will show its file or directory chooser next."
20622
- );
20623
- }
20624
- if (!("event" in request)) throw new Error(`Unsupported confirmation action: ${request.action}`);
20625
- const event = request.event;
20626
- const kind = typeof event.kind === "number" ? event.kind : "unknown";
20627
- const content = typeof event.content === "string" && event.content.length > 0 ? event.content.slice(0, 240) : "(empty content)";
20628
- return {
20629
- title: request.action === "sign" ? "Sign this Nostr event?" : "Publish this Nostr event?",
20630
- summary: request.action === "sign" ? "The active signer will authorize this event." : "Paja will send this event to the configured relay set.",
20631
- details: `Kind: ${kind}
20632
- Content: ${content}`,
20633
- approveLabel: request.action === "sign" ? "Sign" : "Publish"
20634
- };
20635
- }
20636
- function recordPajaConfirmation(state, request, allowed2) {
20637
- if (request.action === "upload") {
20638
- appendPajaMessageLog(state, "paja", {
20639
- type: `paja.upload.${allowed2 ? "confirmed" : "denied"}`,
20640
- windowId: request.windowId,
20641
- dTag: request.napplet.dTag,
20642
- aggregateHash: request.napplet.aggregateHash,
20643
- filename: request.filename ?? "(unnamed blob)",
20644
- size: request.size,
20645
- mimeType: request.mimeType ?? "application/octet-stream",
20646
- server: request.server,
20647
- warning: request.warning
20648
- });
20649
- return;
20650
- }
20651
- if (request.action === "link") {
20652
- appendPajaMessageLog(state, "paja", {
20653
- type: `paja.link.${allowed2 ? "confirmed" : "denied"}`,
20654
- windowId: request.windowId,
20655
- dTag: request.napplet.dTag,
20656
- aggregateHash: request.napplet.aggregateHash,
20657
- url: request.url
20658
- });
20659
- return;
20660
- }
20661
- if (request.action === "serial" || request.action === "ble") {
20662
- appendPajaMessageLog(state, "paja", {
20663
- type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
20664
- windowId: request.windowId,
20665
- label: request.label
20666
- });
20667
- return;
20668
- }
20669
- if (request.action === "notify") {
20670
- appendPajaMessageLog(state, "paja", {
20671
- type: `paja.notify.${allowed2 ? "confirmed" : "denied"}`,
20672
- windowId: request.windowId,
20673
- dTag: request.napplet.dTag,
20674
- aggregateHash: request.napplet.aggregateHash,
20675
- channel: request.channel
20676
- });
20677
- return;
20678
- }
20679
- if (request.action === "webrtc") {
20680
- appendPajaMessageLog(state, "paja", {
20681
- type: `paja.webrtc.${allowed2 ? "confirmed" : "denied"}`,
20682
- windowId: request.windowId,
20683
- dTag: request.napplet.dTag,
20684
- aggregateHash: request.napplet.aggregateHash
20685
- });
20686
- return;
20687
- }
20688
- if (request.action === "dm") {
20689
- appendPajaMessageLog(state, "paja", {
20690
- type: `paja.dm.${allowed2 ? "confirmed" : "denied"}`,
20691
- recipients: request.recipients
20692
- });
20693
- return;
20694
- }
20695
- if (request.action === "fs") {
20696
- appendPajaMessageLog(state, "paja", {
20697
- type: `paja.fs.${allowed2 ? "confirmed" : "denied"}`,
20698
- windowId: request.windowId,
20699
- kind: request.kind
20700
- });
20701
- return;
20702
- }
20703
- if (!("event" in request)) return;
20704
- const event = request.event;
20705
- appendPajaMessageLog(state, "paja", {
20706
- type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
20707
- kind: typeof event.kind === "number" ? event.kind : "unknown"
20708
- });
20709
- }
20710
- function createHostSignerController(getState, refreshState, confirmRequest) {
20711
- return createPajaSignerController({
20712
- confirmRequest,
20713
- onChange(signer) {
20714
- const state = getState();
20715
- if (!state) return;
20716
- state.signer = signer;
20717
- appendPajaMessageLog(state, "paja", {
20718
- type: `paja.signer.${signer.method}.${signer.status}`,
20719
- pubkey: signer.pubkey,
20720
- relay: signer.relay,
20721
- error: signer.error
20722
- });
20723
- refreshState(state);
20724
- if (signer.status === "connected") state.reload();
20725
- }
20726
- });
20727
- }
20728
- function hasNip07Signer() {
20729
- const signer = globalThis.nostr;
20730
- return typeof signer === "object" && signer !== null;
20731
- }
20732
-
20733
- // src/browser-host-runtime.ts
20734
- function unregisterSingleFrameWindow(bridge, runtime, windowId) {
20735
- if (!windowId) return;
20736
- bridge.runtime.destroyWindow(windowId);
20737
- bridge.runtime.sessionRegistry.unregister(windowId);
20738
- originRegistry.unregister(windowId);
20739
- runtime.readyWindowIds.delete(windowId);
20740
- if (runtime.currentWindowId === windowId) runtime.currentWindowId = null;
20741
- }
20742
-
20743
- // src/browser-runtime-tabs.ts
20744
- var PAJA_RUNTIME_TABS_STORAGE_KEY = "kehto:paja:runtime-tabs:v1";
20745
- function resolvedTargetKey(target) {
20746
- return [
20747
- target.event.kind,
20748
- target.event.pubkey,
20749
- target.dTag,
20750
- target.aggregateHash
20751
- ].join(":");
20752
- }
20753
- function runtimeTabGenerationId(tab) {
20754
- return `${tab.id}:${tab.generation}`;
20755
- }
20756
- function getActiveTab(state) {
20757
- return state.tabs.find((tab) => tab.id === state.activeTabId) ?? null;
20758
- }
20759
- function setEmptyStageVisible(visible) {
20760
- const empty = document.getElementById("empty-runtime-stage");
20761
- if (empty instanceof HTMLElement) empty.hidden = !visible;
20762
- }
20763
- function createPajaShareUrl(pointerValue, href = window.location.href) {
20764
- const pointer = pointerValue.trim();
20765
- const url = new URL(href);
20766
- url.search = "";
20767
- url.hash = "";
20768
- url.searchParams.set(getPointerParamName(pointer), pointer);
20769
- return url.href;
20770
- }
20771
- function snapshotRuntimeTabs(state) {
20772
- const pointers = state.tabs.map((tab) => tab.pointerValue.trim()).filter((pointer) => pointer.length > 0);
20773
- if (pointers.length === 0) return null;
20774
- const activeIndex = Math.max(
20775
- 0,
20776
- state.tabs.findIndex((tab) => tab.id === state.activeTabId)
20777
- );
20778
- return {
20779
- version: 1,
20780
- pointers,
20781
- activeIndex: Math.min(activeIndex, pointers.length - 1)
20782
- };
20783
- }
20784
- function parseRuntimeTabsSnapshot(value) {
20785
- if (!value) return null;
20786
- let parsed;
20787
- try {
20788
- parsed = JSON.parse(value);
20789
- } catch {
20790
- return null;
21251
+ "Allow notifications",
21252
+ `Napplet: ${request.napplet.dTag} (${request.windowId})`,
21253
+ `Identity: ${request.napplet.aggregateHash}`,
21254
+ `Channel: ${request.channel ?? "all notifications"}`
21255
+ );
20791
21256
  }
20792
- if (typeof parsed !== "object" || parsed === null) return null;
20793
- const candidate = parsed;
20794
- if (candidate.version !== 1 || !Array.isArray(candidate.pointers)) return null;
20795
- const pointers = candidate.pointers.filter((pointer) => typeof pointer === "string").map((pointer) => pointer.trim()).filter((pointer) => pointer.length > 0);
20796
- if (pointers.length === 0) return null;
20797
- const rawActiveIndex = typeof candidate.activeIndex === "number" && Number.isInteger(candidate.activeIndex) ? candidate.activeIndex : 0;
21257
+ if (request.action === "webrtc") {
21258
+ return confirmationCopy(
21259
+ "Start a peer connection?",
21260
+ `${request.napplet.dTag} requests a WebRTC data session.`,
21261
+ "Connect",
21262
+ `Napplet: ${request.napplet.dTag} (${request.windowId})`,
21263
+ `Scope: ${request.scope}`,
21264
+ request.warning
21265
+ );
21266
+ }
21267
+ if (request.action === "dm") {
21268
+ return confirmationCopy(
21269
+ "Send this encrypted message?",
21270
+ "A napplet requests NIP-17 direct-message delivery.",
21271
+ "Send message",
21272
+ `Recipients: ${request.recipients.join(", ")}`,
21273
+ `Content: ${request.content.slice(0, 240)}`,
21274
+ request.warning
21275
+ );
21276
+ }
21277
+ if (request.action === "fs") {
21278
+ return confirmationCopy(
21279
+ "Choose filesystem access?",
21280
+ "A napplet requests a browser-mediated filesystem selection.",
21281
+ "Open chooser",
21282
+ `Napplet window: ${request.windowId}`,
21283
+ `Selection: ${request.kind}`,
21284
+ `Purpose: ${request.description}`,
21285
+ "The browser will show its file or directory chooser next."
21286
+ );
21287
+ }
21288
+ if (!("event" in request)) throw new Error(`Unsupported confirmation action: ${request.action}`);
21289
+ const event = request.event;
21290
+ const kind = typeof event.kind === "number" ? event.kind : "unknown";
21291
+ const content = typeof event.content === "string" && event.content.length > 0 ? event.content.slice(0, 240) : "(empty content)";
21292
+ const signerContext = request.signerContext;
20798
21293
  return {
20799
- version: 1,
20800
- pointers,
20801
- activeIndex: Math.max(0, Math.min(rawActiveIndex, pointers.length - 1))
21294
+ title: request.action === "sign" ? "Sign this Nostr event?" : "Publish this Nostr event?",
21295
+ summary: request.action === "sign" ? signerContext ? `${signerContext.napplet.dTag} requests a signature from the active signer.` : "A Paja host operation requests a signature from the active signer." : "Paja will send this event to the configured relay set.",
21296
+ details: confirmationDetails(
21297
+ signerContext ? `Napplet: ${signerContext.napplet.dTag} (${signerContext.windowId})` : void 0,
21298
+ signerContext ? `Identity hash: ${signerContext.napplet.aggregateHash}` : void 0,
21299
+ signerContext?.runtimeScope.startsWith("target-url:") ? `Target URL: ${signerContext.runtimeScope.slice("target-url:".length)}` : void 0,
21300
+ signerContext ? `Signer: ${signerContext.signerPubkey}` : void 0,
21301
+ `Kind: ${kind}`,
21302
+ `Content: ${content}`
21303
+ ),
21304
+ approveLabel: request.action === "sign" ? "Sign" : "Publish"
20802
21305
  };
20803
21306
  }
20804
- function renderRuntimeTabs(state) {
20805
- const tabsEl = document.getElementById("napplet-tabs");
20806
- if (!(tabsEl instanceof HTMLElement)) return;
20807
- tabsEl.replaceChildren(...state.tabs.map((tab) => renderTab(state, tab)));
20808
- }
20809
- function activateRuntimeTab(state, context, tabId) {
20810
- const tab = state.tabs.find((entry) => entry.id === tabId);
20811
- if (!tab) return;
20812
- state.activeTabId = tab.id;
20813
- state.resolvedTarget = tab.resolvedTarget;
20814
- state.pointerValue = tab.pointerValue;
20815
- state.pointerStatus = tab.pointerStatus;
20816
- context.runtime.currentWindowId = tab.windowId;
20817
- const input = document.getElementById("runtime-pointer-input");
20818
- if (input instanceof HTMLInputElement) input.value = tab.pointerValue;
20819
- context.setPointerStatus(state, tab.pointerStatus);
20820
- context.setStatus(state, tab.status);
20821
- setEmptyStageVisible(false);
20822
- for (const entry of state.tabs) entry.frame.hidden = entry.id !== tab.id;
20823
- renderRuntimeTabs(state);
20824
- renderPajaDevtools(state, { bridge: context.bridge, devSignerPubkey: PAJA_DEV_SIGNER_PUBKEY });
20825
- }
20826
- function closeRuntimeTab(state, context, tabId) {
20827
- const index = state.tabs.findIndex((tab2) => tab2.id === tabId);
20828
- if (index < 0) return;
20829
- const [tab] = state.tabs.splice(index, 1);
20830
- destroyRuntimeTab(tab, context);
20831
- if (state.activeTabId !== tabId) {
20832
- renderRuntimeTabs(state);
20833
- setEmptyStageVisible(state.tabs.length === 0);
21307
+ function recordPajaConfirmation(state, request, allowed2, signerApprovalMode) {
21308
+ if (request.action === "upload") {
21309
+ appendPajaMessageLog(state, "paja", {
21310
+ type: `paja.upload.${allowed2 ? "confirmed" : "denied"}`,
21311
+ windowId: request.windowId,
21312
+ dTag: request.napplet.dTag,
21313
+ aggregateHash: request.napplet.aggregateHash,
21314
+ filename: request.filename ?? "(unnamed blob)",
21315
+ size: request.size,
21316
+ mimeType: request.mimeType ?? "application/octet-stream",
21317
+ server: request.server,
21318
+ warning: request.warning
21319
+ });
20834
21320
  return;
20835
21321
  }
20836
- const next = state.tabs[Math.min(index, state.tabs.length - 1)] ?? null;
20837
- if (next) {
20838
- activateRuntimeTab(state, context, next.id);
21322
+ if (request.action === "link") {
21323
+ appendPajaMessageLog(state, "paja", {
21324
+ type: `paja.link.${allowed2 ? "confirmed" : "denied"}`,
21325
+ windowId: request.windowId,
21326
+ dTag: request.napplet.dTag,
21327
+ aggregateHash: request.napplet.aggregateHash,
21328
+ url: request.url
21329
+ });
20839
21330
  return;
20840
21331
  }
20841
- state.activeTabId = null;
20842
- state.resolvedTarget = null;
20843
- context.runtime.currentWindowId = null;
20844
- state.pointerStatus = "idle";
20845
- context.setPointerStatus(state, "idle");
20846
- context.setStatus(state, "ready");
20847
- setEmptyStageVisible(true);
20848
- renderRuntimeTabs(state);
20849
- }
20850
- function addRuntimeTab(state, context, pointerValue, resolvedTarget) {
20851
- state.generation += 1;
20852
- const tab = {
20853
- id: `tab-${state.generation}`,
20854
- title: resolvedTargetTitle(resolvedTarget),
20855
- key: resolvedTargetKey(resolvedTarget),
20856
- pointerValue,
20857
- pointerStatus: `${resolvedTarget.dTag}:${resolvedTarget.aggregateHash.slice(0, 12)}`,
20858
- resolvedTarget,
20859
- frame: document.createElement("iframe"),
20860
- generation: state.generation,
20861
- windowId: null,
20862
- status: "booting"
20863
- };
20864
- tab.frame = createRuntimeTabFrame(tab, state, context);
20865
- context.stage.append(tab.frame);
20866
- state.tabs.push(tab);
20867
- activateRuntimeTab(state, context, tab.id);
20868
- startRuntimeTabNavigation(tab, state, context);
20869
- return tab;
20870
- }
20871
- function reloadActiveRuntimeTab(state, context) {
20872
- const tab = getActiveTab(state);
20873
- if (!tab) return;
20874
- destroyRuntimeTab(tab, context);
20875
- tab.generation = ++state.generation;
20876
- tab.windowId = null;
20877
- tab.status = "reloading";
20878
- tab.frame = createRuntimeTabFrame(tab, state, context);
20879
- context.stage.append(tab.frame);
20880
- activateRuntimeTab(state, context, tab.id);
20881
- startRuntimeTabNavigation(tab, state, context);
20882
- }
20883
- function showDuplicatePointerDialog() {
20884
- const backdrop = document.getElementById("duplicate-pointer-dialog");
20885
- const loadAgain = document.getElementById("duplicate-load-again");
20886
- const openTab = document.getElementById("duplicate-open-tab");
20887
- const cancel = document.getElementById("duplicate-cancel");
20888
- if (!(backdrop instanceof HTMLElement) || !(loadAgain instanceof HTMLButtonElement) || !(openTab instanceof HTMLButtonElement) || !(cancel instanceof HTMLButtonElement)) {
20889
- return Promise.resolve("cancel");
21332
+ if (request.action === "serial" || request.action === "ble") {
21333
+ appendPajaMessageLog(state, "paja", {
21334
+ type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
21335
+ windowId: request.windowId,
21336
+ label: request.label
21337
+ });
21338
+ return;
20890
21339
  }
20891
- return new Promise((resolve) => {
20892
- const cleanup = (choice) => {
20893
- backdrop.hidden = true;
20894
- loadAgain.removeEventListener("click", onLoadAgain);
20895
- openTab.removeEventListener("click", onOpenTab);
20896
- cancel.removeEventListener("click", onCancel);
20897
- backdrop.removeEventListener("keydown", onKeydown);
20898
- resolve(choice);
20899
- };
20900
- const onLoadAgain = () => cleanup("load-again");
20901
- const onOpenTab = () => cleanup("open-tab");
20902
- const onCancel = () => cleanup("cancel");
20903
- const onKeydown = (event) => {
20904
- if (event.key === "Escape") cleanup("cancel");
20905
- };
20906
- loadAgain.addEventListener("click", onLoadAgain);
20907
- openTab.addEventListener("click", onOpenTab);
20908
- cancel.addEventListener("click", onCancel);
20909
- backdrop.addEventListener("keydown", onKeydown);
20910
- backdrop.hidden = false;
20911
- loadAgain.focus();
20912
- });
20913
- }
20914
- function renderTab(state, tab) {
20915
- const tabButton = document.createElement("div");
20916
- tabButton.className = "tab";
20917
- tabButton.dataset.tabId = tab.id;
20918
- tabButton.dataset.active = String(tab.id === state.activeTabId);
20919
- tabButton.setAttribute("role", "tab");
20920
- tabButton.setAttribute("aria-selected", String(tab.id === state.activeTabId));
20921
- tabButton.tabIndex = 0;
20922
- tabButton.title = tab.pointerValue;
20923
- tabButton.addEventListener("click", () => state.activateTab(tab.id));
20924
- tabButton.addEventListener("keydown", (event) => {
20925
- if (event.key !== "Enter" && event.key !== " ") return;
20926
- event.preventDefault();
20927
- state.activateTab(tab.id);
20928
- });
20929
- const label = document.createElement("span");
20930
- label.className = "tab-label";
20931
- label.textContent = tab.title;
20932
- tabButton.append(label, renderShareButton(tab), renderCloseButton(state, tab));
20933
- return tabButton;
20934
- }
20935
- function renderShareButton(tab) {
20936
- const share = document.createElement("button");
20937
- share.type = "button";
20938
- share.className = "tab-share";
20939
- share.setAttribute("aria-label", `Copy share link for ${tab.title}`);
20940
- share.title = "Copy share link";
20941
- share.textContent = "\u2197";
20942
- share.addEventListener("click", (event) => {
20943
- event.stopPropagation();
20944
- void shareRuntimeTab(tab, share).catch((error) => console.error(error));
20945
- });
20946
- share.addEventListener("keydown", (event) => {
20947
- if (event.key !== "Enter" && event.key !== " ") return;
20948
- event.preventDefault();
20949
- event.stopPropagation();
20950
- void shareRuntimeTab(tab, share).catch((error) => console.error(error));
20951
- });
20952
- return share;
20953
- }
20954
- function renderCloseButton(state, tab) {
20955
- const close = document.createElement("button");
20956
- close.type = "button";
20957
- close.className = "tab-close";
20958
- close.setAttribute("aria-label", `Close ${tab.title}`);
20959
- close.textContent = "x";
20960
- close.addEventListener("click", (event) => {
20961
- event.stopPropagation();
20962
- state.closeTab(tab.id);
20963
- });
20964
- close.addEventListener("keydown", (event) => {
20965
- if (event.key !== "Enter" && event.key !== " ") return;
20966
- event.preventDefault();
20967
- event.stopPropagation();
20968
- state.closeTab(tab.id);
20969
- });
20970
- return close;
20971
- }
20972
- async function shareRuntimeTab(tab, button2) {
20973
- const shareUrl = createPajaShareUrl(tab.pointerValue);
20974
- const clipboard = navigator.clipboard;
20975
- if (!clipboard) {
20976
- window.prompt("Copy Paja share link", shareUrl);
21340
+ if (request.action === "notify") {
21341
+ appendPajaMessageLog(state, "paja", {
21342
+ type: `paja.notify.${allowed2 ? "confirmed" : "denied"}`,
21343
+ windowId: request.windowId,
21344
+ dTag: request.napplet.dTag,
21345
+ aggregateHash: request.napplet.aggregateHash,
21346
+ channel: request.channel
21347
+ });
21348
+ return;
21349
+ }
21350
+ if (request.action === "webrtc") {
21351
+ appendPajaMessageLog(state, "paja", {
21352
+ type: `paja.webrtc.${allowed2 ? "confirmed" : "denied"}`,
21353
+ windowId: request.windowId,
21354
+ dTag: request.napplet.dTag,
21355
+ aggregateHash: request.napplet.aggregateHash
21356
+ });
20977
21357
  return;
20978
21358
  }
20979
- try {
20980
- await clipboard.writeText(shareUrl);
20981
- } catch {
20982
- window.prompt("Copy Paja share link", shareUrl);
21359
+ if (request.action === "dm") {
21360
+ appendPajaMessageLog(state, "paja", {
21361
+ type: `paja.dm.${allowed2 ? "confirmed" : "denied"}`,
21362
+ recipients: request.recipients
21363
+ });
20983
21364
  return;
20984
21365
  }
20985
- button2.title = "Share link copied";
20986
- window.setTimeout(() => {
20987
- button2.title = "Copy share link";
20988
- }, 1200);
20989
- }
20990
- function getPointerParamName(pointer) {
20991
- if (pointer.startsWith("naddr")) return "naddr";
20992
- if (pointer.startsWith("nevent")) return "nevent";
20993
- return "pointer";
20994
- }
20995
- function resolvedTargetTitle(target) {
20996
- const titleTag = target.event.tags.find((tag) => tag[0] === "title")?.[1];
20997
- const title = typeof target.manifest.title === "string" && target.manifest.title.trim().length > 0 ? target.manifest.title.trim() : titleTag?.trim();
20998
- return title && title.length > 0 ? title : target.dTag || target.aggregateHash.slice(0, 12);
20999
- }
21000
- function destroyRuntimeTab(tab, context) {
21001
- context.onTabDestroyed?.(tab);
21002
- if (tab.windowId) {
21003
- context.bridge.runtime.destroyWindow(tab.windowId);
21004
- context.bridge.runtime.sessionRegistry.unregister(tab.windowId);
21005
- originRegistry.unregister(tab.windowId);
21006
- context.runtime.readyWindowIds.delete(tab.windowId);
21007
- if (context.runtime.currentWindowId === tab.windowId) context.runtime.currentWindowId = null;
21366
+ if (request.action === "fs") {
21367
+ appendPajaMessageLog(state, "paja", {
21368
+ type: `paja.fs.${allowed2 ? "confirmed" : "denied"}`,
21369
+ windowId: request.windowId,
21370
+ kind: request.kind
21371
+ });
21372
+ return;
21008
21373
  }
21009
- tab.frame.remove();
21010
- }
21011
- function createRuntimeTabFrame(tab, state, context) {
21012
- const frame = document.createElement("iframe");
21013
- frame.id = `napplet-frame-${tab.id}`;
21014
- frame.className = "tab-panel";
21015
- frame.title = `Napplet runtime target: ${tab.title}`;
21016
- frame.sandbox.add("allow-scripts");
21017
- frame.sandbox.remove("allow-same-origin");
21018
- frame.dataset.tabId = tab.id;
21019
- frame.dataset.targetUrl = tab.pointerValue;
21020
- frame.hidden = true;
21021
- frame.addEventListener("error", () => {
21022
- tab.status = "error";
21023
- if (state.activeTabId === tab.id) context.setStatus(state, "error");
21024
- renderRuntimeTabs(state);
21374
+ if (!("event" in request)) return;
21375
+ const event = request.event;
21376
+ const signerContext = request.signerContext;
21377
+ appendPajaMessageLog(state, "paja", {
21378
+ type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
21379
+ kind: typeof event.kind === "number" ? event.kind : "unknown",
21380
+ windowId: signerContext?.windowId,
21381
+ dTag: signerContext?.napplet.dTag,
21382
+ aggregateHash: signerContext?.napplet.aggregateHash,
21383
+ runtimeScope: signerContext?.runtimeScope,
21384
+ signerPubkey: signerContext?.signerPubkey,
21385
+ approval: signerApprovalMode
21025
21386
  });
21026
- return frame;
21027
21387
  }
21028
- function startRuntimeTabNavigation(tab, state, context) {
21029
- const generation = tab.generation;
21030
- tab.status = "booting";
21031
- context.setStatus(state, "booting");
21032
- renderRuntimeTabs(state);
21033
- void context.navigateFrame(
21034
- tab.frame,
21035
- context.config,
21036
- generation,
21037
- context.adapter,
21038
- tab.resolvedTarget,
21039
- runtimeTabWindowId(context.config, tab),
21040
- () => tab.generation === generation,
21041
- (windowId) => {
21042
- if (tab.generation !== generation) return;
21043
- tab.windowId = windowId;
21044
- if (state.activeTabId === tab.id) context.runtime.currentWindowId = windowId;
21388
+ function createHostSignerController(getState, refreshState, confirmRequest) {
21389
+ return createPajaSignerController({
21390
+ confirmRequest,
21391
+ onChange(signer) {
21392
+ const state = getState();
21393
+ if (!state) return;
21394
+ state.signer = signer;
21395
+ appendPajaMessageLog(state, "paja", {
21396
+ type: `paja.signer.${signer.method}.${signer.status}`,
21397
+ pubkey: signer.pubkey,
21398
+ relay: signer.relay,
21399
+ error: signer.error
21400
+ });
21401
+ refreshState(state);
21402
+ if (signer.status === "connected") state.reload();
21045
21403
  }
21046
- ).then((windowId) => {
21047
- if (tab.generation !== generation) return;
21048
- tab.windowId = windowId;
21049
- if (state.activeTabId === tab.id) context.runtime.currentWindowId = windowId;
21050
- }).catch((error) => {
21051
- if (tab.generation !== generation) return;
21052
- tab.frame.removeAttribute("src");
21053
- tab.frame.srcdoc = context.renderTargetErrorHtml(error);
21054
- tab.status = "error";
21055
- if (state.activeTabId === tab.id) context.setStatus(state, "error");
21056
- appendPajaMessageLog(state, "paja", {
21057
- type: "paja.target.error",
21058
- error: error instanceof Error ? error.message : String(error)
21059
- }, tab.windowId ?? void 0);
21060
- renderRuntimeTabs(state);
21061
- console.error(error);
21062
21404
  });
21063
21405
  }
21064
- function runtimeTabWindowId(config, tab) {
21065
- return `${config.window.id}:${tab.id}:${tab.generation}`;
21406
+ function hasNip07Signer() {
21407
+ const signer = globalThis.nostr;
21408
+ return typeof signer === "object" && signer !== null;
21066
21409
  }
21067
21410
 
21068
- // src/runtime-resolver.ts
21069
- var PAJA_NAPPLET_MANIFEST_KINDS = NAPPLET_KINDS;
21070
- function decodePajaPointer(value) {
21071
- const trimmed = value.trim();
21072
- const decoded = decode(trimmed);
21073
- if (decoded.type === "naddr") {
21074
- return {
21075
- type: "naddr",
21076
- value: trimmed,
21077
- identifier: decoded.data.identifier,
21078
- pubkey: decoded.data.pubkey,
21079
- kind: decoded.data.kind,
21080
- relays: [...decoded.data.relays ?? []]
21081
- };
21082
- }
21083
- if (decoded.type === "nevent") {
21084
- return {
21085
- type: "nevent",
21086
- value: trimmed,
21087
- id: decoded.data.id,
21088
- ...decoded.data.author ? { author: decoded.data.author } : {},
21089
- ...decoded.data.kind ? { kind: decoded.data.kind } : {},
21090
- relays: [...decoded.data.relays ?? []]
21091
- };
21092
- }
21093
- throw new Error(`Expected naddr or nevent pointer, got ${decoded.type}.`);
21411
+ // src/browser-host-runtime.ts
21412
+ function unregisterSingleFrameWindow(bridge, runtime, windowId) {
21413
+ if (!windowId) return;
21414
+ bridge.runtime.destroyWindow(windowId);
21415
+ bridge.runtime.sessionRegistry.unregister(windowId);
21416
+ originRegistry.unregister(windowId);
21417
+ runtime.readyWindowIds.delete(windowId);
21418
+ if (runtime.currentWindowId === windowId) runtime.currentWindowId = null;
21094
21419
  }
21095
- async function resolvePajaPointer(value, options = {}) {
21096
- const pointer = decodePajaPointer(value);
21097
- const relays = uniqueRelayUrls([...pointer.relays, ...options.relays ?? []]);
21098
- if (relays.length === 0) {
21099
- throw new Error("Pointer has no relay hints or enabled configured live relays.");
21100
- }
21101
- const pool = options.pool ?? new SimplePool();
21102
- const ownsPool = options.pool === void 0;
21103
- try {
21104
- const event = await resolvePointerEvent(pointer, pool, relays, normalizeMaxWaitMs(options.maxWaitMs));
21105
- const blossomServers = [...options.blossomServers ?? []];
21106
- const resolved = await resolveNapplet({
21107
- event,
21108
- cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
21109
- fetchBlob: (sha256Hex, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex, async (url) => {
21110
- const response = await (options.fetcher ?? fetch)(url);
21111
- if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
21112
- return new Uint8Array(await response.arrayBuffer());
21113
- })
21114
- });
21115
- return {
21116
- pointer,
21117
- event,
21118
- relays,
21119
- blossomServers,
21120
- dTag: resolved.dTag,
21121
- aggregateHash: resolved.aggregateHash,
21122
- indexHtml: resolved.indexHtml,
21123
- manifest: resolved.manifest
21124
- };
21125
- } finally {
21126
- if (ownsPool) {
21127
- pool.close?.(relays);
21128
- pool.destroy?.();
21129
- }
21130
- }
21420
+
21421
+ // src/browser-runtime-tabs.ts
21422
+ var PAJA_RUNTIME_TABS_STORAGE_KEY = "kehto:paja:runtime-tabs:v1";
21423
+ function resolvedTargetKey(target) {
21424
+ return [
21425
+ target.event.kind,
21426
+ target.event.pubkey,
21427
+ target.dTag,
21428
+ target.aggregateHash
21429
+ ].join(":");
21131
21430
  }
21132
- function injectPajaRuntimeCsp(html, origins) {
21133
- const grantedOrigins = [...new Set(origins)].sort();
21134
- const connectSrc = grantedOrigins.length > 0 ? `connect-src ${grantedOrigins.join(" ")}` : "connect-src 'none'";
21135
- const value = [
21136
- "default-src 'none'",
21137
- "script-src 'unsafe-inline'",
21138
- "style-src 'unsafe-inline'",
21139
- "img-src data: blob:",
21140
- "font-src data:",
21141
- connectSrc,
21142
- "worker-src 'none'",
21143
- "child-src 'none'",
21144
- "frame-src 'none'",
21145
- "media-src 'none'",
21146
- "object-src 'none'",
21147
- "manifest-src 'none'",
21148
- "prefetch-src 'none'",
21149
- "base-uri 'none'",
21150
- "form-action 'none'",
21151
- "frame-ancestors 'self'"
21152
- ].join("; ");
21153
- const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(value)}">`;
21154
- if (/<head[^>]*>/i.test(html)) {
21155
- return html.replace(/<head[^>]*>/i, (open) => `${open}${meta}`);
21156
- }
21157
- if (/<html[^>]*>/i.test(html)) {
21158
- return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${meta}</head>`);
21431
+ function runtimeTabGenerationId(tab) {
21432
+ return `${tab.id}:${tab.generation}`;
21433
+ }
21434
+ function getActiveTab(state) {
21435
+ return state.tabs.find((tab) => tab.id === state.activeTabId) ?? null;
21436
+ }
21437
+ function setEmptyStageVisible(visible) {
21438
+ const empty = document.getElementById("empty-runtime-stage");
21439
+ if (empty instanceof HTMLElement) empty.hidden = !visible;
21440
+ }
21441
+ function createPajaShareUrl(pointerValue, href = window.location.href) {
21442
+ const pointer = pointerValue.trim();
21443
+ const url = new URL(href);
21444
+ url.search = "";
21445
+ url.hash = "";
21446
+ url.searchParams.set(getPointerParamName(pointer), pointer);
21447
+ return url.href;
21448
+ }
21449
+ function snapshotRuntimeTabs(state) {
21450
+ const pointers = state.tabs.map((tab) => tab.pointerValue.trim()).filter((pointer) => pointer.length > 0);
21451
+ if (pointers.length === 0) return null;
21452
+ const activeIndex = Math.max(
21453
+ 0,
21454
+ state.tabs.findIndex((tab) => tab.id === state.activeTabId)
21455
+ );
21456
+ return {
21457
+ version: 1,
21458
+ pointers,
21459
+ activeIndex: Math.min(activeIndex, pointers.length - 1)
21460
+ };
21461
+ }
21462
+ function parseRuntimeTabsSnapshot(value) {
21463
+ if (!value) return null;
21464
+ let parsed;
21465
+ try {
21466
+ parsed = JSON.parse(value);
21467
+ } catch {
21468
+ return null;
21159
21469
  }
21160
- return `${meta}${html}`;
21470
+ if (typeof parsed !== "object" || parsed === null) return null;
21471
+ const candidate = parsed;
21472
+ if (candidate.version !== 1 || !Array.isArray(candidate.pointers)) return null;
21473
+ const pointers = candidate.pointers.filter((pointer) => typeof pointer === "string").map((pointer) => pointer.trim()).filter((pointer) => pointer.length > 0);
21474
+ if (pointers.length === 0) return null;
21475
+ const rawActiveIndex = typeof candidate.activeIndex === "number" && Number.isInteger(candidate.activeIndex) ? candidate.activeIndex : 0;
21476
+ return {
21477
+ version: 1,
21478
+ pointers,
21479
+ activeIndex: Math.max(0, Math.min(rawActiveIndex, pointers.length - 1))
21480
+ };
21161
21481
  }
21162
- async function resolvePointerEvent(pointer, pool, relays, maxWaitMs) {
21163
- const filter = pointer.type === "naddr" ? addressableFilter(pointer) : eventFilter(pointer);
21164
- const result2 = await queryPointerRelays(pool, relays, filter, maxWaitMs);
21165
- const event = result2.events.filter((candidate) => matchesPointer(pointer, candidate)).sort((a, b) => b.created_at - a.created_at)[0];
21166
- if (!event) {
21167
- if (result2.timedOut) {
21168
- throw new Error(
21169
- `Relay resolution timed out after ${maxWaitMs}ms for ${pointer.type} pointer across ${relays.length} relays before complete EOSE.`
21170
- );
21171
- }
21172
- const failures = relayFailureReasons(result2.reasons);
21173
- if (result2.error || failures.length > 0) {
21174
- const detail = result2.error instanceof Error ? result2.error.message : result2.error === void 0 ? failures.join("; ") : String(result2.error);
21175
- throw new Error(`Relay query failed for ${pointer.type} pointer before complete EOSE: ${detail}`);
21176
- }
21177
- throw new Error(
21178
- `All ${relays.length} relays reached EOSE without a matching ${pointer.type} napplet manifest event.`
21179
- );
21482
+ function renderRuntimeTabs(state) {
21483
+ const tabsEl = document.getElementById("napplet-tabs");
21484
+ if (!(tabsEl instanceof HTMLElement)) return;
21485
+ tabsEl.replaceChildren(...state.tabs.map((tab) => renderTab(state, tab)));
21486
+ }
21487
+ function activateRuntimeTab(state, context, tabId) {
21488
+ const tab = state.tabs.find((entry) => entry.id === tabId);
21489
+ if (!tab) return;
21490
+ state.activeTabId = tab.id;
21491
+ state.resolvedTarget = tab.resolvedTarget;
21492
+ state.pointerValue = tab.pointerValue;
21493
+ state.pointerStatus = tab.pointerStatus;
21494
+ context.runtime.currentWindowId = tab.windowId;
21495
+ const input = document.getElementById("runtime-pointer-input");
21496
+ if (input instanceof HTMLInputElement) input.value = tab.pointerValue;
21497
+ context.setPointerStatus(state, tab.pointerStatus);
21498
+ context.setStatus(state, tab.status);
21499
+ setEmptyStageVisible(false);
21500
+ for (const entry of state.tabs) entry.frame.hidden = entry.id !== tab.id;
21501
+ renderRuntimeTabs(state);
21502
+ renderPajaDevtools(state, { bridge: context.bridge, devSignerPubkey: PAJA_DEV_SIGNER_PUBKEY });
21503
+ }
21504
+ function closeRuntimeTab(state, context, tabId) {
21505
+ const index = state.tabs.findIndex((tab2) => tab2.id === tabId);
21506
+ if (index < 0) return;
21507
+ const [tab] = state.tabs.splice(index, 1);
21508
+ destroyRuntimeTab(tab, context);
21509
+ if (state.activeTabId !== tabId) {
21510
+ renderRuntimeTabs(state);
21511
+ setEmptyStageVisible(state.tabs.length === 0);
21512
+ return;
21180
21513
  }
21181
- if (!isNappletManifestKind(event.kind)) {
21182
- throw new Error(`Pointer resolved kind ${event.kind}; expected ${formatNappletManifestKinds()}.`);
21514
+ const next = state.tabs[Math.min(index, state.tabs.length - 1)] ?? null;
21515
+ if (next) {
21516
+ activateRuntimeTab(state, context, next.id);
21517
+ return;
21183
21518
  }
21184
- return event;
21519
+ state.activeTabId = null;
21520
+ state.resolvedTarget = null;
21521
+ context.runtime.currentWindowId = null;
21522
+ state.pointerStatus = "idle";
21523
+ context.setPointerStatus(state, "idle");
21524
+ context.setStatus(state, "ready");
21525
+ setEmptyStageVisible(true);
21526
+ renderRuntimeTabs(state);
21185
21527
  }
21186
- function queryPointerRelays(pool, relays, filter, maxWaitMs) {
21187
- if (pool.subscribeManyEose) {
21188
- return queryPointerRelaySubscription(pool, relays, filter, maxWaitMs);
21189
- }
21190
- return queryPointerRelaySync(pool, relays, filter, maxWaitMs);
21528
+ function addRuntimeTab(state, context, pointerValue, resolvedTarget) {
21529
+ state.generation += 1;
21530
+ const tab = {
21531
+ id: `tab-${state.generation}`,
21532
+ title: resolvedTargetTitle(resolvedTarget),
21533
+ key: resolvedTargetKey(resolvedTarget),
21534
+ pointerValue,
21535
+ pointerStatus: `${resolvedTarget.dTag}:${resolvedTarget.aggregateHash.slice(0, 12)}`,
21536
+ resolvedTarget,
21537
+ frame: document.createElement("iframe"),
21538
+ generation: state.generation,
21539
+ windowId: null,
21540
+ status: "booting"
21541
+ };
21542
+ tab.frame = createRuntimeTabFrame(tab, state, context);
21543
+ context.stage.append(tab.frame);
21544
+ state.tabs.push(tab);
21545
+ activateRuntimeTab(state, context, tab.id);
21546
+ startRuntimeTabNavigation(tab, state, context);
21547
+ return tab;
21191
21548
  }
21192
- function queryPointerRelaySubscription(pool, relays, filter, maxWaitMs) {
21549
+ function reloadActiveRuntimeTab(state, context) {
21550
+ const tab = getActiveTab(state);
21551
+ if (!tab) return;
21552
+ destroyRuntimeTab(tab, context);
21553
+ tab.generation = ++state.generation;
21554
+ tab.windowId = null;
21555
+ tab.status = "reloading";
21556
+ tab.frame = createRuntimeTabFrame(tab, state, context);
21557
+ context.stage.append(tab.frame);
21558
+ activateRuntimeTab(state, context, tab.id);
21559
+ startRuntimeTabNavigation(tab, state, context);
21560
+ }
21561
+ function showDuplicatePointerDialog() {
21562
+ const backdrop = document.getElementById("duplicate-pointer-dialog");
21563
+ const loadAgain = document.getElementById("duplicate-load-again");
21564
+ const openTab = document.getElementById("duplicate-open-tab");
21565
+ const cancel = document.getElementById("duplicate-cancel");
21566
+ if (!(backdrop instanceof HTMLElement) || !(loadAgain instanceof HTMLButtonElement) || !(openTab instanceof HTMLButtonElement) || !(cancel instanceof HTMLButtonElement)) {
21567
+ return Promise.resolve("cancel");
21568
+ }
21193
21569
  return new Promise((resolve) => {
21194
- const events = [];
21195
- let settled = false;
21196
- let subscription;
21197
- const finish = (result2) => {
21198
- if (settled) return;
21199
- settled = true;
21200
- clearTimeout(deadline);
21201
- resolve({ events, ...result2 });
21570
+ const cleanup = (choice) => {
21571
+ backdrop.hidden = true;
21572
+ loadAgain.removeEventListener("click", onLoadAgain);
21573
+ openTab.removeEventListener("click", onOpenTab);
21574
+ cancel.removeEventListener("click", onCancel);
21575
+ backdrop.removeEventListener("keydown", onKeydown);
21576
+ resolve(choice);
21202
21577
  };
21203
- const deadline = setTimeout(() => {
21204
- finish({ reasons: [], timedOut: true });
21205
- void subscription?.close("Paja pointer relay deadline exceeded");
21206
- }, maxWaitMs);
21207
- try {
21208
- subscription = pool.subscribeManyEose([...relays], filter, {
21209
- maxWait: maxWaitMs,
21210
- label: "kehto-paja-runtime",
21211
- onevent(event) {
21212
- events.push(event);
21213
- },
21214
- onclose(reasons) {
21215
- finish({ reasons, timedOut: false });
21216
- }
21217
- });
21218
- } catch (error) {
21219
- finish({ reasons: [], timedOut: false, error });
21220
- }
21578
+ const onLoadAgain = () => cleanup("load-again");
21579
+ const onOpenTab = () => cleanup("open-tab");
21580
+ const onCancel = () => cleanup("cancel");
21581
+ const onKeydown = (event) => {
21582
+ if (event.key === "Escape") cleanup("cancel");
21583
+ };
21584
+ loadAgain.addEventListener("click", onLoadAgain);
21585
+ openTab.addEventListener("click", onOpenTab);
21586
+ cancel.addEventListener("click", onCancel);
21587
+ backdrop.addEventListener("keydown", onKeydown);
21588
+ backdrop.hidden = false;
21589
+ loadAgain.focus();
21221
21590
  });
21222
21591
  }
21223
- function queryPointerRelaySync(pool, relays, filter, maxWaitMs) {
21224
- return new Promise((resolve) => {
21225
- let settled = false;
21226
- const finish = (result2) => {
21227
- if (settled) return;
21228
- settled = true;
21229
- clearTimeout(deadline);
21230
- resolve(result2);
21231
- };
21232
- const deadline = setTimeout(() => {
21233
- finish({ events: [], reasons: [], timedOut: true });
21234
- }, maxWaitMs);
21235
- void pool.querySync([...relays], filter, {
21236
- maxWait: maxWaitMs,
21237
- label: "kehto-paja-runtime"
21238
- }).then(
21239
- (events) => finish({ events, reasons: [], timedOut: false }),
21240
- (error) => finish({ events: [], reasons: [], timedOut: false, error })
21241
- );
21592
+ function renderTab(state, tab) {
21593
+ const tabButton = document.createElement("div");
21594
+ tabButton.className = "tab";
21595
+ tabButton.dataset.tabId = tab.id;
21596
+ tabButton.dataset.active = String(tab.id === state.activeTabId);
21597
+ tabButton.setAttribute("role", "tab");
21598
+ tabButton.setAttribute("aria-selected", String(tab.id === state.activeTabId));
21599
+ tabButton.tabIndex = 0;
21600
+ tabButton.title = tab.pointerValue;
21601
+ tabButton.addEventListener("click", () => state.activateTab(tab.id));
21602
+ tabButton.addEventListener("keydown", (event) => {
21603
+ if (event.key !== "Enter" && event.key !== " ") return;
21604
+ event.preventDefault();
21605
+ state.activateTab(tab.id);
21242
21606
  });
21607
+ const label = document.createElement("span");
21608
+ label.className = "tab-label";
21609
+ label.textContent = tab.title;
21610
+ tabButton.append(label, renderShareButton(tab), renderCloseButton(state, tab));
21611
+ return tabButton;
21243
21612
  }
21244
- function relayFailureReasons(reasons) {
21245
- 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);
21613
+ function renderShareButton(tab) {
21614
+ const share = document.createElement("button");
21615
+ share.type = "button";
21616
+ share.className = "tab-share";
21617
+ share.setAttribute("aria-label", `Copy share link for ${tab.title}`);
21618
+ share.title = "Copy share link";
21619
+ share.textContent = "\u2197";
21620
+ share.addEventListener("click", (event) => {
21621
+ event.stopPropagation();
21622
+ void shareRuntimeTab(tab, share).catch((error) => console.error(error));
21623
+ });
21624
+ share.addEventListener("keydown", (event) => {
21625
+ if (event.key !== "Enter" && event.key !== " ") return;
21626
+ event.preventDefault();
21627
+ event.stopPropagation();
21628
+ void shareRuntimeTab(tab, share).catch((error) => console.error(error));
21629
+ });
21630
+ return share;
21246
21631
  }
21247
- function normalizeMaxWaitMs(value) {
21248
- if (value === void 0) return 5e3;
21249
- if (!Number.isFinite(value) || value <= 0) {
21250
- throw new Error("Pointer relay deadline must be a positive number of milliseconds.");
21251
- }
21252
- return Math.floor(value);
21632
+ function renderCloseButton(state, tab) {
21633
+ const close = document.createElement("button");
21634
+ close.type = "button";
21635
+ close.className = "tab-close";
21636
+ close.setAttribute("aria-label", `Close ${tab.title}`);
21637
+ close.textContent = "x";
21638
+ close.addEventListener("click", (event) => {
21639
+ event.stopPropagation();
21640
+ state.closeTab(tab.id);
21641
+ });
21642
+ close.addEventListener("keydown", (event) => {
21643
+ if (event.key !== "Enter" && event.key !== " ") return;
21644
+ event.preventDefault();
21645
+ event.stopPropagation();
21646
+ state.closeTab(tab.id);
21647
+ });
21648
+ return close;
21253
21649
  }
21254
- function addressableFilter(pointer) {
21255
- if (pointer.kind !== NAPPLET_KIND_NAMED) {
21256
- throw new Error(`naddr kind ${pointer.kind} is not a named NIP-5D napplet manifest kind.`);
21650
+ async function shareRuntimeTab(tab, button2) {
21651
+ const shareUrl = createPajaShareUrl(tab.pointerValue);
21652
+ const clipboard = navigator.clipboard;
21653
+ if (!clipboard) {
21654
+ window.prompt("Copy Paja share link", shareUrl);
21655
+ return;
21257
21656
  }
21258
- return {
21259
- kinds: [NAPPLET_KIND_NAMED],
21260
- authors: [pointer.pubkey],
21261
- "#d": [pointer.identifier],
21262
- limit: 1
21263
- };
21657
+ try {
21658
+ await clipboard.writeText(shareUrl);
21659
+ } catch {
21660
+ window.prompt("Copy Paja share link", shareUrl);
21661
+ return;
21662
+ }
21663
+ button2.title = "Share link copied";
21664
+ window.setTimeout(() => {
21665
+ button2.title = "Copy share link";
21666
+ }, 1200);
21264
21667
  }
21265
- function eventFilter(pointer) {
21266
- return {
21267
- ids: [pointer.id],
21268
- ...pointer.kind ? { kinds: [pointer.kind] } : {},
21269
- ...pointer.author ? { authors: [pointer.author] } : {},
21270
- limit: 1
21271
- };
21668
+ function getPointerParamName(pointer) {
21669
+ if (pointer.startsWith("naddr")) return "naddr";
21670
+ if (pointer.startsWith("nevent")) return "nevent";
21671
+ return "pointer";
21272
21672
  }
21273
- function matchesPointer(pointer, event) {
21274
- if (pointer.type === "nevent") {
21275
- if (event.id !== pointer.id) return false;
21276
- if (pointer.author && event.pubkey !== pointer.author) return false;
21277
- if (pointer.kind && event.kind !== pointer.kind) return false;
21278
- return true;
21279
- }
21280
- return event.kind === pointer.kind && event.pubkey === pointer.pubkey && event.tags.some((tag) => tag[0] === "d" && tag[1] === pointer.identifier);
21673
+ function resolvedTargetTitle(target) {
21674
+ const titleTag = target.event.tags.find((tag) => tag[0] === "title")?.[1];
21675
+ const title = typeof target.manifest.title === "string" && target.manifest.title.trim().length > 0 ? target.manifest.title.trim() : titleTag?.trim();
21676
+ return title && title.length > 0 ? title : target.dTag || target.aggregateHash.slice(0, 12);
21281
21677
  }
21282
- function uniqueRelayUrls(values) {
21283
- const out = [];
21284
- const seen = /* @__PURE__ */ new Set();
21285
- for (const value of values) {
21286
- const url = normalizeRelayUrl2(value);
21287
- if (!url || seen.has(url)) continue;
21288
- seen.add(url);
21289
- out.push(url);
21678
+ function destroyRuntimeTab(tab, context) {
21679
+ context.onTabDestroyed?.(tab);
21680
+ if (tab.windowId) {
21681
+ context.bridge.runtime.destroyWindow(tab.windowId);
21682
+ context.bridge.runtime.sessionRegistry.unregister(tab.windowId);
21683
+ originRegistry.unregister(tab.windowId);
21684
+ context.runtime.readyWindowIds.delete(tab.windowId);
21685
+ if (context.runtime.currentWindowId === tab.windowId) context.runtime.currentWindowId = null;
21290
21686
  }
21291
- return out;
21687
+ tab.frame.remove();
21292
21688
  }
21293
- function normalizeRelayUrl2(value) {
21294
- const raw = value.trim();
21295
- if (!raw) return void 0;
21296
- try {
21297
- const url = new URL(raw);
21298
- if (url.protocol !== "wss:" && url.protocol !== "ws:") return void 0;
21299
- url.hash = "";
21300
- url.search = "";
21301
- return url.href.replace(/\/$/, "");
21302
- } catch {
21303
- return void 0;
21304
- }
21689
+ function createRuntimeTabFrame(tab, state, context) {
21690
+ const frame = document.createElement("iframe");
21691
+ frame.id = `napplet-frame-${tab.id}`;
21692
+ frame.className = "tab-panel";
21693
+ frame.title = `Napplet runtime target: ${tab.title}`;
21694
+ frame.sandbox.add("allow-scripts");
21695
+ frame.sandbox.remove("allow-same-origin");
21696
+ frame.dataset.tabId = tab.id;
21697
+ frame.dataset.targetUrl = tab.pointerValue;
21698
+ frame.hidden = true;
21699
+ frame.addEventListener("error", () => {
21700
+ tab.status = "error";
21701
+ if (state.activeTabId === tab.id) context.setStatus(state, "error");
21702
+ renderRuntimeTabs(state);
21703
+ });
21704
+ return frame;
21305
21705
  }
21306
- function escapeAttribute(value) {
21307
- return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
21706
+ function startRuntimeTabNavigation(tab, state, context) {
21707
+ const generation = tab.generation;
21708
+ tab.status = "booting";
21709
+ context.setStatus(state, "booting");
21710
+ renderRuntimeTabs(state);
21711
+ void context.navigateFrame(
21712
+ tab.frame,
21713
+ context.config,
21714
+ generation,
21715
+ context.adapter,
21716
+ tab.resolvedTarget,
21717
+ runtimeTabWindowId(context.config, tab),
21718
+ () => tab.generation === generation,
21719
+ (windowId) => {
21720
+ if (tab.generation !== generation) return;
21721
+ tab.windowId = windowId;
21722
+ if (state.activeTabId === tab.id) context.runtime.currentWindowId = windowId;
21723
+ }
21724
+ ).then((windowId) => {
21725
+ if (tab.generation !== generation) return;
21726
+ tab.windowId = windowId;
21727
+ if (state.activeTabId === tab.id) context.runtime.currentWindowId = windowId;
21728
+ }).catch((error) => {
21729
+ if (tab.generation !== generation) return;
21730
+ tab.frame.removeAttribute("src");
21731
+ tab.frame.srcdoc = context.renderTargetErrorHtml(error);
21732
+ tab.status = "error";
21733
+ if (state.activeTabId === tab.id) context.setStatus(state, "error");
21734
+ appendPajaMessageLog(state, "paja", {
21735
+ type: "paja.target.error",
21736
+ error: error instanceof Error ? error.message : String(error)
21737
+ }, tab.windowId ?? void 0);
21738
+ renderRuntimeTabs(state);
21739
+ console.error(error);
21740
+ });
21308
21741
  }
21309
- function formatNappletManifestKinds() {
21310
- return PAJA_NAPPLET_MANIFEST_KINDS.join(" / ");
21742
+ function runtimeTabWindowId(config, tab) {
21743
+ return `${config.window.id}:${tab.id}:${tab.generation}`;
21311
21744
  }
21312
21745
 
21313
21746
  // src/browser-intent-host.ts
@@ -21489,113 +21922,6 @@ function createPajaThemeBroadcastLink() {
21489
21922
  };
21490
21923
  }
21491
21924
 
21492
- // src/browser-target-frame.ts
21493
- function resolvePajaFrameEnvironment(hooks, identity) {
21494
- if (!identity.dTag || !identity.aggregateHash) {
21495
- throw new Error("Paja frame identity requires a dTag and aggregate hash");
21496
- }
21497
- return resolveShellEnvironment(hooks, identity);
21498
- }
21499
- function getTargetIdentity2(config, resolvedTarget) {
21500
- return {
21501
- pubkey: "",
21502
- dTag: resolvedTarget?.dTag ?? config.window.dTag,
21503
- aggregateHash: resolvedTarget?.aggregateHash ?? config.window.aggregateHash
21504
- };
21505
- }
21506
- function getTargetOriginIdentity(config, resolvedTarget) {
21507
- const target = getTargetIdentity2(config, resolvedTarget);
21508
- return Object.freeze({ dTag: target.dTag, aggregateHash: target.aggregateHash });
21509
- }
21510
- function registerFrameForGeneration(frame, config, generation, identity, environment, windowId = `${config.window.id}:${generation}`) {
21511
- const win = frame.contentWindow;
21512
- if (!win) return null;
21513
- originRegistry.register(win, windowId, identity);
21514
- originRegistry.setEnvironment(win, environment);
21515
- return windowId;
21516
- }
21517
- async function navigateFrame(frame, config, generation, adapter, resolvedTarget, windowId, isCurrent, onRegistered) {
21518
- const identity = getTargetOriginIdentity(config, resolvedTarget);
21519
- const environment = resolvePajaFrameEnvironment(adapter, identity);
21520
- const domains = environment.capabilities.domains;
21521
- if (config.target.mode === "runtime-pointer") {
21522
- if (!resolvedTarget) {
21523
- frame.removeAttribute("src");
21524
- frame.srcdoc = "<!doctype html><html><body></body></html>";
21525
- return null;
21526
- }
21527
- if (isCurrent && !isCurrent()) return null;
21528
- const registeredWindowId2 = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
21529
- onRegistered?.(registeredWindowId2);
21530
- frame.removeAttribute("src");
21531
- frame.srcdoc = injectNappletNamespacePrelude(
21532
- injectPajaRuntimeCsp(
21533
- resolvedTarget.indexHtml,
21534
- connectOrigins([...resolvedTarget.relays, ...resolvedTarget.blossomServers])
21535
- ),
21536
- { domains }
21537
- );
21538
- return registeredWindowId2;
21539
- }
21540
- const html = await fetchTargetHtml();
21541
- if (isCurrent && !isCurrent()) return null;
21542
- const registeredWindowId = registerFrameForGeneration(frame, config, generation, identity, environment, windowId);
21543
- onRegistered?.(registeredWindowId);
21544
- frame.removeAttribute("src");
21545
- frame.srcdoc = injectNappletNamespacePrelude(
21546
- injectBaseHref(html, config.target.url),
21547
- { domains }
21548
- );
21549
- return registeredWindowId;
21550
- }
21551
- function renderTargetErrorHtml(error) {
21552
- const message = error instanceof Error ? error.message : String(error);
21553
- return `<!doctype html><html><body><pre>${escapeHtml(message)}</pre></body></html>`;
21554
- }
21555
- function connectOrigins(urls) {
21556
- const out = /* @__PURE__ */ new Set();
21557
- for (const value of urls) {
21558
- try {
21559
- const url = new URL(value);
21560
- if (url.protocol === "wss:" || url.protocol === "ws:") {
21561
- out.add(value.replace(/\/$/, ""));
21562
- } else {
21563
- out.add(url.origin);
21564
- }
21565
- } catch {
21566
- }
21567
- }
21568
- return [...out];
21569
- }
21570
- async function fetchTargetHtml() {
21571
- const response = await fetch(new URL("./__kehto/target.html", window.location.href), {
21572
- cache: "no-store",
21573
- headers: {
21574
- accept: "text/html, application/xhtml+xml;q=0.9, */*;q=0.8"
21575
- }
21576
- });
21577
- if (!response.ok) {
21578
- throw new Error(`Paja target fetch failed: ${response.status} ${response.statusText}`);
21579
- }
21580
- return response.text();
21581
- }
21582
- function injectBaseHref(html, targetUrl) {
21583
- const base = `<base href="${escapeAttribute2(targetUrl)}">`;
21584
- if (/<head[^>]*>/i.test(html)) {
21585
- return html.replace(/<head[^>]*>/i, (open) => `${open}${base}`);
21586
- }
21587
- if (/<html[^>]*>/i.test(html)) {
21588
- return html.replace(/<html[^>]*>/i, (open) => `${open}<head>${base}</head>`);
21589
- }
21590
- return `${base}${html}`;
21591
- }
21592
- function escapeAttribute2(value) {
21593
- return escapeHtml(value).replaceAll('"', "&quot;");
21594
- }
21595
- function escapeHtml(value) {
21596
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
21597
- }
21598
-
21599
21925
  // src/browser-target-diagnostics.ts
21600
21926
  async function reportTargetCorsDiagnostic(state) {
21601
21927
  let diagnostic;
@@ -22316,6 +22642,7 @@ function snapshotPajaBrowserState(state, runtime) {
22316
22642
  services: state.services,
22317
22643
  simulation: runtime.currentSimulation,
22318
22644
  signer: state.signer,
22645
+ signerConsentCount: state.signerConsentCount,
22319
22646
  resolvedTarget: state.resolvedTarget,
22320
22647
  pointerStatus: state.pointerStatus,
22321
22648
  activeTabId: state.activeTabId,
@@ -22331,13 +22658,22 @@ function snapshotPajaBrowserState(state, runtime) {
22331
22658
  };
22332
22659
  }
22333
22660
  function createPajaBrowserState(context) {
22334
- const { config, bridge, adapter, signerController, capabilities, runtime } = context;
22661
+ const {
22662
+ config,
22663
+ bridge,
22664
+ adapter,
22665
+ signerController,
22666
+ confirmationController,
22667
+ capabilities,
22668
+ runtime
22669
+ } = context;
22335
22670
  return {
22336
22671
  config,
22337
22672
  capabilities,
22338
22673
  services: Object.keys(adapter.services ?? {}).sort(),
22339
22674
  simulation: runtime.currentSimulation,
22340
22675
  signer: signerController.getState(),
22676
+ signerConsentCount: confirmationController.getSignerConsentCount(),
22341
22677
  resolvedTarget: null,
22342
22678
  pointerValue: readInitialPointerValue(config),
22343
22679
  pointerStatus: config.target.mode === "runtime-pointer" ? "idle" : "",
@@ -22374,7 +22710,7 @@ function createPajaBrowserState(context) {
22374
22710
  setRuntimeDomainEnabled(this, context, domain, enabled);
22375
22711
  },
22376
22712
  setAclCapability(capability, enabled) {
22377
- const identity = getTargetIdentity2(config, this.resolvedTarget);
22713
+ const identity = getTargetIdentity(config, this.resolvedTarget);
22378
22714
  if (enabled) bridge.runtime.aclState.grant(identity.pubkey, identity.dTag, identity.aggregateHash, capability);
22379
22715
  else bridge.runtime.aclState.revoke(identity.pubkey, identity.dTag, identity.aggregateHash, capability);
22380
22716
  bridge.runtime.aclState.persist();
@@ -22390,6 +22726,12 @@ function createPajaBrowserState(context) {
22390
22726
  connectBunker(uri) {
22391
22727
  return signerController.connectBunker(uri);
22392
22728
  },
22729
+ clearSignerConsent() {
22730
+ const cleared = confirmationController.clearSignerConsent();
22731
+ appendPajaMessageLog(this, "paja", {
22732
+ type: cleared ? "paja.signer-consent.cleared" : "paja.signer-consent.clear-failed"
22733
+ });
22734
+ },
22393
22735
  async loadPointer(value) {
22394
22736
  await loadRuntimePointer(this, context, value);
22395
22737
  },
@@ -22425,7 +22767,16 @@ async function installPajaHost() {
22425
22767
  () => contextRef,
22426
22768
  { persistTabs: persistRuntimeTabs }
22427
22769
  ));
22428
- const confirmationController = createPajaConfirmationController(() => stateRef);
22770
+ const confirmationController = createPajaConfirmationController(
22771
+ () => stateRef,
22772
+ {
22773
+ onSignerConsentChange(count) {
22774
+ if (!stateRef) return;
22775
+ stateRef.signerConsentCount = count;
22776
+ setSimulationStatus(stateRef);
22777
+ }
22778
+ }
22779
+ );
22429
22780
  const signerController = createHostSignerController(
22430
22781
  () => stateRef,
22431
22782
  setSimulationStatus,
@@ -22433,7 +22784,7 @@ async function installPajaHost() {
22433
22784
  );
22434
22785
  const getWindowIdentity = (windowId) => {
22435
22786
  const tabTarget = stateRef?.tabs.find((tab) => tab.windowId === windowId)?.resolvedTarget;
22436
- return getTargetIdentity2(config, tabTarget ?? stateRef?.resolvedTarget);
22787
+ return getTargetIdentity(config, tabTarget ?? stateRef?.resolvedTarget);
22437
22788
  };
22438
22789
  const notifyController = createPajaNotifyController({
22439
22790
  confirm: confirmationController.confirm,
@@ -22466,6 +22817,7 @@ async function installPajaHost() {
22466
22817
  bridge,
22467
22818
  adapter,
22468
22819
  signerController,
22820
+ confirmationController,
22469
22821
  capabilities,
22470
22822
  runtime,
22471
22823
  navigateFrame,