@dvmkit/sdk 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-SGSHDMHP.js → chunk-4QYADZFN.js} +613 -153
  4. package/dist/{chunk-V73WRF46.js → chunk-5CCQEAQV.js} +1 -1
  5. package/dist/{chunk-4IFZHJQK.js → chunk-5RLNRLLB.js} +1 -1
  6. package/dist/{chunk-7QGS27UT.js → chunk-72A4PHLD.js} +1 -1
  7. package/dist/{chunk-XBQTFOIT.js → chunk-7RWE6ELV.js} +1 -1
  8. package/dist/{chunk-N4SBDUVD.js → chunk-A6EY4K46.js} +1 -1
  9. package/dist/{chunk-5U2RG3ZW.js → chunk-ALTE7I6C.js} +5 -0
  10. package/dist/{chunk-K4ZQE7IB.js → chunk-B5MPLDRC.js} +2 -2
  11. package/dist/{chunk-PP5WHB6T.js → chunk-C5XNZLZ7.js} +1 -1
  12. package/dist/{chunk-2UZOJRRS.js → chunk-CVN2YNMG.js} +1 -1
  13. package/dist/{chunk-MD34LE6M.js → chunk-CXRI4N4Y.js} +1 -1
  14. package/dist/{chunk-ELNWQS24.js → chunk-EIDRCGTY.js} +1 -1
  15. package/dist/{chunk-ICHC4VDZ.js → chunk-KRX7VNBG.js} +1 -1
  16. package/dist/{chunk-TN6MWTZI.js → chunk-LY2JYSJS.js} +1 -1
  17. package/dist/{chunk-7N5VDCG3.js → chunk-MWDUGMN2.js} +2 -2
  18. package/dist/{chunk-FRGKANTZ.js → chunk-ORBWKYDE.js} +1 -1
  19. package/dist/{chunk-VQHBJXBL.js → chunk-QTKGFG3I.js} +1 -1
  20. package/dist/{chunk-PBT3H2LZ.js → chunk-RX6FT7XA.js} +1 -1
  21. package/dist/{chunk-OYBSDJR6.js → chunk-VKFR5PCW.js} +2 -2
  22. package/dist/{chunk-V4CBI5HB.js → chunk-Y6JJDOGO.js} +1 -1
  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-B239nIY5.d.ts → credit-menu-D-h0zJCZ.d.ts} +42 -8
  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 +33 -29
  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,6 +428,27 @@ async function insertAccumulatorRows(db, args) {
428
428
  client.release();
429
429
  }
