@kehto/paja 0.16.4 → 0.16.5

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.
@@ -15255,15 +15255,35 @@ var KIND_GIFT_WRAP_EPHEMERAL = 21059;
15255
15255
  var KIND_GIFT_WRAP_REGULAR = 1059;
15256
15256
  var KIND_ANNOUNCE_SERVER = 11316;
15257
15257
  var KIND_ANNOUNCE_TOOLS = 11317;
15258
+ var SUPPORT_ENCRYPTION = "support_encryption";
15259
+ var SUPPORT_ENCRYPTION_EPHEMERAL = "support_encryption_ephemeral";
15260
+ var SUPPORT_OVERSIZED_TRANSFER = "support_oversized_transfer";
15261
+ var SUPPORT_OPEN_STREAM = "support_open_stream";
15258
15262
  var DEFAULT_TIMEOUT_MS = 3e4;
15259
15263
  var DEFAULT_DISCOVER_TIMEOUT_MS = 6e3;
15260
15264
  var MCP_PROTOCOL_VERSION = "2025-11-25";
15261
15265
  var SEEN_WRAP_LIMIT = 512;
15266
+ var DIGEST_PREFIX = "sha256:";
15267
+ var MAX_TRANSFER_CHUNKS = 1e4;
15268
+ var MAX_TRANSFER_BYTES = 100 * 1024 * 1024;
15269
+ var MAX_ACTIVE_TRANSFERS = 8;
15270
+ var MAX_TRANSFER_LIFETIME_MS = 12e4;
15271
+ var COMPLETION_MODE_RENDER = "render";
15272
+ var DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e4;
15273
+ var DEFAULT_STREAM_PROBE_TIMEOUT_MS = 1e4;
15274
+ var MAX_STREAM_BUFFERED_CHUNKS = 256;
15275
+ var MAX_STREAM_BUFFERED_CODE_UNITS = 8 * 1024 * 1024;
15276
+ var MAX_PING_NONCE_BYTES = 64;
15262
15277
  var correlationCounter = 0;
15263
15278
  function nextCorrelationId() {
15264
15279
  correlationCounter += 1;
15265
15280
  return `cvm-${correlationCounter}-${getPublicKey(generateSecretKey()).slice(0, 8)}`;
15266
15281
  }
