@kehto/paja 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 = {}) => {
@@ -2835,8 +2832,8 @@ function createAuthAdapter(shellHooks) {
2835
2832
  getUserPubkey() {
2836
2833
  return shellHooks.auth.getUserPubkey();
2837
2834
  },
2838
- getSigner() {
2839
- return shellHooks.auth.getSigner();
2835
+ getSigner(windowId) {
2836
+ return shellHooks.auth.getSigner(windowId);
2840
2837
  }
2841
2838
  };
2842
2839
  }
@@ -11087,11 +11084,20 @@ function requestKey(windowId, requestId) {
11087
11084
  return `${windowId}\0${requestId}`;
11088
11085
  }
11089
11086
  function assertResourceOptions(options) {
11090
- if (typeof options?.fetch !== "function" || typeof options?.isOriginGranted !== "function" || typeof options?.getConnectGrants !== "function" || typeof options?.resolveIdentity !== "function") {
11087
+ if (typeof options?.fetch !== "function") {
11091
11088
  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."
11089
+ "[RESOURCE-01] createResourceService requires a runtime-provided fetch resolver."
11093
11090
  );
11094
11091
  }
11092
+ const hasOriginPolicy = options.isOriginGranted !== void 0 || options.getConnectGrants !== void 0;
11093
+ if (hasOriginPolicy && (typeof options.isOriginGranted !== "function" || typeof options.getConnectGrants !== "function" || typeof options.resolveIdentity !== "function")) {
11094
+ throw new Error(
11095
+ "[RESOURCE-01] optional origin policy requires {isOriginGranted, getConnectGrants, resolveIdentity} together."
11096
+ );
11097
+ }
11098
+ if (options.resolveIdentity !== void 0 && typeof options.resolveIdentity !== "function") {
11099
+ throw new Error("[RESOURCE-01] resolveIdentity must be a function when provided.");
11100
+ }
11095
11101
  }
11096
11102
  function trackRequest(state, requestId, windowId, controller) {
11097
11103
  const key = requestKey(windowId, requestId);
@@ -11147,7 +11153,7 @@ function normalizeResourceInfo(info) {
11147
11153
  }
11148
11154
  async function resolveResourceInfo(options, windowId) {
11149
11155
  const configured = options.resourceInfo ?? DEFAULT_RESOURCE_INFO;
11150
- const info = typeof configured === "function" ? await configured({ windowId, identity: options.resolveIdentity(windowId) }) : configured;
11156
+ const info = typeof configured === "function" ? await configured({ windowId, identity: options.resolveIdentity?.(windowId) ?? null }) : configured;
11151
11157
  return normalizeResourceInfo(info);
11152
11158
  }
11153
11159
  async function handleInfo(options, windowId, requestId, send) {
@@ -11181,22 +11187,29 @@ function classifyResourceFailure(error) {
11181
11187
  message: error instanceof Error ? error.message : String(error)
11182
11188
  };
11183
11189
  }
11184
- async function fetchResourceItem(options, identity, info, url, signal) {
11190
+ async function fetchResourceItem(options, windowId, info, url, signal) {
11185
11191
  const parsedUrl = parseResourceUrl(url);
11186
11192
  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
- };
11193
+ if (options.isOriginGranted && options.getConnectGrants) {
11194
+ const identity = options.resolveIdentity?.(windowId) ?? null;
11195
+ if (!identity) {
11196
+ return {
11197
+ ok: false,
11198
+ url,
11199
+ error: "blocked-by-policy",
11200
+ message: "napplet identity not resolvable for runtime origin policy"
11201
+ };
11202
+ }
11203
+ const origin = parsedUrl.origin;
11204
+ const grants = options.getConnectGrants(identity.dTag, identity.aggregateHash);
11205
+ if (!options.isOriginGranted(origin, grants)) {
11206
+ return {
11207
+ ok: false,
11208
+ url,
11209
+ error: "blocked-by-policy",
11210
+ message: `origin ${origin} not granted by runtime policy`
11211
+ };
11212
+ }
11200
11213
  }
11201
11214
  try {
11202
11215
  const response = await options.fetch(url, {
@@ -11226,16 +11239,11 @@ async function fetchResourceItem(options, identity, info, url, signal) {
11226
11239
  }
11227
11240
  async function handleBytes(options, state, windowId, msg, send) {
11228
11241
  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
11242
  const controller = new AbortController();
11235
11243
  trackRequest(state, requestId, windowId, controller);
11236
11244
  try {
11237
11245
  const info = await resolveResourceInfo(options, windowId);
11238
- const item = await fetchResourceItem(options, identity, info, url, controller.signal);
11246
+ const item = await fetchResourceItem(options, windowId, info, url, controller.signal);
11239
11247
  if (controller.signal.aborted) return;
11240
11248
  if (!item.ok) {
11241
11249
  sendResourceError(send, requestId, item.message, item.error);
@@ -11262,11 +11270,6 @@ async function handleBytesMany(options, state, windowId, msg, send) {
11262
11270
  sendBytesManyError(send, requestId, "resource.bytesMany requires a non-empty urls array", "invalid-request");
11263
11271
  return;
11264
11272
  }
11265
- const identity = options.resolveIdentity(windowId);
11266
- if (!identity) {
11267
- sendBytesManyError(send, requestId, "napplet identity not resolvable", "blocked-by-policy");
11268
- return;
11269
- }
11270
11273
  let info;
11271
11274
  try {
11272
11275
  info = await resolveResourceInfo(options, windowId);
@@ -11284,7 +11287,7 @@ async function handleBytesMany(options, state, windowId, msg, send) {
11284
11287
  try {
11285
11288
  const items = [];
11286
11289
  for (const url of urls) {
11287
- const item = await fetchResourceItem(options, identity, info, url, controller.signal);
11290
+ const item = await fetchResourceItem(options, windowId, info, url, controller.signal);
11288
11291
  if (controller.signal.aborted) return;
11289
11292
  if (item.ok) {
11290
11293
  items.push({
@@ -11343,7 +11346,7 @@ function createResourceService(options) {
11343
11346
  const descriptor = {
11344
11347
  name: "resource",
11345
11348
  version: RESOURCE_SERVICE_VERSION,
11346
- description: "NAP-RESOURCE reference service \u2014 shell-proxied authenticated fetch (RESOURCE-01..06)"
11349
+ description: "NAP-RESOURCE service \u2014 runtime-resolved byte transport (RESOURCE-01..06)"
11347
11350
  };
11348
11351
  const handler = {
11349
11352
  descriptor,
@@ -18186,6 +18189,9 @@ function createPajaResourceFetch(options = {}) {
18186
18189
  if (url.protocol === "blossom:") {
18187
18190
  return fetchBlossomResource(value, init.signal, options);
18188
18191
  }
18192
+ if (url.protocol === "http:" || url.protocol === "https:") {
18193
+ return fetchNetworkResource(value, init.signal, options.fetch ?? globalThis.fetch);
18194
+ }
18189
18195
  if (url.protocol !== "data:") {
18190
18196
  throw new ResourceServiceError("unsupported-scheme", `Paja does not enable ${url.protocol}`);
18191
18197
  }
@@ -18205,7 +18211,11 @@ function createPajaResourceFetch(options = {}) {
18205
18211
  };
18206
18212
  }
18207
18213
  function pajaResourceInfo(blossomServers = []) {
18208
- const schemes = [{ scheme: "data", enabled: true }];
18214
+ const schemes = [
18215
+ { scheme: "data", enabled: true },
18216
+ { scheme: "https", enabled: true },
18217
+ { scheme: "http", enabled: true }
18218
+ ];
18209
18219
  if (usableBlossomServers(blossomServers).length > 0) {
18210
18220
  schemes.push({ scheme: "blossom", enabled: true });
18211
18221
  }
@@ -18215,6 +18225,35 @@ function pajaResourceInfo(blossomServers = []) {
18215
18225
  maxUrls: PAJA_RESOURCE_MAX_URLS
18216
18226
  };
18217
18227
  }
18228
+ async function fetchNetworkResource(value, signal, fetcher) {
18229
+ let response;
18230
+ try {
18231
+ response = await fetcher(value, {
18232
+ method: "GET",
18233
+ signal,
18234
+ credentials: "omit",
18235
+ referrerPolicy: "no-referrer"
18236
+ });
18237
+ } catch (error) {
18238
+ if (signal.aborted || isAbortError(error)) throw error;
18239
+ throw new ResourceServiceError(
18240
+ "network-error",
18241
+ error instanceof Error ? error.message : String(error)
18242
+ );
18243
+ }
18244
+ if (response.status === 404 || response.status === 410) {
18245
+ throw new ResourceServiceError("not-found", "HTTP resource was not found");
18246
+ }
18247
+ if (!response.ok) {
18248
+ throw new ResourceServiceError("network-error", `HTTP resource returned ${response.status}`);
18249
+ }
18250
+ const bytes = await readCappedResponse(response, signal);
18251
+ const mime = sniffSafeResourceMime(bytes);
18252
+ if (!mime) {
18253
+ throw new ResourceServiceError("decode-failed", "resource bytes are not an enabled safe media type");
18254
+ }
18255
+ return new Response(arrayBufferFor(bytes), { headers: { "content-type": mime } });
18256
+ }
18218
18257
  async function fetchBlossomResource(value, signal, options) {
18219
18258
  const match = BLOSSOM_RESOURCE_PATTERN.exec(value);
18220
18259
  if (!match?.[1]) {
@@ -19421,12 +19460,17 @@ async function createPajaBrowserFsBackend(options) {
19421
19460
  // src/browser-dev-runtime.ts
19422
19461
  var DEV_SIGNER_SECRET_KEY = generateSecretKey();
19423
19462
  var PAJA_DEV_SIGNER_PUBKEY = getPublicKey(DEV_SIGNER_SECRET_KEY);
19424
- function createPajaDevSigner(getSimulation, confirm) {
19463
+ function createPajaDevSigner(getSimulation, confirm, source) {
19425
19464
  return {
19426
19465
  getPublicKey: () => PAJA_DEV_SIGNER_PUBKEY,
19427
19466
  getRelays: () => Object.fromEntries(getPajaRelayUrls(getSimulation()).map((relay) => [relay, { read: true, write: true }])),
19428
19467
  async signEvent(event) {
19429
- if (!await confirm({ action: "sign", event })) {
19468
+ const signerContext = source ? { ...source, signerPubkey: PAJA_DEV_SIGNER_PUBKEY } : void 0;
19469
+ if (!await confirm({
19470
+ action: "sign",
19471
+ event,
19472
+ ...signerContext ? { signerContext } : {}
19473
+ })) {
19430
19474
  throw new Error("Paja signing request denied");
19431
19475
  }
19432
19476
  const template = { ...event };
@@ -19549,10 +19593,12 @@ function createDevTheme(mode, values) {
19549
19593
  function getRuntimePubkey(getSimulation, signerProvider) {
19550
19594
  return signerProvider?.getPubkey() || (signerProvider?.getMethod() === "dev" ? PAJA_DEV_SIGNER_PUBKEY : "") || getSimulation().identity.pubkey;
19551
19595
  }
19552
- function createRuntimeSigner(getSimulation, confirmRequest, signerProvider) {
19553
- const signer = signerProvider?.getSigner();
19596
+ function createRuntimeSigner(getSimulation, confirmRequest, signerProvider, source) {
19597
+ const signer = signerProvider?.getSigner(source);
19554
19598
  if (!signer) {
19555
- if (signerProvider?.getMethod() === "dev") return createPajaDevSigner(getSimulation, confirmRequest);
19599
+ if (signerProvider?.getMethod() === "dev") {
19600
+ return createPajaDevSigner(getSimulation, confirmRequest, source);
19601
+ }
19556
19602
  const fixedPubkey = getSimulation().identity.pubkey;
19557
19603
  if (fixedPubkey) {
19558
19604
  return {
@@ -19596,9 +19642,6 @@ function createDevServices(backend, getSimulation, relayConfig, onThemeService,
19596
19642
  const services = {
19597
19643
  resource: createResourceService({
19598
19644
  fetch: createPajaResourceFetch({ getBlossomServers }),
19599
- isOriginGranted: (origin, grants) => grants.includes(origin),
19600
- getConnectGrants: () => ["null"],
19601
- resolveIdentity: (windowId) => getIdentity?.(windowId) ?? null,
19602
19645
  resourceInfo: () => pajaResourceInfo(getBlossomServers())
19603
19646
  })
19604
19647
  };
@@ -19772,6 +19815,18 @@ function createDevServices(backend, getSimulation, relayConfig, onThemeService,
19772
19815
  return { services, refreshAvailability };
19773
19816
  }
19774
19817
  function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadcast, confirmRequest, signerProvider, getIdentity, onEnvironmentChanged, intentHost, userActivation, notifyOptions, configOptions) {
19818
+ const resolveIdentity = (windowId) => getIdentity?.(windowId) ?? {
19819
+ dTag: config.window.dTag,
19820
+ aggregateHash: config.window.aggregateHash
19821
+ };
19822
+ const signerSource = (windowId) => {
19823
+ const napplet = resolveIdentity(windowId);
19824
+ return {
19825
+ windowId,
19826
+ napplet,
19827
+ runtimeScope: config.target?.mode === "iframe-url" ? `target-url:${config.target.url}` : `artifact:${napplet.aggregateHash}`
19828
+ };
19829
+ };
19775
19830
  const relayBackend = createPajaRelayBackend(getSimulation, confirmRequest);
19776
19831
  const relayConfig = createPajaRelayConfig(getSimulation);
19777
19832
  const uploadRuntime = getSimulation().upload.mode === "blossom" ? createPajaUploadRuntime({
@@ -19781,10 +19836,7 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
19781
19836
  queryDiscovery: (relayUrls2, filters) => relayBackend.query(relayUrls2, filters),
19782
19837
  getRelayUrls: () => relayConfig.getRelayUrls(["discovery", "super"]),
19783
19838
  confirmRequest,
19784
- getNappletIdentity: (windowId) => getIdentity?.(windowId) ?? {
19785
- dTag: config.window.dTag,
19786
- aggregateHash: config.window.aggregateHash
19787
- },
19839
+ getNappletIdentity: resolveIdentity,
19788
19840
  subscribeSignerChange: signerProvider?.subscribe?.bind(signerProvider)
19789
19841
  }) : void 0;
19790
19842
  const getBlossomServers = () => [
@@ -19804,20 +19856,14 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
19804
19856
  uploadRuntime,
19805
19857
  signerProvider,
19806
19858
  intentHost,
19807
- getIdentity ?? (() => ({
19808
- dTag: config.window.dTag,
19809
- aggregateHash: config.window.aggregateHash
19810
- })),
19859
+ resolveIdentity,
19811
19860
  userActivation,
19812
19861
  notifyOptions,
19813
19862
  configOptions
19814
19863
  );
19815
19864
  const services = serviceBundle.services;
19816
19865
  const ready = createPajaBrowserFsBackend({
19817
- getIdentity: (windowId) => getIdentity?.(windowId) ?? {
19818
- dTag: config.window.dTag,
19819
- aggregateHash: config.window.aggregateHash
19820
- },
19866
+ getIdentity: resolveIdentity,
19821
19867
  userActivation
19822
19868
  }).then((fsBackend) => {
19823
19869
  if (!fsBackend) return;
@@ -19839,7 +19885,12 @@ function createPajaAdapter(config, getSimulation, onThemeService, onThemeBroadca
19839
19885
  windowManager: { createWindow: () => null },
19840
19886
  auth: {
19841
19887
  getUserPubkey: () => getRuntimePubkey(getSimulation, signerProvider),
19842
- getSigner: () => createRuntimeSigner(getSimulation, confirmRequest, signerProvider)
19888
+ getSigner: (windowId) => createRuntimeSigner(
19889
+ getSimulation,
19890
+ confirmRequest,
19891
+ signerProvider,
19892
+ windowId ? signerSource(windowId) : void 0
19893
+ )
19843
19894
  },
19844
19895
  services,
19845
19896
  get capabilities() {
@@ -20105,13 +20156,18 @@ function renderAclControls(state, bridge) {
20105
20156
  function renderSignerStatus(state, signerPubkey) {
20106
20157
  const el = document.getElementById("signer-status");
20107
20158
  const controls = document.getElementById("signer-controls");
20108
- if (!el || !controls) return;
20159
+ const clearConsent = document.getElementById("signer-consent-clear");
20160
+ if (!el || !controls || !(clearConsent instanceof HTMLButtonElement)) return;
20109
20161
  const fallbackPubkey = state.simulation.identity.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
20110
20162
  const pubkey2 = state.simulation.identity.pubkey || state.signer.pubkey || (state.signer.method === "dev" ? signerPubkey : "");
20111
20163
  const methodLabel = state.signer.method === "nip07" ? "NIP-07" : state.signer.method === "nip46" ? "bunker" : state.signer.method === "dev" ? "dev" : "none";
20112
20164
  const relay = state.signer.relay ? ` \xB7 relay ${state.signer.relay}` : "";
20113
20165
  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`;
20166
+ const consentSummary = state.signerConsentCount === 0 ? "signing asks every time" : `${state.signerConsentCount} remembered signing approval${state.signerConsentCount === 1 ? "" : "s"}`;
20167
+ el.textContent = `${prefix} \xB7 ${consentSummary} \xB7 publishing asks every time`;
20168
+ clearConsent.hidden = state.signerConsentCount === 0;
20169
+ clearConsent.textContent = `Forget ${state.signerConsentCount} remembered approval${state.signerConsentCount === 1 ? "" : "s"}`;
20170
+ clearConsent.onclick = () => state.clearSignerConsent();
20115
20171
  const previousInput = controls.querySelector("#signer-bunker-uri")?.value ?? "";
20116
20172
  const devButton = document.createElement("button");
20117
20173
  devButton.type = "button";
@@ -20325,20 +20381,35 @@ function readNip07Signer() {
20325
20381
  if (typeof signer.getPublicKey !== "function" || typeof signer.signEvent !== "function") return null;
20326
20382
  return signer;
20327
20383
  }
20328
- function createConfirmedSigner(signer, confirmRequest) {
20384
+ function createConfirmedSigner(signer, confirmRequest, source) {
20329
20385
  return {
20330
20386
  ...signer,
20331
20387
  async signEvent(event) {
20332
20388
  if (typeof signer.signEvent !== "function") {
20333
20389
  throw new Error("Signer does not support event signing");
20334
20390
  }
20335
- if (!await confirmRequest({ action: "sign", event })) {
20391
+ const signerContext = await resolveSignerContext(signer, source);
20392
+ if (!await confirmRequest({
20393
+ action: "sign",
20394
+ event,
20395
+ ...signerContext ? { signerContext } : {}
20396
+ })) {
20336
20397
  throw new Error("Paja signing request denied");
20337
20398
  }
20338
20399
  return signer.signEvent(event);
20339
20400
  }
20340
20401
  };
20341
20402
  }
20403
+ async function resolveSignerContext(signer, source) {
20404
+ if (!source || typeof signer.getPublicKey !== "function") return void 0;
20405
+ try {
20406
+ const signerPubkey = await signer.getPublicKey();
20407
+ if (!/^[0-9a-f]{64}$/i.test(signerPubkey)) return void 0;
20408
+ return { ...source, signerPubkey };
20409
+ } catch {
20410
+ return void 0;
20411
+ }
20412
+ }
20342
20413
  function createNip07Adapter(signer) {
20343
20414
  return {
20344
20415
  getPublicKey: signer.getPublicKey?.bind(signer),
@@ -20370,7 +20441,10 @@ function createPajaSignerController(options) {
20370
20441
  };
20371
20442
  return {
20372
20443
  getState: () => cloneState(state),
20373
- getSigner: () => activeSigner,
20444
+ getSigner(source) {
20445
+ if (!activeSigner) return null;
20446
+ return createConfirmedSigner(activeSigner, options.confirmRequest, source);
20447
+ },
20374
20448
  getMethod: () => state.method,
20375
20449
  getPubkey: () => state.pubkey,
20376
20450
  subscribe(listener) {
@@ -20396,7 +20470,7 @@ function createPajaSignerController(options) {
20396
20470
  const signer = readNip07Signer();
20397
20471
  if (!signer) throw new Error("NIP-07 signer not found");
20398
20472
  const pubkey2 = await signer.getPublicKey();
20399
- activeSigner = createConfirmedSigner(createNip07Adapter(signer), options.confirmRequest);
20473
+ activeSigner = createNip07Adapter(signer);
20400
20474
  update({ method: "nip07", status: "connected", pubkey: pubkey2, relay: null, error: null });
20401
20475
  } catch (error) {
20402
20476
  activeSigner = null;
@@ -20426,7 +20500,7 @@ function createPajaSignerController(options) {
20426
20500
  });
20427
20501
  const pubkey2 = await client.connect();
20428
20502
  activeClient = client;
20429
- activeSigner = createConfirmedSigner(client.getSigner(), options.confirmRequest);
20503
+ activeSigner = client.getSigner();
20430
20504
  update({ method: "nip46", status: "connected", pubkey: pubkey2, relay: parsed.relay, error: null });
20431
20505
  } catch (error) {
20432
20506
  closeClient();
@@ -20437,6 +20511,171 @@ function createPajaSignerController(options) {
20437
20511
  };
20438
20512
  }
20439
20513
 
20514
+ // src/browser-signer-consent.ts
20515
+ var PAJA_SIGNER_CONSENT_STORAGE_KEY = "kehto:paja:signer-consent:v1";
20516
+ var PAJA_SIGNER_CONSENT_VERSION = 1;
20517
+ var MAX_CONSENT_SUBJECTS = 128;
20518
+ var MAX_KINDS_PER_SUBJECT = 128;
20519
+ var MAX_IDENTITY_FIELD_LENGTH = 512;
20520
+ function createPajaSignerConsentStore(storage = readPajaSignerConsentStorage()) {
20521
+ let grants = readConsentDocument(storage).grants;
20522
+ const persist = () => {
20523
+ if (!storage) return;
20524
+ const document2 = {
20525
+ version: PAJA_SIGNER_CONSENT_VERSION,
20526
+ grants
20527
+ };
20528
+ try {
20529
+ storage.setItem(PAJA_SIGNER_CONSENT_STORAGE_KEY, JSON.stringify(document2));
20530
+ } catch {
20531
+ }
20532
+ };
20533
+ const recordFor = (context) => {
20534
+ const subject = normalizeContext(context);
20535
+ if (!subject) return null;
20536
+ return grants.find((grant2) => sameSubject(grant2, subject)) ?? null;
20537
+ };
20538
+ const ensureRecord = (context) => {
20539
+ const subject = normalizeContext(context);
20540
+ if (!subject) return null;
20541
+ const existing = grants.find((grant2) => sameSubject(grant2, subject));
20542
+ if (existing) return existing;
20543
+ if (grants.length >= MAX_CONSENT_SUBJECTS) return null;
20544
+ const created = { ...subject, trusted: false, kinds: [] };
20545
+ grants = [...grants, created];
20546
+ return created;
20547
+ };
20548
+ return {
20549
+ match(context, kind) {
20550
+ const normalizedKind = normalizeKind(kind);
20551
+ if (normalizedKind === null) return null;
20552
+ const record = recordFor(context);
20553
+ if (!record) return null;
20554
+ if (record.trusted) return "napplet";
20555
+ return record.kinds.includes(normalizedKind) ? "kind" : null;
20556
+ },
20557
+ rememberKind(context, kind) {
20558
+ const normalizedKind = normalizeKind(kind);
20559
+ if (normalizedKind === null) return false;
20560
+ const record = ensureRecord(context);
20561
+ if (!record) return false;
20562
+ if (record.trusted || record.kinds.includes(normalizedKind)) return true;
20563
+ if (record.kinds.length >= MAX_KINDS_PER_SUBJECT) return false;
20564
+ record.kinds = [...record.kinds, normalizedKind].sort((left, right) => left - right);
20565
+ persist();
20566
+ return true;
20567
+ },
20568
+ trustNapplet(context) {
20569
+ const record = ensureRecord(context);
20570
+ if (!record) return false;
20571
+ if (!record.trusted || record.kinds.length > 0) {
20572
+ record.trusted = true;
20573
+ record.kinds = [];
20574
+ persist();
20575
+ }
20576
+ return true;
20577
+ },
20578
+ clear() {
20579
+ const previous = grants;
20580
+ grants = [];
20581
+ if (!storage) return true;
20582
+ try {
20583
+ storage.removeItem(PAJA_SIGNER_CONSENT_STORAGE_KEY);
20584
+ return true;
20585
+ } catch {
20586
+ try {
20587
+ const empty = {
20588
+ version: PAJA_SIGNER_CONSENT_VERSION,
20589
+ grants: []
20590
+ };
20591
+ storage.setItem(PAJA_SIGNER_CONSENT_STORAGE_KEY, JSON.stringify(empty));
20592
+ return true;
20593
+ } catch {
20594
+ grants = previous;
20595
+ return false;
20596
+ }
20597
+ }
20598
+ },
20599
+ count() {
20600
+ return grants.reduce((total, grant2) => total + (grant2.trusted ? 1 : grant2.kinds.length), 0);
20601
+ }
20602
+ };
20603
+ }
20604
+ function isPajaSignerConsentContext(context) {
20605
+ return normalizeContext(context) !== null;
20606
+ }
20607
+ function readPajaSignerConsentStorage() {
20608
+ if (typeof window === "undefined") return null;
20609
+ try {
20610
+ return window.localStorage;
20611
+ } catch {
20612
+ return null;
20613
+ }
20614
+ }
20615
+ function readConsentDocument(storage) {
20616
+ const empty = () => ({ version: PAJA_SIGNER_CONSENT_VERSION, grants: [] });
20617
+ if (!storage) return empty();
20618
+ try {
20619
+ const raw = storage.getItem(PAJA_SIGNER_CONSENT_STORAGE_KEY);
20620
+ if (!raw) return empty();
20621
+ const parsed = JSON.parse(raw);
20622
+ if (parsed.version !== PAJA_SIGNER_CONSENT_VERSION || !Array.isArray(parsed.grants)) return empty();
20623
+ const grants = [];
20624
+ for (const candidate of parsed.grants.slice(0, MAX_CONSENT_SUBJECTS)) {
20625
+ const record = normalizeRecord(candidate);
20626
+ if (!record || grants.some((existing) => sameSubject(existing, record))) continue;
20627
+ grants.push(record);
20628
+ }
20629
+ return { version: PAJA_SIGNER_CONSENT_VERSION, grants };
20630
+ } catch {
20631
+ return empty();
20632
+ }
20633
+ }
20634
+ function normalizeRecord(value) {
20635
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
20636
+ const candidate = value;
20637
+ const subject = normalizeSubject(
20638
+ candidate.signerPubkey,
20639
+ candidate.dTag,
20640
+ candidate.aggregateHash,
20641
+ candidate.runtimeScope
20642
+ );
20643
+ if (!subject || typeof candidate.trusted !== "boolean" || !Array.isArray(candidate.kinds)) return null;
20644
+ 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);
20645
+ return {
20646
+ ...subject,
20647
+ trusted: candidate.trusted,
20648
+ kinds: candidate.trusted ? [] : kinds
20649
+ };
20650
+ }
20651
+ function normalizeContext(context) {
20652
+ return normalizeSubject(
20653
+ context.signerPubkey,
20654
+ context.napplet.dTag,
20655
+ context.napplet.aggregateHash,
20656
+ context.runtimeScope
20657
+ );
20658
+ }
20659
+ function normalizeSubject(signerPubkey, dTag, aggregateHash, runtimeScope) {
20660
+ if (typeof signerPubkey !== "string" || !/^[0-9a-f]{64}$/i.test(signerPubkey)) return null;
20661
+ if (!validIdentityField(dTag) || !validIdentityField(aggregateHash) || !validIdentityField(runtimeScope)) return null;
20662
+ return {
20663
+ signerPubkey: signerPubkey.toLowerCase(),
20664
+ dTag,
20665
+ aggregateHash,
20666
+ runtimeScope
20667
+ };
20668
+ }
20669
+ function validIdentityField(value) {
20670
+ return typeof value === "string" && value.length > 0 && value.length <= MAX_IDENTITY_FIELD_LENGTH;
20671
+ }
20672
+ function normalizeKind(value) {
20673
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 65535 ? value : null;
20674
+ }
20675
+ function sameSubject(left, right) {
20676
+ return left.signerPubkey === right.signerPubkey && left.dTag === right.dTag && left.aggregateHash === right.aggregateHash && left.runtimeScope === right.runtimeScope;
20677
+ }
20678
+
20440
20679
  // src/browser-host-signer.ts
20441
20680
  function confirmationDetails(...lines) {
20442
20681
  return lines.filter((line) => line !== void 0).join("\n");
@@ -20444,27 +20683,86 @@ function confirmationDetails(...lines) {
20444
20683
  function confirmationCopy(title, summary, approveLabel, ...details) {
20445
20684
  return { title, summary, details: confirmationDetails(...details), approveLabel };
20446
20685
  }
20447
- function createPajaConfirmationController(getState) {
20686
+ function createPajaConfirmationController(getState, options = {}) {
20448
20687
  const dialog = document.getElementById("paja-confirmation-dialog");
20449
20688
  const title = document.getElementById("paja-confirmation-title");
20450
20689
  const summary = document.getElementById("paja-confirmation-summary");
20451
20690
  const details = document.getElementById("paja-confirmation-details");
20452
20691
  const approve = document.getElementById("paja-confirmation-approve");
20453
20692
  const deny = document.getElementById("paja-confirmation-deny");
20693
+ const signerConsent = document.getElementById("paja-signer-consent");
20694
+ const signerConsentOnce = document.getElementById("paja-signer-consent-once");
20695
+ const signerConsentKind = document.getElementById("paja-signer-consent-kind");
20696
+ const signerConsentNapplet = document.getElementById("paja-signer-consent-napplet");
20697
+ const signerConsentKindValue = document.getElementById("paja-signer-consent-kind-value");
20698
+ const signerConsentNappletValue = document.getElementById("paja-signer-consent-napplet-value");
20454
20699
  const ready = dialog instanceof HTMLDialogElement && title instanceof HTMLElement && summary instanceof HTMLElement && details instanceof HTMLElement && approve instanceof HTMLButtonElement && deny instanceof HTMLButtonElement;
20700
+ const signerConsentReady = signerConsent instanceof HTMLFieldSetElement && signerConsentOnce instanceof HTMLInputElement && signerConsentKind instanceof HTMLInputElement && signerConsentNapplet instanceof HTMLInputElement && signerConsentKindValue instanceof HTMLElement && signerConsentNappletValue instanceof HTMLElement;
20701
+ const consentStore = createPajaSignerConsentStore(
20702
+ options.storage === void 0 ? void 0 : options.storage
20703
+ );
20455
20704
  const queue = [];
20456
20705
  let active = null;
20457
20706
  let previousFocus = null;
20458
20707
  let disposed = false;
20708
+ const reportSignerConsentChange = () => {
20709
+ options.onSignerConsentChange?.(consentStore.count());
20710
+ };
20711
+ const configureSignerConsent = (request) => {
20712
+ if (!signerConsentReady) return;
20713
+ signerConsent.hidden = true;
20714
+ signerConsentOnce.checked = true;
20715
+ signerConsentKind.checked = false;
20716
+ signerConsentNapplet.checked = false;
20717
+ const candidate = signerConsentCandidate(request);
20718
+ if (!candidate) return;
20719
+ signerConsent.hidden = false;
20720
+ signerConsentKindValue.textContent = String(candidate.kind);
20721
+ signerConsentNappletValue.textContent = candidate.context.napplet.dTag;
20722
+ };
20723
+ const rememberSignerChoice = (request) => {
20724
+ if (!signerConsentReady) return "once";
20725
+ const candidate = signerConsentCandidate(request);
20726
+ if (!candidate) return "once";
20727
+ if (signerConsentNapplet.checked && consentStore.trustNapplet(candidate.context)) {
20728
+ reportSignerConsentChange();
20729
+ return "trust-napplet";
20730
+ }
20731
+ if (signerConsentKind.checked && consentStore.rememberKind(candidate.context, candidate.kind)) {
20732
+ reportSignerConsentChange();
20733
+ return "remember-kind";
20734
+ }
20735
+ return "once";
20736
+ };
20737
+ const rememberedSignerChoice = (request) => {
20738
+ const candidate = signerConsentCandidate(request);
20739
+ return candidate ? consentStore.match(candidate.context, candidate.kind) : null;
20740
+ };
20459
20741
  const pump = () => {
20460
20742
  if (!ready || disposed || active) return;
20461
- active = queue.shift() ?? null;
20743
+ while (!active && queue.length > 0) {
20744
+ const candidate = queue.shift() ?? null;
20745
+ if (!candidate) break;
20746
+ const remembered = rememberedSignerChoice(candidate.request);
20747
+ if (!remembered) {
20748
+ active = candidate;
20749
+ break;
20750
+ }
20751
+ recordPajaConfirmation(
20752
+ getState(),
20753
+ candidate.request,
20754
+ true,
20755
+ remembered === "kind" ? "remembered-kind" : "remembered-napplet"
20756
+ );
20757
+ candidate.approve();
20758
+ }
20462
20759
  if (!active) return;
20463
20760
  const copy = describeConfirmation(active.request);
20464
20761
  title.textContent = copy.title;
20465
20762
  summary.textContent = copy.summary;
20466
20763
  details.textContent = copy.details;
20467
20764
  approve.textContent = copy.approveLabel;
20765
+ configureSignerConsent(active.request);
20468
20766
  previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
20469
20767
  dialog.showModal();
20470
20768
  deny.focus();
@@ -20474,7 +20772,8 @@ function createPajaConfirmationController(getState) {
20474
20772
  if (!current) return;
20475
20773
  active = null;
20476
20774
  if (ready && dialog.open) dialog.close();
20477
- recordPajaConfirmation(getState(), current.request, allowed2);
20775
+ const approvalMode = allowed2 ? rememberSignerChoice(current.request) : void 0;
20776
+ recordPajaConfirmation(getState(), current.request, allowed2, approvalMode);
20478
20777
  if (allowed2) current.approve();
20479
20778
  else current.deny();
20480
20779
  previousFocus?.focus();
@@ -20528,6 +20827,12 @@ function createPajaConfirmationController(getState) {
20528
20827
  });
20529
20828
  }
20530
20829
  },
20830
+ clearSignerConsent() {
20831
+ const cleared = consentStore.clear();
20832
+ reportSignerConsentChange();
20833
+ return cleared;
20834
+ },
20835
+ getSignerConsentCount: () => consentStore.count(),
20531
20836
  dispose() {
20532
20837
  if (disposed) return;
20533
20838
  disposed = true;
@@ -20544,6 +20849,13 @@ function createPajaConfirmationController(getState) {
20544
20849
  }
20545
20850
  };
20546
20851
  }
20852
+ function signerConsentCandidate(request) {
20853
+ if (request.action !== "sign" || !request.signerContext) return null;
20854
+ if (!isPajaSignerConsentContext(request.signerContext)) return null;
20855
+ const kind = request.event.kind;
20856
+ if (typeof kind !== "number" || !Number.isSafeInteger(kind) || kind < 0 || kind > 65535) return null;
20857
+ return { context: request.signerContext, kind };
20858
+ }
20547
20859
  function describeConfirmation(request) {
20548
20860
  if (request.action === "upload") {
20549
20861
  return confirmationCopy(
@@ -20625,15 +20937,22 @@ function describeConfirmation(request) {
20625
20937
  const event = request.event;
20626
20938
  const kind = typeof event.kind === "number" ? event.kind : "unknown";
20627
20939
  const content = typeof event.content === "string" && event.content.length > 0 ? event.content.slice(0, 240) : "(empty content)";
20940
+ const signerContext = request.signerContext;
20628
20941
  return {
20629
20942
  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}`,
20943
+ 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.",
20944
+ details: confirmationDetails(
20945
+ signerContext ? `Napplet: ${signerContext.napplet.dTag} (${signerContext.windowId})` : void 0,
20946
+ signerContext ? `Identity hash: ${signerContext.napplet.aggregateHash}` : void 0,
20947
+ signerContext?.runtimeScope.startsWith("target-url:") ? `Target URL: ${signerContext.runtimeScope.slice("target-url:".length)}` : void 0,
20948
+ signerContext ? `Signer: ${signerContext.signerPubkey}` : void 0,
20949
+ `Kind: ${kind}`,
20950
+ `Content: ${content}`
20951
+ ),
20633
20952
  approveLabel: request.action === "sign" ? "Sign" : "Publish"
20634
20953
  };
20635
20954
  }
20636
- function recordPajaConfirmation(state, request, allowed2) {
20955
+ function recordPajaConfirmation(state, request, allowed2, signerApprovalMode) {
20637
20956
  if (request.action === "upload") {
20638
20957
  appendPajaMessageLog(state, "paja", {
20639
20958
  type: `paja.upload.${allowed2 ? "confirmed" : "denied"}`,
@@ -20702,9 +21021,16 @@ function recordPajaConfirmation(state, request, allowed2) {
20702
21021
  }
20703
21022
  if (!("event" in request)) return;
20704
21023
  const event = request.event;
21024
+ const signerContext = request.signerContext;
20705
21025
  appendPajaMessageLog(state, "paja", {
20706
21026
  type: `paja.${request.action}.${allowed2 ? "confirmed" : "denied"}`,
20707
- kind: typeof event.kind === "number" ? event.kind : "unknown"
21027
+ kind: typeof event.kind === "number" ? event.kind : "unknown",
21028
+ windowId: signerContext?.windowId,
21029
+ dTag: signerContext?.napplet.dTag,
21030
+ aggregateHash: signerContext?.napplet.aggregateHash,
21031
+ runtimeScope: signerContext?.runtimeScope,
21032
+ signerPubkey: signerContext?.signerPubkey,
21033
+ approval: signerApprovalMode
20708
21034
  });
20709
21035
  }
20710
21036
  function createHostSignerController(getState, refreshState, confirmRequest) {
@@ -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" : "",
@@ -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,
@@ -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,