@dvmkit/sdk 0.16.2 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/{chunk-XSF5YTEY.js → chunk-2PN4DKZW.js} +1 -1
  2. package/dist/{chunk-F2LNWZCV.js → chunk-3KRFFLAT.js} +1 -1
  3. package/dist/{chunk-V73WRF46.js → chunk-5CCQEAQV.js} +1 -1
  4. package/dist/{chunk-4IFZHJQK.js → chunk-5RLNRLLB.js} +1 -1
  5. package/dist/{chunk-7QGS27UT.js → chunk-72A4PHLD.js} +1 -1
  6. package/dist/{chunk-XBQTFOIT.js → chunk-7RWE6ELV.js} +1 -1
  7. package/dist/{chunk-N4SBDUVD.js → chunk-A6EY4K46.js} +1 -1
  8. package/dist/{chunk-5U2RG3ZW.js → chunk-ALTE7I6C.js} +5 -0
  9. package/dist/{chunk-K4ZQE7IB.js → chunk-B5MPLDRC.js} +2 -2
  10. package/dist/{chunk-PP5WHB6T.js → chunk-C5XNZLZ7.js} +1 -1
  11. package/dist/{chunk-2UZOJRRS.js → chunk-CVN2YNMG.js} +1 -1
  12. package/dist/{chunk-MD34LE6M.js → chunk-CXRI4N4Y.js} +1 -1
  13. package/dist/{chunk-ELNWQS24.js → chunk-EIDRCGTY.js} +1 -1
  14. package/dist/{chunk-ICHC4VDZ.js → chunk-KRX7VNBG.js} +1 -1
  15. package/dist/{chunk-TN6MWTZI.js → chunk-LY2JYSJS.js} +1 -1
  16. package/dist/{chunk-7N5VDCG3.js → chunk-MWDUGMN2.js} +2 -2
  17. package/dist/{chunk-FRGKANTZ.js → chunk-ORBWKYDE.js} +1 -1
  18. package/dist/{chunk-VQHBJXBL.js → chunk-QTKGFG3I.js} +1 -1
  19. package/dist/{chunk-PBT3H2LZ.js → chunk-RX6FT7XA.js} +1 -1
  20. package/dist/{chunk-OYBSDJR6.js → chunk-VKFR5PCW.js} +2 -2
  21. package/dist/{chunk-V4CBI5HB.js → chunk-Y6JJDOGO.js} +1 -1
  22. package/dist/{chunk-KERFCBT7.js → chunk-YRQMDRI4.js} +502 -129
  23. package/dist/{content-retention-BASsjgKw.d.ts → content-retention-B_hOQS36.d.ts} +1 -1
  24. package/dist/{credit-ledger-VWZ5NP7V.js → credit-ledger-5HMJ6VUX.js} +3 -3
  25. package/dist/{credit-menu-BfXaeHgR.d.ts → credit-menu-D-h0zJCZ.d.ts} +42 -10
  26. package/dist/{fx-BmvWGISH.d.ts → fx-CpqQaIfg.d.ts} +1 -1
  27. package/dist/index.d.ts +5 -5
  28. package/dist/index.js +4 -4
  29. package/dist/internal/caller.d.ts +14 -8
  30. package/dist/internal/caller.js +7 -7
  31. package/dist/internal/server.d.ts +7 -7
  32. package/dist/internal/server.js +20 -20
  33. package/dist/{job-store-CA7u1nHV.d.ts → job-store-CgXT79tS.d.ts} +1 -1
  34. package/dist/{lightning-backend-BOcopzXk.d.ts → lightning-backend-B1hRUIOb.d.ts} +1 -1
  35. package/dist/{memory-credit-ledger-DZWRQGSF.js → memory-credit-ledger-ME6FH462.js} +4 -4
  36. package/dist/{mpp-setup-3NJY354I.js → mpp-setup-BLIYBORK.js} +3 -3
  37. package/dist/{payout-reporter-P3652TLG.js → payout-reporter-5QAHCYIO.js} +2 -2
  38. package/dist/{postgres-consumed-credential-store-S2CBQ4NO.js → postgres-consumed-credential-store-4PCJEZBD.js} +1 -1
  39. package/dist/{postgres-job-store-JRSG54VJ.js → postgres-job-store-E5GXLRB4.js} +2 -2
  40. package/dist/{postgres-replay-store-ZPILIJIW.js → postgres-replay-store-GH5AINQR.js} +2 -2
  41. package/dist/{pricing-3P7KAONY.js → pricing-JJYLTTWK.js} +3 -3
  42. package/dist/{revenue-reporter-2P3PWJJM.js → revenue-reporter-6XARUEG6.js} +2 -2
  43. package/dist/server/index.d.ts +10 -10
  44. package/dist/server/index.js +32 -31
  45. package/dist/{step-cache-BCRHimiY.d.ts → step-cache-Bezn-_ZL.d.ts} +41 -3
  46. package/dist/{tempo-charge-store-2LK6IDLS.js → tempo-charge-store-A3CDF7D5.js} +2 -2
  47. package/dist/{tempo-session-store-5MHCH3YW.js → tempo-session-store-EWBE5HJM.js} +3 -3
  48. package/dist/testing/index.d.ts +3 -3
  49. package/dist/testing/index.js +6 -6
  50. package/dist/{usd-B8_WMfHT.d.ts → usd-CJwU3-A4.d.ts} +1 -1
  51. package/dist/{x402-6T73AZQ6.js → x402-HVCUEWWU.js} +3 -3
  52. package/package.json +1 -1
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-XRLKGCAW.js";
6
6
  import {
7
7
  x402EarnedCeiling
8
- } from "./chunk-ICHC4VDZ.js";
8
+ } from "./chunk-KRX7VNBG.js";
9
9
  import {
10
10
  createX402FacilitatorAuthHeaders,
11
11
  createX402FacilitatorClient,
@@ -14,13 +14,13 @@ import {
14
14
  verifyWithFacilitator,
15
15
  withPaymentDiagnostics,
16
16
  withPaymentStage
17
- } from "./chunk-F2LNWZCV.js";
17
+ } from "./chunk-3KRFFLAT.js";
18
18
  import {
19
19
  MemoryCreditLedger
20
- } from "./chunk-PP5WHB6T.js";
20
+ } from "./chunk-C5XNZLZ7.js";
21
21
  import {
22
22
  PostgresJobStore
23
- } from "./chunk-N4SBDUVD.js";
23
+ } from "./chunk-A6EY4K46.js";
24
24
  import {
25
25
  SIGNED_ENVELOPE_FIELDS,
26
26
  SignedRequestError,
@@ -28,7 +28,7 @@ import {
28
28
  humanizeZodIssues,
29
29
  signedRequestInput,
30
30
  zodIssuesOf
31
- } from "./chunk-V73WRF46.js";
31
+ } from "./chunk-5CCQEAQV.js";
32
32
  import {
33
33
  MemoryJobStore,
34
34
  UNCAPPABLE_ASK_TOTAL,
@@ -37,7 +37,7 @@ import {
37
37
  isJobRetentionStore,
38
38
  isReceiptIssuingStore,
39
39
  isStreamableJobStore
40
- } from "./chunk-V4CBI5HB.js";
40
+ } from "./chunk-Y6JJDOGO.js";
41
41
  import {
42
42
  ContentHoldError,
43
43
  JobCancelledError,
@@ -78,7 +78,7 @@ import {
78
78
  receiptPubkeyFromSecret,
79
79
  toLockPubkey,
80
80
  verifyChallenge
81
- } from "./chunk-XBQTFOIT.js";
81
+ } from "./chunk-7RWE6ELV.js";
82
82
  import {
83
83
  X402_BATCH_SETTLEMENT_SCHEME,
84
84
  X402_DEFAULT_FACILITATOR,
@@ -105,7 +105,7 @@ import {
105
105
  } from "./chunk-SXL44PL5.js";
106
106
  import {
107
107
  createConsoleLogger
108
- } from "./chunk-VQHBJXBL.js";
108
+ } from "./chunk-QTKGFG3I.js";
109
109
  import {
110
110
  StepCache
111
111
  } from "./chunk-BQJIFRAZ.js";
@@ -122,7 +122,7 @@ import {
122
122
  roundUsd,
123
123
  satsToFiat,
124
124
  validateCurrency
125
- } from "./chunk-7QGS27UT.js";
125
+ } from "./chunk-72A4PHLD.js";
126
126
  import {
127
127
  CreditLedger,
128
128
  CreditLedgerError,
@@ -138,7 +138,7 @@ import {
138
138
  isNonChannelBitcoinRail,
139
139
  netOwedSats,
140
140
  x402SettlementPending
141
- } from "./chunk-K4ZQE7IB.js";
141
+ } from "./chunk-B5MPLDRC.js";
142
142
  import {
143
143
  parseStoreValue
144
144
  } from "./chunk-YG7G4DPZ.js";
@@ -156,7 +156,7 @@ import {
156
156
  } from "./chunk-5UC7TOQC.js";
157
157
  import {
158
158
  withPostgresRecoveryContext
159
- } from "./chunk-ELNWQS24.js";
159
+ } from "./chunk-EIDRCGTY.js";
160
160
  import {
161
161
  DvmError,
162
162
  x402NetworkByCaip2
@@ -174,17 +174,17 @@ import {
174
174
  installHostedTempoCloseGuard,
175
175
  isTempoInsufficientFundsError,
176
176
  resolveTempoNetwork
177
- } from "./chunk-7N5VDCG3.js";
177
+ } from "./chunk-MWDUGMN2.js";
178
178
  import {
179
179
  loggers,
180
180
  pinDimension,
181
181
  withSpan,
182
182
  withTraceContext
183
- } from "./chunk-TN6MWTZI.js";
183
+ } from "./chunk-LY2JYSJS.js";
184
184
  import {
185
185
  createDiagnosticCapabilityAuthority,
186
186
  diagnosticConsole
187
- } from "./chunk-5U2RG3ZW.js";
187
+ } from "./chunk-ALTE7I6C.js";
188
188
 
189
189
  // src/sdk/server/lock-pubkey-store.ts
190
190
  async function initLockPubkeyTable(db) {
@@ -428,13 +428,26 @@ async function insertAccumulatorRows(db, args) {
428
428
  client.release();
429
429
  }
430
430
  }