15282
+ var streamNonceCounter = 0;
15283
+ function nextStreamNonce() {
15284
+ streamNonceCounter += 1;
15285
+ return `cvm-ping-${streamNonceCounter}-${getPublicKey(generateSecretKey()).slice(0, 8)}`;
15286
+ }
15267
15287
  function randomizedPastTimestamp() {
15268
15288
  const jitter = Math.floor(Math.random() * 172800);
15269
15289
  return Math.floor(Date.now() / 1e3) - jitter;
@@ -15271,6 +15291,19 @@ function randomizedPastTimestamp() {
15271
15291
  function tagValue(tags, name) {
15272
15292
  return tags.find((tag) => tag[0] === name)?.[1];
15273
15293
  }
15294
+ function progressTokenKey(token) {
15295
+ if (typeof token === "string") return token.length > 0 ? `s:${token}` : null;
15296
+ if (typeof token === "number" && Number.isFinite(token)) return `n:${token}`;
15297
+ return null;
15298
+ }
15299
+ function isBoundedInteger(value, min, max) {
15300
+ return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
15301
+ }
15302
+ async function sha256Hex(text) {
15303
+ const bytes = new TextEncoder().encode(text);
15304
+ const buf = await crypto.subtle.digest("SHA-256", bytes);
15305
+ return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
15306
+ }
15274
15307
  function simplePoolAdapter(sp) {
15275
15308
  return {
15276
15309
  subscribe(relays, filter, params) {
@@ -15295,11 +15328,14 @@ function createNostrCvmTransport(options = {}) {
15295
15328
  const clientSecretKey = options.clientSecretKey ?? generateSecretKey();
15296
15329
  const clientPubkey = getPublicKey(clientSecretKey);
15297
15330
  const clientInfo = options.clientInfo ?? { name: "kehto-cvm", version: "1.0.0" };
15331
+ const streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
15332
+ const streamProbeTimeoutMs = options.streamProbeTimeoutMs ?? DEFAULT_STREAM_PROBE_TIMEOUT_MS;
15298
15333
  const sessions = /* @__PURE__ */ new Map();
15299
15334
  const pending = /* @__PURE__ */ new Map();
15300
15335
  const eventHandlers = /* @__PURE__ */ new Set();
15301
15336
  const relayRefcount = /* @__PURE__ */ new Map();
15302
15337
  const seenWraps = /* @__PURE__ */ new Set();
15338
+ const issuedTokens = /* @__PURE__ */ new Map();
15303
15339
  let inbound = null;
15304
15340
  let subscribedRelays = "";
15305
15341
  function resolveRelays(server) {
@@ -15361,23 +15397,379 @@ function createNostrCvmTransport(options = {}) {
15361
15397
  } catch {
15362
15398
  return;
15363
15399
  }
15400
+ if (mcp.method === "notifications/progress") {
15401
+ const params = mcp.params ?? {};
15402
+ const cvm = params.cvm;
15403
+ if (cvm && typeof cvm.type === "string") {
15404
+ if (cvm.type === "oversized-transfer") {
15405
+ void handleOversizedFrame(serverPubkey, params, cvm);
15406
+ return;
15407
+ }
15408
+ if (cvm.type === "open-stream") {
15409
+ const entry = admitFrame(serverPubkey, params.progressToken);
15410
+ if (!entry) return;
15411
+ handleOpenStreamFrame(serverPubkey, entry, params, cvm, mcp);
15412
+ return;
15413
+ }
15414
+ }
15415
+ }
15416
+ routeMessage(serverPubkey, mcp);
15417
+ }
15418
+ async function publishStreamFrame(entry, stream, frame) {
15419
+ const session = sessions.get(entry.serverPubkey);
15420
+ if (!session) return;
15421
+ stream.localProgress += 1;
15422
+ await publishMcp(
15423
+ { pubkey: entry.serverPubkey },
15424
+ session.relays,
15425
+ {
15426
+ jsonrpc: "2.0",
15427
+ method: "notifications/progress",
15428
+ params: {
15429
+ progressToken: stream.token,
15430
+ progress: stream.localProgress,
15431
+ cvm: { type: "open-stream", ...frame }
15432
+ }
15433
+ }
15434
+ );
15435
+ }
15436
+ function admitFrame(serverPubkey, token) {
15437
+ const tokenKey = progressTokenKey(token);
15438
+ if (!tokenKey) return null;
15439
+ const correlationId = issuedTokens.get(`${serverPubkey}:${tokenKey}`);
15440
+ if (!correlationId) return null;
15441
+ const entry = pending.get(correlationId);
15442
+ if (!entry || entry.serverPubkey !== serverPubkey) return null;
15443
+ return entry;
15444
+ }
15445
+ function releaseTransfer(entry) {
15446
+ if (!entry.oversized) return;
15447
+ clearTimeout(entry.oversized.expiryTimer);
15448
+ entry.oversized = void 0;
15449
+ }
15450
+ function releaseStream(entry) {
15451
+ if (!entry.stream) return;
15452
+ clearTimeout(entry.stream.idleTimer);
15453
+ if (entry.stream.probeTimer) clearTimeout(entry.stream.probeTimer);
15454
+ entry.stream = void 0;
15455
+ }
15456
+ function endStream(entry) {
15457
+ releaseStream(entry);
15458
+ entry.streamEnded = true;
15459
+ }
15460
+ function failStream(entry, reason) {
15461
+ const stream = entry.stream;
15462
+ if (!stream || entry.streamEnded) return;
15463
+ endStream(entry);
15464
+ void publishStreamFrame(entry, stream, { frameType: "abort", reason }).catch(() => {
15465
+ });
15466
+ }
15467
+ function resetStreamIdleTimer(entry, stream) {
15468
+ clearTimeout(stream.idleTimer);
15469
+ stream.idleTimer = setTimeout(() => onStreamIdle(entry), streamIdleTimeoutMs);
15470
+ }
15471
+ function onStreamIdle(entry) {
15472
+ const stream = entry.stream;
15473
+ if (!stream || entry.streamEnded) return;
15474
+ if (stream.pendingNonce !== null) return;
15475
+ const session = sessions.get(entry.serverPubkey);
15476
+ if (!session) {
15477
+ failStream(entry, "session closed");
15478
+ return;
15479
+ }
15480
+ stream.pendingNonce = nextStreamNonce();
15481
+ stream.probeTimer = setTimeout(() => failStream(entry, "probe timeout"), streamProbeTimeoutMs);
15482
+ void publishStreamFrame(entry, stream, { frameType: "ping", nonce: stream.pendingNonce }).catch(() => failStream(entry, "ping publication failed"));
15483
+ }
15484
+ function releaseFrameState(entry) {
15485
+ releaseTransfer(entry);
15486
+ releaseStream(entry);
15487
+ if (entry.tokenKey && issuedTokens.get(entry.tokenKey) === entry.correlationId) {
15488
+ issuedTokens.delete(entry.tokenKey);
15489
+ }
15490
+ entry.tokenKey = void 0;
15491
+ }
15492
+ function settlePending(correlationId) {
15493
+ const entry = pending.get(correlationId);
15494
+ if (!entry) return null;
15495
+ clearTimeout(entry.timer);
15496
+ pending.delete(correlationId);
15497
+ releaseFrameState(entry);
15498
+ return entry;
15499
+ }
15500
+ function countActiveTransfers() {
15501
+ let active = 0;
15502
+ for (const entry of pending.values()) {
15503
+ if (entry.oversized) active += 1;
15504
+ }
15505
+ return active;
15506
+ }
15507
+ async function handleOversizedFrame(serverPubkey, params, cvm) {
15508
+ const entry = admitFrame(serverPubkey, params.progressToken);
15509
+ if (!entry) return;
15510
+ const progress = Number(params.progress ?? NaN);
15511
+ if (!Number.isSafeInteger(progress) || progress < 0) return;
15512
+ if (cvm.frameType === "start") {
15513
+ const digest = typeof cvm.digest === "string" ? cvm.digest : "";
15514
+ const { totalBytes, totalChunks } = cvm;
15515
+ if (cvm.completionMode !== COMPLETION_MODE_RENDER) return;
15516
+ if (digest === "") return;
15517
+ if (!isBoundedInteger(totalChunks, 1, MAX_TRANSFER_CHUNKS)) return;
15518
+ if (!isBoundedInteger(totalBytes, 1, MAX_TRANSFER_BYTES)) return;
15519
+ if (progress > Number.MAX_SAFE_INTEGER - totalChunks - 1) return;
15520
+ if (!entry.oversized && countActiveTransfers() >= MAX_ACTIVE_TRANSFERS) return;
15521
+ releaseTransfer(entry);
15522
+ entry.oversized = {
15523
+ digest,
15524
+ totalBytes,
15525
+ totalChunks,
15526
+ startProgress: progress,
15527
+ acceptProgress: null,
15528
+ chunks: /* @__PURE__ */ new Map(),
15529
+ codeUnitsReceived: 0,
15530
+ // Hard expiry: progress frames extend the request deadline but never
15531
+ // how long buffers may be held.
15532
+ expiryTimer: setTimeout(() => releaseTransfer(entry), MAX_TRANSFER_LIFETIME_MS)
15533
+ };
15534
+ refreshPendingTimeout(entry);
15535
+ return;
15536
+ }
15537
+ const transfer = entry.oversized;
15538
+ if (!transfer) return;
15539
+ if (cvm.frameType === "accept") {
15540
+ if (progress !== transfer.startProgress + 1) {
15541
+ releaseTransfer(entry);
15542
+ return;
15543
+ }
15544
+ transfer.acceptProgress = progress;
15545
+ refreshPendingTimeout(entry);
15546
+ return;
15547
+ }
15548
+ if (cvm.frameType === "chunk") {
15549
+ if (typeof cvm.data !== "string" || progress <= transfer.startProgress) return;
15550
+ if (transfer.chunks.has(progress)) return;
15551
+ if (transfer.chunks.size >= transfer.totalChunks) {
15552
+ releaseTransfer(entry);
15553
+ return;
15554
+ }
15555
+ transfer.codeUnitsReceived += cvm.data.length;
15556
+ if (transfer.codeUnitsReceived > MAX_TRANSFER_BYTES) {
15557
+ releaseTransfer(entry);
15558
+ return;
15559
+ }
15560
+ transfer.chunks.set(progress, cvm.data);
15561
+ refreshPendingTimeout(entry);
15562
+ return;
15563
+ }
15564
+ if (cvm.frameType === "abort") {
15565
+ releaseTransfer(entry);
15566
+ return;
15567
+ }
15568
+ if (cvm.frameType === "end") {
15569
+ const message = await assembleTransfer(transfer, progress);
15570
+ releaseTransfer(entry);
15571
+ if (message) routeMessage(serverPubkey, message);
15572
+ return;
15573
+ }
15574
+ }
15575
+ async function assembleTransfer(transfer, endProgress) {
15576
+ const { startProgress, acceptProgress, totalChunks } = transfer;
15577
+ const hasCompleteRange = (first) => {
15578
+ for (let i4 = 0; i4 < totalChunks; i4++) {
15579
+ if (!transfer.chunks.has(first + i4)) return false;
15580
+ }
15581
+ return true;
15582
+ };
15583
+ const directStart = startProgress + 1;
15584
+ const acceptGatedStart = startProgress + 2;
15585
+ const firstProgress = hasCompleteRange(directStart) && endProgress === directStart + totalChunks ? directStart : acceptProgress === startProgress + 1 && hasCompleteRange(acceptGatedStart) && endProgress === acceptGatedStart + totalChunks ? acceptGatedStart : null;
15586
+ if (firstProgress === null) return null;
15587
+ let payload = "";
15588
+ for (let i4 = 0; i4 < totalChunks; i4++) {
15589
+ payload += transfer.chunks.get(firstProgress + i4);
15590
+ }
15591
+ if (new TextEncoder().encode(payload).byteLength !== transfer.totalBytes) return null;
15592
+ const expected = transfer.digest.startsWith(DIGEST_PREFIX) ? transfer.digest.slice(DIGEST_PREFIX.length) : transfer.digest;
15593
+ if (await sha256Hex(payload) !== expected) return null;
15594
+ try {
15595
+ return JSON.parse(payload);
15596
+ } catch {
15597
+ return null;
15598
+ }
15599
+ }
15600
+ function handleOpenStreamFrame(serverPubkey, entry, params, cvm, mcp) {
15601
+ const progress = Number(params.progress ?? NaN);
15602
+ if (!Number.isSafeInteger(progress) || progress < 0) return;
15603
+ if (entry.streamEnded) return;
15604
+ const stream = entry.stream;
15605
+ if (!stream) {
15606
+ if (cvm.frameType !== "start") return;
15607
+ entry.stream = {
15608
+ token: params.progressToken,
15609
+ lastProgress: progress,
15610
+ localProgress: 0,
15611
+ nextChunkIndex: 0,
15612
+ outOfOrder: /* @__PURE__ */ new Map(),
15613
+ outOfOrderCodeUnits: 0,
15614
+ pendingNonce: null,
15615
+ idleTimer: setTimeout(() => onStreamIdle(entry), streamIdleTimeoutMs),
15616
+ probeTimer: null
15617
+ };
15618
+ refreshPendingTimeout(entry);
15619
+ routeEvent(serverPubkey, mcp);
15620
+ return;
15621
+ }
15622
+ const admitOrderedActivity = () => {
15623
+ if (progress <= stream.lastProgress) {
15624
+ failStream(entry, "non-monotonic progress");
15625
+ return false;
15626
+ }
15627
+ stream.lastProgress = progress;
15628
+ resetStreamIdleTimer(entry, stream);
15629
+ refreshPendingTimeout(entry);
15630
+ return true;
15631
+ };
15632
+ switch (cvm.frameType) {
15633
+ case "start":
15634
+ failStream(entry, "duplicate start");
15635
+ return;
15636
+ case "accept":
15637
+ if (!admitOrderedActivity()) return;
15638
+ routeEvent(serverPubkey, mcp);
15639
+ return;
15640
+ case "chunk": {
15641
+ if (typeof cvm.data !== "string" || !isBoundedInteger(cvm.chunkIndex, 0, Number.MAX_SAFE_INTEGER)) {
15642
+ failStream(entry, "malformed chunk");
15643
+ return;
15644
+ }
15645
+ const index = cvm.chunkIndex;
15646
+ if (index < stream.nextChunkIndex) return;
15647
+ if (index > stream.nextChunkIndex) {
15648
+ const existing = stream.outOfOrder.get(index);
15649
+ if (existing) {
15650
+ if (existing.progress !== progress || JSON.stringify(existing.message) !== JSON.stringify(mcp)) {
15651
+ failStream(entry, "conflicting duplicate chunk");
15652
+ }
15653
+ return;
15654
+ }
15655
+ if (progress <= stream.lastProgress || stream.outOfOrder.size >= MAX_STREAM_BUFFERED_CHUNKS || stream.outOfOrderCodeUnits + cvm.data.length > MAX_STREAM_BUFFERED_CODE_UNITS) {
15656
+ failStream(entry, progress <= stream.lastProgress ? "non-monotonic progress" : "gap buffer exhausted");
15657
+ return;
15658
+ }
15659
+ stream.outOfOrder.set(index, { message: mcp, progress, codeUnits: cvm.data.length });
15660
+ stream.outOfOrderCodeUnits += cvm.data.length;
15661
+ resetStreamIdleTimer(entry, stream);
15662
+ refreshPendingTimeout(entry);
15663
+ return;
15664
+ }
15665
+ const ready = [
15666
+ { message: mcp, progress, codeUnits: 0 }
15667
+ ];
15668
+ let nextIndex = index + 1;
15669
+ while (stream.outOfOrder.has(nextIndex)) {
15670
+ ready.push(stream.outOfOrder.get(nextIndex));
15671
+ nextIndex += 1;
15672
+ }
15673
+ let previousProgress = stream.lastProgress;
15674
+ for (const item of ready) {
15675
+ if (item.progress <= previousProgress) {
15676
+ failStream(entry, "non-monotonic chunk order");
15677
+ return;
15678
+ }
15679
+ previousProgress = item.progress;
15680
+ }
15681
+ for (let bufferedIndex = index + 1; bufferedIndex < nextIndex; bufferedIndex++) {
15682
+ const buffered = stream.outOfOrder.get(bufferedIndex);
15683
+ stream.outOfOrder.delete(bufferedIndex);
15684
+ stream.outOfOrderCodeUnits -= buffered.codeUnits;
15685
+ }
15686
+ for (const item of ready) {
15687
+ routeEvent(serverPubkey, item.message);
15688
+ stream.nextChunkIndex += 1;
15689
+ }
15690
+ stream.lastProgress = previousProgress;
15691
+ resetStreamIdleTimer(entry, stream);
15692
+ refreshPendingTimeout(entry);
15693
+ return;
15694
+ }
15695
+ case "ping": {
15696
+ if (typeof cvm.nonce !== "string") return;
15697
+ if (new TextEncoder().encode(cvm.nonce).byteLength > MAX_PING_NONCE_BYTES) return;
15698
+ if (!admitOrderedActivity()) return;
15699
+ void publishStreamFrame(entry, stream, { frameType: "pong", nonce: cvm.nonce }).catch(() => {
15700
+ failStream(entry, "pong publication failed");
15701
+ });
15702
+ return;
15703
+ }
15704
+ case "pong": {
15705
+ if (stream.pendingNonce === null || cvm.nonce !== stream.pendingNonce) return;
15706
+ if (!admitOrderedActivity()) return;
15707
+ stream.pendingNonce = null;
15708
+ if (stream.probeTimer) clearTimeout(stream.probeTimer);
15709
+ stream.probeTimer = null;
15710
+ return;
15711
+ }
15712
+ case "close": {
15713
+ if (cvm.lastChunkIndex !== void 0) {
15714
+ const bound = cvm.lastChunkIndex;
15715
+ const complete = isBoundedInteger(bound, 0, Number.MAX_SAFE_INTEGER) && stream.nextChunkIndex === bound + 1 && stream.outOfOrder.size === 0;
15716
+ if (!complete) {
15717
+ failStream(entry, "close with unresolved chunk gaps");
15718
+ return;
15719
+ }
15720
+ }
15721
+ if (!admitOrderedActivity()) return;
15722
+ routeEvent(serverPubkey, mcp);
15723
+ endStream(entry);
15724
+ return;
15725
+ }
15726
+ case "abort":
15727
+ if (!admitOrderedActivity()) return;
15728
+ routeEvent(serverPubkey, mcp);
15729
+ endStream(entry);
15730
+ return;
15731
+ default:
15732
+ return;
15733
+ }
15734
+ }
15735
+ function refreshPendingTimeout(entry) {
15736
+ clearTimeout(entry.timer);
15737
+ entry.timer = setTimeout(() => {
15738
+ const settled = settlePending(entry.correlationId);
15739
+ settled?.reject(new Error("relay timeout"));
15740
+ }, entry.timeoutMs);
15741
+ }
15742
+ function routeMessage(serverPubkey, mcp) {
15364
15743
  const id = mcp.id;
15365
15744
  if (id != null && pending.has(String(id))) {
15366
15745
  const entry = pending.get(String(id));
15367
15746
  if (entry.serverPubkey !== serverPubkey) return;
15368
- pending.delete(String(id));
15369
- clearTimeout(entry.timer);
15370
- entry.resolve({ ...mcp, id: entry.originalId });
15747
+ const settled = settlePending(String(id));
15748
+ settled?.resolve({ ...mcp, id: entry.originalId });
15371
15749
  return;
15372
15750
  }
15751
+ routeEvent(serverPubkey, mcp);
15752
+ }
15753
+ function routeEvent(serverPubkey, mcp) {
15373
15754
  if (mcp.method !== void 0 && sessions.has(serverPubkey)) {
15374
15755
  const server = { pubkey: serverPubkey };
15375
15756
  for (const handler of eventHandlers) handler(server, mcp);
15376
15757
  }
15377
15758
  }
15378
15759
  async function publishMcp(server, relays, message) {
15760
+ const capabilityTags = [
15761
+ ...encrypt22 ? [[SUPPORT_ENCRYPTION]] : [],
15762
+ ...encrypt22 && wrapKind === KIND_GIFT_WRAP_EPHEMERAL ? [[SUPPORT_ENCRYPTION_EPHEMERAL]] : [],
15763
+ [SUPPORT_OVERSIZED_TRANSFER],
15764
+ [SUPPORT_OPEN_STREAM]
15765
+ ];
15379
15766
  const inner = finalizeEvent(
15380
- { kind: KIND_CVM, created_at: Math.floor(Date.now() / 1e3), tags: [["p", server.pubkey]], content: JSON.stringify(message) },
15767
+ {
15768
+ kind: KIND_CVM,
15769
+ created_at: Math.floor(Date.now() / 1e3),
15770
+ tags: [["p", server.pubkey], ...capabilityTags],
15771
+ content: JSON.stringify(message)
15772
+ },
15381
15773
  clientSecretKey
15382
15774
  );
15383
15775
  if (!encrypt22) {
@@ -15401,17 +15793,52 @@ function createNostrCvmTransport(options = {}) {
15401
15793
  function sendCorrelated(server, relays, message, timeout) {
15402
15794
  const correlationId = nextCorrelationId();
15403
15795
  const originalId = message.id;
15404
- const outgoing = { ...message, id: correlationId };
15796
+ let outgoing = { ...message, id: correlationId };
15797
+ let issuedToken;
15798
+ const params = message.params && typeof message.params === "object" ? message.params : void 0;
15799
+ const meta = params?._meta && typeof params._meta === "object" ? params._meta : void 0;
15800
+ const explicit = meta?.progressToken;
15801
+ if (message.method === "tools/call") {
15802
+ issuedToken = typeof explicit === "string" || typeof explicit === "number" ? explicit : correlationId;
15803
+ outgoing = {
15804
+ ...outgoing,
15805
+ params: {
15806
+ ...params,
15807
+ _meta: {
15808
+ ...meta,
15809
+ progressToken: issuedToken
15810
+ }
15811
+ }
15812
+ };
15813
+ } else if (typeof explicit === "string" || typeof explicit === "number") {
15814
+ issuedToken = explicit;
15815
+ }
15816
+ const tokenKey = progressTokenKey(issuedToken);
15817
+ const issuedKey = tokenKey ? `${server.pubkey}:${tokenKey}` : null;
15818
+ if (issuedKey && issuedTokens.has(issuedKey)) {
15819
+ return Promise.reject(new Error("duplicate progress token"));
15820
+ }
15405
15821
  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 });
15822
+ const entry = {
15823
+ resolve,
15824
+ reject: reject2,
15825
+ timer: setTimeout(() => {
15826
+ const settled = settlePending(correlationId);
15827
+ settled?.reject(new Error("relay timeout"));
15828
+ }, timeout),
15829
+ timeoutMs: timeout,
15830
+ originalId,
15831
+ serverPubkey: server.pubkey,
15832
+ correlationId
15833
+ };
15834
+ if (issuedKey) {
15835
+ entry.tokenKey = issuedKey;
15836
+ issuedTokens.set(issuedKey, correlationId);
15837
+ }
15838
+ pending.set(correlationId, entry);
15411
15839
  void publishMcp(server, relays, outgoing).catch((err2) => {
15412
- pending.delete(correlationId);
15413
- clearTimeout(timer);
15414
- reject2(err2 instanceof Error ? err2 : new Error("publish failed"));
15840
+ const settled = settlePending(correlationId);
15841
+ settled?.reject(err2 instanceof Error ? err2 : new Error("publish failed"));
15415
15842
  });
15416
15843
  });
15417
15844
  }
@@ -15509,6 +15936,10 @@ function createNostrCvmTransport(options = {}) {
15509
15936
  if (!session) return;
15510
15937
  sessions.delete(server.pubkey);
15511
15938
  releaseRelays(session.relays);
15939
+ for (const [correlationId, entry] of pending) {
15940
+ if (entry.serverPubkey !== server.pubkey) continue;
15941
+ settlePending(correlationId)?.reject(new Error("server closed"));
15942
+ }
15512
15943
  },
15513
15944
  onEvent(handler) {
15514
15945
  eventHandlers.add(handler);
@@ -15523,9 +15954,12 @@ function createNostrCvmTransport(options = {}) {
15523
15954
  inbound = null;
15524
15955
  for (const entry of pending.values()) {
15525
15956
  clearTimeout(entry.timer);
15957
+ releaseTransfer(entry);
15958
+ releaseStream(entry);
15526
15959
  entry.reject(new Error("transport disposed"));
15527
15960
  }
15528
15961
  pending.clear();
15962
+ issuedTokens.clear();
15529
15963
  sessions.clear();
15530
15964
  relayRefcount.clear();
15531
15965
  eventHandlers.clear();
@@ -16430,14 +16864,14 @@ var CacheStorageNappletArtifactCache = class {
16430
16864
  this.softCeilingBytes = options.softCeilingBytes ?? DEFAULT_SOFT_CEILING_BYTES;
16431
16865
  this.hardCeilingBytes = options.hardCeilingBytes ?? DEFAULT_HARD_CEILING_BYTES;
16432
16866
  }
16433
- async readBlob(sha256Hex) {
16867
+ async readBlob(sha256Hex2) {
16434
16868
  try {
16435
16869
  const cache2 = await this.open();
16436
- const response = await cache2.match(this.blobUrl(sha256Hex));
16870
+ const response = await cache2.match(this.blobUrl(sha256Hex2));
16437
16871
  if (!response) return void 0;
16438
16872
  const bytes = await responseBytes(response);
16439
16873
  const index = await this.readIndex(cache2);
16440
- const entry = index.blobs[sha256Hex];
16874
+ const entry = index.blobs[sha256Hex2];
16441
16875
  if (entry) {
16442
16876
  entry.lastAccessed = this.now();
16443
16877
  index.updatedAt = entry.lastAccessed;
@@ -16445,20 +16879,20 @@ var CacheStorageNappletArtifactCache = class {
16445
16879
  }
16446
16880
  return bytes;
16447
16881
  } catch (cause) {
16448
- this.diagnostic({ type: "cache-read-failed", message: `failed to read blob ${sha256Hex}`, cause });
16882
+ this.diagnostic({ type: "cache-read-failed", message: `failed to read blob ${sha256Hex2}`, cause });
16449
16883
  return void 0;
16450
16884
  }
16451
16885
  }
16452
- async deleteBlob(sha256Hex) {
16886
+ async deleteBlob(sha256Hex2) {
16453
16887
  try {
16454
16888
  const cache2 = await this.open();
16455
- await cache2.delete(this.blobUrl(sha256Hex));
16889
+ await cache2.delete(this.blobUrl(sha256Hex2));
16456
16890
  const index = await this.readIndex(cache2);
16457
- delete index.blobs[sha256Hex];
16891
+ delete index.blobs[sha256Hex2];
16458
16892
  index.updatedAt = this.now();
16459
16893
  await this.writeIndex(cache2, index);
16460
16894
  } catch (cause) {
16461
- this.diagnostic({ type: "cache-delete-failed", message: `failed to delete blob ${sha256Hex}`, cause });
16895
+ this.diagnostic({ type: "cache-delete-failed", message: `failed to delete blob ${sha256Hex2}`, cause });
16462
16896
  }
16463
16897
  }
16464
16898
  async writeVerifiedResolution(input) {
@@ -16839,22 +17273,22 @@ function verifyManifestSignature(event) {
16839
17273
  return false;
16840
17274
  }
16841
17275
  }
16842
- function verifyBlobHash(bytes, sha256Hex) {
16843
- return bytesToHex2(sha256(bytes)) === sha256Hex;
17276
+ function verifyBlobHash(bytes, sha256Hex2) {
17277
+ return bytesToHex2(sha256(bytes)) === sha256Hex2;
16844
17278
  }
16845
17279
  function stripTrailingSlash(url) {
16846
17280
  return url.endsWith("/") ? url.slice(0, -1) : url;
16847
17281
  }
16848
- async function fetchBlob(servers, sha256Hex, fetchBytes) {
17282
+ async function fetchBlob(servers, sha256Hex2, fetchBytes) {
16849
17283
  for (const server of servers) {
16850
- const url = `${stripTrailingSlash(server)}/${sha256Hex}`;
17284
+ const url = `${stripTrailingSlash(server)}/${sha256Hex2}`;
16851
17285
  try {
16852
17286
  const bytes = await fetchBytes(url);
16853
- if (verifyBlobHash(bytes, sha256Hex)) return bytes;
17287
+ if (verifyBlobHash(bytes, sha256Hex2)) return bytes;
16854
17288
  } catch {
16855
17289
  }
16856
17290
  }
16857
- throw new NappletResolutionError("blob-unavailable", `no server served blob ${sha256Hex}`);
17291
+ throw new NappletResolutionError("blob-unavailable", `no server served blob ${sha256Hex2}`);
16858
17292
  }
16859
17293
  var INDEX_PATHS = ["/index.html", "index.html", "/"];
16860
17294
  function defaultDecode(bytes) {
@@ -20466,7 +20900,7 @@ async function resolvePajaPointer(value, options = {}) {
20466
20900
  const resolved = await resolveNapplet({
20467
20901
  event,
20468
20902
  cache: await openNappletArtifactCache({ requireStorageEstimate: false }),
20469
- fetchBlob: (sha256Hex, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex, async (url) => {
20903
+ fetchBlob: (sha256Hex2, servers) => fetchBlob([...servers, ...blossomServers], sha256Hex2, async (url) => {
20470
20904
  const response = await (options.fetcher ?? fetch)(url);
20471
20905
  if (!response.ok) throw new Error(`[paja-runtime] blob ${url}: ${response.status}`);
20472
20906
  return new Uint8Array(await response.arrayBuffer());