@kehto/paja 0.16.4 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,7 +59,11 @@ warning, when the target would block the sandboxed frame.
59
59
 
60
60
  The console shows supported interfaces with per-domain injection toggles,
61
61
  runtime ACL controls, signer controls, and a filterable message log with visible
62
- error details. In runtime-pointer mode, ACL controls always display and mutate
62
+ error details. It starts expanded and collapses to the left with the chevron
63
+ button in the top bar; the same button brings the whole panel back. Collapsing
64
+ is presentation-only — the target iframe keeps its identity and the loaded
65
+ napplet keeps running — and Paja remembers the choice per browser origin. In
66
+ runtime-pointer mode, ACL controls always display and mutate
63
67
  the active tab's resolver-verified d-tag and aggregate hash; a grant or revoke
64
68
  rerenders that same identity immediately. Paja auto-connects a browser NIP-07 signer when `window.nostr` is
65
69
  available, can connect to a bunker/NIP-46 URI, and only uses the generated local
@@ -5363,6 +5363,39 @@ function nappletNamespacePrelude(domains) {
5363
5363
  });
5364
5364
  }
5365
5365
 
5366
+ // src/browser-console-panel.ts
5367
+ var PAJA_CONSOLE_PANEL_STORAGE_KEY = "kehto:paja:console:v1";
5368
+ var PAJA_CONSOLE_TOGGLE_ID = "paja-console-toggle";
5369
+ var PAJA_CONSOLE_COLLAPSE_LABEL = "Collapse the Paja development console";
5370
+ var PAJA_CONSOLE_EXPAND_LABEL = "Expand the Paja development console";
5371
+ function installPajaConsolePanel() {
5372
+ const root = document.documentElement;
5373
+ const button2 = document.getElementById(PAJA_CONSOLE_TOGGLE_ID);
5374
+ let collapsed = false;
5375
+ try {
5376
+ collapsed = localStorage.getItem(PAJA_CONSOLE_PANEL_STORAGE_KEY) === "collapsed";
5377
+ } catch {
5378
+ }
5379
+ const apply = () => {
5380
+ root.dataset.pajaConsole = collapsed ? "collapsed" : "expanded";
5381
+ const label = collapsed ? PAJA_CONSOLE_EXPAND_LABEL : PAJA_CONSOLE_COLLAPSE_LABEL;
5382
+ button2?.setAttribute("aria-expanded", String(!collapsed));
5383
+ button2?.setAttribute("aria-label", label);
5384
+ button2?.setAttribute("title", label);
5385
+ };
5386
+ const toggle = () => {
5387
+ collapsed = !collapsed;
5388
+ apply();
5389
+ try {
5390
+ localStorage.setItem(PAJA_CONSOLE_PANEL_STORAGE_KEY, root.dataset.pajaConsole);
5391
+ } catch {
5392
+ }
5393
+ };
5394
+ apply();
5395
+ button2?.addEventListener("click", toggle);
5396
+ return () => button2?.removeEventListener("click", toggle);
5397
+ }
5398
+
5366
5399
  // ../../node_modules/.pnpm/@noble+hashes@2.0.1/node_modules/@noble/hashes/utils.js