431
- async function hasAccumulatedProof(db, dvmId, secrets) {
432
- if (secrets.length === 0) return false;
431
+ async function claimAccumulatorRequestId(tx, dvmId, requestId) {
432
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
433
+ JSON.stringify(["wallet_accumulator_request", dvmId, requestId])
434
+ ]);
435
+ const { rows: rows2 } = await tx.query(
436
+ `SELECT 1 FROM wallet_accumulator WHERE dvm_id = $1 AND request_id = $2 LIMIT 1`,
437
+ [dvmId, requestId]
438
+ );
439
+ if (rows2.length > 0) throw new AccumulatorReplayError(dvmId, requestId);
440
+ }
441
+ async function acceptedRequestIds(db, dvmId, secrets) {
442
+ if (secrets.length === 0) return [];
443
+ const hashes = secrets.map(hashProofSecret);
433
444
  const { rows: rows2 } = await db.query(
434
- `SELECT 1 FROM wallet_accumulator WHERE dvm_id = $1 AND proof_secret_hash = ANY($2::text[]) LIMIT 1`,
435
- [dvmId, secrets.map(hashProofSecret)]
445
+ `SELECT proof_secret_hash, request_id FROM wallet_accumulator
446
+ WHERE dvm_id = $1 AND proof_secret_hash = ANY($2::text[])`,
447
+ [dvmId, hashes]
436
448
  );
437
- return rows2.length > 0;
449
+ const byHash = new Map(rows2.map((row) => [row.proof_secret_hash, row.request_id]));
450
+ return hashes.map((hash3) => byHash.get(hash3));
438
451
  }
439
452
  async function listPendingBatches(db, dvmId) {
440
453
  const { rows: rows2 } = await db.query(
@@ -1161,6 +1174,9 @@ function pinAskFiat(args) {
1161
1174
  return { currency: args.currency, amountMicro: Math.max(1, rounded) };
1162
1175
  }
1163
1176
  function fundedMicroFor(priceFiat, paidMsats, requiredMsats) {
1177
+ if (!Number.isFinite(requiredMsats) || requiredMsats <= 0) {
1178
+ throw new Error(`fundedMicroFor needs a positive reference price in msats (got ${requiredMsats})`);
1179
+ }
1164
1180
  if (paidMsats === requiredMsats) return priceFiat.amountMicro;
1165
1181
  return Math.round(priceFiat.amountMicro * paidMsats / requiredMsats);
1166
1182
  }
@@ -3234,7 +3250,7 @@ function paymentFailureFields(error) {
3234
3250
  }
3235
3251
 
3236
3252
  // src/sdk/server/payment.ts
3237
- import { createHash as createHash4, randomBytes, randomUUID as randomUUID2 } from "crypto";
3253
+ import { createHash as createHash5, randomBytes, randomUUID as randomUUID2 } from "crypto";
3238
3254
  import { Challenge } from "mppx";
3239
3255
 
3240
3256
  // src/lib/cashu/verify.ts
@@ -3328,6 +3344,63 @@ function methodNativeAmountToDecimal(amount, methodName) {
3328
3344
  return `${nativeAmount2 / scale}.${(nativeAmount2 % scale).toString().padStart(decimals, "0")}`;
3329
3345
  }
3330
3346
 
3347
+ // src/sdk/server/cashu-payment-request.ts
3348
+ import { createHash as createHash4 } from "crypto";
3349
+ import { PaymentRequest } from "@cashu/cashu-ts";
3350
+ function cashuPaymentRequest(args) {
3351
+ return new PaymentRequest(
3352
+ void 0,
3353
+ void 0,
3354
+ Math.ceil(args.requiredMsats / 1e3),
3355
+ "sat",
3356
+ args.mints,
3357
+ void 0,
3358
+ false,
3359
+ { kind: "P2PK", data: args.lockPubkey, tags: [] }
3360
+ ).toEncodedCreqA();
3361
+ }
3362
+ function proofSetRequestId(secrets) {
3363
+ const canonical = JSON.stringify([...secrets].sort());
3364
+ return `${DERIVED_REQUEST_ID_PREFIX}${createHash4("sha256").update(canonical).digest("hex")}`;
3365
+ }
3366
+ var DERIVED_REQUEST_ID_PREFIX = "nut24-";
3367
+ function withFxHistory(inner, opts = {}) {
3368
+ const holdWindowMs = opts.holdWindowMs ?? FX_HOLD_WINDOW_MS;
3369
+ const now = opts.now ?? Date.now;
3370
+ const history = [];
3371
+ return {
3372
+ async fetch() {
3373
+ const snapshot = await inner.fetch();
3374
+ const latest = history.at(-1);
3375
+ if (latest && sameRates(latest.snapshot, snapshot)) {
3376
+ latest.lastPricedAt = now();
3377
+ } else {
3378
+ history.push({ snapshot, lastPricedAt: now() });
3379
+ if (history.length > 4) history.shift();
3380
+ }
3381
+ return snapshot;
3382
+ },
3383
+ supportedCurrencies: () => inner.supportedCurrencies(),
3384
+ ...inner.lastKnown && { lastKnown: inner.lastKnown.bind(inner) },
3385
+ previousFor(pricedAt) {
3386
+ for (let i = history.length - 1; i > 0; i--) {
3387
+ if (!sameRates(history[i].snapshot, pricedAt)) continue;
3388
+ const replaced = history[i - 1];
3389
+ return now() - replaced.lastPricedAt <= holdWindowMs ? replaced.snapshot : void 0;
3390
+ }
3391
+ return void 0;
3392
+ }
3393
+ };
3394
+ }
3395
+ function sameRates(a, b) {
3396
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a.rates), ...Object.keys(b.rates)]);
3397
+ for (const key of keys) {
3398
+ if (a.rates[key] !== b.rates[key]) return false;
3399
+ }
3400
+ return true;
3401
+ }
3402
+ var FX_HOLD_WINDOW_MS = 10 * 6e4;
3403
+
3331
3404
  // src/lib/mints.ts