430
430
  }
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);
444
+ const { rows: rows2 } = await db.query(
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]
448
+ );
449
+ const byHash = new Map(rows2.map((row) => [row.proof_secret_hash, row.request_id]));
450
+ return hashes.map((hash3) => byHash.get(hash3));
451
+ }
431
452
  async function listPendingBatches(db, dvmId) {
432
453
  const { rows: rows2 } = await db.query(
433
454
  `SELECT id, lock_pubkey, mint_url, proof_keyset_id, proof_secret, proof_c, proof_amount, proof_dleq, received_at, melt_quote_id, t_expire
@@ -3226,7 +3247,7 @@ function paymentFailureFields(error) {
3226
3247
  }
3227
3248
 
3228
3249
  // src/sdk/server/payment.ts
3229
- import { createHash as createHash4, randomBytes, randomUUID as randomUUID2 } from "crypto";
3250
+ import { createHash as createHash5, randomBytes, randomUUID as randomUUID2 } from "crypto";
3230
3251
  import { Challenge } from "mppx";
3231
3252
 
3232
3253
  // src/lib/cashu/verify.ts
@@ -3320,6 +3341,63 @@ function methodNativeAmountToDecimal(amount, methodName) {
3320
3341
  return `${nativeAmount2 / scale}.${(nativeAmount2 % scale).toString().padStart(decimals, "0")}`;
3321
3342
  }
3322
3343
 
3344
+ // src/sdk/server/cashu-payment-request.ts
3345
+ import { createHash as createHash4 } from "crypto";
3346
+ import { PaymentRequest } from "@cashu/cashu-ts";
3347
+ function cashuPaymentRequest(args) {
3348
+ return new PaymentRequest(
3349
+ void 0,
3350
+ void 0,
3351
+ Math.ceil(args.requiredMsats / 1e3),
3352
+ "sat",
3353
+ args.mints,
3354
+ void 0,
3355
+ false,
3356
+ { kind: "P2PK", data: args.lockPubkey, tags: [] }
3357
+ ).toEncodedCreqA();
3358
+ }
3359
+ function proofSetRequestId(secrets) {
3360
+ const canonical = JSON.stringify([...secrets].sort());
3361
+ return `${DERIVED_REQUEST_ID_PREFIX}${createHash4("sha256").update(canonical).digest("hex")}`;
3362
+ }
3363
+ var DERIVED_REQUEST_ID_PREFIX = "nut24-";
3364
+ function withFxHistory(inner, opts = {}) {
3365
+ const holdWindowMs = opts.holdWindowMs ?? FX_HOLD_WINDOW_MS;
3366
+ const now = opts.now ?? Date.now;
3367
+ const history = [];
3368
+ return {
3369
+ async fetch() {
3370
+ const snapshot = await inner.fetch();
3371
+ const latest = history.at(-1);
3372
+ if (latest && sameRates(latest.snapshot, snapshot)) {
3373
+ latest.lastPricedAt = now();
3374
+ } else {
3375
+ history.push({ snapshot, lastPricedAt: now() });
3376
+ if (history.length > 4) history.shift();
3377
+ }
3378
+ return snapshot;
3379
+ },
3380
+ supportedCurrencies: () => inner.supportedCurrencies(),
3381
+ ...inner.lastKnown && { lastKnown: inner.lastKnown.bind(inner) },
3382
+ previousFor(pricedAt) {
3383
+ for (let i = history.length - 1; i > 0; i--) {
3384
+ if (!sameRates(history[i].snapshot, pricedAt)) continue;
3385
+ const replaced = history[i - 1];
3386
+ return now() - replaced.lastPricedAt <= holdWindowMs ? replaced.snapshot : void 0;
3387
+ }
3388
+ return void 0;
3389
+ }
3390
+ };
3391
+ }
3392
+ function sameRates(a, b) {
3393
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a.rates), ...Object.keys(b.rates)]);
3394
+ for (const key of keys) {
3395
+ if (a.rates[key] !== b.rates[key]) return false;
3396
+ }
3397
+ return true;
3398
+ }
3399
+ var FX_HOLD_WINDOW_MS = 10 * 6e4;
3400
+
3323
3401
  // src/lib/mints.ts
3324
3402
  function isTestMintUrl(url) {
3325
3403
  let host;
@@ -3341,7 +3419,11 @@ import {
3341
3419
  Amount as Amount2,
3342
3420
  CheckStateEnum,
3343
3421
  getP2PKExpectedWitnessPubkeys,
3422
+ getSecretData,
3423
+ getSecretKind,
3344
3424
  getTagInt,
3425
+ getTags,
3426
+ parseP2PKSecret,
3345
3427
  verifyDleqIfPresent
3346
3428
  } from "@cashu/cashu-ts";
3347
3429
  function valueCashuFunding(wallet, proofs) {
@@ -3358,17 +3440,109 @@ function valueCashuFunding(wallet, proofs) {
3358
3440
  netMsats: netSats.multiplyBy(1e3).toNumber()
3359
3441
  };
3360
3442
  }
3443
+ var CASHU_MIN_LOCKTIME_SECONDS = 604800;
3444
+ var CASHU_LOCKTIME_RESUME_MARGIN_SECONDS = 86400;
3445
+ var P2PK_TAG_KEYS = /* @__PURE__ */ new Set(["locktime", "refund", "pubkeys", "n_sigs", "sigflag", "n_sigs_refund"]);
3446
+ var SINGLE_VALUE_TAG_KEYS = /* @__PURE__ */ new Set(["locktime", "n_sigs", "sigflag", "n_sigs_refund"]);
3447
+ function assessCashuLock(secret, lockPubkeys, nowSec) {
3448
+ try {
3449
+ parseP2PKSecret(secret);
3450
+ if (getSecretKind(secret) !== "P2PK") throw new Error("not P2PK");
3451
+ } catch {
3452
+ return { kind: "rejected", code: "not_p2pk_locked", message: "Proof secret is not a NUT-11 P2PK secret." };
3453
+ }
3454
+ const dvmKeyFor = (x) => x === void 0 ? void 0 : lockPubkeys.find((pk) => compressedX(pk) === x);
3455
+ const data = getSecretData(secret).data;
3456
+ const dataX = typeof data === "string" ? compressedX(data) : void 0;
3457
+ const lockPubkey = dvmKeyFor(dataX);
3458
+ if (!dataX || !lockPubkey) {
3459
+ return {
3460
+ kind: "rejected",
3461
+ code: "wrong_lock_pubkey",
3462
+ message: "Proof is not locked to one of this DVM's active lock pubkeys, spelled as a 33-byte compressed hex key."
3463
+ };
3464
+ }
3465
+ const notExclusive = (message, locktimeBelowFloor) => ({
3466
+ kind: "rejected",
3467
+ code: "lock_not_exclusive",
3468
+ message,
3469
+ ...locktimeBelowFloor && { locktimeBelowFloor }
3470
+ });
3471
+ try {
3472
+ getP2PKExpectedWitnessPubkeys(secret);
3473
+ } catch (err) {
3474
+ return notExclusive(`Proof carries an unreadable spending condition: ${err instanceof Error ? err.message : String(err)}`);
3475
+ }
3476
+ const tags = /* @__PURE__ */ new Map();
3477
+ for (const tag of getTags(secret)) {
3478
+ if (!Array.isArray(tag) || !tag.every((item) => typeof item === "string")) {
3479
+ return notExclusive("Proof carries a malformed tag.");
3480
+ }
3481
+ const [key, ...values] = tag;
3482
+ if (key === void 0 || !P2PK_TAG_KEYS.has(key)) return notExclusive(`Proof carries the unsupported tag ${JSON.stringify(key)}.`);
3483
+ if (tags.has(key)) return notExclusive(`Proof repeats the ${key} tag.`);
3484
+ if (values.length === 0) return notExclusive(`Proof carries a ${key} tag without a value.`);
3485
+ if (SINGLE_VALUE_TAG_KEYS.has(key) && values.length !== 1) return notExclusive(`Proof's ${key} tag must carry exactly one value.`);
3486
+ tags.set(key, values);
3487
+ }
3488
+ const sigflag = tags.get("sigflag")?.[0];
3489
+ if (sigflag !== void 0 && sigflag !== "SIG_INPUTS") {
3490
+ return notExclusive(`Proof requires sigflag ${sigflag}; this DVM accepts SIG_INPUTS only.`);
3491
+ }
3492
+ const nSigs = tags.get("n_sigs")?.[0];
3493
+ if (nSigs !== void 0 && nSigs !== "1") {
3494
+ return notExclusive(`Proof requires n_sigs=${nSigs}; this DVM accepts a single-signature lock only.`);
3495
+ }
3496
+ const mainX = [dataX];
3497
+ for (const key of tags.get("pubkeys") ?? []) {
3498
+ const x = compressedX(key);
3499
+ if (!x || !dvmKeyFor(x)) return notExclusive("Proof's pubkeys tag names a key other than this DVM's lock pubkeys.");
3500
+ if (mainX.includes(x)) return notExclusive("Proof repeats a key across data and pubkeys.");
3501
+ mainX.push(x);
3502
+ }
3503
+ const refundX = [];
3504
+ for (const key of tags.get("refund") ?? []) {
3505
+ const x = compressedX(key);
3506
+ if (!x) return notExclusive("Proof's refund tag names a key that is not a 33-byte compressed hex key.");
3507
+ if (refundX.includes(x)) return notExclusive("Proof repeats a key in its refund tag.");
3508
+ refundX.push(x);
3509
+ }
3510
+ const nSigsRefund = tags.get("n_sigs_refund")?.[0];
3511
+ if (nSigsRefund !== void 0 && !(/^[1-9][0-9]*$/.test(nSigsRefund) && Number(nSigsRefund) <= refundX.length)) {
3512
+ return notExclusive(`Proof's n_sigs_refund ${nSigsRefund} is not a count between 1 and its number of refund keys.`);
3513
+ }
3514
+ const locktimeValue = tags.get("locktime")?.[0];
3515
+ if (locktimeValue !== void 0) {
3516
+ const locktime = /^(0|[1-9][0-9]*)$/.test(locktimeValue) ? Number(locktimeValue) : NaN;
3517
+ if (!Number.isSafeInteger(locktime)) return notExclusive(`Proof's locktime ${locktimeValue} is not a unix-seconds integer.`);
3518
+ const floor = nowSec + CASHU_MIN_LOCKTIME_SECONDS - CASHU_LOCKTIME_RESUME_MARGIN_SECONDS;
3519
+ if (locktime < floor) {
3520
+ return notExclusive(
3521
+ `Proof locktime ${locktime} is earlier than the accepted floor ${floor} (unix seconds).`,
3522
+ true
3523
+ );
3524
+ }
3525
+ }
3526
+ return { kind: "exclusive", lockPubkey };
3527
+ }
3361
3528
  async function acceptAccumulatorPayment(opts) {
3362
3529
  if (opts.disposable && !opts.mints.every(isTestMintUrl)) {
3363
3530
  throw new Error("Disposable Cashu accepts only known test or loopback mints.");
3364
3531
  }
3365
- if (!opts.requestId) {
3532
+ if (!opts.requestId && !opts.deriveRequestId) {
3366
3533
  return {
3367
3534
  kind: "rejected",
3368
3535
  code: "missing_request_id",
3369
3536
  message: "X-Cashu-Request-Id header required for the agent-wallet path."
3370
3537
  };
3371
3538
  }
3539
+ if (opts.requestId?.startsWith(DERIVED_REQUEST_ID_PREFIX)) {
3540
+ return {
3541
+ kind: "rejected",
3542
+ code: "reserved_request_id",
3543
+ message: `A Cashu request id may not start with "${DERIVED_REQUEST_ID_PREFIX}".`
3544
+ };
3545
+ }
3372
3546
  if (opts.lockPubkeys.length === 0) {
3373
3547
  return {
3374
3548
  kind: "rejected",
@@ -3389,6 +3563,9 @@ async function acceptAccumulatorPayment(opts) {
3389
3563
  if (meta.unit !== "sat" || proofs.length === 0) {
3390
3564
  throw new Error("Cashu payment requires nonempty sat proofs.");
3391
3565
  }
3566
+ if (new Set(proofs.map((proof) => proof.secret)).size !== proofs.length) {
3567
+ throw new Error("Cashu payment carries the same proof more than once.");
3568
+ }
3392
3569
  for (const proof of proofs) {
3393
3570
  if (!proof.dleq || !verifyDleqIfPresent(proof, wallet.getKeyset(proof.id))) {
3394
3571
  throw new Error(
@@ -3404,46 +3581,32 @@ async function acceptAccumulatorPayment(opts) {
3404
3581
  message: err instanceof Error ? err.message : "Cashu token decode failed"
3405
3582
  };
3406
3583
  }
3584
+ const requestId = opts.requestId || proofSetRequestId(proofs.map((proof) => proof.secret));
3407
3585
  const acceptedValue = {
3408
3586
  ...valuation,
3409
3587
  mintUrl,
3410
- paidMsats: valuation.netMsats
3588
+ paidMsats: valuation.netMsats,
3589
+ requestId
3411
3590
  };
3591
+ const nowSec = Math.floor(Date.now() / 1e3);
3412
3592
  const tExpireMsByIndex = [];
3413
3593
  const matchedLockPubkeyByIndex = [];
3594
+ let belowFloorMessage;
3414
3595
  for (const p of proofs) {
3415
- let allowed;
3416
- try {
3417
- allowed = getP2PKExpectedWitnessPubkeys(p.secret);
3418
- } catch {
3419
- return {
3420
- kind: "rejected",
3421
- code: "not_p2pk_locked",
3422
- message: "Proof secret is not a NUT-11 P2PK secret."
3423
- };
3424
- }
3425
- if (allowed.length === 0) {
3426
- return {
3427
- kind: "rejected",
3428
- code: "not_p2pk_locked",
3429
- message: "Proof is unlocked or the P2PK lock has expired."
3430
- };
3431
- }
3432
- const allowedNormalised = allowed.map(toLockPubkey);
3433
- if (opts.disposable && (allowedNormalised.length !== 1 || (getTagInt(p.secret, "n_sigs") ?? 1) !== 1)) {
3434
- return { kind: "rejected", code: "not_p2pk_locked", message: "Disposable Cashu requires an exclusive single-signature P2PK lock." };
3596
+ const lock = assessCashuLock(p.secret, accepted, nowSec);
3597
+ if (lock.kind === "rejected") {
3598
+ if (!lock.locktimeBelowFloor) return { kind: "rejected", code: lock.code, message: lock.message };
3599
+ belowFloorMessage ??= lock.message;
3600
+ continue;
3435
3601
  }
3436
- const match = accepted.find((pk) => allowedNormalised.includes(pk));
3437
- if (!match) {
3438
- return {
3439
- kind: "rejected",
3440
- code: "wrong_lock_pubkey",
3441
- message: "Proof is not locked to one of this DVM's active lock pubkeys."
3442
- };
3443
- }
3444
- matchedLockPubkeyByIndex.push(match);
3602
+ matchedLockPubkeyByIndex.push(lock.lockPubkey);
3445
3603
  tExpireMsByIndex.push(parseNut11LocktimeMs(p.secret));
3446
3604
  }
3605
+ if (belowFloorMessage) {
3606
+ const banked = await acceptedUnder(opts, proofs);
3607
+ if (banked.some((id) => id !== void 0)) return replayRejected(banked, requestId);
3608
+ return { kind: "rejected", code: "lock_not_exclusive", message: belowFloorMessage };
3609
+ }
3447
3610
  try {
3448
3611
  const walletProvider = opts.walletProvider ?? getWallet;
3449
3612
  const wallet = await walletProvider(mintUrl);
@@ -3485,23 +3648,24 @@ async function acceptAccumulatorPayment(opts) {
3485
3648
  lockPubkeyByIndex: matchedLockPubkeyByIndex,
3486
3649
  mintUrl,
3487
3650
  proofs,
3488
- requestId: opts.requestId,
3651
+ requestId,
3489
3652
  tExpireMsByIndex
3490
3653
  };
3491
3654
  try {
3492
3655
  if (opts.disposable) {
3493
3656
  const account = opts.withDisposableLedger;
3494
3657
  if (!account) throw new Error("Disposable Cashu requires atomic credit accounting.");
3495
- await opts.disposable.commit(opts.requestId, proofs, (ledger) => account(ledger, acceptedValue));
3658
+ await opts.disposable.commit(requestId, proofs, (ledger) => account(ledger, acceptedValue));
3496
3659
  } else if (!opts.db) {
3497
3660
  throw new Error("Cashu accumulator requires a storage backend.");
3498
- } else if (opts.withCommitTx) {
3499
- const withCommitTx = opts.withCommitTx;
3661
+ } else {
3500
3662
  const client = await opts.db.connect();
3501
3663
  try {
3502
3664
  await client.query("BEGIN");
3665
+ await client.query("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
3666
+ await claimAccumulatorRequestId(client, opts.dvmId, requestId);
3503
3667
  await insertAccumulatorRows(opts.db, { ...insertArgs, tx: client });
3504
- await withCommitTx(client, acceptedValue);
3668
+ await opts.withCommitTx?.(client, acceptedValue);
3505
3669
  await client.query("COMMIT");
3506
3670
  } catch (err) {
3507
3671
  try {
@@ -3512,16 +3676,12 @@ async function acceptAccumulatorPayment(opts) {
3512
3676
  } finally {
3513
3677
  client.release();
3514
3678
  }
3515
- } else {
3516
- await insertAccumulatorRows(opts.db, insertArgs);
3517
3679
  }
3518
3680
  } catch (err) {
3519
3681
  if (err instanceof AccumulatorReplayError) {
3520
- return {
3521
- kind: "rejected",
3522
- code: "replay_rejected",
3523
- message: "Proofs or request id already accepted at this DVM."
3524
- };
3682
+ const banked = await acceptedUnder(opts, proofs);
3683
+ if (banked.every((id) => id === void 0)) return requestIdReused(requestId);
3684
+ return replayRejected(banked, requestId);
3525
3685
  }
3526
3686
  throw err;
3527
3687
  }
@@ -3531,7 +3691,33 @@ async function acceptAccumulatorPayment(opts) {
3531
3691
  grossMsats: valuation.grossMsats,
3532
3692
  inputFeeMsats: valuation.inputFeeMsats,
3533
3693
  netMsats: valuation.netMsats,
3534
- mintUrl
3694
+ mintUrl,
3695
+ requestId
3696
+ };
3697
+ }
3698
+ async function acceptedUnder(opts, proofs) {
3699
+ const secrets = proofs.map((p) => p.secret);
3700
+ if (opts.disposable) return await opts.disposable.acceptedRequestIds(secrets);
3701
+ if (opts.db) return await acceptedRequestIds(opts.db, opts.dvmId, secrets);
3702
+ return secrets.map(() => void 0);
3703
+ }
3704
+ function requestIdReused(requestId) {
3705
+ return {
3706
+ kind: "rejected",
3707
+ code: "request_id_reused",
3708
+ requestId,
3709
+ message: "This request id already carries a different payment at this DVM; these proofs were not accepted."
3710
+ };
3711
+ }
3712
+ function replayRejected(accepted, requestId) {
3713
+ const [first] = accepted;
3714
+ const bound = first !== void 0 && accepted.every((id) => id === first);
3715
+ return {
3716
+ kind: "rejected",
3717
+ code: "replay_rejected",
3718
+ message: "Proofs or request id already accepted at this DVM.",
3719
+ requestId,
3720
+ ...bound && { acceptedRequestId: first }
3535
3721
  };
3536
3722
  }
3537
3723
  function parseNut11LocktimeMs(secret) {
@@ -3542,6 +3728,9 @@ function parseNut11LocktimeMs(secret) {
3542
3728
  return null;
3543
3729
  }
3544
3730
  }
3731
+ function compressedX(raw) {
3732
+ return /^0[23][0-9a-fA-F]{64}$/.test(raw) ? raw.slice(2).toLowerCase() : void 0;
3733
+ }
3545
3734
 
3546
3735
  // src/sdk/server/payment.ts
3547
3736
  var UPFRONT_CHALLENGE_TTL_MS = 5 * 60 * 1e3;
@@ -3764,7 +3953,7 @@ async function verifyUpfrontPaymentInScope(opts) {
3764
3953
  }
3765
3954
  };
3766
3955
  }
3767
- if (opts.cashuMode === "p2pk-accumulator" && (opts.accumulatorDb || opts.disposableCashu) && opts.dvmId && opts.lockPubkeys && opts.lockPubkeys.length > 0) {
3956
+ if (cashuAccumulatorReady(opts)) {
3768
3957
  return verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx);
3769
3958
  }
3770
3959
  return {
@@ -3928,7 +4117,7 @@ async function verifyUpfrontPaymentInScope(opts) {
3928
4117
  x402Requirements
3929
4118
  });
3930
4119
  }
3931
- const { verifyX402Payment } = await import("./x402-6T73AZQ6.js");
4120
+ const { verifyX402Payment } = await import("./x402-HVCUEWWU.js");
3932
4121
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
3933
4122
  x402Payment,
3934
4123
  x402Config,
@@ -4383,6 +4572,8 @@ async function verifyUpfrontPaymentInScope(opts) {
4383
4572
  opts.fundOnly?.allowOneShotStablecoin,
4384
4573
  opts.priceFiat
4385
4574
  ) : void 0;
4575
+ const lockPubkey = opts.lockPubkeys?.[0];
4576
+ const cashuRequest = opts.nut24 && opts.nut24.mints.length > 0 && !opts.fundOnly && cashuAccumulatorReady(opts) && lockPubkey ? cashuPaymentRequest({ requiredMsats, mints: opts.nut24.mints, lockPubkey }) : void 0;
4386
4577
  return {
4387
4578
  paidMsats: 0,
4388
4579
  receivedProofs: [],
@@ -4393,6 +4584,7 @@ async function verifyUpfrontPaymentInScope(opts) {
4393
4584
  ...mints && { mints }
4394
4585
  },
4395
4586
  status: 402,
4587
+ ...cashuRequest && { cashuPaymentRequest: cashuRequest },
4396
4588
  ...mppChallenges && { mppChallenges },
4397
4589
  ...x402Requirements && { x402Requirements },
4398
4590
  ...x402BatchRequirements && { x402V2Requirements: x402BatchRequirements }
@@ -4526,6 +4718,11 @@ var PAYMENT_ERROR_COPY = {
4526
4718
  hint: "Request a fresh quote and pay again \u2014 the previous payment can't be reused.",
4527
4719
  retryable: false
4528
4720
  },
4721
+ payment_method_requires_signature: {
4722
+ display: "This service accepts unsigned requests only when they are paid with Cashu, so this payment was not applied to the job.",
4723
+ hint: "Pay with Cashu to call without signing, or sign the request (secp256k1 Schnorr) to use another payment method.",
4724
+ retryable: false
4725
+ },
4529
4726
  x402_not_offered: {
4530
4727
  display: "This service does not offer x402 payment for prices in its configured currency. Nothing was charged.",
4531
4728
  hint: "Use one of the payment methods advertised by this service and submit the request again.",
@@ -4751,6 +4948,11 @@ var PAYMENT_ERROR_COPY = {
4751
4948
  hint: "Include the X-Cashu-Request-Id header and pay again.",
4752
4949
  retryable: false
4753
4950
  },
4951
+ reserved_request_id: {
4952
+ display: "The payment's request identifier isn't allowed, so the payment wasn't processed. Nothing was charged.",
4953
+ hint: 'Send a fresh UUIDv4 as X-Cashu-Request-Id. Ids starting with "nut24-" are reserved for payments sent without one.',
4954
+ retryable: false
4955
+ },
4754
4956
  missing_lock_pubkey: {
4755
4957
  display: "This service can't accept the payment right now. Nothing was charged.",
4756
4958
  hint: "The service is missing payment configuration; retrying won't help. Contact the service operator.",
@@ -4771,6 +4973,11 @@ var PAYMENT_ERROR_COPY = {
4771
4973
  hint: "Re-fetch this service's current lock key from /v1/info and lock a fresh token to it.",
4772
4974
  retryable: false
4773
4975
  },
4976
+ lock_not_exclusive: {
4977
+ display: "The payment token's lock would let a key other than this service's spend it, so it wasn't accepted. Nothing was charged.",
4978
+ hint: "Lock a fresh token with data set to cashu.lock_pubkeys[0] from /v1/info as a 33-byte compressed hex key. Use only the locktime, refund, pubkeys, n_sigs, sigflag and n_sigs_refund tags, each once and with a value: pubkeys may list only other cashu.lock_pubkeys entries, n_sigs must be 1, sigflag SIG_INPUTS, refund keys compressed and distinct, n_sigs_refund at most the number of refund keys, and any locktime at least cashu.min_locktime minus 24 hours in the future.",
4979
+ retryable: false
4980
+ },
4774
4981
  proof_not_unspent: {
4775
4982
  display: "The payment token has already been spent. Nothing was charged.",
4776
4983
  hint: "Those funds are gone; pay with a fresh token.",
@@ -4812,6 +5019,11 @@ var PAYMENT_ERROR_COPY = {
4812
5019
  // agent could follow straight into paying twice for the same charge.
4813
5020
  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.",
4814
5021
  retryable: false
5022
+ },
5023
+ request_id_reused: {
5024
+ display: "This payment was not accepted and nothing was charged.",
5025
+ hint: "Each X-Cashu-Request-Id belongs to the first payment made under it. Send these proofs again under a new request id.",
5026
+ retryable: false
4815
5027
  }
4816
5028
  };
4817
5029
  function paymentErrorBody(code, opts) {
@@ -4953,7 +5165,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
4953
5165
  snapshot
4954
5166
  });
4955
5167
  }
4956
- const { verifyX402Payment } = await import("./x402-6T73AZQ6.js");
5168
+ const { verifyX402Payment } = await import("./x402-HVCUEWWU.js");
4957
5169
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
4958
5170
  body.content.x402_payment,
4959
5171
  opts.x402Config,
@@ -5404,7 +5616,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
5404
5616
  try {
5405
5617
  const requiredMsats = job.pendingPaymentMsats ?? 0;
5406
5618
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
5407
- const { verifyX402Payment } = await import("./x402-6T73AZQ6.js");
5619
+ const { verifyX402Payment } = await import("./x402-HVCUEWWU.js");
5408
5620
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
5409
5621
  body.content.x402_payment,
5410
5622
  opts.x402Config,
@@ -5529,6 +5741,16 @@ function creditPendingPayment(job) {
5529
5741
  job.pendingPaymentMsats = void 0;
5530
5742
  }
5531
5743
  }
5744
+ function cashuAccumulatorReady(opts) {
5745
+ return opts.cashuMode === "p2pk-accumulator" && Boolean(opts.accumulatorDb || opts.disposableCashu) && Boolean(opts.dvmId) && (opts.lockPubkeys?.length ?? 0) > 0;
5746
+ }
5747
+ var NUT24_PAYER_FIXABLE = /* @__PURE__ */ new Set([
5748
+ "invalid_token",
5749
+ "not_p2pk_locked",
5750
+ "wrong_lock_pubkey",
5751
+ "lock_not_exclusive",
5752
+ "payment_insufficient"
5753
+ ]);
5532
5754
  async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5533
5755
  const { cashuToken, accumulatorDb, dvmId, lockPubkeys, mintHealthTracker } = opts;
5534
5756
  if (!cashuToken || !accumulatorDb && !opts.disposableCashu || !dvmId || !lockPubkeys || lockPubkeys.length === 0) {
@@ -5577,7 +5799,13 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5577
5799
  };
5578
5800
  }
5579
5801
  let credited;
5580
- const requestId = opts.requestId;
5802
+ const headerless = !opts.requestId && opts.nut24 !== void 0;
5803
+ const previousRequired = headerless ? opts.nut24?.previousRequiredMsats : void 0;
5804
+ const requiredFor = (paidMsats) => requiredMsats !== void 0 && paidMsats < requiredMsats && previousRequired !== void 0 && paidMsats >= previousRequired ? previousRequired : requiredMsats;
5805
+ const held = (ctx, paidMsats) => ({
5806
+ ...ctx,
5807
+ requiredMsats: requiredFor(paidMsats) ?? ctx.requiredMsats
5808
+ });
5581
5809
  const signedFund = ledgerCtx?.creditEnvelope?.fund;
5582
5810
  const beforeCommit = ledgerCtx && signedFund ? (accepted) => {
5583
5811
  const expected = fundedMicroFor(
@@ -5589,12 +5817,12 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5589
5817
  throw new CashuFundingAmountMismatchError(signedFund.amountMicro, expected);
5590
5818
  }
5591
5819
  } : void 0;
5592
- const withCommitTx = ledgerCtx && requestId ? async (tx, accepted) => {
5820
+ const withCommitTx = ledgerCtx ? async (tx, accepted) => {
5593
5821
  credited = await fundAndDraw(
5594
- ledgerCtx,
5822
+ held(ledgerCtx, accepted.paidMsats),
5595
5823
  {
5596
5824
  rail: "cashu",
5597
- paymentId: requestId,
5825
+ paymentId: accepted.requestId,
5598
5826
  paidMsats: accepted.paidMsats,
5599
5827
  grossPaidMsats: accepted.grossMsats,
5600
5828
  cashuInputFeeMsats: accepted.inputFeeMsats,
@@ -5608,13 +5836,14 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5608
5836
  result = await acceptAccumulatorPayment({
5609
5837
  cashuToken,
5610
5838
  requestId: opts.requestId,
5839
+ deriveRequestId: headerless,
5611
5840
  mints,
5612
5841
  db: accumulatorDb,
5613
5842
  disposable: opts.disposableCashu,
5614
- withDisposableLedger: ledgerCtx && requestId ? async (ledger, accepted) => {
5615
- credited = await fundAndDraw({ ...ledgerCtx, ledger }, {
5843
+ withDisposableLedger: ledgerCtx ? async (ledger, accepted) => {
5844
+ credited = await fundAndDraw({ ...held(ledgerCtx, accepted.paidMsats), ledger }, {
5616
5845
  rail: "cashu",
5617
- paymentId: requestId,
5846
+ paymentId: accepted.requestId,
5618
5847
  paidMsats: accepted.paidMsats,
5619
5848
  grossPaidMsats: accepted.grossMsats,
5620
5849
  cashuInputFeeMsats: accepted.inputFeeMsats,
@@ -5638,24 +5867,35 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5638
5867
  throw err;
5639
5868
  }
5640
5869
  if (result.kind === "rejected") {
5641
- const status = result.code === "replay_rejected" ? 409 : 402;
5870
+ 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;
5871
+ const requestId = result.requestId;
5642
5872
  return {
5643
5873
  paidMsats: 0,
5644
5874
  receivedProofs: [],
5645
5875
  error: {
5646
- body: paymentErrorBody(result.code, { message: result.message, extra: { mints } }),
5876
+ body: paymentErrorBody(result.code, {
5877
+ message: result.message,
5878
+ // Without a request id the proof set is the payment's identity, so
5879
+ // only these exact proofs with the original body recover its job.
5880
+ ...headerless && result.code === "replay_rejected" && {
5881
+ 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."
5882
+ },
5883
+ extra: { mints }
5884
+ }),
5647
5885
  status,
5648
- // The accumulator keys replay on the request id, which is also the
5649
- // funding id a mid-job top-up committed under, so the route can hand
5650
- // back the original draw funding evidence beside the 409.
5651
- ...status === 409 && requestId && { replayPaymentId: requestId }
5886
+ // The request id is also the funding id a mid-job top-up committed
5887
+ // under and the job's payment reference, so the route can return the
5888
+ // original evidence beside the 409. It is passed on only when every
5889
+ // presented proof was accepted under this same request id.
5890
+ ...requestId && result.acceptedRequestId === requestId && { replayPaymentId: requestId }
5652
5891
  }
5653
5892
  };
5654
5893
  }
5655
5894
  if (credited?.drawRefusal) {
5656
5895
  return creditLedgerErrorResult(credited.drawRefusal, void 0, credited.deposit);
5657
5896
  }
5658
- if (requiredMsats && result.paidMsats < requiredMsats && !ledgerCtx?.fundOnly && !ledgerCtx?.creditEnvelope) {
5897
+ const heldRequired = requiredFor(result.paidMsats);
5898
+ if (heldRequired && result.paidMsats < heldRequired && !ledgerCtx?.fundOnly && !ledgerCtx?.creditEnvelope) {
5659
5899
  let forfeit;
5660
5900
  if (credited?.draw && ledgerCtx && !ledgerCtx.midJob) {
5661
5901
  const drawId = credited.draw.drawId;
@@ -5677,36 +5917,39 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5677
5917
  });
5678
5918
  }
5679
5919
  }
5680
- const held = ledgerCtx?.midJob ? credited : void 0;
5920
+ const held2 = ledgerCtx?.midJob ? credited : void 0;
5681
5921
  return {
5682
5922
  paidMsats: 0,
5683
5923
  receivedProofs: [],
5684
5924
  error: {
5685
5925
  body: paymentErrorBody("payment_insufficient", {
5686
5926
  message: `Payment of ${result.paidMsats} msats is short of the ${requiredMsats} msats required.`,
5687
- ...held && {
5927
+ ...held2 && {
5688
5928
  // Per-code copy can't say where the money went, and this is the one
5689
5929
  // site where that is the caller's next action.
5690
- 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.`
5930
+ 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.`
5931
+ },
5932
+ ...!held2 && headerless && {
5933
+ 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."
5691
5934
  },
5692
5935
  extra: {
5693
5936
  required_msats: requiredMsats,
5694
5937
  received_msats: result.paidMsats,
5695
5938
  mints,
5696
- ...held && {
5697
- credit_id: held.creditId,
5698
- credited_micro: held.deposit.amountMicro,
5699
- credit_currency: held.deposit.currency
5939
+ ...held2 && {
5940
+ credit_id: held2.creditId,
5941
+ credited_micro: held2.deposit.amountMicro,
5942
+ credit_currency: held2.deposit.currency
5700
5943
  }
5701
5944
  }
5702
5945
  }),
5703
- status: 402,
5946
+ status: headerless ? 400 : 402,
5704
5947
  forfeit,
5705
5948
  // Report the deposit on the failure path too, or the platform never
5706
5949
  // learns about a liability it is carrying. Mutually exclusive with
5707
5950
  // `forfeit` by construction: one is money still owed, the other is
5708
5951
  // money nothing is owed against.
5709
- ...held && { funding: held.deposit }
5952
+ ...held2 && { funding: held2.deposit }
5710
5953
  }
5711
5954
  };
5712
5955
  }
@@ -5716,13 +5959,11 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5716
5959
  paymentMint: result.mintUrl,
5717
5960
  paymentRail: "cashu",
5718
5961
  receivedProofs: [],
5719
- // requestId is non-null here because acceptAccumulatorPayment returns
5720
- // kind:"rejected" with code "missing_request_id" when unset (handled by
5721
- // the rejected branch above). Thread it as the per-call settlement
5722
- // reference for revenue_events.tx_hash. Unique per
5723
- // (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint, so
5724
- // it serves as a stable id even though it isn't an on-chain hash.
5725
- paymentTxHash: opts.requestId,
5962
+ // The request id the proofs were banked under, supplied or derived, is
5963
+ // the per-call settlement reference for revenue_events.tx_hash. Unique
5964
+ // per (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint,
5965
+ // so it serves as a stable id even though it isn't an on-chain hash.
5966
+ paymentTxHash: result.requestId,
5726
5967
  cashuFlow: "p2pk_accumulator"
5727
5968
  },
5728
5969
  ledgerCtx,
@@ -5798,7 +6039,7 @@ async function resolveTopUpLedgerContext(opts, snapshot, binding) {
5798
6039
  draw_id: snapshot.drawId,
5799
6040
  ...extra,
5800
6041
  ...typeof opts.jobId === "string" && {
5801
- job_id_hash: createHash4("sha256").update(opts.jobId, "utf8").digest("hex")
6042
+ job_id_hash: createHash5("sha256").update(opts.jobId, "utf8").digest("hex")
5802
6043
  }
5803
6044
  }
5804
6045
  );
@@ -9501,7 +9742,7 @@ async function handle(c, opts, action) {
9501
9742
  }
9502
9743
 
9503
9744
  // src/sdk/server/x402-exact-settlement.ts
9504
- import { createHash as createHash5 } from "crypto";
9745
+ import { createHash as createHash6 } from "crypto";
9505
9746
  var X402ExactSettlementServer = class {
9506
9747
  constructor(opts) {
9507
9748
  this.opts = opts;
@@ -9827,7 +10068,7 @@ function settlementMayHaveMoved(response) {
9827
10068
  return reason.includes("authorization_already_used") || reason.includes("authorization already used") || /facilitator_status_5\d\d/.test(reason);
9828
10069
  }
9829
10070
  function hash(parts) {
9830
- return createHash5("sha256").update(JSON.stringify(parts)).digest("hex");
10071
+ return createHash6("sha256").update(JSON.stringify(parts)).digest("hex");
9831
10072
  }
9832
10073
  function safeError(err) {
9833
10074
  if (err instanceof Error) return err.name;
@@ -9835,7 +10076,7 @@ function safeError(err) {
9835
10076
  }
9836
10077
 
9837
10078
  // src/sdk/server/hosting-settlement-report-obligations.ts
9838
- import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
10079
+ import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
9839
10080
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
9840
10081
  import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS3 } from "@x402/evm";
9841
10082
  import { computeChannelId as computeChannelId2 } from "@x402/evm/batch-settlement/client";
@@ -9908,7 +10149,7 @@ var TEMPO_CLOSED_EVENT = TempoSession.Precompile.escrowAbi.find(
9908
10149
  (item) => item.type === "event" && item.name === "ChannelClosed"
9909
10150
  );
9910
10151
  function hash2(value) {
9911
- return createHash6("sha256").update(JSON.stringify(value)).digest("hex");
10152
+ return createHash7("sha256").update(JSON.stringify(value)).digest("hex");
9912
10153
  }
9913
10154
  function hex(value, bytes) {
9914
10155
  return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${bytes * 2}}$`).test(value);
@@ -11296,7 +11537,7 @@ var HostingCleanupFence = class {
11296
11537
  };
11297
11538
 
11298
11539
  // src/sdk/server/hosting-channel-evidence.ts
11299
- import { createHash as createHash7 } from "crypto";
11540
+ import { createHash as createHash8 } from "crypto";
11300
11541
  import { Session as TempoSession2 } from "mppx/tempo";
11301
11542
  import { createPublicClient as createPublicClient5, http as http5 } from "viem";
11302
11543
  import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS4 } from "@x402/evm";
@@ -11347,7 +11588,7 @@ async function readHostedChannelSnapshot(query) {
11347
11588
  };
11348
11589
  const x402HistoryComplete = await history(names[6]);
11349
11590
  const tempoHistoryComplete = await history(names[7]);
11350
- const digest = createHash7("sha256").update(JSON.stringify({
11591
+ const digest = createHash8("sha256").update(JSON.stringify({
11351
11592
  present: [...exists].sort(),
11352
11593
  x402,
11353
11594
  x402Tombstones,
@@ -12497,9 +12738,9 @@ import { AsyncLocalStorage } from "async_hooks";
12497
12738
  import { randomBytes as randomBytes3 } from "crypto";
12498
12739
 
12499
12740
  // src/sdk/server/instrumented-kv.ts
12500
- import { createHash as createHash8 } from "crypto";
12741
+ import { createHash as createHash9 } from "crypto";
12501
12742
  function hashKey(key) {
12502
- return createHash8("sha256").update(key).digest("hex").slice(0, 16);
12743
+ return createHash9("sha256").update(key).digest("hex").slice(0, 16);
12503
12744
  }
12504
12745
  function instrumentKVStore(inner) {
12505
12746
  return {
@@ -12813,7 +13054,7 @@ function buildContext(job, opts) {
12813
13054
  let x402Nonce;
12814
13055
  let x402AmountUsdcMicro;
12815
13056
  const hasExplicitX402 = Boolean(payOpts?.x402);
12816
- if ((opts.pricingCurrency ?? "usd") === "usd") {
13057
+ if (!opts.cashuOnly && (opts.pricingCurrency ?? "usd") === "usd") {
12817
13058
  if (payOpts?.x402) {
12818
13059
  paymentRequestContent.x402 = payOpts.x402;
12819
13060
  } else if (opts.paymentMethods?.includes("x402") && opts.x402 && (opts.x402FacilitatorHealth?.support().available ?? true)) {
@@ -12852,7 +13093,8 @@ function buildContext(job, opts) {
12852
13093
  job.pendingX402Nonce = x402Nonce;
12853
13094
  job.pendingX402AmountUsdcMicro = x402AmountUsdcMicro;
12854
13095
  let mppChallengeIds;
12855
- if (payOpts?.mpp) {
13096
+ if (opts.cashuOnly) {
13097
+ } else if (payOpts?.mpp) {
12856
13098
  const challenges = payOpts.mpp.challenges.filter((c) => c.intent !== "session");
12857
13099
  if (challenges.length > 0) {
12858
13100
  paymentRequestContent.tempo = { challenges };
@@ -13260,6 +13502,39 @@ function resultHashFor(record) {
13260
13502
  return computeResultHash(summary, artifacts);
13261
13503
  }
13262
13504
 
13505
+ // src/sdk/server/unsigned-request.ts
13506
+ function takesUnsignedPath(auth, body) {
13507
+ if (!auth?.allowUnsignedPaid) return false;
13508
+ const raw = signedRequestInput(body);
13509
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return true;
13510
+ return !SIGNED_ENVELOPE_FIELDS.some((field) => field in raw);
13511
+ }
13512
+ function carriesCreditFields(raw) {
13513
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return false;
13514
+ return CREDIT_ENVELOPE_KEYS.some((field) => field in raw);
13515
+ }
13516
+ function unsignedInputSchema(schema) {
13517
+ if (narrowed.has(schema)) return narrowed.get(schema);
13518
+ const result = narrow(schema);
13519
+ narrowed.set(schema, result);
13520
+ return result;
13521
+ }
13522
+ var narrowed = /* @__PURE__ */ new WeakMap();
13523
+ function narrow(schema) {
13524
+ const candidate = schema;
13525
+ const def = candidate._zod?.def;
13526
+ if (def?.type !== "object" || typeof candidate.clone !== "function" || candidate.shape === null || typeof candidate.shape !== "object") {
13527
+ return void 0;
13528
+ }
13529
+ const shape = { ...candidate.shape };
13530
+ for (const field of SIGNED_ENVELOPE_FIELDS) delete shape[field];
13531
+ try {
13532
+ return candidate.clone({ ...def, shape });
13533
+ } catch {
13534
+ return void 0;
13535
+ }
13536
+ }
13537
+
13263
13538
  // src/lib/cashu/mint-info.ts
13264
13539
  async function getKeysetFinalExpiry(mintUrl, keysetId) {
13265
13540
  try {
@@ -13723,11 +13998,12 @@ var JobManager = class {
13723
13998
  * `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
13724
13999
  * Returns `undefined` for dynamic-priced capabilities (those that declare
13725
14000
  * `onQuote`) — the caller falls back to `computeDynamicPrice` in that path.
14001
+ * A caller that already holds the snapshot it prices at passes it as `fx`.
13726
14002
  */
13727
- async currentPriceMsats(capability) {
14003
+ async currentPriceMsats(capability, fx) {
13728
14004
  const cap = this.requireCapability(capability);
13729
14005
  if (cap.price === void 0) return void 0;
13730
- return resolveStaticPriceMsats(cap.price, this.opts.fxFetcher);
14006
+ return resolveStaticPriceMsats(cap.price, fx ?? await this.opts.fxFetcher.fetch());
13731
14007
  }
13732
14008
  /**
13733
14009
  * Translate a capability's static `price` into the fiat envelope used by
@@ -13760,12 +14036,16 @@ var JobManager = class {
13760
14036
  * explicit draw as an unknown key, before the envelope was even extracted.
13761
14037
  * The persisted `record.input` the reactivation path re-reads is the
13762
14038
  * pre-Zod wire form, so it comes through the second branch carrying them too.
14039
+ *
14040
+ * `unsigned` validates against the schema without its envelope fields; pass
14041
+ * it only when {@link unsignedPaidInput} accepted the same body.
13763
14042
  */
13764
- parseInput(body, capability) {
14043
+ parseInput(body, capability, unsigned2 = false) {
13765
14044
  const cap = this.requireCapability(capability);
13766
14045
  const rawSchema = cap.input;
13767
14046
  if (!rawSchema || !isZodSchema(rawSchema)) return body.input ?? "";
13768
- const schema = rawSchema;
14047
+ const schema = unsigned2 ? unsignedInputSchema(rawSchema) : rawSchema;
14048
+ if (!schema) throw new Error(`capability "${capability}" cannot validate unsigned input`);
13769
14049
  if (body.data !== void 0) {
13770
14050
  return schema.parse(stripProtocolEnvelope(schema, body.data));
13771
14051
  }
@@ -13775,6 +14055,16 @@ var JobManager = class {
13775
14055
  }
13776
14056
  throw new MissingStructuredInputError();
13777
14057
  }
14058
+ /**
14059
+ * Whether this body takes the unsigned paid path: the auth scheme allows it,
14060
+ * the raw input carries no envelope field, and the capability schema can be
14061
+ * narrowed to exclude the envelope. Otherwise the signed path applies.
14062
+ */
14063
+ unsignedPaidInput(body, capability) {
14064
+ if (!takesUnsignedPath(this.descriptor.auth, body)) return false;
14065
+ const rawSchema = this.requireCapability(capability).input;
14066
+ return isZodSchema(rawSchema) && unsignedInputSchema(rawSchema) !== void 0;
14067
+ }
13778
14068
  /**
13779
14069
  * Look up the per-capability descriptor by name. Throws when the
13780
14070
  * name doesn't exist — the route layer validates body.capability first, so
@@ -15261,8 +15551,9 @@ var JobManager = class {
15261
15551
  * it re-verified clean.
15262
15552
  */
15263
15553
  reactivateJob(record) {
15264
- const parsedInput = this.parseInput({ input: record.input }, record.capability);
15265
- if (this.descriptor.auth) {
15554
+ const unsigned2 = record.authRequestPath === void 0 && record.requesterPubkey === void 0 && this.unsignedPaidInput({ input: record.input }, record.capability);
15555
+ const parsedInput = this.parseInput({ input: record.input }, record.capability, unsigned2);
15556
+ if (this.descriptor.auth && !unsigned2) {
15266
15557
  const cap = this.requireCapability(record.capability);
15267
15558
  if (!cap.input || !isZodSchema(cap.input)) {
15268
15559
  throw new ReactivationAuthSchemaError(record.id, record.capability);
@@ -15806,6 +16097,7 @@ var JobManager = class {
15806
16097
  store: this.opts.store,
15807
16098
  env: this.opts.env,
15808
16099
  paymentMethods: this.opts.paymentMethods,
16100
+ cashuOnly: Boolean(this.descriptor.auth?.allowUnsignedPaid) && job.requesterPubkey === void 0,
15809
16101
  x402: this.opts.x402,
15810
16102
  x402FacilitatorHealth: this.opts.x402FacilitatorHealth,
15811
16103
  mpp: this.opts.mpp,
@@ -16610,9 +16902,8 @@ function unappliedCopy(reason, creditId) {
16610
16902
  }
16611
16903
  }[reason];
16612
16904
  }
16613
- async function resolveStaticPriceMsats(price, fxFetcher) {
16905
+ function resolveStaticPriceMsats(price, fx) {
16614
16906
  const usd = parseUsdPrice(price, "price");
16615
- const fx = await fxFetcher.fetch();
16616
16907
  return fiatToSatsCeil(usd, fxRateFor(fx, "usd")) * 1e3;
16617
16908
  }
16618
16909
  function deriveAuthFromParsedInput(descriptor, parsedInput) {
@@ -17678,7 +17969,7 @@ function withWallClockDeadline(promise, timeoutMs) {
17678
17969
  }
17679
17970
 
17680
17971
  // src/sdk/server/balance-payment.ts
17681
- import { createHash as createHash9 } from "crypto";
17972
+ import { createHash as createHash10 } from "crypto";
17682
17973
  import { z as z2 } from "zod";
17683
17974
  var BALANCE_PAYMENT_SCHEMA = z2.object({
17684
17975
  credit_id: z2.string().min(1).max(256),
@@ -17708,7 +17999,7 @@ async function commitBalancePayment(args) {
17708
17999
  args.auth.recheckSignature(BALANCE_PAYMENT_SCHEMA, args.raw, args.domain);
17709
18000
  const auth = BALANCE_PAYMENT_SCHEMA.parse(args.raw);
17710
18001
  const { pubkey, credit_id, draw_id, payment_id, request_seq, amount_micro, currency } = auth;
17711
- const fingerprint = createHash9("sha256").update(JSON.stringify([
18002
+ const fingerprint = createHash10("sha256").update(JSON.stringify([
17712
18003
  args.domain,
17713
18004
  args.jobId,
17714
18005
  pubkey,
@@ -17832,7 +18123,7 @@ async function commitBalancePayment(args) {
17832
18123
  }
17833
18124
 
17834
18125
  // src/sdk/server/app.ts
17835
- import { createHash as createHash11, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
18126
+ import { createHash as createHash12, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
17836
18127
  import { Hono } from "hono";
17837
18128
  import { getCookie } from "hono/cookie";
17838
18129
  import { cors } from "hono/cors";
@@ -20883,7 +21174,7 @@ initSchemaForm();
20883
21174
  }
20884
21175
 
20885
21176
  // src/sdk/server/request-fingerprint.ts
20886
- import { createHash as createHash10 } from "crypto";
21177
+ import { createHash as createHash11 } from "crypto";
20887
21178
  function requestFingerprint(req, opts) {
20888
21179
  const canonical = canonicalize({
20889
21180
  capability: req.capability,
@@ -20891,7 +21182,7 @@ function requestFingerprint(req, opts) {
20891
21182
  params: req.params ?? {},
20892
21183
  data: opts.stripAuthEnvelope ? stripEnvelope(req.data) : req.data
20893
21184
  });
20894
- return createHash10("sha256").update(canonical).digest("hex");
21185
+ return createHash11("sha256").update(canonical).digest("hex");
20895
21186
  }
20896
21187
  var ENVELOPE_KEYS = /* @__PURE__ */ new Set(["pubkey", "signature", "timestamp", "nonce", "auth_statement"]);
20897
21188
  function stripEnvelope(data) {
@@ -21014,7 +21305,7 @@ async function createDVMServer(descriptor, opts) {
21014
21305
  }
21015
21306
  }
21016
21307
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
21017
- const { RevenueReporter } = await import("./revenue-reporter-2P3PWJJM.js");
21308
+ const { RevenueReporter } = await import("./revenue-reporter-6XARUEG6.js");
21018
21309
  const reporter = new RevenueReporter(
21019
21310
  opts.db,
21020
21311
  platformUrl,
@@ -21024,7 +21315,7 @@ async function createDVMServer(descriptor, opts) {
21024
21315
  revenueReporter = reporter;
21025
21316
  await reporter.init();
21026
21317
  reporter.startRetryLoop();
21027
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-P3652TLG.js");
21318
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-5QAHCYIO.js");
21028
21319
  payoutReporter = new PayoutReporter({
21029
21320
  dvmId,
21030
21321
  db: opts.db,
@@ -21305,7 +21596,9 @@ var DVMServer = class {
21305
21596
  this.consumedCredentialStore = opts.consumedCredentialStore ?? new MemoryConsumedCredentialStore();
21306
21597
  this.adminCashuNonceStore = opts.adminCashuNonceStore ?? createAdminCashuNonceStore();
21307
21598
  const fxEnv = opts.env;
21308
- this.fxFetcher = opts.fxFetcher ?? createFxFetcher({ source: resolveFxSourceFromEnv(fxEnv) });
21599
+ this.fxFetcher = withFxHistory(
21600
+ opts.fxFetcher ?? createFxFetcher({ source: resolveFxSourceFromEnv(fxEnv) })
21601
+ );
21309
21602
  const fxCurrencies = this.fxFetcher.supportedCurrencies();
21310
21603
  if (!fxCurrencies.includes(this.pricingCurrency)) {
21311
21604
  throw new Error(
@@ -21414,6 +21707,15 @@ var DVMServer = class {
21414
21707
  this.app = new Hono();
21415
21708
  try {
21416
21709
  this.registerRoutes();
21710
+ if (opts.mints?.length && !this.resolvePaymentMethods().includes("cashu")) {
21711
+ diagnosticConsole.warn({
21712
+ event: "cashu_not_advertised",
21713
+ cashu_explicitly_excluded: opts.paymentMethods !== void 0 && !opts.paymentMethods.includes("cashu"),
21714
+ cashu_mode_missing: opts.cashuMode !== "p2pk-accumulator",
21715
+ cashu_lock_pubkey_missing: !opts.lockPubkey,
21716
+ cashu_store_missing: !opts.db && !opts.disposableCashu
21717
+ });
21718
+ }
21417
21719
  if (opts.hostingControlInstance) {
21418
21720
  this.hostingSweepTimer = setInterval(() => {
21419
21721
  void this.sweepHostingDeadline().catch((error) => {
@@ -21623,7 +21925,7 @@ var DVMServer = class {
21623
21925
  })
21624
21926
  );
21625
21927
  if (this.opts.devMode) {
21626
- this.app.use("*", cors());
21928
+ this.app.use("*", cors({ exposeHeaders: ["X-Cashu"] }));
21627
21929
  this.app.get("/_dev", (c) => c.html(devPageHtml(this.descriptor.name)));
21628
21930
  this.app.all("/_proxy/*", (c) => this.handleProxy(c));
21629
21931
  }
@@ -21937,7 +22239,7 @@ var DVMServer = class {
21937
22239
  const dvmId = this.opts.dvmId ?? `dev:${this.descriptor.name}`;
21938
22240
  return {
21939
22241
  dvm_id: dvmId,
21940
- builder_pubkey: createHash11("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
22242
+ builder_pubkey: createHash12("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
21941
22243
  };
21942
22244
  }
21943
22245
  throw new AuthAudienceError();
@@ -22035,7 +22337,7 @@ var DVMServer = class {
22035
22337
  const methods = this.resolvePaymentMethods().filter(
22036
22338
  (method) => method !== "tempo" || creditMpp
22037
22339
  );
22038
- if (this.x402CurrencySupported() && this.x402BatchAvailable() && !methods.includes("x402")) {
22340
+ if ((this.opts.paymentMethods === void 0 || this.opts.paymentMethods.includes("x402")) && this.x402CurrencySupported() && this.x402BatchAvailable() && !methods.includes("x402")) {
22039
22341
  methods.push("x402");
22040
22342
  }
22041
22343
  if (this.opts.lightningReceive?.available()) methods.push("lightning");
@@ -22058,7 +22360,9 @@ var DVMServer = class {
22058
22360
  * distinction, and a reason it can relay.
22059
22361
  */
22060
22362
  creditMppSplit() {
22061
- if (!this.opts.mpp) return { offered: [], withheld: [] };
22363
+ if (!this.opts.mpp || !this.resolvePaymentMethods().includes("tempo")) {
22364
+ return { offered: [], withheld: [] };
22365
+ }
22062
22366
  const configured = advertisedMethods(this.opts.mpp).filter(
22063
22367
  (method) => method.intent === "charge" || method.intent === "session"
22064
22368
  ).sort(
@@ -22290,7 +22594,7 @@ var DVMServer = class {
22290
22594
  const paymentAuthorization = ctx.req.header("Authorization");
22291
22595
  const mppArtifact = paymentAuthorization ? (await import("mppx")).Credential.extractPaymentScheme(paymentAuthorization) : void 0;
22292
22596
  const attachedPayment = ctx.req.header("X-Cashu") ?? ctx.req.header("PAYMENT-SIGNATURE") ?? ctx.req.header("X-PAYMENT") ?? mppArtifact;
22293
- const paymentArtifactHash = attachedPayment ? createHash11("sha256").update(attachedPayment).digest("hex") : void 0;
22597
+ const paymentArtifactHash = attachedPayment ? createHash12("sha256").update(attachedPayment).digest("hex") : void 0;
22294
22598
  hostingFundingArtifactHash = paymentArtifactHash;
22295
22599
  if (!await control.admit(admissionId, workId, {
22296
22600
  creditId,
@@ -22390,7 +22694,7 @@ var DVMServer = class {
22390
22694
  name: this.descriptor.name,
22391
22695
  description: this.descriptor.description || void 0,
22392
22696
  tags: this.descriptor.tags,
22393
- auth: this.descriptor.auth ? [this.descriptor.auth.id] : ["none"],
22697
+ auth: !this.descriptor.auth ? ["none"] : this.descriptor.auth.allowUnsignedPaid ? [this.descriptor.auth.id, "none"] : [this.descriptor.auth.id],
22394
22698
  ...this.authAudience ? { auth_audience: this.authAudience } : {},
22395
22699
  protocol_version: 1,
22396
22700
  sdk_sha: this.opts.env.GIT_SHA ?? "unknown",
@@ -22409,7 +22713,7 @@ var DVMServer = class {
22409
22713
  if (this.opts.mints) info.mints = this.advertisedMints();
22410
22714
  info.capabilities = this.renderCapabilities();
22411
22715
  info.payment = { methods: this.resolvePaymentMethods() };
22412
- if (this.x402CurrencySupported() && this.opts.x402 && this.x402Support()?.available) {
22716
+ if (info.payment.methods.includes("x402") && this.opts.x402 && this.x402Support()?.available) {
22413
22717
  info.x402 = {
22414
22718
  pay_to: this.opts.x402.payTo,
22415
22719
  network: this.opts.x402.network,
@@ -22419,7 +22723,7 @@ var DVMServer = class {
22419
22723
  dual_serve: this.opts.x402.dualServe ?? true
22420
22724
  };
22421
22725
  }
22422
- if (this.opts.mpp) {
22726
+ if (info.payment.methods.includes("tempo") && this.opts.mpp) {
22423
22727
  const configured = advertisedMethods(this.opts.mpp);
22424
22728
  const withheld = configured.flatMap((method) => {
22425
22729
  const reason = this.tempoSessionWithheldReason(method);
@@ -22455,7 +22759,7 @@ var DVMServer = class {
22455
22759
  mints,
22456
22760
  lock_pubkeys: activePubkeySet(active),
22457
22761
  lock_pubkey_grace_seconds: this.graceSeconds(),
22458
- min_locktime: 604800,
22762
+ min_locktime: CASHU_MIN_LOCKTIME_SECONDS,
22459
22763
  canonical_id: this.opts.canonicalDvmId ?? this.opts.dvmId ?? null
22460
22764
  };
22461
22765
  }
@@ -22794,6 +23098,48 @@ var DVMServer = class {
22794
23098
  400
22795
23099
  );
22796
23100
  }
23101
+ /**
23102
+ * Refuse an x402 or Tempo mid-job payment on an unsigned paid job before it
23103
+ * is recorded or verified: those rails settle into credit owned by a caller
23104
+ * pubkey, which the job lacks. Signed jobs always store their pubkey.
23105
+ */
23106
+ unsignedJobRailRefusal(job, body) {
23107
+ if (!this.descriptor.auth?.allowUnsignedPaid || job.requesterPubkey !== void 0) return void 0;
23108
+ if (body.type !== "payment" || !body.content.x402_payment && !body.content.tempo_credential) {
23109
+ return void 0;
23110
+ }
23111
+ return buildPaymentErrorResponse(
23112
+ paymentErrorBody("payment_method_requires_signature", {
23113
+ message: "Unsigned jobs may be paid only with Cashu; an x402 or Tempo payment was sent."
23114
+ }),
23115
+ void 0,
23116
+ void 0,
23117
+ 402
23118
+ );
23119
+ }
23120
+ /** Whether this DVM takes Cashu, the only rail an unsigned payer may use. */
23121
+ unsignedCashuOffered() {
23122
+ return (this.opts.mints?.length ?? 0) > 0 && this.resolvePaymentMethods().includes("cashu");
23123
+ }
23124
+ /**
23125
+ * The error the signed path raises for this envelope-free body, so a refused
23126
+ * unsigned request reads exactly as it does on a DVM without the option.
23127
+ */
23128
+ unsignedRequestRefusal(c, schema, body, capabilityName) {
23129
+ try {
23130
+ if (this.descriptor.auth && isZodSchema(schema)) {
23131
+ this.descriptor.auth.checkAuth(
23132
+ schema,
23133
+ signedRequestInput(body),
23134
+ this.signedRequestDomain(c, capabilityName)
23135
+ );
23136
+ }
23137
+ } catch (err) {
23138
+ if (err instanceof SignedRequestError) return err;
23139
+ throw err;
23140
+ }
23141
+ return new SignedRequestError("signed request required", "signature_invalid");
23142
+ }
22797
23143
  /**
22798
23144
  * On authenticated DVMs, envelope-only parse failures defer to auth
22799
23145
  * (401 signature_invalid). Builder input issues remain 400 invalid_input.
@@ -22823,6 +23169,25 @@ var DVMServer = class {
22823
23169
  if (healthy.length > 0) return healthy;
22824
23170
  return all.filter((m) => !tracker.isNutIncompatible(m));
22825
23171
  }
23172
+ /**
23173
+ * This request's price at the rates `pricedAt` replaced, or `undefined` when
23174
+ * there are none or they were replaced longer ago than the hold window.
23175
+ * Computed the way the current price is, so the figure matches the one an
23176
+ * earlier 402 quoted.
23177
+ */
23178
+ previousPriceMsats(cap, dynamicUpfront, pricedAt) {
23179
+ const previous = pricedAt && this.fxFetcher.previousFor(pricedAt);
23180
+ if (!previous) return void 0;
23181
+ try {
23182
+ if (dynamicUpfront) {
23183
+ return fiatToSatsCeil(dynamicUpfront.amount, fxRateFor(previous, dynamicUpfront.currency)) * 1e3;
23184
+ }
23185
+ if (cap.price === void 0) return void 0;
23186
+ return fiatToSatsCeil(parseUsdPrice(cap.price, "price"), fxRateFor(previous, "usd")) * 1e3;
23187
+ } catch {
23188
+ return void 0;
23189
+ }
23190
+ }
22826
23191
  /** Authenticated, read-only recovery of a previously accepted public request id. */
22827
23192
  async handleRequestResult(c) {
22828
23193
  const requestId = c.req.param("requestId");
@@ -22893,6 +23258,14 @@ var DVMServer = class {
22893
23258
  400
22894
23259
  );
22895
23260
  }
23261
+ if (c.req.header("X-Cashu-Request-Id")?.startsWith(DERIVED_REQUEST_ID_PREFIX)) {
23262
+ return c.json(
23263
+ paymentErrorBody("reserved_request_id", {
23264
+ message: `X-Cashu-Request-Id may not start with "${DERIVED_REQUEST_ID_PREFIX}".`
23265
+ }),
23266
+ 400
23267
+ );
23268
+ }
22896
23269
  const publicRequestId = requestIdFromEnvelope(body.data);
22897
23270
  const recoveryStore = requestRecoveryStore(this.jobManager.store);
22898
23271
  const resumedRequestId = resumeOnly ? c.req.param("requestId") : void 0;
@@ -22911,6 +23284,17 @@ var DVMServer = class {
22911
23284
  const capabilityResult = this.requireCapabilityFromBody(c, body.capability);
22912
23285
  if ("error" in capabilityResult) return capabilityResult.error;
22913
23286
  const capabilityName = capabilityResult.name;
23287
+ const unsignedPaid = this.jobManager.unsignedPaidInput(body, capabilityName);
23288
+ if (unsignedPaid && publicRequestId) {
23289
+ return c.json(
23290
+ {
23291
+ error: "request_recovery_unsupported",
23292
+ message: "Public request ids require a signed request.",
23293
+ hint: "Sign the request to use request_id recovery, or omit request_id."
23294
+ },
23295
+ 400
23296
+ );
23297
+ }
22914
23298
  pinDimension("capability", capabilityName);
22915
23299
  const cap = this.descriptor.capabilities[capabilityName];
22916
23300
  let parsedInput;
@@ -22919,13 +23303,23 @@ var DVMServer = class {
22919
23303
  parsedInput = await loggers.isolate.span(
22920
23304
  "dvm.input.validate",
22921
23305
  { capability: capabilityName, stage: "input" },
22922
- () => this.jobManager.parseInput(body, capabilityName),
23306
+ () => this.jobManager.parseInput(body, capabilityName, unsignedPaid),
22923
23307
  { onError: () => ({ "error.code": "invalid_input" }) }
22924
23308
  );
22925
23309
  } catch (err) {
22926
- if (!this.deferToAuthGate(err)) return this.invalidInputResponse(c, err);
23310
+ if (unsignedPaid || !this.deferToAuthGate(err)) return this.invalidInputResponse(c, err);
22927
23311
  deferredInputError = err;
22928
23312
  }
23313
+ if (unsignedPaid && carriesCreditFields(signedRequestInput(body))) {
23314
+ return c.json(
23315
+ {
23316
+ error: "invalid_credit_fields",
23317
+ message: "credit_id, draw_id and fund require a signed request.",
23318
+ hint: "Sign the request to draw on or fund prepaid credit, or remove the credit fields and pay per call."
23319
+ },
23320
+ 400
23321
+ );
23322
+ }
22929
23323
  let creditEnvelope;
22930
23324
  try {
22931
23325
  creditEnvelope = extractCreditEnvelope(body.data);
@@ -22942,9 +23336,26 @@ var DVMServer = class {
22942
23336
  }
22943
23337
  throw err;
22944
23338
  }
23339
+ if (unsignedPaid && (cap.price === void 0 && !cap.onQuote || devModeSkipsPaymentVerification(this.opts) || !this.unsignedCashuOffered())) {
23340
+ return buildAuthErrorResponse(
23341
+ c,
23342
+ this.unsignedRequestRefusal(c, cap.input, body, capabilityName),
23343
+ this.publicOrigin
23344
+ );
23345
+ }
23346
+ if (unsignedPaid && await carriesNonCashuPayment(c)) {
23347
+ return buildPaymentErrorResponse(
23348
+ paymentErrorBody("payment_method_requires_signature", {
23349
+ message: "Unsigned requests may pay only with Cashu; an x402 or Tempo payment was attached."
23350
+ }),
23351
+ void 0,
23352
+ void 0,
23353
+ 402
23354
+ );
23355
+ }
22945
23356
  let authEnvelope;
22946
23357
  let authPubkey;
22947
- if (this.descriptor.auth) {
23358
+ if (this.descriptor.auth && !unsignedPaid) {
22948
23359
  const auth = this.descriptor.auth;
22949
23360
  try {
22950
23361
  const result = await loggers.isolate.span(
@@ -22992,14 +23403,14 @@ var DVMServer = class {
22992
23403
  { capability: capabilityName, input: body.input, params: body.params, data: body.data },
22993
23404
  { stripAuthEnvelope: this.descriptor.auth !== void 0 }
22994
23405
  );
22995
- const authFingerprint = this.descriptor.auth ? requestFingerprint(
23406
+ const authFingerprint = authEnvelope ? requestFingerprint(
22996
23407
  { capability: capabilityName, input: body.input, params: body.params, data: body.data },
22997
23408
  { stripAuthEnvelope: false }
22998
23409
  ) : void 0;
22999
23410
  const authorization = c.req.header("Authorization");
23000
23411
  const mppArtifact = authorization ? (await import("mppx")).Credential.extractPaymentScheme(authorization) : void 0;
23001
23412
  const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? mppArtifact;
23002
- const hostingPaymentArtifactHash = attachedPayment ? createHash11("sha256").update(attachedPayment).digest("hex") : void 0;
23413
+ const hostingPaymentArtifactHash = attachedPayment ? createHash12("sha256").update(attachedPayment).digest("hex") : void 0;
23003
23414
  if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
23004
23415
  const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
23005
23416
  hostingPaymentArtifactHash
@@ -23081,9 +23492,11 @@ var DVMServer = class {
23081
23492
  }
23082
23493
  const skipPayment = devModeSkipsPaymentVerification(this.opts);
23083
23494
  let priceMsats = resumeOnly ? payment.paidMsats || void 0 : void 0;
23495
+ let pricedAt;
23084
23496
  if (!resumeOnly && !skipPayment) {
23085
23497
  try {
23086
- priceMsats = await this.jobManager.currentPriceMsats(capabilityName);
23498
+ if (cap.price !== void 0) pricedAt = await this.fxFetcher.fetch();
23499
+ priceMsats = await this.jobManager.currentPriceMsats(capabilityName, pricedAt);
23087
23500
  } catch (err) {
23088
23501
  const fxErr = pickFxRateUnavailable(err);
23089
23502
  if (fxErr) return c.json({ error: fxErr }, 503);
@@ -23097,17 +23510,26 @@ var DVMServer = class {
23097
23510
  parsedInput,
23098
23511
  normalisedBody,
23099
23512
  authPubkey,
23100
- skipPayment
23513
+ skipPayment,
23514
+ unsignedPaid
23101
23515
  );
23102
23516
  if (dyn.error) return c.json(dyn.error.body, dyn.error.status);
23103
23517
  priceMsats = dyn.priceMsats;
23104
23518
  dynamicUpfront = dyn.upfront;
23519
+ pricedAt = dyn.pricedAt;
23105
23520
  }
23106
23521
  const priceFiat = resumeOnly ? void 0 : resolvePriceFiat({
23107
23522
  price: typeof cap.price === "string" ? cap.price : void 0,
23108
23523
  priceMsats,
23109
23524
  dynamicUpfront
23110
23525
  });
23526
+ if (unsignedPaid && !priceMsats) {
23527
+ return buildAuthErrorResponse(
23528
+ c,
23529
+ this.unsignedRequestRefusal(c, cap.input, body, capabilityName),
23530
+ this.publicOrigin
23531
+ );
23532
+ }
23111
23533
  if (!resumeOnly && creditEnvelope?.creditId) {
23112
23534
  await this.settleLightningFunding(authPubkey, creditEnvelope.creditId);
23113
23535
  }
@@ -23130,7 +23552,13 @@ var DVMServer = class {
23130
23552
  }
23131
23553
  }
23132
23554
  if (!skipPayment && !resumedAcceptedPayment) {
23133
- const railOpts = await this.railVerifyOpts(c);
23555
+ const railOpts = unsignedPaid ? cashuOnlyRailOpts(await this.railVerifyOpts(c)) : await this.railVerifyOpts(c);
23556
+ if (!this.descriptor.auth || unsignedPaid) {
23557
+ railOpts.nut24 = {
23558
+ mints: this.advertisedMints(),
23559
+ previousRequiredMsats: this.previousPriceMsats(cap, dynamicUpfront, pricedAt)
23560
+ };
23561
+ }
23134
23562
  hostingPaymentMayHaveLanded = Boolean(
23135
23563
  creditEnvelope || railOpts.cashuToken || railOpts.x402Payment || railOpts.mppCredential
23136
23564
  );
@@ -23168,7 +23596,7 @@ var DVMServer = class {
23168
23596
  this.reportShortPayForfeit(err.detail.forfeit);
23169
23597
  if (err.detail.body.error === "replay_rejected") {
23170
23598
  const recovered = await this.recoverReplayedJob(c, {
23171
- requestId: err.detail.replayPaymentId ?? c.req.header("X-Cashu-Request-Id"),
23599
+ requestId: err.detail.replayPaymentId,
23172
23600
  fingerprint,
23173
23601
  callerPubkey: authPubkey
23174
23602
  });
@@ -23196,15 +23624,17 @@ var DVMServer = class {
23196
23624
  }
23197
23625
  }
23198
23626
  if (!resumedAcceptedPayment) {
23199
- attachCreditMenu(
23200
- err.detail.body,
23201
- await this.creditMenu(this.creditCurrency(), authPubkey)
23202
- );
23627
+ if (!unsignedPaid) {
23628
+ attachCreditMenu(
23629
+ err.detail.body,
23630
+ await this.creditMenu(this.creditCurrency(), authPubkey)
23631
+ );
23632
+ }
23203
23633
  if (priceFiat) {
23204
23634
  err.detail.body.price_micro = priceFiat.amountMicro;
23205
23635
  err.detail.body.price_currency = priceFiat.currency;
23206
23636
  }
23207
- return buildPaymentErrorResponse(
23637
+ const response2 = buildPaymentErrorResponse(
23208
23638
  err.detail.body,
23209
23639
  err.detail.mppChallenges,
23210
23640
  err.detail.x402Requirements,
@@ -23214,6 +23644,11 @@ var DVMServer = class {
23214
23644
  this.x402Support()?.exact,
23215
23645
  err.detail.x402ErrorCode
23216
23646
  );
23647
+ if (err.detail.cashuPaymentRequest) {
23648
+ response2.headers.set("X-Cashu", err.detail.cashuPaymentRequest);
23649
+ response2.headers.set("Cache-Control", "no-store");
23650
+ }
23651
+ return response2;
23217
23652
  }
23218
23653
  }
23219
23654
  throw err;
@@ -23297,7 +23732,7 @@ var DVMServer = class {
23297
23732
  }
23298
23733
  const isAnonymous = requesterId === "anonymous" || requesterId === "dev-anonymous";
23299
23734
  const jobToken = isAnonymous ? randomBytes5(32).toString("hex") : void 0;
23300
- const requesterTokenHash = jobToken ? createHash11("sha256").update(jobToken).digest("hex") : void 0;
23735
+ const requesterTokenHash = jobToken ? createHash12("sha256").update(jobToken).digest("hex") : void 0;
23301
23736
  const job = jm.createJob(
23302
23737
  normalisedBody,
23303
23738
  requesterId,
@@ -23310,7 +23745,7 @@ var DVMServer = class {
23310
23745
  requestFingerprint: fingerprint,
23311
23746
  requesterPubkey: authPubkey,
23312
23747
  requestId: publicRequestId,
23313
- authRequestPath: this.descriptor.auth ? canonicalRequestPath(c.req.url) : void 0,
23748
+ authRequestPath: authEnvelope ? canonicalRequestPath(c.req.url) : void 0,
23314
23749
  jobId
23315
23750
  }
23316
23751
  );
@@ -23406,15 +23841,16 @@ var DVMServer = class {
23406
23841
  }
23407
23842
  /**
23408
23843
  * Recover a lost paid response only for matching request fingerprint,
23409
- * authenticated pubkey and requester identity. A spent bearer token alone
23410
- * is not proof of request ownership.
23844
+ * authenticated pubkey and requester identity. `requestId` must be the
23845
+ * payment reference the rail bound the presented payment to; a spent bearer
23846
+ * token alone is not proof of request ownership.
23411
23847
  */
23412
23848
  async recoverReplayedJob(c, opts) {
23413
23849
  if (!opts.requestId) return void 0;
23414
23850
  const prior = await this.jobManager.store.findJobByPaymentTxHash(opts.requestId);
23415
23851
  if (!prior) return void 0;
23416
23852
  const bodyMatches = prior.requestFingerprint !== void 0 && prior.requestFingerprint === opts.fingerprint;
23417
- const pubkeyMatches = this.descriptor.auth === void 0 || prior.requesterPubkey !== void 0 && prior.requesterPubkey === opts.callerPubkey;
23853
+ const pubkeyMatches = this.descriptor.auth === void 0 || prior.requesterPubkey === opts.callerPubkey;
23418
23854
  const requesterMatches = prior.requesterId === c.get("requesterId");
23419
23855
  if (!bodyMatches || !pubkeyMatches || !requesterMatches) {
23420
23856
  diagnosticConsole.warn(
@@ -23547,16 +23983,17 @@ var DVMServer = class {
23547
23983
  * schema separately. Refuse invalid input/results, currency mismatch or missing
23548
23984
  * FX when payment needs a price (400/500/503 as classified below).
23549
23985
  */
23550
- async computeDynamicPrice(onQuote, parsedInput, body, callerPubkey, skipRateConversion = false) {
23986
+ async computeDynamicPrice(onQuote, parsedInput, body, callerPubkey, skipRateConversion = false, unsigned2 = false) {
23551
23987
  let data = parsedInput;
23552
23988
  if (isZodSchema(onQuote.schema)) {
23553
- const parsed = onQuote.schema.safeParse(parsedInput);
23989
+ const schema = unsigned2 ? unsignedInputSchema(onQuote.schema) ?? onQuote.schema : onQuote.schema;
23990
+ const parsed = schema.safeParse(parsedInput);
23554
23991
  if (!parsed.success) {
23555
23992
  return {
23556
23993
  error: {
23557
23994
  body: {
23558
23995
  error: "quote_unavailable",
23559
- message: "DVM uses dynamic pricing but the job input does not satisfy onQuote.schema. Submit the same payload to /v1/quote to confirm pricing."
23996
+ 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."
23560
23997
  },
23561
23998
  status: 400
23562
23999
  }
@@ -23586,7 +24023,8 @@ var DVMServer = class {
23586
24023
  const priceMsats = fiatToSatsCeil(validated.upfront.amount, fxRateFor(fx, upfrontCurrency)) * 1e3;
23587
24024
  return {
23588
24025
  priceMsats,
23589
- upfront: { amount: validated.upfront.amount, currency: upfrontCurrency }
24026
+ upfront: { amount: validated.upfront.amount, currency: upfrontCurrency },
24027
+ pricedAt: fx
23590
24028
  };
23591
24029
  } catch (err) {
23592
24030
  if (err instanceof InvalidCurrencyError || err instanceof UnsupportedCurrencyError) {
@@ -23868,6 +24306,8 @@ var DVMServer = class {
23868
24306
  this.opts.devMode === true
23869
24307
  );
23870
24308
  if (contentErr2) return c.json({ error: "invalid_message", message: contentErr2 }, 400);
24309
+ const railRefusal2 = this.unsignedJobRailRefusal(activeJob, body2);
24310
+ if (railRefusal2) return railRefusal2;
23871
24311
  const inboundSeq2 = await jm.recordInboundPending(activeJob, body2.type, body2.content);
23872
24312
  const lockPubkeys2 = await this.resolveLockPubkeysForReceive();
23873
24313
  let unappliedCredit2;
@@ -23928,6 +24368,8 @@ var DVMServer = class {
23928
24368
  }
23929
24369
  const contentErr = validateMessageContent(body.type, body.content, this.opts.devMode === true);
23930
24370
  if (contentErr) return c.json({ error: "invalid_message", message: contentErr }, 400);
24371
+ const railRefusal = this.unsignedJobRailRefusal(record, body);
24372
+ if (railRefusal) return railRefusal;
23931
24373
  if (isStreamableJobStore(jm.store)) {
23932
24374
  record.messages = await jm.store.getMessages(id, 0);
23933
24375
  record.seq = Math.max(record.seq, ...record.messages.map((message) => message.seq));
@@ -24127,7 +24569,7 @@ function isJobOwner(c, job) {
24127
24569
  if (job.requesterTokenHash) {
24128
24570
  const token = c.req.header("X-Job-Token");
24129
24571
  if (!token) return false;
24130
- const provided = createHash11("sha256").update(token).digest("hex");
24572
+ const provided = createHash12("sha256").update(token).digest("hex");
24131
24573
  return timingSafeEqualHex(provided, job.requesterTokenHash);
24132
24574
  }
24133
24575
  return c.get("requesterId") === job.requesterId;
@@ -24250,6 +24692,24 @@ function removeUnavailableContent(fields) {
24250
24692
  delete fields.payload_in;
24251
24693
  }
24252
24694
  }
24695
+ async function carriesNonCashuPayment(c) {
24696
+ if (c.req.header("PAYMENT-SIGNATURE") || c.req.header("X-PAYMENT")) return true;
24697
+ const authorization = c.req.header("Authorization");
24698
+ if (!authorization) return false;
24699
+ return Boolean((await import("mppx")).Credential.extractPaymentScheme(authorization));
24700
+ }
24701
+ function cashuOnlyRailOpts(opts) {
24702
+ return {
24703
+ ...opts,
24704
+ x402Payment: void 0,
24705
+ x402Config: void 0,
24706
+ x402ExactSettlement: void 0,
24707
+ x402ExactVersions: void 0,
24708
+ x402BatchSettlement: void 0,
24709
+ mppCredential: void 0,
24710
+ mpp: void 0
24711
+ };
24712
+ }
24253
24713
  function terminalResponseFields(job) {
24254
24714
  const fields = {};
24255
24715
  if (job.upfrontPayment !== void 0) fields.upfront_payment = job.upfrontPayment;