5367
5400
  function isBytes(a) {
5368
5401
  return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
@@ -15255,15 +15288,35 @@ var KIND_GIFT_WRAP_EPHEMERAL = 21059;
15255
15288
  var KIND_GIFT_WRAP_REGULAR = 1059;
15256
15289
  var KIND_ANNOUNCE_SERVER = 11316;
15257
15290
  var KIND_ANNOUNCE_TOOLS = 11317;
15291
+ var SUPPORT_ENCRYPTION = "support_encryption";
15292
+ var SUPPORT_ENCRYPTION_EPHEMERAL = "support_encryption_ephemeral";
15293
+ var SUPPORT_OVERSIZED_TRANSFER = "support_oversized_transfer";
15294
+ var SUPPORT_OPEN_STREAM = "support_open_stream";
15258
15295
  var DEFAULT_TIMEOUT_MS = 3e4;
15259
15296
  var DEFAULT_DISCOVER_TIMEOUT_MS = 6e3;
15260
15297
  var MCP_PROTOCOL_VERSION = "2025-11-25";
15261
15298
  var SEEN_WRAP_LIMIT = 512;
15299
+ var DIGEST_PREFIX = "sha256:";
15300
+ var MAX_TRANSFER_CHUNKS = 1e4;
15301
+ var MAX_TRANSFER_BYTES = 100 * 1024 * 1024;
15302
+ var MAX_ACTIVE_TRANSFERS = 8;
15303
+ var MAX_TRANSFER_LIFETIME_MS = 12e4;
15304
+ var COMPLETION_MODE_RENDER = "render";
15305
+ var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e4;
15306
+ var DEFAULT_STREAM_PROBE_TIMEOUT_MS = 1e4;
15307
+ var MAX_STREAM_BUFFERED_CHUNKS = 256;
15308
+ var MAX_STREAM_BUFFERED_CODE_UNITS = 8 * 1024 * 1024;
15309
+ var MAX_PING_NONCE_BYTES = 64;
15262
15310
  var correlationCounter = 0;
15263
15311
  function nextCorrelationId() {
15264
15312
  correlationCounter += 1;
15265
15313
  return `cvm-${correlationCounter}-${getPublicKey(generateSecretKey()).slice(0, 8)}`;
15266
15314
  }
15315
+ var streamNonceCounter = 0;
15316
+ function nextStreamNonce() {
15317
+ streamNonceCounter += 1;
15318
+ return `cvm-ping-${streamNonceCounter}-${getPublicKey(generateSecretKey()).slice(0, 8)}`;
15319
+ }
15267
15320
  function randomizedPastTimestamp() {
15268
15321
  const jitter = Math.floor(Math.random() * 172800);
15269
15322
  return Math.floor(Date.now() / 1e3) - jitter;
@@ -15271,6 +15324,19 @@ function randomizedPastTimestamp() {
15271
15324
  function tagValue(tags, name) {
15272
15325
  return tags.find((tag) => tag[0] === name)?.[1];
15273
15326
  }
15327
+ function progressTokenKey(token) {
15328
+ if (typeof token === "string") return token.length > 0 ? `s:${token}` : null;
15329
+ if (typeof token === "number" && Number.isFinite(token)) return `n:${token}`;
15330
+ return null;
15331
+ }
15332
+ function isBoundedInteger(value, min, max) {
15333
+ return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
15334
+ }
15335
+ async function sha256Hex(text) {
15336
+ const bytes = new TextEncoder().encode(text);
15337
+ const buf = await crypto.subtle.digest("SHA-256", bytes);
15338
+ return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
15339
+ }
15274
15340
  function simplePoolAdapter(sp) {
15275
15341
  return {
15276
15342
  subscribe(relays, filter, params) {
@@ -15295,11 +15361,14 @@ function createNostrCvmTransport(options = {}) {
15295
15361
  const clientSecretKey = options.clientSecretKey ?? generateSecretKey();
15296
15362
  const clientPubkey = getPublicKey(clientSecretKey);
15297
15363
  const clientInfo = options.clientInfo ?? { name: "kehto-cvm", version: "1.0.0" };
15364
+ const streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
15365
+ const streamProbeTimeoutMs = options.streamProbeTimeoutMs ?? DEFAULT_STREAM_PROBE_TIMEOUT_MS;
15298
15366
  const sessions = /* @__PURE__ */ new Map();
15299
15367
  const pending = /* @__PURE__ */ new Map();
15300
15368
  const eventHandlers = /* @__PURE__ */ new Set();
15301
15369
  const relayRefcount = /* @__PURE__ */ new Map();
15302
15370
  const seenWraps = /* @__PURE__ */ new Set();
15371
+ const issuedTokens = /* @__PURE__ */ new Map();
15303
15372
  let inbound = null;
15304
15373
  let subscribedRelays = "";
15305
15374
  function resolveRelays(server) {
@@ -15361,23 +15430,379 @@ function createNostrCvmTransport(options = {}) {
15361
15430
  } catch {
15362
15431
  return;
15363
15432
  }
15433
+ if (mcp.method === "notifications/progress") {
15434
+ const params = mcp.params ?? {};
15435
+ const cvm = params.cvm;
15436
+ if (cvm && typeof cvm.type === "string") {
15437
+ if (cvm.type === "oversized-transfer") {
15438
+ void handleOversizedFrame(serverPubkey, params, cvm);
15439
+ return;
15440
+ }
15441
+ if (cvm.type === "open-stream") {
15442
+ const entry = admitFrame(serverPubkey, params.progressToken);
15443
+ if (!entry) return;
15444
+ handleOpenStreamFrame(serverPubkey, entry, params, cvm, mcp);
15445
+ return;
15446
+ }
15447
+ }
15448
+ }
15449
+ routeMessage(serverPubkey, mcp);
15450
+ }
15451
+ async function publishStreamFrame(entry, stream, frame) {
15452
+ const session = sessions.get(entry.serverPubkey);
15453
+ if (!session) return;
15454
+ stream.localProgress += 1;
15455
+ await publishMcp(
15456
+ { pubkey: entry.serverPubkey },
15457
+ session.relays,
15458
+ {
15459
+ jsonrpc: "2.0",
15460
+ method: "notifications/progress",
15461
+ params: {
15462
+ progressToken: stream.token,
15463
+ progress: stream.localProgress,
15464
+ cvm: { type: "open-stream", ...frame }
15465
+ }
15466
+ }
15467
+ );
15468
+ }
15469
+ function admitFrame(serverPubkey, token) {
15470
+ const tokenKey = progressTokenKey(token);
15471
+ if (!tokenKey) return null;
15472
+ const correlationId = issuedTokens.get(`${serverPubkey}:${tokenKey}`);
15473
+ if (!correlationId) return null;
15474
+ const entry = pending.get(correlationId);
15475
+ if (!entry || entry.serverPubkey !== serverPubkey) return null;
15476
+ return entry;
15477
+ }
15478
+ function releaseTransfer(entry) {
15479
+ if (!entry.oversized) return;
15480
+ clearTimeout(entry.oversized.expiryTimer);
15481
+ entry.oversized = void 0;
15482
+ }
15483
+ function releaseStream(entry) {
15484
+ if (!entry.stream) return;
15485
+ clearTimeout(entry.stream.idleTimer);
15486
+ if (entry.stream.probeTimer) clearTimeout(entry.stream.probeTimer);
15487
+ entry.stream = void 0;
15488
+ }
15489
+ function endStream(entry) {
15490
+ releaseStream(entry);
15491
+ entry.streamEnded = true;
15492
+ }
15493
+ function failStream(entry, reason) {
15494
+ const stream = entry.stream;
15495
+ if (!stream || entry.streamEnded) return;
15496
+ endStream(entry);
15497
+ void publishStreamFrame(entry, stream, { frameType: "abort", reason }).catch(() => {
15498
+ });
15499
+ }
15500
+ function resetStreamIdleTimer(entry, stream) {
15501
+ clearTimeout(stream.idleTimer);
15502
+ stream.idleTimer = setTimeout(() => onStreamIdle(entry), streamIdleTimeoutMs);
15503
+ }
15504
+ function onStreamIdle(entry) {
15505
+ const stream = entry.stream;
15506
+ if (!stream || entry.streamEnded) return;
15507
+ if (stream.pendingNonce !== null) return;
15508
+ const session = sessions.get(entry.serverPubkey);
15509
+ if (!session) {
15510
+ failStream(entry, "session closed");
15511
+ return;
15512
+ }
15513
+ stream.pendingNonce = nextStreamNonce();
15514
+ stream.probeTimer = setTimeout(() => failStream(entry, "probe timeout"), streamProbeTimeoutMs);
15515
+ void publishStreamFrame(entry, stream, { frameType: "ping", nonce: stream.pendingNonce }).catch(() => failStream(entry, "ping publication failed"));
15516
+ }
15517
+ function releaseFrameState(entry) {
15518
+ releaseTransfer(entry);
15519
+ releaseStream(entry);
15520
+ if (entry.tokenKey && issuedTokens.get(entry.tokenKey) === entry.correlationId) {
15521
+ issuedTokens.delete(entry.tokenKey);
15522
+ }
15523
+ entry.tokenKey = void 0;
15524
+ }
15525
+ function settlePending(correlationId) {
15526
+ const entry = pending.get(correlationId);
15527
+ if (!entry) return null;
15528
+ clearTimeout(entry.timer);
15529
+ pending.delete(correlationId);
15530
+ releaseFrameState(entry);
15531
+ return entry;
15532
+ }
15533
+ function countActiveTransfers() {
15534
+ let active = 0;
15535
+ for (const entry of pending.values()) {
15536
+ if (entry.oversized) active += 1;
15537
+ }
15538
+ return active;
15539
+ }
15540
+ async function handleOversizedFrame(serverPubkey, params, cvm) {
15541
+ const entry = admitFrame(serverPubkey, params.progressToken);
15542
+ if (!entry) return;
15543
+ const progress = Number(params.progress ?? NaN);
15544
+ if (!Number.isSafeInteger(progress) || progress < 0) return;
15545
+ if (cvm.frameType === "start") {
15546
+ const digest = typeof cvm.digest === "string" ? cvm.digest : "";
15547
+ const { totalBytes, totalChunks } = cvm;
15548
+ if (cvm.completionMode !== COMPLETION_MODE_RENDER) return;
15549
+ if (digest === "") return;
15550
+ if (!isBoundedInteger(totalChunks, 1, MAX_TRANSFER_CHUNKS)) return;
15551
+ if (!isBoundedInteger(totalBytes, 1, MAX_TRANSFER_BYTES)) return;
15552
+ if (progress > Number.MAX_SAFE_INTEGER - totalChunks - 1) return;
15553
+ if (!entry.oversized && countActiveTransfers() >= MAX_ACTIVE_TRANSFERS) return;
15554
+ releaseTransfer(entry);
15555
+ entry.oversized = {
15556
+ digest,
15557
+ totalBytes,
15558
+ totalChunks,
15559
+ startProgress: progress,
15560
+ acceptProgress: null,
15561
+ chunks: /* @__PURE__ */ new Map(),
15562
+ codeUnitsReceived: 0,
15563
+ // Hard expiry: progress frames extend the request deadline but never
15564
+ // how long buffers may be held.
15565
+ expiryTimer: setTimeout(() => releaseTransfer(entry), MAX_TRANSFER_LIFETIME_MS)
15566
+ };
15567
+ refreshPendingTimeout(entry);
15568
+ return;
15569
+ }
15570
+ const transfer = entry.oversized;
15571
+ if (!transfer) return;
15572
+ if (cvm.frameType === "accept") {
15573
+ if (progress !== transfer.startProgress + 1) {
15574
+ releaseTransfer(entry);
15575
+ return;
15576
+ }
15577
+ transfer.acceptProgress = progress;
15578
+ refreshPendingTimeout(entry);
15579
+ return;
15580
+ }
15581
+ if (cvm.frameType === "chunk") {
15582
+ if (typeof cvm.data !== "string" || progress <= transfer.startProgress) return;
15583
+ if (transfer.chunks.has(progress)) return;
15584
+ if (transfer.chunks.size >= transfer.totalChunks) {
15585
+ releaseTransfer(entry);
15586
+ return;
15587
+ }
15588
+ transfer.codeUnitsReceived += cvm.data.length;
15589
+ if (transfer.codeUnitsReceived > MAX_TRANSFER_BYTES) {
15590
+ releaseTransfer(entry);
15591
+ return;
15592
+ }
15593
+ transfer.chunks.set(progress, cvm.data);
15594
+ refreshPendingTimeout(entry);
15595
+ return;
15596
+ }
15597
+ if (cvm.frameType === "abort") {
15598
+ releaseTransfer(entry);
15599
+ return;
15600
+ }
15601
+ if (cvm.frameType === "end") {
15602
+ const message = await assembleTransfer(transfer, progress);
15603
+ releaseTransfer(entry);
15604
+ if (message) routeMessage(serverPubkey, message);
15605
+ return;
15606
+ }
15607
+ }
15608
+ async function assembleTransfer(transfer, endProgress) {
15609
+ const { startProgress, acceptProgress, totalChunks } = transfer;
15610
+ const hasCompleteRange = (first) => {
15611
+ for (let i4 = 0; i4 < totalChunks; i4++) {
15612
+ if (!transfer.chunks.has(first + i4)) return false;
15613
+ }
15614
+ return true;
15615
+ };
15616
+ const directStart = startProgress + 1;
15617
+ const acceptGatedStart = startProgress + 2;
15618
+ const firstProgress = hasCompleteRange(directStart) && endProgress === directStart + totalChunks ? directStart : acceptProgress === startProgress + 1 && hasCompleteRange(acceptGatedStart) && endProgress === acceptGatedStart + totalChunks ? acceptGatedStart : null;
15619
+ if (firstProgress === null) return null;
15620
+ let payload = "";
15621
+ for (let i4 = 0; i4 < totalChunks; i4++) {
15622
+ payload += transfer.chunks.get(firstProgress + i4);
15623
+ }
15624
+ if (new TextEncoder().encode(payload).byteLength !== transfer.totalBytes) return null;
15625
+ const expected = transfer.digest.startsWith(DIGEST_PREFIX) ? transfer.digest.slice(DIGEST_PREFIX.length) : transfer.digest;
15626
+ if (await sha256Hex(payload) !== expected) return null;
15627
+ try {
15628
+ return JSON.parse(payload);
15629
+ } catch {
15630
+ return null;
15631
+ }
15632
+ }
15633
+ function handleOpenStreamFrame(serverPubkey, entry, params, cvm, mcp) {
15634
+ const progress = Number(params.progress ?? NaN);
15635
+ if (!Number.isSafeInteger(progress) || progress < 0) return;
15636
+ if (entry.streamEnded) return;
15637
+ const stream = entry.stream;
15638
+ if (!stream) {
15639
+ if (cvm.frameType !== "start") return;
15640
+ entry.stream = {
15641
+ token: params.progressToken,
15642
+ lastProgress: progress,
15643
+ localProgress: 0,
15644
+ nextChunkIndex: 0,
15645
+ outOfOrder: /* @__PURE__ */ new Map(),
15646
+ outOfOrderCodeUnits: 0,
15647
+ pendingNonce: null,
15648
+ idleTimer: setTimeout(() => onStreamIdle(entry), streamIdleTimeoutMs),
15649
+ probeTimer: null
15650
+ };
15651
+ refreshPendingTimeout(entry);
15652
+ routeEvent(serverPubkey, mcp);
15653
+ return;
15654
+ }
15655
+ const admitOrderedActivity = () => {
15656
+ if (progress <= stream.lastProgress) {
15657
+ failStream(entry, "non-monotonic progress");
15658
+ return false;
15659
+ }
15660
+ stream.lastProgress = progress;
15661
+ resetStreamIdleTimer(entry, stream);
15662
+ refreshPendingTimeout(entry);
15663
+ return true;
15664
+ };
15665
+ switch (cvm.frameType) {
15666
+ case "start":
15667
+ failStream(entry, "duplicate start");
15668
+ return;
15669
+ case "accept":
15670
+ if (!admitOrderedActivity()) return;
15671
+ routeEvent(serverPubkey, mcp);
15672
+ return;
15673
+ case "chunk": {
15674
+ if (typeof cvm.data !== "string" || !isBoundedInteger(cvm.chunkIndex, 0, Number.MAX_SAFE_INTEGER)) {
15675
+ failStream(entry, "malformed chunk");
15676
+ return;
15677
+ }
15678
+ const index = cvm.chunkIndex;
15679
+ if (index < stream.nextChunkIndex) return;
15680
+ if (index > stream.nextChunkIndex) {
15681
+ const existing = stream.outOfOrder.get(index);
15682
+ if (existing) {
15683
+ if (existing.progress !== progress || JSON.stringify(existing.message) !== JSON.stringify(mcp)) {
15684
+ failStream(entry, "conflicting duplicate chunk");
15685
+ }
15686
+ return;
15687
+ }
15688
+ if (progress <= stream.lastProgress || stream.outOfOrder.size >= MAX_STREAM_BUFFERED_CHUNKS || stream.outOfOrderCodeUnits + cvm.data.length > MAX_STREAM_BUFFERED_CODE_UNITS) {
15689
+ failStream(entry, progress <= stream.lastProgress ? "non-monotonic progress" : "gap buffer exhausted");
15690
+ return;
15691
+ }
15692
+ stream.outOfOrder.set(index, { message: mcp, progress, codeUnits: cvm.data.length });
15693
+ stream.outOfOrderCodeUnits += cvm.data.length;
15694
+ resetStreamIdleTimer(entry, stream);
15695
+ refreshPendingTimeout(entry);
15696
+ return;
15697
+ }
15698
+ const ready = [
15699
+ { message: mcp, progress, codeUnits: 0 }
15700
+ ];
15701
+ let nextIndex = index + 1;
15702
+ while (stream.outOfOrder.has(nextIndex)) {
15703
+ ready.push(stream.outOfOrder.get(nextIndex));
15704
+ nextIndex += 1;
15705
+ }
15706
+ let previousProgress = stream.lastProgress;
15707
+ for (const item of ready) {
15708
+ if (item.progress <= previousProgress) {
15709
+ failStream(entry, "non-monotonic chunk order");
15710
+ return;
15711
+ }
15712
+ previousProgress = item.progress;
15713
+ }
15714
+ for (let bufferedIndex = index + 1; bufferedIndex < nextIndex; bufferedIndex++) {
15715
+ const buffered = stream.outOfOrder.get(bufferedIndex);
15716
+ stream.outOfOrder.delete(bufferedIndex);
15717
+ stream.outOfOrderCodeUnits -= buffered.codeUnits;
15718
+ }
15719
+ for (const item of ready) {
15720
+ routeEvent(serverPubkey, item.message);
15721
+ stream.nextChunkIndex += 1;
15722
+ }
15723
+ stream.lastProgress = previousProgress;
15724
+ resetStreamIdleTimer(entry, stream);
15725
+ refreshPendingTimeout(entry);
15726
+ return;
15727
+ }
15728
+ case "ping": {
15729
+ if (typeof cvm.nonce !== "string") return;
15730
+ if (new TextEncoder().encode(cvm.nonce).byteLength > MAX_PING_NONCE_BYTES) return;
15731
+ if (!admitOrderedActivity()) return;
15732
+ void publishStreamFrame(entry, stream, { frameType: "pong", nonce: cvm.nonce }).catch(() => {
15733
+ failStream(entry, "pong publication failed");
15734
+ });
15735
+ return;
15736
+ }
15737
+ case "pong": {
15738
+ if (stream.pendingNonce === null || cvm.nonce !== stream.pendingNonce) return;
15739
+ if (!admitOrderedActivity()) return;
15740
+ stream.pendingNonce = null;
15741
+ if (stream.probeTimer) clearTimeout(stream.probeTimer);
15742
+ stream.probeTimer = null;
15743
+ return;
15744
+ }
15745
+ case "close": {
15746
+ if (cvm.lastChunkIndex !== void 0) {
15747
+ const bound = cvm.lastChunkIndex;
15748
+ const complete = isBoundedInteger(bound, 0, Number.MAX_SAFE_INTEGER) && stream.nextChunkIndex === bound + 1 && stream.outOfOrder.size === 0;
15749
+ if (!complete) {
15750
+ failStream(entry, "close with unresolved chunk gaps");
15751
+ return;
15752
+ }
15753
+ }
15754
+ if (!admitOrderedActivity()) return;
15755
+ routeEvent(serverPubkey, mcp);
15756
+ endStream(entry);
15757
+ return;
15758
+ }
15759
+ case "abort":
15760
+ if (!admitOrderedActivity()) return;
15761
+ routeEvent(serverPubkey, mcp);
15762
+ endStream(entry);
15763
+ return;
15764
+ default:
15765
+ return;
15766
+ }
15767
+ }
15768
+ function refreshPendingTimeout(entry) {
15769
+ clearTimeout(entry.timer);
15770
+ entry.timer = setTimeout(() => {
15771
+ const settled = settlePending(entry.correlationId);
15772
+ settled?.reject(new Error("relay timeout"));
15773
+ }, entry.timeoutMs);
15774
+ }
15775
+ function routeMessage(serverPubkey, mcp) {
15364
15776
  const id = mcp.id;
15365
15777
  if (id != null && pending.has(String(id))) {
15366
15778
  const entry = pending.get(String(id));
15367
15779
  if (entry.serverPubkey !== serverPubkey) return;
15368
- pending.delete(String(id));
15369
- clearTimeout(entry.timer);
15370
- entry.resolve({ ...mcp, id: entry.originalId });
15780
+ const settled = settlePending(String(id));
15781
+ settled?.resolve({ ...mcp, id: entry.originalId });
15371
15782
  return;
15372
15783
  }
15784
+ routeEvent(serverPubkey, mcp);
15785
+ }
15786
+ function routeEvent(serverPubkey, mcp) {
15373
15787
  if (mcp.method !== void 0 && sessions.has(serverPubkey)) {
15374
15788
  const server = { pubkey: serverPubkey };
15375
15789
  for (const handler of eventHandlers) handler(server, mcp);
15376
15790
  }
15377
15791
  }
15378
15792
  async function publishMcp(server, relays, message) {
15793
+ const capabilityTags = [
15794
+ ...encrypt22 ? [[SUPPORT_ENCRYPTION]] : [],
15795
+ ...encrypt22 && wrapKind === KIND_GIFT_WRAP_EPHEMERAL ? [[SUPPORT_ENCRYPTION_EPHEMERAL]] : [],
15796
+ [SUPPORT_OVERSIZED_TRANSFER],
15797
+ [SUPPORT_OPEN_STREAM]
15798
+ ];
15379
15799
  const inner = finalizeEvent(
15380
- { kind: KIND_CVM, created_at: Math.floor(Date.now() / 1e3), tags: [["p", server.pubkey]], content: JSON.stringify(message) },
15800
+ {
15801
+ kind: KIND_CVM,
15802
+ created_at: Math.floor(Date.now() / 1e3),
15803
+ tags: [["p", server.pubkey], ...capabilityTags],
15804
+ content: JSON.stringify(message)
15805
+ },
15381
15806
  clientSecretKey
15382
15807
  );
15383
15808
  if (!encrypt22) {
@@ -15401,17 +15826,52 @@ function createNostrCvmTransport(options = {}) {
15401
15826
  function sendCorrelated(server, relays, message, timeout) {
15402
15827
  const correlationId = nextCorrelationId();
15403
15828
  const originalId = message.id;
15404
- const outgoing = { ...message, id: correlationId };
15829
+ let outgoing = { ...message, id: correlationId };
15830
+ let issuedToken;
15831
+ const params = message.params && typeof message.params === "object" ? message.params : void 0;
15832
+ const meta = params?._meta && typeof params._meta === "object" ? params._meta : void 0;
15833
+ const explicit = meta?.progressToken;
15834
+ if (message.method === "tools/call") {
15835
+ issuedToken = typeof explicit === "string" || typeof explicit === "number" ? explicit : correlationId;
15836
+ outgoing = {
15837
+ ...outgoing,
15838
+ params: {
15839
+ ...params,
15840
+ _meta: {
15841
+ ...meta,
15842
+ progressToken: issuedToken
15843
+ }
15844
+ }
15845
+ };
15846
+ } else if (typeof explicit === "string" || typeof explicit === "number") {
15847
+ issuedToken = explicit;
15848
+ }
15849
+ const tokenKey = progressTokenKey(issuedToken);
15850
+ const issuedKey = tokenKey ? `${server.pubkey}:${tokenKey}` : null;
15851
+ if (issuedKey && issuedTokens.has(issuedKey)) {
15852
+ return Promise.reject(new Error("duplicate progress token"));
15853
+ }
15405
15854
  return new Promise((resolve, reject2) => {
15406
- const timer = setTimeout(() => {
15407
- pending.delete(correlationId);
15408
- reject2(new Error("relay timeout"));
15409
- }, timeout);
15410
- pending.set(correlationId, { resolve, reject: reject2, timer, originalId, serverPubkey: server.pubkey });
15855
+ const entry = {
15856
+ resolve,
15857
+ reject: reject2,
15858
+ timer: setTimeout(() => {
15859
+ const settled = settlePending(correlationId);
15860
+ settled?.reject(new Error("relay timeout"));
15861
+ }, timeout),
15862
+ timeoutMs: timeout,
15863
+ originalId,
15864
+ serverPubkey: server.pubkey,
15865
+ correlationId
15866
+ };
15867
+ if (issuedKey) {
15868
+ entry.tokenKey = issuedKey;
15869
+ issuedTokens.set(issuedKey, correlationId);
15870
+ }
15871
+ pending.set(correlationId, entry);
15411
15872
  void publishMcp(server, relays, outgoing).catch((err2) => {
15412
- pending.delete(correlationId);
15413
- clearTimeout(timer);
15414
- reject2(err2 instanceof Error ? err2 : new Error("publish failed"));
15873
+ const settled = settlePending(correlationId);
15874
+ settled?.reject(err2 instanceof Error ? err2 : new Error("publish failed"));
15415
15875
  });
15416
15876
  });
15417
15877
  }
@@ -15509,6 +15969,10 @@ function createNostrCvmTransport(options = {}) {
15509
15969
  if (!session) return;
15510
15970
  sessions.delete(server.pubkey);
15511
15971
  releaseRelays(session.relays);
15972
+ for (const [correlationId, entry] of pending) {
15973
+ if (entry.serverPubkey !== server.pubkey) continue;
15974
+ settlePending(correlationId)?.reject(new Error("server closed"));
15975
+ }
15512
15976
  },
15513
15977
  onEvent(handler) {
15514
15978
  eventHandlers.add(handler);
@@ -15523,9 +15987,12 @@ function createNostrCvmTransport(options = {}) {
15523
15987
  inbound = null;
15524
15988
  for (const entry of pending.values()) {
15525
15989
  clearTimeout(entry.timer);
15990
+ releaseTransfer(entry);
15991
+ releaseStream(entry);
15526
15992
  entry.reject(new Error("transport disposed"));
15527
15993
  }
15528
15994
  pending.clear();
15995
+ issuedTokens.clear();
15529
15996
  sessions.clear();
15530
15997
  relayRefcount.clear();
15531
15998
  eventHandlers.clear();
@@ -16430,14 +16897,14 @@ var CacheStorageNappletArtifactCache = class {
16430
16897
  this.softCeilingBytes = options.softCeilingBytes ?? DEFAULT_SOFT_CEILING_BYTES;
16431
16898
  this.hardCeilingBytes = options.hardCeilingBytes ?? DEFAULT_HARD_CEILING_BYTES;
16432
16899
  }
16433
- async readBlob(sha256Hex) {
16900
+ async readBlob(sha256Hex2) {
16434
16901
  try {
16435
16902
  const cache2 = await this.open();
16436
- const response = await cache2.match(this.blobUrl(sha256Hex));
16903
+ const response = await cache2.match(this.blobUrl(sha256Hex2));
16437
16904
  if (!response) return void 0;
16438
16905
  const bytes = await responseBytes(response);
16439
16906
  const index = await this.readIndex(cache2);
16440
- const entry = index.blobs[sha256Hex];
16907
+ const entry = index.blobs[sha256Hex2];
16441
16908
  if (entry) {
16442
16909
  entry.lastAccessed = this.now();
16443
16910
  index.updatedAt = entry.lastAccessed;
@@ -16445,20 +16912,20 @@ var CacheStorageNappletArtifactCache = class {
16445
16912
  }
16446
16913
  return bytes;
16447
16914
  } catch (cause) {
16448
- this.diagnostic({ type: "cache-read-failed", message: `failed to read blob ${sha256Hex}`, cause });
16915
+ this.diagnostic({ type: "cache-read-failed", message: `failed to read blob ${sha256Hex2}`, cause });
16449
16916
  return void 0;
16450
16917
  }
16451
16918
  }
16452
- async deleteBlob(sha256Hex) {
16919
+ async deleteBlob(sha256Hex2) {
16453
16920
  try {
16454
16921
  const cache2 = await this.open();
16455
- await cache2.delete(this.blobUrl(sha256Hex));
16922
+ await cache2.delete(this.blobUrl(sha256Hex2));
16456
16923
  const index = await this.readIndex(cache2);
16457
- delete index.blobs[sha256Hex];
16924
+ delete index.blobs[sha256Hex2];
16458
16925
  index.updatedAt = this.now();
16459
16926
  await this.writeIndex(cache2, index);
16460
16927
  } catch (cause) {
16461
- this.diagnostic({ type: "cache-delete-failed", message: `failed to delete blob ${sha256Hex}`, cause });
16928
+ this.diagnostic({ type: "cache-delete-failed", message: `failed to delete blob ${sha256Hex2}`, cause });
16462
16929
  }
16463
16930
  }
16464
16931
  async writeVerifiedResolution(input) {
@@ -16839,22 +17306,22 @@ function verifyManifestSignature(event) {
16839
17306
  return false;
16840
17307
  }
16841
17308
  }
16842
- function verifyBlobHash(bytes, sha256Hex) {
16843
- return bytesToHex2(sha256(bytes)) === sha256Hex;
17309
+ function verifyBlobHash(bytes, sha256Hex2) {
17310
+ return bytesToHex2(sha256(bytes)) === sha256Hex2;
16844
17311
  }
16845
17312
  function stripTrailingSlash(url) {
16846
17313
  return url.endsWith("/") ? url.slice(0, -1) : url;
16847
17314
  }
16848
- async function fetchBlob(servers, sha256Hex, fetchBytes) {
17315
+ async function fetchBlob(servers, sha256Hex2, fetchBytes) {
16849
17316
  for (const server of servers) {
16850
- const url = `${stripTrailingSlash(server)}/${sha256Hex}`;
17317
+ const url = `${stripTrailingSlash(server)}/${sha256Hex2}`;
16851
17318
  try {
16852
17319
  const bytes = await fetchBytes(url);
16853
- if (verifyBlobHash(bytes, sha256Hex)) return bytes;
17320
+ if (verifyBlobHash(bytes, sha256Hex2)) return bytes;
16854
17321
  } catch {
16855
17322
  }
16856
17323
  }
16857
- throw new NappletResolutionError("blob-unavailable", `no server served blob ${sha256Hex}`);
17324
+ throw new NappletResolutionError("blob-unavailable", `no server served blob ${sha256Hex2}`);
16858
17325
  }
16859
17326
  var INDEX_PATHS = ["/index.html", "index.html", "/"];
16860
17327
  function defaultDecode(bytes) {
@@ -20466,7 +20933,7 @@ async function resolvePajaPointer(value, options = {}) {
20466
20933
  const resolved = await resolveNapplet({
20467
20934
  event,
20468
20935
  cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
20469
- fetchBlob: (sha256Hex, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex, async (url) => {
20936
+ fetchBlob: (sha256Hex2, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex2, async (url) => {
20470
20937
  const response = await (options.fetcher ?? fetch)(url);
20471
20938
  if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
20472
20939
  return new Uint8Array(await response.arrayBuffer());
@@ -23247,6 +23714,7 @@ function createPajaBrowserState(context) {
23247
23714
  };
23248
23715
  }
23249
23716
  async function installPajaHost() {
23717
+ const disposeConsolePanel = installPajaConsolePanel();
23250
23718
  const config = await readLatestConfig(readConfig());
23251
23719
  const stage = getStage();
23252
23720
  const frame = config.target.mode === "runtime-pointer" ? null : getFrame();
@@ -23337,6 +23805,7 @@ async function installPajaHost() {
23337
23805
  window.addEventListener("pagehide", () => confirmationController.dispose(), { once: true });
23338
23806
  window.addEventListener("pagehide", () => notifyController?.dispose(), { once: true });
23339
23807
  window.addEventListener("pagehide", () => configController?.dispose(), { once: true });
23808
+ window.addEventListener("pagehide", disposeConsolePanel, { once: true });
23340
23809
  window.__KEHTO_PAJA__ = state;
23341
23810
  window.addEventListener("message", (event) => {
23342
23811
  const source = event.source;