3332
3405
  function isTestMintUrl(url) {
3333
3406
  let host;
@@ -3459,13 +3532,20 @@ async function acceptAccumulatorPayment(opts) {
3459
3532
  if (opts.disposable && !opts.mints.every(isTestMintUrl)) {
3460
3533
  throw new Error("Disposable Cashu accepts only known test or loopback mints.");
3461
3534
  }
3462
- if (!opts.requestId) {
3535
+ if (!opts.requestId && !opts.deriveRequestId) {
3463
3536
  return {
3464
3537
  kind: "rejected",
3465
3538
  code: "missing_request_id",
3466
3539
  message: "X-Cashu-Request-Id header required for the agent-wallet path."
3467
3540
  };
3468
3541
  }
3542
+ if (opts.requestId?.startsWith(DERIVED_REQUEST_ID_PREFIX)) {
3543
+ return {
3544
+ kind: "rejected",
3545
+ code: "reserved_request_id",
3546
+ message: `A Cashu request id may not start with "${DERIVED_REQUEST_ID_PREFIX}".`
3547
+ };
3548
+ }
3469
3549
  if (opts.lockPubkeys.length === 0) {
3470
3550
  return {
3471
3551
  kind: "rejected",
@@ -3486,6 +3566,9 @@ async function acceptAccumulatorPayment(opts) {
3486
3566
  if (meta.unit !== "sat" || proofs.length === 0) {
3487
3567
  throw new Error("Cashu payment requires nonempty sat proofs.");
3488
3568
  }
3569
+ if (new Set(proofs.map((proof) => proof.secret)).size !== proofs.length) {
3570
+ throw new Error("Cashu payment carries the same proof more than once.");
3571
+ }
3489
3572
  for (const proof of proofs) {
3490
3573
  if (!proof.dleq || !verifyDleqIfPresent(proof, wallet.getKeyset(proof.id))) {
3491
3574
  throw new Error(
@@ -3501,10 +3584,12 @@ async function acceptAccumulatorPayment(opts) {
3501
3584
  message: err instanceof Error ? err.message : "Cashu token decode failed"
3502
3585
  };
3503
3586
  }
3587
+ const requestId = opts.requestId || proofSetRequestId(proofs.map((proof) => proof.secret));
3504
3588
  const acceptedValue = {
3505
3589
  ...valuation,
3506
3590
  mintUrl,
3507
- paidMsats: valuation.netMsats
3591
+ paidMsats: valuation.netMsats,
3592
+ requestId
3508
3593
  };
3509
3594
  const nowSec = Math.floor(Date.now() / 1e3);
3510
3595
  const tExpireMsByIndex = [];
@@ -3521,9 +3606,8 @@ async function acceptAccumulatorPayment(opts) {
3521
3606
  tExpireMsByIndex.push(parseNut11LocktimeMs(p.secret));
3522
3607
  }
3523
3608
  if (belowFloorMessage) {
3524
- const secrets = proofs.map((p) => p.secret);
3525
- const banked = opts.disposable ? await opts.disposable.hasAccepted(secrets) : opts.db ? await hasAccumulatedProof(opts.db, opts.dvmId, secrets) : false;
3526
- if (banked) return replayRejected();
3609
+ const banked = await acceptedUnder(opts, proofs);
3610
+ if (banked.some((id) => id !== void 0)) return replayRejected(banked, requestId);
3527
3611
  return { kind: "rejected", code: "lock_not_exclusive", message: belowFloorMessage };
3528
3612
  }
3529
3613
  try {
@@ -3567,23 +3651,24 @@ async function acceptAccumulatorPayment(opts) {
3567
3651
  lockPubkeyByIndex: matchedLockPubkeyByIndex,
3568
3652
  mintUrl,
3569
3653
  proofs,
3570
- requestId: opts.requestId,
3654
+ requestId,
3571
3655
  tExpireMsByIndex
3572
3656
  };
3573
3657
  try {
3574
3658
  if (opts.disposable) {
3575
3659
  const account = opts.withDisposableLedger;
3576
3660
  if (!account) throw new Error("Disposable Cashu requires atomic credit accounting.");
3577
- await opts.disposable.commit(opts.requestId, proofs, (ledger) => account(ledger, acceptedValue));
3661
+ await opts.disposable.commit(requestId, proofs, (ledger) => account(ledger, acceptedValue));
3578
3662
  } else if (!opts.db) {
3579
3663
  throw new Error("Cashu accumulator requires a storage backend.");
3580
- } else if (opts.withCommitTx) {
3581
- const withCommitTx = opts.withCommitTx;
3664
+ } else {
3582
3665
  const client = await opts.db.connect();
3583
3666
  try {
3584
3667
  await client.query("BEGIN");
3668
+ await client.query("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
3669
+ await claimAccumulatorRequestId(client, opts.dvmId, requestId);
3585
3670
  await insertAccumulatorRows(opts.db, { ...insertArgs, tx: client });
3586
- await withCommitTx(client, acceptedValue);
3671
+ await opts.withCommitTx?.(client, acceptedValue);
3587
3672
  await client.query("COMMIT");
3588
3673
  } catch (err) {
3589
3674
  try {
@@ -3594,11 +3679,13 @@ async function acceptAccumulatorPayment(opts) {
3594
3679
  } finally {
3595
3680
  client.release();
3596
3681
  }
3597
- } else {
3598
- await insertAccumulatorRows(opts.db, insertArgs);
3599
3682
  }
3600
3683
  } catch (err) {
3601
- if (err instanceof AccumulatorReplayError) return replayRejected();
3684
+ if (err instanceof AccumulatorReplayError) {
3685
+ const banked = await acceptedUnder(opts, proofs);
3686
+ if (banked.every((id) => id === void 0)) return requestIdReused(requestId);
3687
+ return replayRejected(banked, requestId);
3688
+ }
3602
3689
  throw err;
3603
3690
  }
3604
3691
  return {
@@ -3607,14 +3694,33 @@ async function acceptAccumulatorPayment(opts) {
3607
3694
  grossMsats: valuation.grossMsats,
3608
3695
  inputFeeMsats: valuation.inputFeeMsats,
3609
3696
  netMsats: valuation.netMsats,
3610
- mintUrl
3697
+ mintUrl,
3698
+ requestId
3611
3699
  };
3612
3700
  }
3613
- function replayRejected() {
3701
+ async function acceptedUnder(opts, proofs) {
3702
+ const secrets = proofs.map((p) => p.secret);
3703
+ if (opts.disposable) return await opts.disposable.acceptedRequestIds(secrets);
3704
+ if (opts.db) return await acceptedRequestIds(opts.db, opts.dvmId, secrets);
3705
+ return secrets.map(() => void 0);
3706
+ }
3707
+ function requestIdReused(requestId) {
3708
+ return {
3709
+ kind: "rejected",
3710
+ code: "request_id_reused",
3711
+ requestId,
3712
+ message: "This request id already carries a different payment at this DVM; these proofs were not accepted."
3713
+ };
3714
+ }
3715
+ function replayRejected(accepted, requestId) {
3716
+ const [first] = accepted;
3717
+ const bound = first !== void 0 && accepted.every((id) => id === first);
3614
3718
  return {
3615
3719
  kind: "rejected",
3616
3720
  code: "replay_rejected",
3617
- message: "Proofs or request id already accepted at this DVM."
3721
+ message: "Proofs or request id already accepted at this DVM.",
3722
+ requestId,
3723
+ ...bound && { acceptedRequestId: first }
3618
3724
  };
3619
3725
  }
3620
3726
  function parseNut11LocktimeMs(secret) {
@@ -3850,7 +3956,7 @@ async function verifyUpfrontPaymentInScope(opts) {
3850
3956
  }
3851
3957
  };
3852
3958
  }
3853
- if (opts.cashuMode === "p2pk-accumulator" && (opts.accumulatorDb || opts.disposableCashu) && opts.dvmId && opts.lockPubkeys && opts.lockPubkeys.length > 0) {
3959
+ if (cashuAccumulatorReady(opts)) {
3854
3960
  return verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx);
3855
3961
  }
3856
3962
  return {
@@ -4014,7 +4120,7 @@ async function verifyUpfrontPaymentInScope(opts) {
4014
4120
  x402Requirements
4015
4121
  });
4016
4122
  }
4017
- const { verifyX402Payment } = await import("./x402-6T73AZQ6.js");
4123
+ const { verifyX402Payment } = await import("./x402-HVCUEWWU.js");
4018
4124
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
4019
4125
  x402Payment,
4020
4126
  x402Config,
@@ -4469,6 +4575,8 @@ async function verifyUpfrontPaymentInScope(opts) {
4469
4575
  opts.fundOnly?.allowOneShotStablecoin,
4470
4576
  opts.priceFiat
4471
4577
  ) : void 0;
4578
+ const lockPubkey = opts.lockPubkeys?.[0];
4579
+ const cashuRequest = opts.nut24 && opts.nut24.mints.length > 0 && !opts.fundOnly && cashuAccumulatorReady(opts) && lockPubkey ? cashuPaymentRequest({ requiredMsats, mints: opts.nut24.mints, lockPubkey }) : void 0;
4472
4580
  return {
4473
4581
  paidMsats: 0,
4474
4582
  receivedProofs: [],
@@ -4479,6 +4587,7 @@ async function verifyUpfrontPaymentInScope(opts) {
4479
4587
  ...mints && { mints }
4480
4588
  },
4481
4589
  status: 402,
4590
+ ...cashuRequest && { cashuPaymentRequest: cashuRequest },
4482
4591
  ...mppChallenges && { mppChallenges },
4483
4592
  ...x402Requirements && { x402Requirements },
4484
4593
  ...x402BatchRequirements && { x402V2Requirements: x402BatchRequirements }
@@ -4612,6 +4721,11 @@ var PAYMENT_ERROR_COPY = {
4612
4721
  hint: "Request a fresh quote and pay again \u2014 the previous payment can't be reused.",
4613
4722
  retryable: false
4614
4723
  },
4724
+ payment_method_requires_signature: {
4725
+ display: "This service accepts unsigned requests only when they are paid with Cashu, so this payment was not applied to the job.",
4726
+ hint: "Pay with Cashu to call without signing, or sign the request (secp256k1 Schnorr) to use another payment method.",
4727
+ retryable: false
4728
+ },
4615
4729
  x402_not_offered: {
4616
4730
  display: "This service does not offer x402 payment for prices in its configured currency. Nothing was charged.",
4617
4731
  hint: "Use one of the payment methods advertised by this service and submit the request again.",
@@ -4837,6 +4951,11 @@ var PAYMENT_ERROR_COPY = {
4837
4951
  hint: "Include the X-Cashu-Request-Id header and pay again.",
4838
4952
  retryable: false
4839
4953
  },
4954
+ reserved_request_id: {
4955
+ display: "The payment's request identifier isn't allowed, so the payment wasn't processed. Nothing was charged.",
4956
+ hint: 'Send a fresh UUIDv4 as X-Cashu-Request-Id. Ids starting with "nut24-" are reserved for payments sent without one.',
4957
+ retryable: false
4958
+ },
4840
4959
  missing_lock_pubkey: {
4841
4960
  display: "This service can't accept the payment right now. Nothing was charged.",
4842
4961
  hint: "The service is missing payment configuration; retrying won't help. Contact the service operator.",
@@ -4903,6 +5022,11 @@ var PAYMENT_ERROR_COPY = {
4903
5022
  // agent could follow straight into paying twice for the same charge.
4904
5023
  hint: "Don't re-send this payment \u2014 the proofs already landed under this request id. Only a genuinely new charge needs a fresh token and request id.",
4905
5024
  retryable: false
5025
+ },
5026
+ request_id_reused: {
5027
+ display: "This payment was not accepted and nothing was charged.",
5028
+ hint: "Each X-Cashu-Request-Id belongs to the first payment made under it. Send these proofs again under a new request id.",
5029
+ retryable: false
4906
5030
  }
4907
5031
  };
4908
5032
  function paymentErrorBody(code, opts) {
@@ -5044,7 +5168,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
5044
5168
  snapshot
5045
5169
  });
5046
5170
  }
5047
- const { verifyX402Payment } = await import("./x402-6T73AZQ6.js");
5171
+ const { verifyX402Payment } = await import("./x402-HVCUEWWU.js");
5048
5172
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
5049
5173
  body.content.x402_payment,
5050
5174
  opts.x402Config,
@@ -5495,7 +5619,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
5495
5619
  try {
5496
5620
  const requiredMsats = job.pendingPaymentMsats ?? 0;
5497
5621
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
5498
- const { verifyX402Payment } = await import("./x402-6T73AZQ6.js");
5622
+ const { verifyX402Payment } = await import("./x402-HVCUEWWU.js");
5499
5623
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
5500
5624
  body.content.x402_payment,
5501
5625
  opts.x402Config,
@@ -5620,6 +5744,16 @@ function creditPendingPayment(job) {
5620
5744
  job.pendingPaymentMsats = void 0;
5621
5745
  }
5622
5746
  }
5747
+ function cashuAccumulatorReady(opts) {
5748
+ return opts.cashuMode === "p2pk-accumulator" && Boolean(opts.accumulatorDb || opts.disposableCashu) && Boolean(opts.dvmId) && (opts.lockPubkeys?.length ?? 0) > 0;
5749
+ }
5750
+ var NUT24_PAYER_FIXABLE = /* @__PURE__ */ new Set([
5751
+ "invalid_token",
5752
+ "not_p2pk_locked",
5753
+ "wrong_lock_pubkey",
5754
+ "lock_not_exclusive",
5755
+ "payment_insufficient"
5756
+ ]);
5623
5757
  async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5624
5758
  const { cashuToken, accumulatorDb, dvmId, lockPubkeys, mintHealthTracker } = opts;
5625
5759
  if (!cashuToken || !accumulatorDb && !opts.disposableCashu || !dvmId || !lockPubkeys || lockPubkeys.length === 0) {
@@ -5668,7 +5802,14 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5668
5802
  };
5669
5803
  }
5670
5804
  let credited;
5671
- const requestId = opts.requestId;
5805
+ const headerless = !opts.requestId && opts.nut24 !== void 0;
5806
+ const previousRequired = headerless ? opts.nut24?.previousRequiredMsats : void 0;
5807
+ const heldAt = (paidMsats) => requiredMsats !== void 0 && paidMsats < requiredMsats && previousRequired !== void 0 && paidMsats >= previousRequired ? previousRequired : void 0;
5808
+ const requiredFor = (paidMsats) => heldAt(paidMsats) ?? requiredMsats;
5809
+ const held = (ctx, paidMsats) => {
5810
+ const previous = heldAt(paidMsats);
5811
+ return previous === void 0 ? ctx : { ...ctx, requiredMsats: previous };
5812
+ };
5672
5813
  const signedFund = ledgerCtx?.creditEnvelope?.fund;
5673
5814
  const beforeCommit = ledgerCtx && signedFund ? (accepted) => {
5674
5815
  const expected = fundedMicroFor(
@@ -5680,12 +5821,12 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5680
5821
  throw new CashuFundingAmountMismatchError(signedFund.amountMicro, expected);
5681
5822
  }
5682
5823
  } : void 0;
5683
- const withCommitTx = ledgerCtx && requestId ? async (tx, accepted) => {
5824
+ const withCommitTx = ledgerCtx ? async (tx, accepted) => {
5684
5825
  credited = await fundAndDraw(
5685
- ledgerCtx,
5826
+ held(ledgerCtx, accepted.paidMsats),
5686
5827
  {
5687
5828
  rail: "cashu",
5688
- paymentId: requestId,
5829
+ paymentId: accepted.requestId,
5689
5830
  paidMsats: accepted.paidMsats,
5690
5831
  grossPaidMsats: accepted.grossMsats,
5691
5832
  cashuInputFeeMsats: accepted.inputFeeMsats,
@@ -5699,13 +5840,14 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5699
5840
  result = await acceptAccumulatorPayment({
5700
5841
  cashuToken,
5701
5842
  requestId: opts.requestId,
5843
+ deriveRequestId: headerless,
5702
5844
  mints,
5703
5845
  db: accumulatorDb,
5704
5846
  disposable: opts.disposableCashu,
5705
- withDisposableLedger: ledgerCtx && requestId ? async (ledger, accepted) => {
5706
- credited = await fundAndDraw({ ...ledgerCtx, ledger }, {
5847
+ withDisposableLedger: ledgerCtx ? async (ledger, accepted) => {
5848
+ credited = await fundAndDraw({ ...held(ledgerCtx, accepted.paidMsats), ledger }, {
5707
5849
  rail: "cashu",
5708
- paymentId: requestId,
5850
+ paymentId: accepted.requestId,
5709
5851
  paidMsats: accepted.paidMsats,
5710
5852
  grossPaidMsats: accepted.grossMsats,
5711
5853
  cashuInputFeeMsats: accepted.inputFeeMsats,
@@ -5729,24 +5871,35 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5729
5871
  throw err;
5730
5872
  }
5731
5873
  if (result.kind === "rejected") {
5732
- const status = result.code === "replay_rejected" ? 409 : 402;
5874
+ const status = result.code === "replay_rejected" || result.code === "request_id_reused" ? 409 : result.code === "reserved_request_id" || headerless && NUT24_PAYER_FIXABLE.has(result.code) ? 400 : 402;
5875
+ const requestId = result.requestId;
5733
5876
  return {
5734
5877
  paidMsats: 0,
5735
5878
  receivedProofs: [],
5736
5879
  error: {
5737
- body: paymentErrorBody(result.code, { message: result.message, extra: { mints } }),
5880
+ body: paymentErrorBody(result.code, {
5881
+ message: result.message,
5882
+ // Without a request id the proof set is the payment's identity, so
5883
+ // only these exact proofs with the original body recover its job.
5884
+ ...headerless && result.code === "replay_rejected" && {
5885
+ hint: "These proofs already paid for an earlier request. Resend them with that request's exact body to fetch its job; a new request needs new proofs."
5886
+ },
5887
+ extra: { mints }
5888
+ }),
5738
5889
  status,
5739
- // The accumulator keys replay on the request id, which is also the
5740
- // funding id a mid-job top-up committed under, so the route can hand
5741
- // back the original draw funding evidence beside the 409.
5742
- ...status === 409 && requestId && { replayPaymentId: requestId }
5890
+ // The request id is also the funding id a mid-job top-up committed
5891
+ // under and the job's payment reference, so the route can return the
5892
+ // original evidence beside the 409. It is passed on only when every
5893
+ // presented proof was accepted under this same request id.
5894
+ ...requestId && result.acceptedRequestId === requestId && { replayPaymentId: requestId }
5743
5895
  }
5744
5896
  };
5745
5897
  }
5746
5898
  if (credited?.drawRefusal) {
5747
5899
  return creditLedgerErrorResult(credited.drawRefusal, void 0, credited.deposit);
5748
5900
  }
5749
- if (requiredMsats && result.paidMsats < requiredMsats && !ledgerCtx?.fundOnly && !ledgerCtx?.creditEnvelope) {
5901
+ const heldRequired = requiredFor(result.paidMsats);
5902
+ if (heldRequired && result.paidMsats < heldRequired && !ledgerCtx?.fundOnly && !ledgerCtx?.creditEnvelope) {
5750
5903
  let forfeit;
5751
5904
  if (credited?.draw && ledgerCtx && !ledgerCtx.midJob) {
5752
5905
  const drawId = credited.draw.drawId;
@@ -5768,36 +5921,39 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5768
5921
  });
5769
5922
  }
5770
5923
  }
5771
- const held = ledgerCtx?.midJob ? credited : void 0;
5924
+ const held2 = ledgerCtx?.midJob ? credited : void 0;
5772
5925
  return {
5773
5926
  paidMsats: 0,
5774
5927
  receivedProofs: [],
5775
5928
  error: {
5776
5929
  body: paymentErrorBody("payment_insufficient", {
5777
5930
  message: `Payment of ${result.paidMsats} msats is short of the ${requiredMsats} msats required.`,
5778
- ...held && {
5931
+ ...held2 && {
5779
5932
  // Per-code copy can't say where the money went, and this is the one
5780
5933
  // site where that is the caller's next action.
5781
- hint: `The ${result.paidMsats} msats already sent are held on credit ${held.creditId} and do not reduce what is owed \u2014 retry with a single payment of required_msats, or reclaim the held balance with op "drain" at POST /v1/credit.`
5934
+ hint: `The ${result.paidMsats} msats already sent are held on credit ${held2.creditId} and do not reduce what is owed \u2014 retry with a single payment of required_msats, or reclaim the held balance with op "drain" at POST /v1/credit.`
5935
+ },
5936
+ ...!held2 && headerless && {
5937
+ hint: "The proofs already sent are kept and do not count toward this request. Pay again with new proofs worth required_msats after the mint's input fee."
5782
5938
  },
5783
5939
  extra: {
5784
5940
  required_msats: requiredMsats,
5785
5941
  received_msats: result.paidMsats,
5786
5942
  mints,
5787
- ...held && {
5788
- credit_id: held.creditId,
5789
- credited_micro: held.deposit.amountMicro,
5790
- credit_currency: held.deposit.currency
5943
+ ...held2 && {
5944
+ credit_id: held2.creditId,
5945
+ credited_micro: held2.deposit.amountMicro,
5946
+ credit_currency: held2.deposit.currency
5791
5947
  }
5792
5948
  }
5793
5949
  }),
5794
- status: 402,
5950
+ status: headerless ? 400 : 402,
5795
5951
  forfeit,
5796
5952
  // Report the deposit on the failure path too, or the platform never
5797
5953
  // learns about a liability it is carrying. Mutually exclusive with
5798
5954
  // `forfeit` by construction: one is money still owed, the other is
5799
5955
  // money nothing is owed against.
5800
- ...held && { funding: held.deposit }
5956
+ ...held2 && { funding: held2.deposit }
5801
5957
  }
5802
5958
  };
5803
5959
  }
@@ -5807,13 +5963,11 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5807
5963
  paymentMint: result.mintUrl,
5808
5964
  paymentRail: "cashu",
5809
5965
  receivedProofs: [],
5810
- // requestId is non-null here because acceptAccumulatorPayment returns
5811
- // kind:"rejected" with code "missing_request_id" when unset (handled by
5812
- // the rejected branch above). Thread it as the per-call settlement
5813
- // reference for revenue_events.tx_hash. Unique per
5814
- // (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint, so
5815
- // it serves as a stable id even though it isn't an on-chain hash.
5816
- paymentTxHash: opts.requestId,
5966
+ // The request id the proofs were banked under, supplied or derived, is
5967
+ // the per-call settlement reference for revenue_events.tx_hash. Unique
5968
+ // per (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint,
5969
+ // so it serves as a stable id even though it isn't an on-chain hash.
5970
+ paymentTxHash: result.requestId,
5817
5971
  cashuFlow: "p2pk_accumulator"
5818
5972
  },
5819
5973
  ledgerCtx,
@@ -5889,7 +6043,7 @@ async function resolveTopUpLedgerContext(opts, snapshot, binding) {
5889
6043
  draw_id: snapshot.drawId,
5890
6044
  ...extra,
5891
6045
  ...typeof opts.jobId === "string" && {
5892
- job_id_hash: createHash4("sha256").update(opts.jobId, "utf8").digest("hex")
6046
+ job_id_hash: createHash5("sha256").update(opts.jobId, "utf8").digest("hex")
5893
6047
  }
5894
6048
  }
5895
6049
  );
@@ -9592,7 +9746,7 @@ async function handle(c, opts, action) {
9592
9746
  }
9593
9747
 
9594
9748
  // src/sdk/server/x402-exact-settlement.ts
9595
- import { createHash as createHash5 } from "crypto";
9749
+ import { createHash as createHash6 } from "crypto";
9596
9750
  var X402ExactSettlementServer = class {
9597
9751
  constructor(opts) {
9598
9752
  this.opts = opts;
@@ -9918,7 +10072,7 @@ function settlementMayHaveMoved(response) {
9918
10072
  return reason.includes("authorization_already_used") || reason.includes("authorization already used") || /facilitator_status_5\d\d/.test(reason);
9919
10073
  }
9920
10074
  function hash(parts) {
9921
- return createHash5("sha256").update(JSON.stringify(parts)).digest("hex");
10075
+ return createHash6("sha256").update(JSON.stringify(parts)).digest("hex");
9922
10076
  }
9923
10077
  function safeError(err) {
9924
10078
  if (err instanceof Error) return err.name;
@@ -9926,7 +10080,7 @@ function safeError(err) {
9926
10080
  }
9927
10081
 
9928
10082
  // src/sdk/server/hosting-settlement-report-obligations.ts
9929
- import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
10083
+ import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
9930
10084
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
9931
10085
  import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS3 } from "@x402/evm";
9932
10086
  import { computeChannelId as computeChannelId2 } from "@x402/evm/batch-settlement/client";
@@ -9999,7 +10153,7 @@ var TEMPO_CLOSED_EVENT = TempoSession.Precompile.escrowAbi.find(
9999
10153
  (item) => item.type === "event" && item.name === "ChannelClosed"
10000
10154
  );
10001
10155
  function hash2(value) {
10002
- return createHash6("sha256").update(JSON.stringify(value)).digest("hex");
10156
+ return createHash7("sha256").update(JSON.stringify(value)).digest("hex");
10003
10157
  }
10004
10158
  function hex(value, bytes) {
10005
10159
  return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${bytes * 2}}$`).test(value);
@@ -11387,7 +11541,7 @@ var HostingCleanupFence = class {
11387
11541
  };
11388
11542
 
11389
11543
  // src/sdk/server/hosting-channel-evidence.ts
11390
- import { createHash as createHash7 } from "crypto";
11544
+ import { createHash as createHash8 } from "crypto";
11391
11545
  import { Session as TempoSession2 } from "mppx/tempo";
11392
11546
  import { createPublicClient as createPublicClient5, http as http5 } from "viem";
11393
11547
  import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS4 } from "@x402/evm";
@@ -11438,7 +11592,7 @@ async function readHostedChannelSnapshot(query) {
11438
11592
  };
11439
11593
  const x402HistoryComplete = await history(names[6]);
11440
11594
  const tempoHistoryComplete = await history(names[7]);
11441
- const digest = createHash7("sha256").update(JSON.stringify({
11595
+ const digest = createHash8("sha256").update(JSON.stringify({
11442
11596
  present: [...exists].sort(),
11443
11597
  x402,
11444
11598
  x402Tombstones,
@@ -12588,9 +12742,9 @@ import { AsyncLocalStorage } from "async_hooks";
12588
12742
  import { randomBytes as randomBytes3 } from "crypto";
12589
12743
 
12590
12744
  // src/sdk/server/instrumented-kv.ts
12591
- import { createHash as createHash8 } from "crypto";
12745
+ import { createHash as createHash9 } from "crypto";
12592
12746
  function hashKey(key) {
12593
- return createHash8("sha256").update(key).digest("hex").slice(0, 16);
12747
+ return createHash9("sha256").update(key).digest("hex").slice(0, 16);
12594
12748
  }
12595
12749
  function instrumentKVStore(inner) {
12596
12750
  return {
@@ -12904,7 +13058,7 @@ function buildContext(job, opts) {
12904
13058
  let x402Nonce;
12905
13059
  let x402AmountUsdcMicro;
12906
13060
  const hasExplicitX402 = Boolean(payOpts?.x402);
12907
- if ((opts.pricingCurrency ?? "usd") === "usd") {
13061
+ if (!opts.cashuOnly && (opts.pricingCurrency ?? "usd") === "usd") {
12908
13062
  if (payOpts?.x402) {
12909
13063
  paymentRequestContent.x402 = payOpts.x402;
12910
13064
  } else if (opts.paymentMethods?.includes("x402") && opts.x402 && (opts.x402FacilitatorHealth?.support().available ?? true)) {
@@ -12943,7 +13097,8 @@ function buildContext(job, opts) {
12943
13097
  job.pendingX402Nonce = x402Nonce;
12944
13098
  job.pendingX402AmountUsdcMicro = x402AmountUsdcMicro;
12945
13099
  let mppChallengeIds;
12946
- if (payOpts?.mpp) {
13100
+ if (opts.cashuOnly) {
13101
+ } else if (payOpts?.mpp) {
12947
13102
  const challenges = payOpts.mpp.challenges.filter((c) => c.intent !== "session");
12948
13103
  if (challenges.length > 0) {
12949
13104
  paymentRequestContent.tempo = { challenges };
@@ -13351,6 +13506,39 @@ function resultHashFor(record) {
13351
13506
  return computeResultHash(summary, artifacts);
13352
13507
  }
13353
13508
 
13509
+ // src/sdk/server/unsigned-request.ts
13510
+ function takesUnsignedPath(auth, body) {
13511
+ if (!auth?.allowUnsignedPaid) return false;
13512
+ const raw = signedRequestInput(body);
13513
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return true;
13514
+ return !SIGNED_ENVELOPE_FIELDS.some((field) => field in raw);
13515
+ }
13516
+ function carriesCreditFields(raw) {
13517
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return false;
13518
+ return CREDIT_ENVELOPE_KEYS.some((field) => field in raw);
13519
+ }
13520
+ function unsignedInputSchema(schema) {
13521
+ if (narrowed.has(schema)) return narrowed.get(schema);
13522
+ const result = narrow(schema);
13523
+ narrowed.set(schema, result);
13524
+ return result;
13525
+ }
13526
+ var narrowed = /* @__PURE__ */ new WeakMap();
13527
+ function narrow(schema) {
13528
+ const candidate = schema;
13529
+ const def = candidate._zod?.def;
13530
+ if (def?.type !== "object" || typeof candidate.clone !== "function" || candidate.shape === null || typeof candidate.shape !== "object") {
13531
+ return void 0;
13532
+ }
13533
+ const shape = { ...candidate.shape };
13534
+ for (const field of SIGNED_ENVELOPE_FIELDS) delete shape[field];
13535
+ try {
13536
+ return candidate.clone({ ...def, shape });
13537
+ } catch {
13538
+ return void 0;
13539
+ }
13540
+ }
13541
+
13354
13542
  // src/lib/cashu/mint-info.ts
13355
13543
  async function getKeysetFinalExpiry(mintUrl, keysetId) {
13356
13544
  try {
@@ -13814,11 +14002,12 @@ var JobManager = class {
13814
14002
  * `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
13815
14003
  * Returns `undefined` for dynamic-priced capabilities (those that declare
13816
14004
  * `onQuote`) — the caller falls back to `computeDynamicPrice` in that path.
14005
+ * A caller that already holds the snapshot it prices at passes it as `fx`.
13817
14006
  */
13818
- async currentPriceMsats(capability) {
14007
+ async currentPriceMsats(capability, fx) {
13819
14008
  const cap = this.requireCapability(capability);
13820
14009
  if (cap.price === void 0) return void 0;
13821
- return resolveStaticPriceMsats(cap.price, this.opts.fxFetcher);
14010
+ return resolveStaticPriceMsats(cap.price, fx ?? await this.opts.fxFetcher.fetch());
13822
14011
  }
13823
14012
  /**
13824
14013
  * Translate a capability's static `price` into the fiat envelope used by
@@ -13851,12 +14040,16 @@ var JobManager = class {
13851
14040
  * explicit draw as an unknown key, before the envelope was even extracted.
13852
14041
  * The persisted `record.input` the reactivation path re-reads is the
13853
14042
  * pre-Zod wire form, so it comes through the second branch carrying them too.
14043
+ *
14044
+ * `unsigned` validates against the schema without its envelope fields; pass
14045
+ * it only when {@link unsignedPaidInput} accepted the same body.
13854
14046
  */
13855
- parseInput(body, capability) {
14047
+ parseInput(body, capability, unsigned2 = false) {
13856
14048
  const cap = this.requireCapability(capability);
13857
14049
  const rawSchema = cap.input;
13858
14050
  if (!rawSchema || !isZodSchema(rawSchema)) return body.input ?? "";
13859
- const schema = rawSchema;
14051
+ const schema = unsigned2 ? unsignedInputSchema(rawSchema) : rawSchema;
14052
+ if (!schema) throw new Error(`capability "${capability}" cannot validate unsigned input`);
13860
14053
  if (body.data !== void 0) {
13861
14054
  return schema.parse(stripProtocolEnvelope(schema, body.data));
13862
14055
  }
@@ -13866,6 +14059,16 @@ var JobManager = class {
13866
14059
  }
13867
14060
  throw new MissingStructuredInputError();
13868
14061
  }
14062
+ /**
14063
+ * Whether this body takes the unsigned paid path: the auth scheme allows it,
14064
+ * the raw input carries no envelope field, and the capability schema can be
14065
+ * narrowed to exclude the envelope. Otherwise the signed path applies.
14066
+ */
14067
+ unsignedPaidInput(body, capability) {
14068
+ if (!takesUnsignedPath(this.descriptor.auth, body)) return false;
14069
+ const rawSchema = this.requireCapability(capability).input;
14070
+ return isZodSchema(rawSchema) && unsignedInputSchema(rawSchema) !== void 0;
14071
+ }
13869
14072
  /**
13870
14073
  * Look up the per-capability descriptor by name. Throws when the
13871
14074
  * name doesn't exist — the route layer validates body.capability first, so
@@ -15352,8 +15555,9 @@ var JobManager = class {
15352
15555
  * it re-verified clean.
15353
15556
  */
15354
15557
  reactivateJob(record) {
15355
- const parsedInput = this.parseInput({ input: record.input }, record.capability);
15356
- if (this.descriptor.auth) {
15558
+ const unsigned2 = record.authRequestPath === void 0 && record.requesterPubkey === void 0 && this.unsignedPaidInput({ input: record.input }, record.capability);
15559
+ const parsedInput = this.parseInput({ input: record.input }, record.capability, unsigned2);
15560
+ if (this.descriptor.auth && !unsigned2) {
15357
15561
  const cap = this.requireCapability(record.capability);
15358
15562
  if (!cap.input || !isZodSchema(cap.input)) {
15359
15563
  throw new ReactivationAuthSchemaError(record.id, record.capability);
@@ -15897,6 +16101,7 @@ var JobManager = class {
15897
16101
  store: this.opts.store,
15898
16102
  env: this.opts.env,
15899
16103
  paymentMethods: this.opts.paymentMethods,
16104
+ cashuOnly: Boolean(this.descriptor.auth?.allowUnsignedPaid) && job.requesterPubkey === void 0,
15900
16105
  x402: this.opts.x402,
15901
16106
  x402FacilitatorHealth: this.opts.x402FacilitatorHealth,
15902
16107
  mpp: this.opts.mpp,
@@ -16701,9 +16906,8 @@ function unappliedCopy(reason, creditId) {
16701
16906
  }
16702
16907
  }[reason];
16703
16908
  }
16704
- async function resolveStaticPriceMsats(price, fxFetcher) {
16909
+ function resolveStaticPriceMsats(price, fx) {
16705
16910
  const usd = parseUsdPrice(price, "price");
16706
- const fx = await fxFetcher.fetch();
16707
16911
  return fiatToSatsCeil(usd, fxRateFor(fx, "usd")) * 1e3;
16708
16912
  }
16709
16913
  function deriveAuthFromParsedInput(descriptor, parsedInput) {
@@ -17769,7 +17973,7 @@ function withWallClockDeadline(promise, timeoutMs) {
17769
17973
  }
17770
17974
 
17771
17975
  // src/sdk/server/balance-payment.ts
17772
- import { createHash as createHash9 } from "crypto";
17976
+ import { createHash as createHash10 } from "crypto";
17773
17977
  import { z as z2 } from "zod";
17774
17978
  var BALANCE_PAYMENT_SCHEMA = z2.object({
17775
17979
  credit_id: z2.string().min(1).max(256),
@@ -17799,7 +18003,7 @@ async function commitBalancePayment(args) {
17799
18003
  args.auth.recheckSignature(BALANCE_PAYMENT_SCHEMA, args.raw, args.domain);
17800
18004
  const auth = BALANCE_PAYMENT_SCHEMA.parse(args.raw);
17801
18005
  const { pubkey, credit_id, draw_id, payment_id, request_seq, amount_micro, currency } = auth;
17802
- const fingerprint = createHash9("sha256").update(JSON.stringify([
18006
+ const fingerprint = createHash10("sha256").update(JSON.stringify([
17803
18007
  args.domain,
17804
18008
  args.jobId,
17805
18009
  pubkey,
@@ -17923,7 +18127,7 @@ async function commitBalancePayment(args) {
17923
18127
  }
17924
18128
 
17925
18129
  // src/sdk/server/app.ts
17926
- import { createHash as createHash11, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
18130
+ import { createHash as createHash12, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
17927
18131
  import { Hono } from "hono";
17928
18132
  import { getCookie } from "hono/cookie";
17929
18133
  import { cors } from "hono/cors";
@@ -20974,7 +21178,7 @@ initSchemaForm();
20974
21178
  }
20975
21179
 
20976
21180
  // src/sdk/server/request-fingerprint.ts
20977
- import { createHash as createHash10 } from "crypto";
21181
+ import { createHash as createHash11 } from "crypto";
20978
21182
  function requestFingerprint(req, opts) {
20979
21183
  const canonical = canonicalize({
20980
21184
  capability: req.capability,
@@ -20982,7 +21186,7 @@ function requestFingerprint(req, opts) {
20982
21186
  params: req.params ?? {},
20983
21187
  data: opts.stripAuthEnvelope ? stripEnvelope(req.data) : req.data
20984
21188
  });
20985
- return createHash10("sha256").update(canonical).digest("hex");
21189
+ return createHash11("sha256").update(canonical).digest("hex");
20986
21190
  }
20987
21191
  var ENVELOPE_KEYS = /* @__PURE__ */ new Set(["pubkey", "signature", "timestamp", "nonce", "auth_statement"]);
20988
21192
  function stripEnvelope(data) {
@@ -21105,7 +21309,7 @@ async function createDVMServer(descriptor, opts) {
21105
21309
  }
21106
21310
  }
21107
21311
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
21108
- const { RevenueReporter } = await import("./revenue-reporter-2P3PWJJM.js");
21312
+ const { RevenueReporter } = await import("./revenue-reporter-6XARUEG6.js");
21109
21313
  const reporter = new RevenueReporter(
21110
21314
  opts.db,
21111
21315
  platformUrl,
@@ -21115,7 +21319,7 @@ async function createDVMServer(descriptor, opts) {
21115
21319
  revenueReporter = reporter;
21116
21320
  await reporter.init();
21117
21321
  reporter.startRetryLoop();
21118
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-P3652TLG.js");
21322
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-5QAHCYIO.js");
21119
21323
  payoutReporter = new PayoutReporter({
21120
21324
  dvmId,
21121
21325
  db: opts.db,
@@ -21396,7 +21600,9 @@ var DVMServer = class {
21396
21600
  this.consumedCredentialStore = opts.consumedCredentialStore ?? new MemoryConsumedCredentialStore();
21397
21601
  this.adminCashuNonceStore = opts.adminCashuNonceStore ?? createAdminCashuNonceStore();
21398
21602
  const fxEnv = opts.env;
21399
- this.fxFetcher = opts.fxFetcher ?? createFxFetcher({ source: resolveFxSourceFromEnv(fxEnv) });
21603
+ this.fxFetcher = withFxHistory(
21604
+ opts.fxFetcher ?? createFxFetcher({ source: resolveFxSourceFromEnv(fxEnv) })
21605
+ );
21400
21606
  const fxCurrencies = this.fxFetcher.supportedCurrencies();
21401
21607
  if (!fxCurrencies.includes(this.pricingCurrency)) {
21402
21608
  throw new Error(
@@ -21505,6 +21711,15 @@ var DVMServer = class {
21505
21711
  this.app = new Hono();
21506
21712
  try {
21507
21713
  this.registerRoutes();
21714
+ if (opts.mints?.length && !this.resolvePaymentMethods().includes("cashu")) {
21715
+ diagnosticConsole.warn({
21716
+ event: "cashu_not_advertised",
21717
+ cashu_explicitly_excluded: opts.paymentMethods !== void 0 && !opts.paymentMethods.includes("cashu"),
21718
+ cashu_mode_missing: opts.cashuMode !== "p2pk-accumulator",
21719
+ cashu_lock_pubkey_missing: !opts.lockPubkey,
21720
+ cashu_store_missing: !opts.db && !opts.disposableCashu
21721
+ });
21722
+ }
21508
21723
  if (opts.hostingControlInstance) {
21509
21724
  this.hostingSweepTimer = setInterval(() => {
21510
21725
  void this.sweepHostingDeadline().catch((error) => {
@@ -21714,7 +21929,7 @@ var DVMServer = class {
21714
21929
  })
21715
21930
  );
21716
21931
  if (this.opts.devMode) {
21717
- this.app.use("*", cors());
21932
+ this.app.use("*", cors({ exposeHeaders: ["X-Cashu"] }));
21718
21933
  this.app.get("/_dev", (c) => c.html(devPageHtml(this.descriptor.name)));
21719
21934
  this.app.all("/_proxy/*", (c) => this.handleProxy(c));
21720
21935
  }
@@ -22028,7 +22243,7 @@ var DVMServer = class {
22028
22243
  const dvmId = this.opts.dvmId ?? `dev:${this.descriptor.name}`;
22029
22244
  return {
22030
22245
  dvm_id: dvmId,
22031
- builder_pubkey: createHash11("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
22246
+ builder_pubkey: createHash12("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
22032
22247
  };
22033
22248
  }
22034
22249
  throw new AuthAudienceError();
@@ -22126,7 +22341,7 @@ var DVMServer = class {
22126
22341
  const methods = this.resolvePaymentMethods().filter(
22127
22342
  (method) => method !== "tempo" || creditMpp
22128
22343
  );
22129
- if (this.x402CurrencySupported() && this.x402BatchAvailable() && !methods.includes("x402")) {
22344
+ if ((this.opts.paymentMethods === void 0 || this.opts.paymentMethods.includes("x402")) && this.x402CurrencySupported() && this.x402BatchAvailable() && !methods.includes("x402")) {
22130
22345
  methods.push("x402");
22131
22346
  }
22132
22347
  if (this.opts.lightningReceive?.available()) methods.push("lightning");
@@ -22149,7 +22364,9 @@ var DVMServer = class {
22149
22364
  * distinction, and a reason it can relay.
22150
22365
  */
22151
22366
  creditMppSplit() {
22152
- if (!this.opts.mpp) return { offered: [], withheld: [] };
22367
+ if (!this.opts.mpp || !this.resolvePaymentMethods().includes("tempo")) {
22368
+ return { offered: [], withheld: [] };
22369
+ }
22153
22370
  const configured = advertisedMethods(this.opts.mpp).filter(
22154
22371
  (method) => method.intent === "charge" || method.intent === "session"
22155
22372
  ).sort(
@@ -22381,7 +22598,7 @@ var DVMServer = class {
22381
22598
  const paymentAuthorization = ctx.req.header("Authorization");
22382
22599
  const mppArtifact = paymentAuthorization ? (await import("mppx")).Credential.extractPaymentScheme(paymentAuthorization) : void 0;
22383
22600
  const attachedPayment = ctx.req.header("X-Cashu") ?? ctx.req.header("PAYMENT-SIGNATURE") ?? ctx.req.header("X-PAYMENT") ?? mppArtifact;
22384
- const paymentArtifactHash = attachedPayment ? createHash11("sha256").update(attachedPayment).digest("hex") : void 0;
22601
+ const paymentArtifactHash = attachedPayment ? createHash12("sha256").update(attachedPayment).digest("hex") : void 0;
22385
22602
  hostingFundingArtifactHash = paymentArtifactHash;
22386
22603
  if (!await control.admit(admissionId, workId, {
22387
22604
  creditId,
@@ -22481,7 +22698,7 @@ var DVMServer = class {
22481
22698
  name: this.descriptor.name,
22482
22699
  description: this.descriptor.description || void 0,
22483
22700
  tags: this.descriptor.tags,
22484
- auth: this.descriptor.auth ? [this.descriptor.auth.id] : ["none"],
22701
+ auth: !this.descriptor.auth ? ["none"] : this.descriptor.auth.allowUnsignedPaid ? [this.descriptor.auth.id, "none"] : [this.descriptor.auth.id],
22485
22702
  ...this.authAudience ? { auth_audience: this.authAudience } : {},
22486
22703
  protocol_version: 1,
22487
22704
  sdk_sha: this.opts.env.GIT_SHA ?? "unknown",
@@ -22500,7 +22717,7 @@ var DVMServer = class {
22500
22717
  if (this.opts.mints) info.mints = this.advertisedMints();
22501
22718
  info.capabilities = this.renderCapabilities();
22502
22719
  info.payment = { methods: this.resolvePaymentMethods() };
22503
- if (this.x402CurrencySupported() && this.opts.x402 && this.x402Support()?.available) {
22720
+ if (info.payment.methods.includes("x402") && this.opts.x402 && this.x402Support()?.available) {
22504
22721
  info.x402 = {
22505
22722
  pay_to: this.opts.x402.payTo,
22506
22723
  network: this.opts.x402.network,
@@ -22510,7 +22727,7 @@ var DVMServer = class {
22510
22727
  dual_serve: this.opts.x402.dualServe ?? true
22511
22728
  };
22512
22729
  }
22513
- if (this.opts.mpp) {
22730
+ if (info.payment.methods.includes("tempo") && this.opts.mpp) {
22514
22731
  const configured = advertisedMethods(this.opts.mpp);
22515
22732
  const withheld = configured.flatMap((method) => {
22516
22733
  const reason = this.tempoSessionWithheldReason(method);
@@ -22885,6 +23102,48 @@ var DVMServer = class {
22885
23102
  400
22886
23103
  );
22887
23104
  }
23105
+ /**
23106
+ * Refuse an x402 or Tempo mid-job payment on an unsigned paid job before it
23107
+ * is recorded or verified: those rails settle into credit owned by a caller
23108
+ * pubkey, which the job lacks. Signed jobs always store their pubkey.
23109
+ */
23110
+ unsignedJobRailRefusal(job, body) {
23111
+ if (!this.descriptor.auth?.allowUnsignedPaid || job.requesterPubkey !== void 0) return void 0;
23112
+ if (body.type !== "payment" || !body.content.x402_payment && !body.content.tempo_credential) {
23113
+ return void 0;
23114
+ }
23115
+ return buildPaymentErrorResponse(
23116
+ paymentErrorBody("payment_method_requires_signature", {
23117
+ message: "Unsigned jobs may be paid only with Cashu; an x402 or Tempo payment was sent."
23118
+ }),
23119
+ void 0,
23120
+ void 0,
23121
+ 402
23122
+ );
23123
+ }
23124
+ /** Whether this DVM takes Cashu, the only rail an unsigned payer may use. */
23125
+ unsignedCashuOffered() {
23126
+ return (this.opts.mints?.length ?? 0) > 0 && this.resolvePaymentMethods().includes("cashu");
23127
+ }
23128
+ /**
23129
+ * The error the signed path raises for this envelope-free body, so a refused
23130
+ * unsigned request reads exactly as it does on a DVM without the option.
23131
+ */
23132
+ unsignedRequestRefusal(c, schema, body, capabilityName) {
23133
+ try {
23134
+ if (this.descriptor.auth && isZodSchema(schema)) {
23135
+ this.descriptor.auth.checkAuth(
23136
+ schema,
23137
+ signedRequestInput(body),
23138
+ this.signedRequestDomain(c, capabilityName)
23139
+ );
23140
+ }
23141
+ } catch (err) {
23142
+ if (err instanceof SignedRequestError) return err;
23143
+ throw err;
23144
+ }
23145
+ return new SignedRequestError("signed request required", "signature_invalid");
23146
+ }
22888
23147
  /**
22889
23148
  * On authenticated DVMs, envelope-only parse failures defer to auth
22890
23149
  * (401 signature_invalid). Builder input issues remain 400 invalid_input.
@@ -22914,6 +23173,25 @@ var DVMServer = class {
22914
23173
  if (healthy.length > 0) return healthy;
22915
23174
  return all.filter((m) => !tracker.isNutIncompatible(m));
22916
23175
  }
23176
+ /**
23177
+ * This request's price at the rates `pricedAt` replaced, or `undefined` when
23178
+ * there are none or they were replaced longer ago than the hold window.
23179
+ * Computed the way the current price is, so the figure matches the one an
23180
+ * earlier 402 quoted.
23181
+ */
23182
+ previousPriceMsats(cap, dynamicUpfront, pricedAt) {
23183
+ const previous = pricedAt && this.fxFetcher.previousFor(pricedAt);
23184
+ if (!previous) return void 0;
23185
+ try {
23186
+ if (dynamicUpfront) {
23187
+ return fiatToSatsCeil(dynamicUpfront.amount, fxRateFor(previous, dynamicUpfront.currency)) * 1e3;
23188
+ }
23189
+ if (cap.price === void 0) return void 0;
23190
+ return fiatToSatsCeil(parseUsdPrice(cap.price, "price"), fxRateFor(previous, "usd")) * 1e3;
23191
+ } catch {
23192
+ return void 0;
23193
+ }
23194
+ }
22917
23195
  /** Authenticated, read-only recovery of a previously accepted public request id. */
22918
23196
  async handleRequestResult(c) {
22919
23197
  const requestId = c.req.param("requestId");
@@ -22984,6 +23262,14 @@ var DVMServer = class {
22984
23262
  400
22985
23263
  );
22986
23264
  }
23265
+ if (c.req.header("X-Cashu-Request-Id")?.startsWith(DERIVED_REQUEST_ID_PREFIX)) {
23266
+ return c.json(
23267
+ paymentErrorBody("reserved_request_id", {
23268
+ message: `X-Cashu-Request-Id may not start with "${DERIVED_REQUEST_ID_PREFIX}".`
23269
+ }),
23270
+ 400
23271
+ );
23272
+ }
22987
23273
  const publicRequestId = requestIdFromEnvelope(body.data);
22988
23274
  const recoveryStore = requestRecoveryStore(this.jobManager.store);
22989
23275
  const resumedRequestId = resumeOnly ? c.req.param("requestId") : void 0;
@@ -23002,6 +23288,17 @@ var DVMServer = class {
23002
23288
  const capabilityResult = this.requireCapabilityFromBody(c, body.capability);
23003
23289
  if ("error" in capabilityResult) return capabilityResult.error;
23004
23290
  const capabilityName = capabilityResult.name;
23291
+ const unsignedPaid = this.jobManager.unsignedPaidInput(body, capabilityName);
23292
+ if (unsignedPaid && publicRequestId) {
23293
+ return c.json(
23294
+ {
23295
+ error: "request_recovery_unsupported",
23296
+ message: "Public request ids require a signed request.",
23297
+ hint: "Sign the request to use request_id recovery, or omit request_id."
23298
+ },
23299
+ 400
23300
+ );
23301
+ }
23005
23302
  pinDimension("capability", capabilityName);
23006
23303
  const cap = this.descriptor.capabilities[capabilityName];
23007
23304
  let parsedInput;
@@ -23010,13 +23307,23 @@ var DVMServer = class {
23010
23307
  parsedInput = await loggers.isolate.span(
23011
23308
  "dvm.input.validate",
23012
23309
  { capability: capabilityName, stage: "input" },
23013
- () => this.jobManager.parseInput(body, capabilityName),
23310
+ () => this.jobManager.parseInput(body, capabilityName, unsignedPaid),
23014
23311
  { onError: () => ({ "error.code": "invalid_input" }) }
23015
23312
  );
23016
23313
  } catch (err) {
23017
- if (!this.deferToAuthGate(err)) return this.invalidInputResponse(c, err);
23314
+ if (unsignedPaid || !this.deferToAuthGate(err)) return this.invalidInputResponse(c, err);
23018
23315
  deferredInputError = err;
23019
23316
  }
23317
+ if (unsignedPaid && carriesCreditFields(signedRequestInput(body))) {
23318
+ return c.json(
23319
+ {
23320
+ error: "invalid_credit_fields",
23321
+ message: "credit_id, draw_id and fund require a signed request.",
23322
+ hint: "Sign the request to draw on or fund prepaid credit, or remove the credit fields and pay per call."
23323
+ },
23324
+ 400
23325
+ );
23326
+ }
23020
23327
  let creditEnvelope;
23021
23328
  try {
23022
23329
  creditEnvelope = extractCreditEnvelope(body.data);
@@ -23033,9 +23340,26 @@ var DVMServer = class {
23033
23340
  }
23034
23341
  throw err;
23035
23342
  }
23343
+ if (unsignedPaid && (cap.price === void 0 && !cap.onQuote || devModeSkipsPaymentVerification(this.opts) || !this.unsignedCashuOffered())) {
23344
+ return buildAuthErrorResponse(
23345
+ c,
23346
+ this.unsignedRequestRefusal(c, cap.input, body, capabilityName),
23347
+ this.publicOrigin
23348
+ );
23349
+ }
23350
+ if (unsignedPaid && await carriesNonCashuPayment(c)) {
23351
+ return buildPaymentErrorResponse(
23352
+ paymentErrorBody("payment_method_requires_signature", {
23353
+ message: "Unsigned requests may pay only with Cashu; an x402 or Tempo payment was attached."
23354
+ }),
23355
+ void 0,
23356
+ void 0,
23357
+ 402
23358
+ );
23359
+ }
23036
23360
  let authEnvelope;
23037
23361
  let authPubkey;
23038
- if (this.descriptor.auth) {
23362
+ if (this.descriptor.auth && !unsignedPaid) {
23039
23363
  const auth = this.descriptor.auth;
23040
23364
  try {
23041
23365
  const result = await loggers.isolate.span(
@@ -23083,14 +23407,14 @@ var DVMServer = class {
23083
23407
  { capability: capabilityName, input: body.input, params: body.params, data: body.data },
23084
23408
  { stripAuthEnvelope: this.descriptor.auth !== void 0 }
23085
23409
  );
23086
- const authFingerprint = this.descriptor.auth ? requestFingerprint(
23410
+ const authFingerprint = authEnvelope ? requestFingerprint(
23087
23411
  { capability: capabilityName, input: body.input, params: body.params, data: body.data },
23088
23412
  { stripAuthEnvelope: false }
23089
23413
  ) : void 0;
23090
23414
  const authorization = c.req.header("Authorization");
23091
23415
  const mppArtifact = authorization ? (await import("mppx")).Credential.extractPaymentScheme(authorization) : void 0;
23092
23416
  const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? mppArtifact;
23093
- const hostingPaymentArtifactHash = attachedPayment ? createHash11("sha256").update(attachedPayment).digest("hex") : void 0;
23417
+ const hostingPaymentArtifactHash = attachedPayment ? createHash12("sha256").update(attachedPayment).digest("hex") : void 0;
23094
23418
  if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
23095
23419
  const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
23096
23420
  hostingPaymentArtifactHash
@@ -23172,9 +23496,11 @@ var DVMServer = class {
23172
23496
  }
23173
23497
  const skipPayment = devModeSkipsPaymentVerification(this.opts);
23174
23498
  let priceMsats = resumeOnly ? payment.paidMsats || void 0 : void 0;
23499
+ let pricedAt;
23175
23500
  if (!resumeOnly && !skipPayment) {
23176
23501
  try {
23177
- priceMsats = await this.jobManager.currentPriceMsats(capabilityName);
23502
+ if (cap.price !== void 0) pricedAt = await this.fxFetcher.fetch();
23503
+ priceMsats = await this.jobManager.currentPriceMsats(capabilityName, pricedAt);
23178
23504
  } catch (err) {
23179
23505
  const fxErr = pickFxRateUnavailable(err);
23180
23506
  if (fxErr) return c.json({ error: fxErr }, 503);
@@ -23188,17 +23514,26 @@ var DVMServer = class {
23188
23514
  parsedInput,
23189
23515
  normalisedBody,
23190
23516
  authPubkey,
23191
- skipPayment
23517
+ skipPayment,
23518
+ unsignedPaid
23192
23519
  );
23193
23520
  if (dyn.error) return c.json(dyn.error.body, dyn.error.status);
23194
23521
  priceMsats = dyn.priceMsats;
23195
23522
  dynamicUpfront = dyn.upfront;
23523
+ pricedAt = dyn.pricedAt;
23196
23524
  }
23197
23525
  const priceFiat = resumeOnly ? void 0 : resolvePriceFiat({
23198
23526
  price: typeof cap.price === "string" ? cap.price : void 0,
23199
23527
  priceMsats,
23200
23528
  dynamicUpfront
23201
23529
  });
23530
+ if (unsignedPaid && !priceMsats) {
23531
+ return buildAuthErrorResponse(
23532
+ c,
23533
+ this.unsignedRequestRefusal(c, cap.input, body, capabilityName),
23534
+ this.publicOrigin
23535
+ );
23536
+ }
23202
23537
  if (!resumeOnly && creditEnvelope?.creditId) {
23203
23538
  await this.settleLightningFunding(authPubkey, creditEnvelope.creditId);
23204
23539
  }
@@ -23221,7 +23556,13 @@ var DVMServer = class {
23221
23556
  }
23222
23557
  }
23223
23558
  if (!skipPayment && !resumedAcceptedPayment) {
23224
- const railOpts = await this.railVerifyOpts(c);
23559
+ const railOpts = unsignedPaid ? cashuOnlyRailOpts(await this.railVerifyOpts(c)) : await this.railVerifyOpts(c);
23560
+ if (!this.descriptor.auth || unsignedPaid) {
23561
+ railOpts.nut24 = {
23562
+ mints: this.advertisedMints(),
23563
+ previousRequiredMsats: this.previousPriceMsats(cap, dynamicUpfront, pricedAt)
23564
+ };
23565
+ }
23225
23566
  hostingPaymentMayHaveLanded = Boolean(
23226
23567
  creditEnvelope || railOpts.cashuToken || railOpts.x402Payment || railOpts.mppCredential
23227
23568
  );
@@ -23259,7 +23600,7 @@ var DVMServer = class {
23259
23600
  this.reportShortPayForfeit(err.detail.forfeit);
23260
23601
  if (err.detail.body.error === "replay_rejected") {
23261
23602
  const recovered = await this.recoverReplayedJob(c, {
23262
- requestId: err.detail.replayPaymentId ?? c.req.header("X-Cashu-Request-Id"),
23603
+ requestId: err.detail.replayPaymentId,
23263
23604
  fingerprint,
23264
23605
  callerPubkey: authPubkey
23265
23606
  });
@@ -23287,15 +23628,17 @@ var DVMServer = class {
23287
23628
  }
23288
23629
  }
23289
23630
  if (!resumedAcceptedPayment) {
23290
- attachCreditMenu(
23291
- err.detail.body,
23292
- await this.creditMenu(this.creditCurrency(), authPubkey)
23293
- );
23631
+ if (!unsignedPaid) {
23632
+ attachCreditMenu(
23633
+ err.detail.body,
23634
+ await this.creditMenu(this.creditCurrency(), authPubkey)
23635
+ );
23636
+ }
23294
23637
  if (priceFiat) {
23295
23638
  err.detail.body.price_micro = priceFiat.amountMicro;
23296
23639
  err.detail.body.price_currency = priceFiat.currency;
23297
23640
  }
23298
- return buildPaymentErrorResponse(
23641
+ const response2 = buildPaymentErrorResponse(
23299
23642
  err.detail.body,
23300
23643
  err.detail.mppChallenges,
23301
23644
  err.detail.x402Requirements,
@@ -23305,6 +23648,11 @@ var DVMServer = class {
23305
23648
  this.x402Support()?.exact,
23306
23649
  err.detail.x402ErrorCode
23307
23650
  );
23651
+ if (err.detail.cashuPaymentRequest) {
23652
+ response2.headers.set("X-Cashu", err.detail.cashuPaymentRequest);
23653
+ response2.headers.set("Cache-Control", "no-store");
23654
+ }
23655
+ return response2;
23308
23656
  }
23309
23657
  }
23310
23658
  throw err;
@@ -23388,7 +23736,7 @@ var DVMServer = class {
23388
23736
  }
23389
23737
  const isAnonymous = requesterId === "anonymous" || requesterId === "dev-anonymous";
23390
23738
  const jobToken = isAnonymous ? randomBytes5(32).toString("hex") : void 0;
23391
- const requesterTokenHash = jobToken ? createHash11("sha256").update(jobToken).digest("hex") : void 0;
23739
+ const requesterTokenHash = jobToken ? createHash12("sha256").update(jobToken).digest("hex") : void 0;
23392
23740
  const job = jm.createJob(
23393
23741
  normalisedBody,
23394
23742
  requesterId,
@@ -23401,7 +23749,7 @@ var DVMServer = class {
23401
23749
  requestFingerprint: fingerprint,
23402
23750
  requesterPubkey: authPubkey,
23403
23751
  requestId: publicRequestId,
23404
- authRequestPath: this.descriptor.auth ? canonicalRequestPath(c.req.url) : void 0,
23752
+ authRequestPath: authEnvelope ? canonicalRequestPath(c.req.url) : void 0,
23405
23753
  jobId
23406
23754
  }
23407
23755
  );
@@ -23497,15 +23845,16 @@ var DVMServer = class {
23497
23845
  }
23498
23846
  /**
23499
23847
  * Recover a lost paid response only for matching request fingerprint,
23500
- * authenticated pubkey and requester identity. A spent bearer token alone
23501
- * is not proof of request ownership.
23848
+ * authenticated pubkey and requester identity. `requestId` must be the
23849
+ * payment reference the rail bound the presented payment to; a spent bearer
23850
+ * token alone is not proof of request ownership.
23502
23851
  */
23503
23852
  async recoverReplayedJob(c, opts) {
23504
23853
  if (!opts.requestId) return void 0;
23505
23854
  const prior = await this.jobManager.store.findJobByPaymentTxHash(opts.requestId);
23506
23855
  if (!prior) return void 0;
23507
23856
  const bodyMatches = prior.requestFingerprint !== void 0 && prior.requestFingerprint === opts.fingerprint;
23508
- const pubkeyMatches = this.descriptor.auth === void 0 || prior.requesterPubkey !== void 0 && prior.requesterPubkey === opts.callerPubkey;
23857
+ const pubkeyMatches = this.descriptor.auth === void 0 || prior.requesterPubkey === opts.callerPubkey;
23509
23858
  const requesterMatches = prior.requesterId === c.get("requesterId");
23510
23859
  if (!bodyMatches || !pubkeyMatches || !requesterMatches) {
23511
23860
  diagnosticConsole.warn(
@@ -23638,16 +23987,17 @@ var DVMServer = class {
23638
23987
  * schema separately. Refuse invalid input/results, currency mismatch or missing
23639
23988
  * FX when payment needs a price (400/500/503 as classified below).
23640
23989
  */
23641
- async computeDynamicPrice(onQuote, parsedInput, body, callerPubkey, skipRateConversion = false) {
23990
+ async computeDynamicPrice(onQuote, parsedInput, body, callerPubkey, skipRateConversion = false, unsigned2 = false) {
23642
23991
  let data = parsedInput;
23643
23992
  if (isZodSchema(onQuote.schema)) {
23644
- const parsed = onQuote.schema.safeParse(parsedInput);
23993
+ const schema = unsigned2 ? unsignedInputSchema(onQuote.schema) ?? onQuote.schema : onQuote.schema;
23994
+ const parsed = schema.safeParse(parsedInput);
23645
23995
  if (!parsed.success) {
23646
23996
  return {
23647
23997
  error: {
23648
23998
  body: {
23649
23999
  error: "quote_unavailable",
23650
- message: "DVM uses dynamic pricing but the job input does not satisfy onQuote.schema. Submit the same payload to /v1/quote to confirm pricing."
24000
+ message: unsigned2 ? "DVM uses dynamic pricing but the unsigned job input does not satisfy onQuote.schema. Sign the request, or send input that matches the published schema." : "DVM uses dynamic pricing but the job input does not satisfy onQuote.schema. Submit the same payload to /v1/quote to confirm pricing."
23651
24001
  },
23652
24002
  status: 400
23653
24003
  }
@@ -23677,7 +24027,8 @@ var DVMServer = class {
23677
24027
  const priceMsats = fiatToSatsCeil(validated.upfront.amount, fxRateFor(fx, upfrontCurrency)) * 1e3;
23678
24028
  return {
23679
24029
  priceMsats,
23680
- upfront: { amount: validated.upfront.amount, currency: upfrontCurrency }
24030
+ upfront: { amount: validated.upfront.amount, currency: upfrontCurrency },
24031
+ pricedAt: fx
23681
24032
  };
23682
24033
  } catch (err) {
23683
24034
  if (err instanceof InvalidCurrencyError || err instanceof UnsupportedCurrencyError) {
@@ -23959,6 +24310,8 @@ var DVMServer = class {
23959
24310
  this.opts.devMode === true
23960
24311
  );
23961
24312
  if (contentErr2) return c.json({ error: "invalid_message", message: contentErr2 }, 400);
24313
+ const railRefusal2 = this.unsignedJobRailRefusal(activeJob, body2);
24314
+ if (railRefusal2) return railRefusal2;
23962
24315
  const inboundSeq2 = await jm.recordInboundPending(activeJob, body2.type, body2.content);
23963
24316
  const lockPubkeys2 = await this.resolveLockPubkeysForReceive();
23964
24317
  let unappliedCredit2;
@@ -24019,6 +24372,8 @@ var DVMServer = class {
24019
24372
  }
24020
24373
  const contentErr = validateMessageContent(body.type, body.content, this.opts.devMode === true);
24021
24374
  if (contentErr) return c.json({ error: "invalid_message", message: contentErr }, 400);
24375
+ const railRefusal = this.unsignedJobRailRefusal(record, body);
24376
+ if (railRefusal) return railRefusal;
24022
24377
  if (isStreamableJobStore(jm.store)) {
24023
24378
  record.messages = await jm.store.getMessages(id, 0);
24024
24379
  record.seq = Math.max(record.seq, ...record.messages.map((message) => message.seq));
@@ -24218,7 +24573,7 @@ function isJobOwner(c, job) {
24218
24573
  if (job.requesterTokenHash) {
24219
24574
  const token = c.req.header("X-Job-Token");
24220
24575
  if (!token) return false;
24221
- const provided = createHash11("sha256").update(token).digest("hex");
24576
+ const provided = createHash12("sha256").update(token).digest("hex");
24222
24577
  return timingSafeEqualHex(provided, job.requesterTokenHash);
24223
24578
  }
24224
24579
  return c.get("requesterId") === job.requesterId;
@@ -24341,6 +24696,24 @@ function removeUnavailableContent(fields) {
24341
24696
  delete fields.payload_in;
24342
24697
  }
24343
24698
  }
24699
+ async function carriesNonCashuPayment(c) {
24700
+ if (c.req.header("PAYMENT-SIGNATURE") || c.req.header("X-PAYMENT")) return true;
24701
+ const authorization = c.req.header("Authorization");
24702
+ if (!authorization) return false;
24703
+ return Boolean((await import("mppx")).Credential.extractPaymentScheme(authorization));
24704
+ }
24705
+ function cashuOnlyRailOpts(opts) {
24706
+ return {
24707
+ ...opts,
24708
+ x402Payment: void 0,
24709
+ x402Config: void 0,
24710
+ x402ExactSettlement: void 0,
24711
+ x402ExactVersions: void 0,
24712
+ x402BatchSettlement: void 0,
24713
+ mppCredential: void 0,
24714
+ mpp: void 0
24715
+ };
24716
+ }
24344
24717
  function terminalResponseFields(job) {
24345
24718
  const fields = {};
24346
24719
  if (job.upfrontPayment !== void 0) fields.upfront_payment = job.upfrontPayment;