@dvmkit/sdk 0.1.3-rc.7 → 0.1.5-rc.7

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 (38) hide show
  1. package/README.md +116 -9
  2. package/dist/{chunk-ANFX5HEG.js → chunk-2UUXIIOC.js} +43 -13
  3. package/dist/{chunk-C6JHBLMW.js → chunk-BIP6G74V.js} +1 -1
  4. package/dist/{chunk-EPNDZ5DH.js → chunk-CEOAHV2I.js} +0 -30
  5. package/dist/{chunk-TSWKITGR.js → chunk-CGKZDODG.js} +8 -1
  6. package/dist/{chunk-YDIYXGYL.js → chunk-E4EVGPDX.js} +16 -9
  7. package/dist/{chunk-NFRM5QYP.js → chunk-EVBK675R.js} +5 -0
  8. package/dist/{chunk-DBCLBYHP.js → chunk-KVEHHC7W.js} +11 -1
  9. package/dist/{chunk-2K7E3N2D.js → chunk-KXCKFIJI.js} +77 -14
  10. package/dist/{chunk-3ZHMQCYP.js → chunk-NJFOV36R.js} +663 -235
  11. package/dist/{chunk-5GFED3GJ.js → chunk-NPIW5VR5.js} +40 -6
  12. package/dist/{chunk-LLXV32HA.js → chunk-SSSZUVWM.js} +96 -3
  13. package/dist/{chunk-7AKBC4PW.js → chunk-TQWGQCNV.js} +1 -1
  14. package/dist/{chunk-4B56DEEV.js → chunk-U6M3ATSG.js} +7 -1
  15. package/dist/{credit-menu-C7zAJElJ.d.ts → credit-menu-B-e3vZGo.d.ts} +136 -21
  16. package/dist/{fx-BF_SG2i0.d.ts → fx-CGRJE8rm.d.ts} +1 -1
  17. package/dist/index.d.ts +5 -5
  18. package/dist/internal/caller.d.ts +54 -22
  19. package/dist/internal/caller.js +171 -38
  20. package/dist/internal/index.js +1 -1
  21. package/dist/internal/server.d.ts +8 -8
  22. package/dist/internal/server.js +7 -7
  23. package/dist/{job-store-Bn23V3QU.d.ts → job-store-Cnlv9pOx.d.ts} +16 -1
  24. package/dist/{lightning-backend-C04nH94l.d.ts → lightning-backend-KvQM0YHi.d.ts} +1 -1
  25. package/dist/{memory-credit-ledger-OP24Z2KO.js → memory-credit-ledger-MNUOTQO5.js} +1 -1
  26. package/dist/{mpp-setup-MOBWGTWJ.js → mpp-setup-SPBOF5AM.js} +11 -1
  27. package/dist/{postgres-job-store-TAONYLIF.js → postgres-job-store-3RAXMNSY.js} +1 -1
  28. package/dist/{revenue-reporter-XXSU5KVB.js → revenue-reporter-ASZ7SHHH.js} +1 -1
  29. package/dist/server/index.d.ts +26 -13
  30. package/dist/server/index.js +219 -55
  31. package/dist/{step-cache-3cT4Shk0.d.ts → step-cache-CwM_Q8rK.d.ts} +149 -3
  32. package/dist/{tempo-lifecycle-SQL3KLEZ.js → tempo-lifecycle-DFIXQ54Q.js} +3 -3
  33. package/dist/{tempo-wallet-QOLEIPCV.js → tempo-wallet-4QKSV65O.js} +10 -2
  34. package/dist/testing/index.d.ts +13 -3
  35. package/dist/testing/index.js +31 -2
  36. package/dist/{usd-BnuXoFl5.d.ts → usd-nYZSb7KQ.d.ts} +1 -1
  37. package/dist/{x402-T2C5MX3T.js → x402-5H27DCBE.js} +2 -2
  38. package/package.json +6 -4
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-BQ2NMWKE.js";
11
11
  import {
12
12
  MemoryCreditLedger
13
- } from "./chunk-DBCLBYHP.js";
13
+ } from "./chunk-KVEHHC7W.js";
14
14
  import {
15
15
  SIGNED_ENVELOPE_FIELDS,
16
16
  SignedRequestError,
@@ -35,7 +35,7 @@ import {
35
35
  localMessage,
36
36
  providerMessage,
37
37
  toJobRecord
38
- } from "./chunk-4B56DEEV.js";
38
+ } from "./chunk-U6M3ATSG.js";
39
39
  import {
40
40
  ADMIN_AUTH_CLOCK_SKEW_SECONDS,
41
41
  ADMIN_AUTH_NONCE_TTL_SECONDS,
@@ -49,6 +49,7 @@ import {
49
49
  createMonotonicClock,
50
50
  getWallet,
51
51
  isPublicRequestId,
52
+ isTestMintUrl,
52
53
  isX402SettlementReconciliationReason,
53
54
  normaliseMintUrl,
54
55
  parseTraceparent,
@@ -59,7 +60,7 @@ import {
59
60
  toLockPubkey,
60
61
  verifyCashuToken,
61
62
  verifyChallenge
62
- } from "./chunk-2K7E3N2D.js";
63
+ } from "./chunk-KXCKFIJI.js";
63
64
  import {
64
65
  X402_BATCH_SETTLEMENT_SCHEME,
65
66
  X402_DEFAULT_FACILITATOR,
@@ -99,7 +100,7 @@ import {
99
100
  pinDimension,
100
101
  withSpan,
101
102
  withTraceContext
102
- } from "./chunk-YDIYXGYL.js";
103
+ } from "./chunk-E4EVGPDX.js";
103
104
  import {
104
105
  FxRateUnavailableError,
105
106
  InvalidCurrencyError,
@@ -140,7 +141,7 @@ import {
140
141
  advertisedMethods,
141
142
  challengeMeta,
142
143
  isTempoInsufficientFundsError
143
- } from "./chunk-5GFED3GJ.js";
144
+ } from "./chunk-NPIW5VR5.js";
144
145
 
145
146
  // src/sdk/server/lock-pubkey-store.ts
146
147
  async function initLockPubkeyTable(db) {
@@ -3074,7 +3075,8 @@ import {
3074
3075
  Amount as Amount2,
3075
3076
  CheckStateEnum,
3076
3077
  getP2PKExpectedWitnessPubkeys,
3077
- getTagInt
3078
+ getTagInt,
3079
+ verifyDleqIfPresent
3078
3080
  } from "@cashu/cashu-ts";
3079
3081
  function valueCashuFunding(wallet, proofs) {
3080
3082
  let grossSats = Amount2.zero();
@@ -3091,6 +3093,9 @@ function valueCashuFunding(wallet, proofs) {
3091
3093
  };
3092
3094
  }
3093
3095
  async function acceptAccumulatorPayment(opts) {
3096
+ if (opts.disposable && !opts.mints.every(isTestMintUrl)) {
3097
+ throw new Error("Disposable Cashu accepts only known test or loopback mints.");
3098
+ }
3094
3099
  if (!opts.requestId) {
3095
3100
  return {
3096
3101
  kind: "rejected",
@@ -3115,6 +3120,16 @@ async function acceptAccumulatorPayment(opts) {
3115
3120
  const walletProvider = opts.walletProvider ?? getWallet;
3116
3121
  const wallet = await walletProvider(mintUrl);
3117
3122
  proofs = wallet.decodeToken(opts.cashuToken).proofs;
3123
+ if (meta.unit !== "sat" || proofs.length === 0) {
3124
+ throw new Error("Cashu payment requires nonempty sat proofs.");
3125
+ }
3126
+ for (const proof of proofs) {
3127
+ if (!proof.dleq || !verifyDleqIfPresent(proof, wallet.getKeyset(proof.id))) {
3128
+ throw new Error(
3129
+ "Cashu payment requires valid mint-provided NUT-12 DLEQ evidence for every proof. Request a fresh token from a compatible mint."
3130
+ );
3131
+ }
3132
+ }
3118
3133
  valuation = valueCashuFunding(wallet, proofs);
3119
3134
  } catch (err) {
3120
3135
  return {
@@ -3149,6 +3164,9 @@ async function acceptAccumulatorPayment(opts) {
3149
3164
  };
3150
3165
  }
3151
3166
  const allowedNormalised = allowed.map(toLockPubkey);
3167
+ if (opts.disposable && (allowedNormalised.length !== 1 || (getTagInt(p.secret, "n_sigs") ?? 1) !== 1)) {
3168
+ return { kind: "rejected", code: "not_p2pk_locked", message: "Disposable Cashu requires an exclusive single-signature P2PK lock." };
3169
+ }
3152
3170
  const match = accepted.find((pk) => allowedNormalised.includes(pk));
3153
3171
  if (!match) {
3154
3172
  return {
@@ -3185,6 +3203,9 @@ async function acceptAccumulatorPayment(opts) {
3185
3203
  message: "One or more proofs are pending at the mint \u2014 retry shortly."
3186
3204
  };
3187
3205
  }
3206
+ if (states.some((state) => state.state !== CheckStateEnum.UNSPENT)) {
3207
+ return { kind: "rejected", code: "mint_unreachable", message: "Mint returned an unknown proof state." };
3208
+ }
3188
3209
  } catch (err) {
3189
3210
  return {
3190
3211
  kind: "rejected",
@@ -3202,7 +3223,13 @@ async function acceptAccumulatorPayment(opts) {
3202
3223
  tExpireMsByIndex
3203
3224
  };
3204
3225
  try {
3205
- if (opts.withCommitTx) {
3226
+ if (opts.disposable) {
3227
+ const account = opts.withDisposableLedger;
3228
+ if (!account) throw new Error("Disposable Cashu requires atomic credit accounting.");
3229
+ await opts.disposable.commit(opts.requestId, proofs, (ledger) => account(ledger, acceptedValue));
3230
+ } else if (!opts.db) {
3231
+ throw new Error("Cashu accumulator requires a storage backend.");
3232
+ } else if (opts.withCommitTx) {
3206
3233
  const withCommitTx = opts.withCommitTx;
3207
3234
  const client = await opts.db.connect();
3208
3235
  try {
@@ -3254,7 +3281,7 @@ function parseNut11LocktimeMs(secret) {
3254
3281
  var UPFRONT_CHALLENGE_TTL_MS = 5 * 60 * 1e3;
3255
3282
  var CONSUMED_CREDENTIAL_TTL_MS = 2 * UPFRONT_CHALLENGE_TTL_MS;
3256
3283
  function devModeSkipsPaymentVerification(opts) {
3257
- return Boolean(opts.devMode) && !opts.mints;
3284
+ return Boolean(opts.devMode) && !opts.mints && !opts.x402 && !opts.mpp && !opts.paymentMethods?.some((method) => method === "cashu" || method === "x402" || method === "tempo");
3258
3285
  }
3259
3286
  var PAYMENT_PROOF_KEYS = ["cashu_token", "x402_payment", "tempo_credential"];
3260
3287
  function hasPaymentProof(content) {
@@ -3457,7 +3484,7 @@ async function verifyUpfrontPayment(opts) {
3457
3484
  }
3458
3485
  };
3459
3486
  }
3460
- if (opts.cashuMode === "p2pk-accumulator" && opts.accumulatorDb && opts.dvmId && opts.lockPubkeys && opts.lockPubkeys.length > 0) {
3487
+ if (opts.cashuMode === "p2pk-accumulator" && (opts.accumulatorDb || opts.disposableCashu) && opts.dvmId && opts.lockPubkeys && opts.lockPubkeys.length > 0) {
3461
3488
  return verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx);
3462
3489
  }
3463
3490
  return {
@@ -3605,7 +3632,7 @@ async function verifyUpfrontPayment(opts) {
3605
3632
  x402Requirements
3606
3633
  });
3607
3634
  }
3608
- const { verifyX402Payment } = await import("./x402-T2C5MX3T.js");
3635
+ const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
3609
3636
  const receipt = await verifyX402Payment(
3610
3637
  x402Payment,
3611
3638
  x402Config,
@@ -4454,7 +4481,7 @@ function buildPaymentErrorResponse(body, challenges, x402Requirements, status, x
4454
4481
  async function verifyIncomingPayment(body, opts, snapshot) {
4455
4482
  if (body.type !== "payment") return {};
4456
4483
  if (body.content.cashu_token) {
4457
- if (opts.cashuMode === "p2pk-accumulator" && opts.accumulatorDb && opts.dvmId && opts.lockPubkeys && opts.lockPubkeys.length > 0) {
4484
+ if (opts.cashuMode === "p2pk-accumulator" && (opts.accumulatorDb || opts.disposableCashu) && opts.dvmId && opts.lockPubkeys && opts.lockPubkeys.length > 0) {
4458
4485
  const mints = opts.mints ?? [];
4459
4486
  if (mints.length === 0) {
4460
4487
  return {
@@ -4472,6 +4499,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
4472
4499
  {
4473
4500
  cashuToken: body.content.cashu_token,
4474
4501
  accumulatorDb: opts.accumulatorDb,
4502
+ disposableCashu: opts.disposableCashu,
4475
4503
  dvmId: opts.dvmId,
4476
4504
  lockPubkeys: opts.lockPubkeys,
4477
4505
  requestId,
@@ -4533,7 +4561,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
4533
4561
  snapshot
4534
4562
  });
4535
4563
  }
4536
- const { verifyX402Payment } = await import("./x402-T2C5MX3T.js");
4564
+ const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
4537
4565
  const receipt = await verifyX402Payment(
4538
4566
  body.content.x402_payment,
4539
4567
  opts.x402Config,
@@ -4897,7 +4925,7 @@ function applyPaymentInfoToJob(job, info) {
4897
4925
  async function processIncomingPayment(job, body, opts) {
4898
4926
  if (body.type !== "payment") return null;
4899
4927
  if (body.content.cashu_token) {
4900
- if (opts.cashuMode === "p2pk-accumulator" && opts.accumulatorDb && opts.dvmId && opts.lockPubkeys && opts.lockPubkeys.length > 0) {
4928
+ if (opts.cashuMode === "p2pk-accumulator" && (opts.accumulatorDb || opts.disposableCashu) && opts.dvmId && opts.lockPubkeys && opts.lockPubkeys.length > 0) {
4901
4929
  const mints = opts.mints ?? [];
4902
4930
  if (mints.length === 0) {
4903
4931
  return {
@@ -4912,6 +4940,7 @@ async function processIncomingPayment(job, body, opts) {
4912
4940
  {
4913
4941
  cashuToken: body.content.cashu_token,
4914
4942
  accumulatorDb: opts.accumulatorDb,
4943
+ disposableCashu: opts.disposableCashu,
4915
4944
  dvmId: opts.dvmId,
4916
4945
  lockPubkeys: opts.lockPubkeys,
4917
4946
  requestId,
@@ -4946,7 +4975,7 @@ async function processIncomingPayment(job, body, opts) {
4946
4975
  try {
4947
4976
  const requiredMsats = job.pendingPaymentMsats ?? 0;
4948
4977
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
4949
- const { verifyX402Payment } = await import("./x402-T2C5MX3T.js");
4978
+ const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
4950
4979
  const receipt = await verifyX402Payment(
4951
4980
  body.content.x402_payment,
4952
4981
  opts.x402Config,
@@ -5066,7 +5095,7 @@ function creditPendingPayment(job) {
5066
5095
  }
5067
5096
  async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5068
5097
  const { cashuToken, accumulatorDb, dvmId, lockPubkeys, mintHealthTracker } = opts;
5069
- if (!cashuToken || !accumulatorDb || !dvmId || !lockPubkeys || lockPubkeys.length === 0) {
5098
+ if (!cashuToken || !accumulatorDb && !opts.disposableCashu || !dvmId || !lockPubkeys || lockPubkeys.length === 0) {
5070
5099
  return {
5071
5100
  paidMsats: 0,
5072
5101
  receivedProofs: [],
@@ -5145,6 +5174,17 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5145
5174
  requestId: opts.requestId,
5146
5175
  mints,
5147
5176
  db: accumulatorDb,
5177
+ disposable: opts.disposableCashu,
5178
+ withDisposableLedger: ledgerCtx && requestId ? async (ledger, accepted) => {
5179
+ credited = await fundAndDraw({ ...ledgerCtx, ledger }, {
5180
+ rail: "cashu",
5181
+ paymentId: requestId,
5182
+ paidMsats: accepted.paidMsats,
5183
+ grossPaidMsats: accepted.grossMsats,
5184
+ cashuInputFeeMsats: accepted.inputFeeMsats,
5185
+ mint: tokenMint
5186
+ });
5187
+ } : void 0,
5148
5188
  dvmId,
5149
5189
  lockPubkeys,
5150
5190
  beforeCommit,
@@ -6360,8 +6400,8 @@ function authErrorBody(err, opts = {}) {
6360
6400
  hint: "Regenerate the request with the current timestamp + a fresh nonce; sign with secp256k1+BIP-340 Schnorr over the canonical JSON form of the input."
6361
6401
  };
6362
6402
  }
6363
- function infoSchemaUrl(requestUrl) {
6364
- return `${new URL(requestUrl).origin}/v1/info`;
6403
+ function infoSchemaUrl(requestUrl, publicOrigin) {
6404
+ return `${publicOrigin ?? new URL(requestUrl).origin}/v1/info`;
6365
6405
  }
6366
6406
  function callerInputIssues(issues) {
6367
6407
  if (!issues) return [];
@@ -9118,6 +9158,7 @@ function isRecord2(value) {
9118
9158
  import { randomBytes as randomBytes4 } from "crypto";
9119
9159
 
9120
9160
  // src/sdk/server/context.ts
9161
+ import { AsyncLocalStorage } from "async_hooks";
9121
9162
  import { randomBytes as randomBytes3 } from "crypto";
9122
9163
 
9123
9164
  // src/sdk/server/instrumented-kv.ts
@@ -9160,6 +9201,7 @@ function instrumentKVStore(inner) {
9160
9201
  }
9161
9202
 
9162
9203
  // src/sdk/server/context.ts
9204
+ var stepCostScope = new AsyncLocalStorage();
9163
9205
  function buildContext(job, opts) {
9164
9206
  const log = createConsoleLogger(job.id);
9165
9207
  const instrumentedStore = instrumentKVStore(opts.store);
@@ -9180,6 +9222,45 @@ function buildContext(job, opts) {
9180
9222
  let creditedToHandlerMsats = 0;
9181
9223
  let creditedToHandlerMicro = 0;
9182
9224
  let poolBasis = false;
9225
+ let declaredCostAmount = 0;
9226
+ let declaredCostCurrency;
9227
+ let declaredCostRevision = 0;
9228
+ let costPoisoned = false;
9229
+ const stepCostOwner = {};
9230
+ const declareCost = (declared) => {
9231
+ if (terminated("cost")) return;
9232
+ const currency = validateCurrency(declared.currency);
9233
+ const total = declaredCostAmount + declared.amount;
9234
+ const micro = Math.round(total * 1e6);
9235
+ if (!Number.isFinite(declared.amount) || declared.amount < 0 || !Number.isSafeInteger(micro)) {
9236
+ throw new RangeError(
9237
+ `ctx.cost amounts must each be a finite, non-negative number of currency units, and must total no more than ${Math.floor(Number.MAX_SAFE_INTEGER / 1e6)}; got ${String(declared.amount)}`
9238
+ );
9239
+ }
9240
+ declaredCostRevision += 1;
9241
+ job.costRevision = declaredCostRevision;
9242
+ const replayable = { amount: declared.amount, currency };
9243
+ const scope = stepCostScope.getStore();
9244
+ if (scope?.owner === stepCostOwner) {
9245
+ for (const frame of scope.frames) frame.push(replayable);
9246
+ }
9247
+ if (declaredCostCurrency !== void 0 && declaredCostCurrency !== currency) {
9248
+ costPoisoned = true;
9249
+ log.error("ctx.cost() currency switched mid-job \u2014 this job reports no cost", {
9250
+ declared: currency,
9251
+ pinned: declaredCostCurrency
9252
+ });
9253
+ }
9254
+ declaredCostCurrency ??= currency;
9255
+ if (costPoisoned) {
9256
+ job.costAmountMicro = void 0;
9257
+ job.costCurrency = void 0;
9258
+ return;
9259
+ }
9260
+ declaredCostAmount = total;
9261
+ job.costAmountMicro = micro;
9262
+ job.costCurrency = currency;
9263
+ };
9183
9264
  return {
9184
9265
  jobId: job.id,
9185
9266
  tags: job.tags,
@@ -9572,10 +9653,21 @@ function buildContext(job, opts) {
9572
9653
  providerMessage(job, "cancel", { reason: error });
9573
9654
  runTerminal();
9574
9655
  },
9656
+ cost: declareCost,
9575
9657
  step: async (id, fn) => {
9576
- if (job.stepCache.has(id)) return job.stepCache.get(id);
9577
- const result = await fn();
9578
- job.stepCache.set(id, result);
9658
+ if (job.stepCache.has(id)) {
9659
+ const record = job.stepCache.getRecord(id);
9660
+ for (const declared of record.costs ?? []) declareCost(declared);
9661
+ return record.value;
9662
+ }
9663
+ const costs = [];
9664
+ const scope = stepCostScope.getStore();
9665
+ const parentFrames = scope?.owner === stepCostOwner ? scope.frames : [];
9666
+ const result = await stepCostScope.run(
9667
+ { owner: stepCostOwner, frames: [...parentFrames, costs] },
9668
+ fn
9669
+ );
9670
+ job.stepCache.set(id, result, costs);
9579
9671
  return result;
9580
9672
  },
9581
9673
  state: job.state,
@@ -9941,6 +10033,9 @@ async function reapExpiredKeysetRows(opts, db) {
9941
10033
  // src/sdk/server/job-manager.ts
9942
10034
  var STALE_JOB_WATCHDOG_HEADROOM_MS = 9e4;
9943
10035
  var DEFAULT_PROCESSING_WATCHDOG_MS = 5 * 6e4;
10036
+ var TERMINAL_COST_RECOVERY_BATCH = 100;
10037
+ var TERMINAL_COST_RECOVERY_MAX_PAGES = 10;
10038
+ var TERMINAL_COST_RECOVERY_INTERVAL_MS = 3e4;
9944
10039
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
9945
10040
  var DEFAULT_REACTIVATION_CLAIM_GRACE_MS = 150;
9946
10041
  var REACTIVATION_CLAIM_POLL_MS = 25;
@@ -9959,7 +10054,12 @@ var ORPHAN_SWEEP_CLAIMS = /* @__PURE__ */ new WeakSet();
9959
10054
  var ATTACH_RAILS = ["cashu", "x402", "tempo"];
9960
10055
  var CREDIT_INBOUND_ATTEMPTS = 3;
9961
10056
  var CREDIT_INBOUND_BACKOFF_MS = 50;
10057
+ var TERMINAL_COST_HANDOFF_RETRY_INITIAL_MS = 100;
10058
+ var TERMINAL_COST_HANDOFF_RETRY_MAX_MS = 3e4;
9962
10059
  var CREDIT_INBOUND_READ_ATTEMPTS = 2;
10060
+ function isTerminalCostRecoveryStore(store) {
10061
+ return "saveForDvm" in store && "listUnreportedTerminalCosts" in store && "markTerminalCostReported" in store;
10062
+ }
9963
10063
  var JobManager = class {
9964
10064
  descriptor;
9965
10065
  opts;
@@ -9967,6 +10067,8 @@ var JobManager = class {
9967
10067
  jobStore;
9968
10068
  idleTimers = /* @__PURE__ */ new Map();
9969
10069
  cleanupTimers = /* @__PURE__ */ new Map();
10070
+ /** Cost revisions held only by a terminal handler owner until its durable merge succeeds. */
10071
+ terminalCostHandoffRetryTimers = /* @__PURE__ */ new Map();
9970
10072
  /** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
9971
10073
  statusWatchers = /* @__PURE__ */ new Map();
9972
10074
  /** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
@@ -9990,6 +10092,7 @@ var JobManager = class {
9990
10092
  staleSweepTimer;
9991
10093
  heartbeatTimer;
9992
10094
  orphanDrawSweepTimer;
10095
+ terminalCostRecoveryTimer;
9993
10096
  jobRetentionSweepTimer;
9994
10097
  jobRetentionBootTimer;
9995
10098
  /** This manager holds {@link ORPHAN_SWEEP_CLAIMS} for its ledger. */
@@ -10007,6 +10110,8 @@ var JobManager = class {
10007
10110
  monotonicNowMs = createMonotonicClock();
10008
10111
  sweepInFlight = false;
10009
10112
  orphanSweepInFlight = false;
10113
+ terminalCostRecoveryInFlight = false;
10114
+ terminalCostRecoveryResumeAfter;
10010
10115
  jobRetentionSweepInFlight = false;
10011
10116
  heartbeatInFlight = false;
10012
10117
  /**
@@ -10132,6 +10237,19 @@ var JobManager = class {
10132
10237
  }
10133
10238
  }
10134
10239
  }
10240
+ if (opts.dvmId && opts.onJobCost && isTerminalCostRecoveryStore(this.jobStore)) {
10241
+ this.terminalCostRecoveryTimer = setInterval(() => {
10242
+ void this.recoverTerminalCosts().catch((err) => {
10243
+ console.error(
10244
+ JSON.stringify({
10245
+ level: "terminal_cost_recovery_failed",
10246
+ error: err instanceof Error ? err.message : String(err)
10247
+ })
10248
+ );
10249
+ });
10250
+ }, TERMINAL_COST_RECOVERY_INTERVAL_MS);
10251
+ this.terminalCostRecoveryTimer.unref();
10252
+ }
10135
10253
  }
10136
10254
  /** Active in-memory jobs. */
10137
10255
  get activeJobs() {
@@ -10431,7 +10549,11 @@ var JobManager = class {
10431
10549
  record.seq = Math.max(record.seq, highSeq);
10432
10550
  }
10433
10551
  }
10434
- await this.jobStore.save(record);
10552
+ if (this.opts.dvmId && isTerminalCostRecoveryStore(this.jobStore)) {
10553
+ await this.jobStore.saveForDvm(record, this.opts.dvmId);
10554
+ } else {
10555
+ await this.jobStore.save(record);
10556
+ }
10435
10557
  }
10436
10558
  /**
10437
10559
  * Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
@@ -10472,6 +10594,7 @@ var JobManager = class {
10472
10594
  job.pendingPrompts.clear();
10473
10595
  job.pendingPayment = null;
10474
10596
  abortJob(job, reason);
10597
+ const terminalCostPersisted = durable.status === "completed" ? true : await this.tryPersistAdoptedTerminalCost(job, 1);
10475
10598
  if (durable.status !== "completed") {
10476
10599
  localMessage(job, "provider", "cancel", { reason });
10477
10600
  }
@@ -10486,8 +10609,78 @@ var JobManager = class {
10486
10609
  this.clearIdleTimer(job.id);
10487
10610
  void this.issueReceiptForStoredJob(job.id);
10488
10611
  this.scheduleCleanup(job);
10612
+ if (durable.status !== "completed" && terminalCostPersisted) {
10613
+ await this.reportJobCost(job);
10614
+ }
10489
10615
  return true;
10490
10616
  }
10617
+ /**
10618
+ * Persist an adopted terminal cost, retaining the in-memory revision for a
10619
+ * later retry when the durable store is transiently unavailable.
10620
+ */
10621
+ async tryPersistAdoptedTerminalCost(job, attempt) {
10622
+ try {
10623
+ await this.persistAdoptedTerminalCost(job);
10624
+ return true;
10625
+ } catch (err) {
10626
+ this.scheduleTerminalCostHandoffRetry(job, attempt, err);
10627
+ return false;
10628
+ }
10629
+ }
10630
+ /** Retry outside the status watcher, which correctly stops once the job is terminal. */
10631
+ scheduleTerminalCostHandoffRetry(job, attempt, err) {
10632
+ if (this.terminalCostHandoffRetryTimers.has(job.id)) return;
10633
+ const delayMs = Math.min(
10634
+ TERMINAL_COST_HANDOFF_RETRY_INITIAL_MS * 2 ** Math.min(attempt - 1, 8),
10635
+ TERMINAL_COST_HANDOFF_RETRY_MAX_MS
10636
+ );
10637
+ console.error(
10638
+ JSON.stringify({
10639
+ level: "terminal_cost_handoff_retry_scheduled",
10640
+ jobId: job.id,
10641
+ attempt,
10642
+ delayMs,
10643
+ error: err instanceof Error ? err.message : String(err)
10644
+ })
10645
+ );
10646
+ const timer = setTimeout(() => {
10647
+ this.terminalCostHandoffRetryTimers.delete(job.id);
10648
+ void this.retryTerminalCostHandoff(job, attempt + 1);
10649
+ }, delayMs);
10650
+ timer.unref();
10651
+ this.terminalCostHandoffRetryTimers.set(job.id, timer);
10652
+ }
10653
+ async retryTerminalCostHandoff(job, attempt) {
10654
+ if (!await this.tryPersistAdoptedTerminalCost(job, attempt)) return;
10655
+ await this.reportJobCost(job);
10656
+ }
10657
+ /** Durably merge a terminal handler owner's latest cost before reporting it. */
10658
+ async persistAdoptedTerminalCost(job) {
10659
+ const dvmId = this.opts.dvmId;
10660
+ const localRevision = job.costRevision;
10661
+ if (localRevision === void 0 || !dvmId || !isTerminalCostRecoveryStore(this.jobStore)) {
10662
+ return;
10663
+ }
10664
+ const durable = await this.jobStore.get(job.id);
10665
+ if (!durable || durable.status !== job.status) {
10666
+ throw new Error(`Terminal cost merge lost durable job ${job.id}`);
10667
+ }
10668
+ if ((durable.costRevision ?? -1) > localRevision) {
10669
+ job.costAmountMicro = durable.costAmountMicro;
10670
+ job.costCurrency = durable.costCurrency;
10671
+ job.costRevision = durable.costRevision;
10672
+ return;
10673
+ }
10674
+ await this.jobStore.saveForDvm(
10675
+ {
10676
+ ...durable,
10677
+ costAmountMicro: job.costAmountMicro,
10678
+ costCurrency: job.costCurrency,
10679
+ costRevision: localRevision
10680
+ },
10681
+ dvmId
10682
+ );
10683
+ }
10491
10684
  /**
10492
10685
  * Watch the durable status of a locally-active job (internal-review).
10493
10686
  *
@@ -10763,7 +10956,7 @@ var JobManager = class {
10763
10956
  );
10764
10957
  if (claimed) {
10765
10958
  swept++;
10766
- await this.issueReceiptForStoredJob(record.id);
10959
+ await this.finalizeStoredTerminal(record.id);
10767
10960
  console.warn(
10768
10961
  JSON.stringify({
10769
10962
  level: awaiting ? "stale_job_cancelled" : "stale_job_failed",
@@ -11543,6 +11736,7 @@ var JobManager = class {
11543
11736
  resourcePath: `/v1/job/${job.id}/messages`,
11544
11737
  cashuMode: this.opts.cashuMode,
11545
11738
  accumulatorDb: this.opts.cashuMode === "p2pk-accumulator" ? this.opts.db : void 0,
11739
+ disposableCashu: this.opts.disposableCashu,
11546
11740
  dvmId: this.opts.dvmId,
11547
11741
  lockPubkeys: extra?.lockPubkeys,
11548
11742
  mintHealthTracker: extra?.mintHealthTracker,
@@ -11981,8 +12175,25 @@ var JobManager = class {
11981
12175
  * `context.ts`). The gap is visible to callers — the `seq` series skips a
11982
12176
  * number — which is precisely the completeness signal receipts exist for.
11983
12177
  */
12178
+ async observePaidCompletion(record) {
12179
+ const callback = this.opts.onPaidJobCompleted;
12180
+ if (!callback || record.status !== "completed" || !record.paymentRail || !record.creditId || !record.drawId || !this.opts.creditLedger) return;
12181
+ const draw = await this.opts.creditLedger.getDraw({ creditId: record.creditId, drawId: record.drawId }).catch(() => void 0);
12182
+ const rail = draw?.railValue.rail;
12183
+ const currency = draw?.railValue.creditCurrency;
12184
+ if (draw?.status !== "settled" || draw.amountMicro <= 0 || !currency || !rail || !isFundingRail(rail)) return;
12185
+ const testFunds = rail === "lightning" ? null : this.opts.paymentMode?.rails[rail].testFunds ?? null;
12186
+ try {
12187
+ await callback({ jobId: record.id, rail, amountMicro: draw.amountMicro, currency, testFunds });
12188
+ } catch (error) {
12189
+ console.error(`[${record.id}] Paid completion observer failed:`, error);
12190
+ }
12191
+ }
11984
12192
  async issueReceipt(record) {
11985
- if (record.receipt) return record.receipt;
12193
+ if (record.receipt) {
12194
+ await this.observePaidCompletion(record);
12195
+ return record.receipt;
12196
+ }
11986
12197
  if (!isReceiptableStatus(record.status)) return void 0;
11987
12198
  let credit;
11988
12199
  try {
@@ -12002,13 +12213,18 @@ var JobManager = class {
12002
12213
  return void 0;
12003
12214
  }
12004
12215
  const issuer = this.opts.receiptIssuer;
12005
- if (!issuer) return void 0;
12216
+ if (!issuer) {
12217
+ await this.observePaidCompletion(record);
12218
+ return void 0;
12219
+ }
12006
12220
  if (!isReceiptIssuingStore(this.jobStore)) return void 0;
12007
12221
  const store = this.jobStore;
12008
12222
  try {
12009
12223
  const seq = await store.claimReceiptSeq(record.id);
12010
12224
  const receipt = issuer.issue(record, seq, void 0, credit);
12011
- return await store.saveReceipt(record.id, receipt) ?? receipt;
12225
+ const saved = await store.saveReceipt(record.id, receipt) ?? receipt;
12226
+ await this.observePaidCompletion(record);
12227
+ return saved;
12012
12228
  } catch (err) {
12013
12229
  console.error(
12014
12230
  JSON.stringify({
@@ -12135,6 +12351,7 @@ var JobManager = class {
12135
12351
  async finalizeTerminal(job) {
12136
12352
  await this.attachReceipt(job);
12137
12353
  await this.persistJob(job).catch(() => void 0);
12354
+ await this.reportJobCost(job);
12138
12355
  }
12139
12356
  /**
12140
12357
  * Issue a receipt for a job this process doesn't hold in `activeJobs` — a
@@ -12151,13 +12368,117 @@ var JobManager = class {
12151
12368
  if (active && receipt) active.receipt = receipt;
12152
12369
  return receipt;
12153
12370
  }
12371
+ /**
12372
+ * Finish accounting for a terminal written directly to the store — the
12373
+ * stale reaper and a cancel handled on a different machine (internal-review).
12374
+ */
12375
+ async finalizeStoredTerminal(jobId) {
12376
+ const receipt = await this.issueReceiptForStoredJob(jobId);
12377
+ const record = await this.jobStore.get(jobId).catch(() => void 0);
12378
+ if (record && isTerminal(record.status)) await this.reportJobCost(record);
12379
+ return receipt;
12380
+ }
12154
12381
  /** Handle job reaching terminal state (completed, failed, cancelled). */
12155
12382
  async handleJobTerminal(job) {
12156
12383
  this.clearIdleTimer(job.id);
12157
12384
  await this.attachReceipt(job);
12158
- await this.bookRevenue(job);
12385
+ const booked = await this.bookRevenue(job);
12386
+ if (!booked) await this.reportJobCost(job);
12159
12387
  this.scheduleCleanup(job);
12160
12388
  }
12389
+ /**
12390
+ * Re-enqueue terminal cost-only rows left between the job commit and outbox
12391
+ * insert. The marker advances only after `onJobCost` resolves, which for the
12392
+ * SDK reporter means the local durable insert has committed; a crash after
12393
+ * that insert but before the marker can duplicate a payload, but the
12394
+ * revisioned platform contract makes that replay harmless.
12395
+ */
12396
+ async recoverTerminalCosts() {
12397
+ const store = this.jobStore;
12398
+ const dvmId = this.opts.dvmId;
12399
+ if (!dvmId || !this.opts.onJobCost || !isTerminalCostRecoveryStore(store)) {
12400
+ return { examined: 0, queued: 0, complete: true };
12401
+ }
12402
+ if (this.terminalCostRecoveryInFlight) {
12403
+ return { examined: 0, queued: 0, complete: false };
12404
+ }
12405
+ this.terminalCostRecoveryInFlight = true;
12406
+ let examined = 0;
12407
+ let queued = 0;
12408
+ let cursor = this.terminalCostRecoveryResumeAfter;
12409
+ try {
12410
+ for (let page = 0; page < TERMINAL_COST_RECOVERY_MAX_PAGES; page++) {
12411
+ const candidates = await store.listUnreportedTerminalCosts({
12412
+ dvmId,
12413
+ limit: TERMINAL_COST_RECOVERY_BATCH,
12414
+ ...cursor ? { afterJobId: cursor } : {}
12415
+ });
12416
+ for (const record of candidates) {
12417
+ examined++;
12418
+ cursor = record.id;
12419
+ if (await this.reportJobCost(record)) queued++;
12420
+ }
12421
+ if (candidates.length < TERMINAL_COST_RECOVERY_BATCH) {
12422
+ this.terminalCostRecoveryResumeAfter = void 0;
12423
+ if (examined > 0) {
12424
+ console.log(
12425
+ JSON.stringify({
12426
+ level: "info",
12427
+ event: "terminal_cost_recovery_complete",
12428
+ dvmId,
12429
+ examined,
12430
+ queued
12431
+ })
12432
+ );
12433
+ }
12434
+ return { examined, queued, complete: true };
12435
+ }
12436
+ }
12437
+ this.terminalCostRecoveryResumeAfter = cursor;
12438
+ console.warn(
12439
+ JSON.stringify({
12440
+ level: "terminal_cost_recovery_truncated",
12441
+ dvmId,
12442
+ examined,
12443
+ queued,
12444
+ resumeAfterJobId: cursor
12445
+ })
12446
+ );
12447
+ return { examined, queued, complete: false };
12448
+ } finally {
12449
+ this.terminalCostRecoveryInFlight = false;
12450
+ }
12451
+ }
12452
+ /** Report the latest declared-cost state only when no revenue event carried it. */
12453
+ async reportJobCost(job) {
12454
+ const onJobCost = this.opts.onJobCost;
12455
+ if (!onJobCost || job.status !== "completed" && job.status !== "failed" && job.status !== "cancelled") {
12456
+ return false;
12457
+ }
12458
+ const costRevision = job.costRevision ?? (job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? 0 : void 0);
12459
+ if (costRevision === void 0) return false;
12460
+ const payload = {
12461
+ dvmId: this.opts.dvmId ?? "",
12462
+ jobId: job.id,
12463
+ ...job.capability ? { capability: job.capability } : {},
12464
+ outcome: job.status,
12465
+ costRevision,
12466
+ cost: job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { amountMicro: job.costAmountMicro, currency: job.costCurrency } : null
12467
+ };
12468
+ try {
12469
+ await onJobCost(payload);
12470
+ if (isTerminalCostRecoveryStore(this.jobStore)) {
12471
+ const marked = await this.jobStore.markTerminalCostReported(job.id, costRevision);
12472
+ if (!marked) {
12473
+ return false;
12474
+ }
12475
+ }
12476
+ } catch (err) {
12477
+ console.error(`[${job.id}] Job cost reporting failed:`, err);
12478
+ return false;
12479
+ }
12480
+ return true;
12481
+ }
12161
12482
  /**
12162
12483
  * Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
12163
12484
  * Fire-and-forget — the `RevenueReporter` owns persistence and retry.
@@ -12296,6 +12617,9 @@ var JobManager = class {
12296
12617
  onJobCompleted({
12297
12618
  dvmId: this.opts.dvmId ?? "",
12298
12619
  jobId: job.id,
12620
+ // Legacy Postgres rows surface a missing capability as `""`. Keep
12621
+ // that absence distinct from every real capability name on the wire.
12622
+ ...job.capability ? { capability: job.capability } : {},
12299
12623
  paidMsats: bookedMsats,
12300
12624
  paymentMint: job.paymentMint,
12301
12625
  rail: job.paymentRail,
@@ -12303,6 +12627,11 @@ var JobManager = class {
12303
12627
  nativeAmount: job.nativeAmount,
12304
12628
  nativeAsset: job.nativeAsset,
12305
12629
  cashuFlow: job.cashuFlow,
12630
+ // Absent unless the handler declared one, and absent is a fact: the
12631
+ // platform shows "no costs reported" rather than a zero cost and a
12632
+ // 100% margin (internal-review). Both halves or neither — a bare amount
12633
+ // in an unnamed currency is not a cost.
12634
+ ...job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { cost: { amountMicro: job.costAmountMicro, currency: job.costCurrency } } : {},
12306
12635
  ...credit
12307
12636
  })
12308
12637
  ).catch((err) => {
@@ -12329,6 +12658,10 @@ var JobManager = class {
12329
12658
  clearTimeout(timer);
12330
12659
  this.cleanupTimers.delete(id);
12331
12660
  }
12661
+ for (const [id, timer] of this.terminalCostHandoffRetryTimers) {
12662
+ clearTimeout(timer);
12663
+ this.terminalCostHandoffRetryTimers.delete(id);
12664
+ }
12332
12665
  for (const id of [...this.statusWatchers.keys()]) {
12333
12666
  this.unwatchDurableStatus(id);
12334
12667
  }
@@ -12348,6 +12681,10 @@ var JobManager = class {
12348
12681
  clearInterval(this.orphanDrawSweepTimer);
12349
12682
  this.orphanDrawSweepTimer = void 0;
12350
12683
  }
12684
+ if (this.terminalCostRecoveryTimer) {
12685
+ clearInterval(this.terminalCostRecoveryTimer);
12686
+ this.terminalCostRecoveryTimer = void 0;
12687
+ }
12351
12688
  if (this.jobRetentionBootTimer) {
12352
12689
  clearTimeout(this.jobRetentionBootTimer);
12353
12690
  this.jobRetentionBootTimer = void 0;
@@ -13380,7 +13717,7 @@ var X402FacilitatorHealth = class {
13380
13717
  facilitator: support.facilitator,
13381
13718
  network: support.network,
13382
13719
  reason: support.reason,
13383
- message: "x402 omitted because the configured facilitator advertises no supported exact generation on this network. Other rails are unaffected."
13720
+ message: "Exact x402 is unavailable because the configured facilitator advertises no supported exact generation on this network. Configured batch settlement is evaluated separately; other rails are unaffected."
13384
13721
  })
13385
13722
  );
13386
13723
  } else if (!support.exact.v1 || !support.exact.v2 || this.config.batchSettlement && !support.batchSettlement) {
@@ -15293,6 +15630,23 @@ function parseSerializedDleq(raw) {
15293
15630
  return { e: dleq.e, s: dleq.s, ...dleq.r !== void 0 ? { r: dleq.r } : {} };
15294
15631
  }
15295
15632
 
15633
+ // src/sdk/server/caller-facing-url.ts
15634
+ function resolvePublicOrigin(value) {
15635
+ if (!value) return void 0;
15636
+ try {
15637
+ const url = new URL(value);
15638
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
15639
+ return url.origin;
15640
+ } catch {
15641
+ return void 0;
15642
+ }
15643
+ }
15644
+ function callerFacingRequestUrl(requestUrl, publicOrigin) {
15645
+ if (!publicOrigin) return requestUrl;
15646
+ const request = new URL(requestUrl);
15647
+ return `${publicOrigin}${request.pathname}${request.search}`;
15648
+ }
15649
+
15296
15650
  // src/sdk/server/credit-expiry-sweep.ts
15297
15651
  function startCreditExpirySweep(ledger, opts = {}) {
15298
15652
  const sweepExpiredCredits = ledger.sweepExpiredCredits?.bind(ledger);
@@ -16308,194 +16662,236 @@ function revenueReporterBannerState(s) {
16308
16662
  return s.devMode ? "(dev \u2014 not reporting)" : "misconfigured";
16309
16663
  }
16310
16664
  async function createDVMServer(descriptor, opts) {
16311
- if (process.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
16312
- console.warn(
16313
- JSON.stringify({
16314
- level: "cashu_opts_env_ignored",
16315
- message: "DVMKIT_CASHU_MINTS is set in env but cashuMode opt is undefined \u2014 cashu is NOT active. Pass cashuMode, lockPubkey, and db via createDVMHost to enable it."
16316
- })
16317
- );
16318
- }
16319
- const envMaybe = opts.env;
16320
- const platformToken = opts.platformToken ?? envMaybe.DVMKIT_PLATFORM_TOKEN;
16321
- const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
16322
- const dvmId = opts.dvmId;
16323
16665
  let revenueReporter;
16324
16666
  let payoutReporter;
16325
- if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
16326
- const { RevenueReporter } = await import("./revenue-reporter-XXSU5KVB.js");
16327
- const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
16328
- await reporter.init();
16329
- reporter.startRetryLoop();
16330
- revenueReporter = reporter;
16331
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-RG6XNGPI.js");
16332
- payoutReporter = new PayoutReporter({
16667
+ let unsubscribeTempoSettlementReadiness;
16668
+ let creditExpirySweep;
16669
+ let x402ChannelStorage;
16670
+ let x402BatchSettlement;
16671
+ let server;
16672
+ let shutdownPromise;
16673
+ const shutdown = () => shutdownPromise ??= (async () => {
16674
+ const errors = [];
16675
+ const capture = async (operation) => {
16676
+ try {
16677
+ await operation();
16678
+ } catch (error) {
16679
+ errors.push(error);
16680
+ }
16681
+ };
16682
+ let settlementStopped;
16683
+ if (server) {
16684
+ try {
16685
+ settlementStopped = server.shutdown();
16686
+ } catch (error) {
16687
+ errors.push(error);
16688
+ }
16689
+ } else if (x402BatchSettlement) {
16690
+ try {
16691
+ settlementStopped = x402BatchSettlement.stop();
16692
+ } catch (error) {
16693
+ errors.push(error);
16694
+ }
16695
+ }
16696
+ await capture(() => payoutReporter?.stop());
16697
+ await capture(() => revenueReporter?.stop());
16698
+ await capture(() => creditExpirySweep?.stop());
16699
+ await capture(() => unsubscribeTempoSettlementReadiness?.());
16700
+ if (settlementStopped) await capture(() => settlementStopped);
16701
+ if (x402ChannelStorage) {
16702
+ await capture(() => x402ChannelStorage?.close());
16703
+ }
16704
+ if (errors.length === 1) throw errors[0];
16705
+ if (errors.length > 1) {
16706
+ throw new AggregateError(errors, `${String(errors.length)} DVM shutdown operations failed`);
16707
+ }
16708
+ })();
16709
+ try {
16710
+ if (process.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
16711
+ console.warn(
16712
+ JSON.stringify({
16713
+ level: "cashu_opts_env_ignored",
16714
+ message: "DVMKIT_CASHU_MINTS is set in env but cashuMode opt is undefined \u2014 cashu is NOT active. Pass cashuMode, lockPubkey, and db via createDVMHost to enable it."
16715
+ })
16716
+ );
16717
+ }
16718
+ const envMaybe = opts.env;
16719
+ const platformToken = opts.platformToken ?? envMaybe.DVMKIT_PLATFORM_TOKEN;
16720
+ const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
16721
+ const dvmId = opts.dvmId;
16722
+ if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
16723
+ const { RevenueReporter } = await import("./revenue-reporter-ASZ7SHHH.js");
16724
+ const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
16725
+ revenueReporter = reporter;
16726
+ await reporter.init();
16727
+ reporter.startRetryLoop();
16728
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-RG6XNGPI.js");
16729
+ payoutReporter = new PayoutReporter({
16730
+ dvmId,
16731
+ db: opts.db,
16732
+ transport: reporter,
16733
+ // The same condition the admin routes and the accumulator monitor mount on.
16734
+ cashu: opts.cashuMode === "p2pk-accumulator" && !!opts.lockPubkey,
16735
+ ...typeof opts.db.connect === "function" ? {} : { batchStore: new MemoryX402BatchStore() }
16736
+ });
16737
+ await payoutReporter.init();
16738
+ payoutReporter.start();
16739
+ }
16740
+ assertRevenueReporterReady({
16741
+ failFast: opts.failFast ?? false,
16742
+ devMode: opts.devMode,
16743
+ platformToken,
16744
+ platformUrl,
16333
16745
  dvmId,
16334
- db: opts.db,
16335
- transport: reporter,
16336
- // The same condition the admin routes and the accumulator monitor mount on.
16337
- cashu: opts.cashuMode === "p2pk-accumulator" && !!opts.lockPubkey,
16338
- ...typeof opts.db.connect === "function" ? {} : { batchStore: new MemoryX402BatchStore() }
16746
+ hasPgPool: !!opts.db
16339
16747
  });
16340
- await payoutReporter.init();
16341
- payoutReporter.start();
16342
- }
16343
- assertRevenueReporterReady({
16344
- failFast: opts.failFast ?? false,
16345
- devMode: opts.devMode,
16346
- platformToken,
16347
- platformUrl,
16348
- dvmId,
16349
- hasPgPool: !!opts.db
16350
- });
16351
- const effectiveOnJobCompleted = opts.onJobCompleted ?? (revenueReporter ? (info) => revenueReporter.report(info) : void 0);
16352
- const effectiveOnPaidJobDeath = opts.onPaidJobDeath ?? (revenueReporter ? (info) => revenueReporter.reportPaidJobDeath(info) : void 0);
16353
- const effectiveOnRevenueSkippedNoRail = opts.onRevenueSkippedNoRail ?? (revenueReporter ? (info) => revenueReporter.reportRevenueSkippedNoRail(info) : void 0);
16354
- const enqueueCreditDeposit = !opts.onCreditFunded && revenueReporter ? revenueReporter.enqueueDeposit.bind(revenueReporter) : void 0;
16355
- const enqueueCreditDrain = revenueReporter ? revenueReporter.enqueueCreditDrain.bind(revenueReporter) : void 0;
16356
- const enqueueCreditDrawRelease = revenueReporter ? revenueReporter.enqueueCreditDrawRelease.bind(revenueReporter) : void 0;
16357
- const unsubscribeTempoSettlementReadiness = opts.tempoSettlementReadiness?.subscribe({
16358
- onBalance: (observation) => revenueReporter && dvmId ? revenueReporter.reportTempoSettlementBalance({
16359
- dvmId,
16360
- address: observation.address,
16361
- network: observation.network,
16362
- token: observation.token,
16363
- balanceMicro: observation.balanceMicro.toString(),
16364
- lowBalanceMicro: observation.lowBalanceMicro.toString(),
16365
- checkedAt: observation.checkedAt,
16366
- ready: observation.ready
16367
- }) : void 0,
16368
- onFailure: (observation) => revenueReporter && dvmId ? revenueReporter.reportTempoSettlementFailure({
16369
- dvmId,
16370
- address: observation.address,
16371
- network: observation.network,
16372
- token: observation.token,
16373
- operation: observation.operation,
16374
- errorClass: observation.errorClass,
16375
- checkedAt: observation.checkedAt,
16376
- ...observation.balanceMicro !== void 0 ? { balanceMicro: observation.balanceMicro.toString() } : {},
16377
- ...observation.channelId ? { channelId: observation.channelId } : {},
16378
- ...observation.creditId ? { creditId: observation.creditId } : {},
16379
- ...observation.drainId ? { drainId: observation.drainId } : {}
16380
- }) : void 0
16381
- });
16382
- opts.tempoSettlementReadiness?.start();
16383
- const creditLedger = opts.creditLedger ?? new MemoryCreditLedger();
16384
- if (revenueReporter && dvmId) {
16385
- creditLedger.useCreditExpiryReleaseOutbox?.({
16386
- dvmId,
16387
- enqueue: revenueReporter.enqueueCreditExpiryRelease.bind(revenueReporter)
16748
+ const activeRevenueReporter = revenueReporter;
16749
+ const effectiveOnJobCompleted = opts.onJobCompleted ?? (activeRevenueReporter ? (info) => activeRevenueReporter.report(info) : void 0);
16750
+ const effectiveOnJobCost = opts.onJobCost ?? (activeRevenueReporter ? (info) => activeRevenueReporter.reportCost(info) : void 0);
16751
+ const effectiveOnPaidJobDeath = opts.onPaidJobDeath ?? (activeRevenueReporter ? (info) => activeRevenueReporter.reportPaidJobDeath(info) : void 0);
16752
+ const effectiveOnRevenueSkippedNoRail = opts.onRevenueSkippedNoRail ?? (activeRevenueReporter ? (info) => activeRevenueReporter.reportRevenueSkippedNoRail(info) : void 0);
16753
+ const enqueueCreditDeposit = !opts.onCreditFunded && activeRevenueReporter ? activeRevenueReporter.enqueueDeposit.bind(activeRevenueReporter) : void 0;
16754
+ const enqueueCreditDrain = activeRevenueReporter ? activeRevenueReporter.enqueueCreditDrain.bind(activeRevenueReporter) : void 0;
16755
+ const enqueueCreditDrawRelease = activeRevenueReporter ? activeRevenueReporter.enqueueCreditDrawRelease.bind(activeRevenueReporter) : void 0;
16756
+ unsubscribeTempoSettlementReadiness = opts.tempoSettlementReadiness?.subscribe({
16757
+ onBalance: (observation) => activeRevenueReporter && dvmId ? activeRevenueReporter.reportTempoSettlementBalance({
16758
+ dvmId,
16759
+ address: observation.address,
16760
+ network: observation.network,
16761
+ token: observation.token,
16762
+ balanceMicro: observation.balanceMicro.toString(),
16763
+ lowBalanceMicro: observation.lowBalanceMicro.toString(),
16764
+ checkedAt: observation.checkedAt,
16765
+ ready: observation.ready
16766
+ }) : void 0,
16767
+ onFailure: (observation) => activeRevenueReporter && dvmId ? activeRevenueReporter.reportTempoSettlementFailure({
16768
+ dvmId,
16769
+ address: observation.address,
16770
+ network: observation.network,
16771
+ token: observation.token,
16772
+ operation: observation.operation,
16773
+ errorClass: observation.errorClass,
16774
+ checkedAt: observation.checkedAt,
16775
+ ...observation.balanceMicro !== void 0 ? { balanceMicro: observation.balanceMicro.toString() } : {},
16776
+ ...observation.channelId ? { channelId: observation.channelId } : {},
16777
+ ...observation.creditId ? { creditId: observation.creditId } : {},
16778
+ ...observation.drainId ? { drainId: observation.drainId } : {}
16779
+ }) : void 0
16388
16780
  });
16389
- }
16390
- const creditExpirySweep = startCreditExpirySweep(creditLedger, opts.creditExpirySweep);
16391
- const processedPayments = opts.processedPayments ?? new MemoryProcessedPaymentStore();
16392
- const bootX402Support = opts.x402 ? opts.x402FacilitatorHealth?.support() ?? configuredX402FacilitatorSupport(opts.x402) : void 0;
16393
- const x402Config = opts.x402;
16394
- const x402BatchConfig = x402Config?.batchSettlement;
16395
- const selfRelayEnabled = !!x402BatchConfig?.selfRelay;
16396
- const effectiveX402BatchConfig = x402BatchConfig?.selfRelay ? {
16397
- ...x402BatchConfig,
16398
- selfRelay: {
16399
- ...x402BatchConfig.selfRelay,
16400
- onBalance: async (observation) => {
16401
- if (revenueReporter && dvmId) {
16402
- await revenueReporter.reportX402SelfRelayGasBalance({
16403
- dvmId,
16404
- address: observation.address,
16405
- network: observation.network,
16406
- balanceWei: observation.balanceWei.toString(),
16407
- lowBalanceWei: observation.lowBalanceWei.toString(),
16408
- checkedAt: observation.checkedAt
16409
- });
16410
- }
16411
- await x402BatchConfig.selfRelay?.onBalance?.(observation);
16412
- },
16413
- onRpcHealth: async (observation) => {
16414
- if (revenueReporter && dvmId) {
16415
- await revenueReporter.reportX402SelfRelayRpcHealth({
16416
- dvmId,
16417
- ...observation
16418
- });
16419
- }
16420
- await x402BatchConfig.selfRelay?.onRpcHealth?.(observation);
16421
- }
16422
- }
16423
- } : x402BatchConfig;
16424
- const x402ChannelStorage = opts.db && x402Config && effectiveX402BatchConfig && !effectiveX402BatchConfig.storage ? new PostgresX402ChannelStorage(opts.db, {
16425
- terminalizeUnbackedCredit: (evidence, tx) => creditLedger.terminalizeX402Credit(evidence, tx)
16426
- }) : void 0;
16427
- if (x402ChannelStorage) {
16428
- await x402ChannelStorage.init();
16429
- creditLedger.useX402SettlementGate?.(x402ChannelStorage);
16430
- }
16431
- const x402BatchSettlementFactory = x402Config && effectiveX402BatchConfig ? () => createX402BatchSettlementServer({
16432
- channelStorage: x402ChannelStorage,
16433
- network: x402Config.network,
16434
- payTo: x402Config.payTo,
16435
- facilitator: x402Config.facilitator,
16436
- ...x402Config.facilitatorAuth ? { facilitatorAuth: x402Config.facilitatorAuth } : {},
16437
- config: effectiveX402BatchConfig,
16438
- db: opts.db,
16439
- allowInMemory: opts.devMode,
16440
- earnedNative: (channelId) => creditLedger.earnedNativeForX402Channel(channelId),
16441
- supportedResponse: selfRelayEnabled ? void 0 : opts.x402FacilitatorHealth?.supportedResponse?.(),
16442
- payout: payoutReporter?.x402Observer()
16443
- }) : void 0;
16444
- const x402BatchSettlement = x402BatchSettlementFactory && (bootX402Support?.batchSettlement || selfRelayEnabled) ? await x402BatchSettlementFactory() : void 0;
16445
- const x402ExactStore = x402Config && opts.db && typeof opts.db.connect === "function" ? new PostgresX402ExactSettlementStore(opts.db) : void 0;
16446
- await x402ExactStore?.init();
16447
- const x402ExactEvidence = x402Config && x402ExactStore ? await createX402SettlementEvidenceReader({
16448
- network: x402Config.network,
16449
- ...x402BatchConfig?.rpcUrl ? { rpcUrl: x402BatchConfig.rpcUrl } : {}
16450
- }) : void 0;
16451
- const x402ExactSettlement = x402Config && x402ExactStore ? new X402ExactSettlementServer({
16452
- store: x402ExactStore,
16453
- evidence: x402ExactEvidence,
16454
- repair: async (effect, receipt, tx) => {
16455
- if (!receipt.txHash) throw new Error("settled exact x402 intent has no transaction");
16456
- await repairX402ExactSettlementEffect({
16457
- effect,
16458
- paymentId: receipt.txHash,
16459
- ledger: creditLedger,
16460
- processedPayments,
16461
- enqueueCreditDeposit,
16462
- tx
16781
+ opts.tempoSettlementReadiness?.start();
16782
+ const creditLedger = opts.creditLedger ?? new MemoryCreditLedger();
16783
+ if (activeRevenueReporter && dvmId) {
16784
+ creditLedger.useCreditExpiryReleaseOutbox?.({
16785
+ dvmId,
16786
+ enqueue: activeRevenueReporter.enqueueCreditExpiryRelease.bind(activeRevenueReporter)
16463
16787
  });
16464
16788
  }
16465
- }) : void 0;
16466
- const server = new DVMServer(descriptor, {
16467
- ...opts,
16468
- creditLedger,
16469
- processedPayments,
16470
- onJobCompleted: effectiveOnJobCompleted,
16471
- onPaidJobDeath: effectiveOnPaidJobDeath,
16472
- onRevenueSkippedNoRail: effectiveOnRevenueSkippedNoRail,
16473
- onCreditFunded: opts.onCreditFunded,
16474
- enqueueCreditDeposit,
16475
- enqueueCreditDrain,
16476
- enqueueCreditDrawRelease,
16477
- x402BatchSettlement,
16478
- x402BatchSettlementFactory,
16479
- x402ExactSettlement,
16480
- payoutReporter
16481
- });
16482
- return {
16483
- app: server.app,
16484
- authAudience: server.authAudience,
16485
- shutdown() {
16486
- const settlementStopped = server.shutdown();
16487
- payoutReporter?.stop();
16488
- revenueReporter?.stop();
16489
- creditExpirySweep?.stop();
16490
- unsubscribeTempoSettlementReadiness?.();
16491
- if (x402ChannelStorage) {
16492
- void settlementStopped.then(() => x402ChannelStorage.close()).catch(() => void 0);
16789
+ creditExpirySweep = startCreditExpirySweep(creditLedger, opts.creditExpirySweep);
16790
+ const processedPayments = opts.processedPayments ?? new MemoryProcessedPaymentStore();
16791
+ const bootX402Support = opts.x402 ? opts.x402FacilitatorHealth?.support() ?? configuredX402FacilitatorSupport(opts.x402) : void 0;
16792
+ const x402Config = opts.x402;
16793
+ const x402BatchConfig = x402Config?.batchSettlement;
16794
+ const selfRelayEnabled = !!x402BatchConfig?.selfRelay;
16795
+ const effectiveX402BatchConfig = x402BatchConfig?.selfRelay ? {
16796
+ ...x402BatchConfig,
16797
+ selfRelay: {
16798
+ ...x402BatchConfig.selfRelay,
16799
+ onBalance: async (observation) => {
16800
+ if (activeRevenueReporter && dvmId) {
16801
+ await activeRevenueReporter.reportX402SelfRelayGasBalance({
16802
+ dvmId,
16803
+ address: observation.address,
16804
+ network: observation.network,
16805
+ balanceWei: observation.balanceWei.toString(),
16806
+ lowBalanceWei: observation.lowBalanceWei.toString(),
16807
+ checkedAt: observation.checkedAt
16808
+ });
16809
+ }
16810
+ await x402BatchConfig.selfRelay?.onBalance?.(observation);
16811
+ },
16812
+ onRpcHealth: async (observation) => {
16813
+ if (activeRevenueReporter && dvmId) {
16814
+ await activeRevenueReporter.reportX402SelfRelayRpcHealth({
16815
+ dvmId,
16816
+ ...observation
16817
+ });
16818
+ }
16819
+ await x402BatchConfig.selfRelay?.onRpcHealth?.(observation);
16820
+ }
16493
16821
  }
16494
- },
16495
- jobManager: server.jobManager,
16496
- revenueReporterActive: !!revenueReporter,
16497
- payoutReporter
16498
- };
16822
+ } : x402BatchConfig;
16823
+ x402ChannelStorage = opts.db && x402Config && effectiveX402BatchConfig && !effectiveX402BatchConfig.storage ? new PostgresX402ChannelStorage(opts.db, {
16824
+ terminalizeUnbackedCredit: (evidence, tx) => creditLedger.terminalizeX402Credit(evidence, tx)
16825
+ }) : void 0;
16826
+ if (x402ChannelStorage) {
16827
+ await x402ChannelStorage.init();
16828
+ creditLedger.useX402SettlementGate?.(x402ChannelStorage);
16829
+ }
16830
+ const x402BatchSettlementFactory = x402Config && effectiveX402BatchConfig ? () => createX402BatchSettlementServer({
16831
+ channelStorage: x402ChannelStorage,
16832
+ network: x402Config.network,
16833
+ payTo: x402Config.payTo,
16834
+ facilitator: x402Config.facilitator,
16835
+ ...x402Config.facilitatorAuth ? { facilitatorAuth: x402Config.facilitatorAuth } : {},
16836
+ config: effectiveX402BatchConfig,
16837
+ db: opts.db,
16838
+ allowInMemory: opts.devMode,
16839
+ earnedNative: (channelId) => creditLedger.earnedNativeForX402Channel(channelId),
16840
+ supportedResponse: selfRelayEnabled ? void 0 : opts.x402FacilitatorHealth?.supportedResponse?.(),
16841
+ payout: payoutReporter?.x402Observer()
16842
+ }) : void 0;
16843
+ x402BatchSettlement = x402BatchSettlementFactory && (bootX402Support?.batchSettlement || selfRelayEnabled) ? await x402BatchSettlementFactory() : void 0;
16844
+ const x402ExactStore = x402Config && opts.db && typeof opts.db.connect === "function" ? new PostgresX402ExactSettlementStore(opts.db) : void 0;
16845
+ await x402ExactStore?.init();
16846
+ const x402ExactEvidence = x402Config && x402ExactStore ? await createX402SettlementEvidenceReader({
16847
+ network: x402Config.network,
16848
+ ...x402BatchConfig?.rpcUrl ? { rpcUrl: x402BatchConfig.rpcUrl } : {}
16849
+ }) : void 0;
16850
+ const x402ExactSettlement = x402Config && x402ExactStore ? new X402ExactSettlementServer({
16851
+ store: x402ExactStore,
16852
+ evidence: x402ExactEvidence,
16853
+ repair: async (effect, receipt, tx) => {
16854
+ if (!receipt.txHash) throw new Error("settled exact x402 intent has no transaction");
16855
+ await repairX402ExactSettlementEffect({
16856
+ effect,
16857
+ paymentId: receipt.txHash,
16858
+ ledger: creditLedger,
16859
+ processedPayments,
16860
+ enqueueCreditDeposit,
16861
+ tx
16862
+ });
16863
+ }
16864
+ }) : void 0;
16865
+ server = new DVMServer(descriptor, {
16866
+ ...opts,
16867
+ creditLedger,
16868
+ processedPayments,
16869
+ onJobCompleted: effectiveOnJobCompleted,
16870
+ onJobCost: effectiveOnJobCost,
16871
+ onPaidJobDeath: effectiveOnPaidJobDeath,
16872
+ onRevenueSkippedNoRail: effectiveOnRevenueSkippedNoRail,
16873
+ onCreditFunded: opts.onCreditFunded,
16874
+ enqueueCreditDeposit,
16875
+ enqueueCreditDrain,
16876
+ enqueueCreditDrawRelease,
16877
+ x402BatchSettlement,
16878
+ x402BatchSettlementFactory,
16879
+ x402ExactSettlement,
16880
+ payoutReporter
16881
+ });
16882
+ await server.jobManager.recoverTerminalCosts();
16883
+ return {
16884
+ app: server.app,
16885
+ authAudience: server.authAudience,
16886
+ shutdown,
16887
+ jobManager: server.jobManager,
16888
+ revenueReporterActive: !!activeRevenueReporter,
16889
+ payoutReporter
16890
+ };
16891
+ } catch (initializationError) {
16892
+ await shutdown().catch(() => void 0);
16893
+ throw initializationError;
16894
+ }
16499
16895
  }
16500
16896
  function unknownRouteNotFound(c) {
16501
16897
  c.set("unknownRoute", true);
@@ -16507,6 +16903,8 @@ var DVMServer = class {
16507
16903
  mintHealthTracker;
16508
16904
  descriptor;
16509
16905
  opts;
16906
+ /** Validated `DVMKIT_PUBLIC_URL` origin, or request-derived URLs when absent. */
16907
+ publicOrigin;
16510
16908
  consumedCredentialStore;
16511
16909
  adminCashuNonceStore;
16512
16910
  ownsMintHealthTracker;
@@ -16550,6 +16948,7 @@ var DVMServer = class {
16550
16948
  this.descriptor = descriptor;
16551
16949
  this.pricingCurrency = descriptor.currency ?? "usd";
16552
16950
  this.opts = opts;
16951
+ this.publicOrigin = resolvePublicOrigin(opts.env.DVMKIT_PUBLIC_URL);
16553
16952
  this.authAudience = this.resolveAuthAudience();
16554
16953
  this.x402BatchSettlement = opts.x402BatchSettlement;
16555
16954
  this.x402ExactSettlement = opts.x402ExactSettlement;
@@ -16568,7 +16967,6 @@ var DVMServer = class {
16568
16967
  this.ownsMintHealthTracker = false;
16569
16968
  } else if (opts.mints && opts.mints.length > 0) {
16570
16969
  this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints });
16571
- this.mintHealthTracker.start();
16572
16970
  this.ownsMintHealthTracker = true;
16573
16971
  } else {
16574
16972
  this.ownsMintHealthTracker = false;
@@ -16581,12 +16979,6 @@ var DVMServer = class {
16581
16979
  this.creditLedger = opts.creditLedger ?? new MemoryCreditLedger();
16582
16980
  this.opts.mpp?.useTempoCreditLedger?.(this.creditLedger);
16583
16981
  this.processedPayments = opts.processedPayments ?? new MemoryProcessedPaymentStore();
16584
- if (this.opts.tempoObserverHealth?.declareSessionCapability) {
16585
- this.undeclareSessionCapability = this.opts.tempoObserverHealth.declareSessionCapability(
16586
- () => this.tempoSessionCapability()
16587
- );
16588
- void this.opts.tempoObserverHealth.refresh?.();
16589
- }
16590
16982
  this.jobManager = new JobManager(descriptor, {
16591
16983
  receiptIssuer: this.receiptIssuer,
16592
16984
  creditLedger: this.creditLedger,
@@ -16615,6 +17007,7 @@ var DVMServer = class {
16615
17007
  mpp: opts.mpp,
16616
17008
  cashuMode: opts.cashuMode,
16617
17009
  lockPubkey: opts.lockPubkey,
17010
+ disposableCashu: opts.disposableCashu,
16618
17011
  db: opts.db,
16619
17012
  dvmId: opts.dvmId,
16620
17013
  authAudience: this.authAudience,
@@ -16631,6 +17024,9 @@ var DVMServer = class {
16631
17024
  pricingCurrency: this.pricingCurrency,
16632
17025
  onCreditFunded: opts.onCreditFunded,
16633
17026
  onJobCompleted: opts.onJobCompleted,
17027
+ onPaidJobCompleted: opts.onPaidJobCompleted,
17028
+ paymentMode: opts.paymentMode,
17029
+ onJobCost: opts.onJobCost,
16634
17030
  onPaidJobDeath: opts.onPaidJobDeath,
16635
17031
  onRevenueSkippedNoRail: opts.onRevenueSkippedNoRail,
16636
17032
  onJobRetentionSweepFailed: opts.onJobRetentionSweepFailed,
@@ -16638,19 +17034,44 @@ var DVMServer = class {
16638
17034
  reactivationClaimGraceMs: opts.reactivationClaimGraceMs
16639
17035
  });
16640
17036
  this.app = new Hono();
16641
- this.registerRoutes();
17037
+ try {
17038
+ this.registerRoutes();
17039
+ if (this.ownsMintHealthTracker) this.mintHealthTracker?.start();
17040
+ if (this.opts.tempoObserverHealth?.declareSessionCapability) {
17041
+ this.undeclareSessionCapability = this.opts.tempoObserverHealth.declareSessionCapability(
17042
+ () => this.tempoSessionCapability()
17043
+ );
17044
+ void this.opts.tempoObserverHealth.refresh?.();
17045
+ }
17046
+ } catch (error) {
17047
+ try {
17048
+ this.jobManager.shutdown();
17049
+ } catch {
17050
+ }
17051
+ try {
17052
+ if (this.ownsMintHealthTracker) this.mintHealthTracker?.stop();
17053
+ } catch {
17054
+ }
17055
+ try {
17056
+ this.undeclareSessionCapability?.();
17057
+ } catch {
17058
+ }
17059
+ this.undeclareSessionCapability = void 0;
17060
+ throw error;
17061
+ }
16642
17062
  }
16643
17063
  /**
16644
17064
  * Tear down this mount. Every leg is synchronous except the batch-settlement
16645
17065
  * server's own `stop()`; the returned promise settles once that has finished,
16646
17066
  * so a caller holding a resource behind it — the settlement pool the spend
16647
17067
  * gate's store owns (internal-review) — can sequence its close instead of pulling it
16648
- * out from under an in-flight stop. It never rejects, so ignoring it is safe.
17068
+ * out from under an in-flight stop. A rejection remains observable so the app
17069
+ * and host teardown layers can finish later cleanup before surfacing it.
16649
17070
  */
16650
17071
  shutdown() {
16651
17072
  this.jobManager.shutdown();
16652
17073
  const settlement = this.x402BatchSettlementInit ?? Promise.resolve(this.x402BatchSettlement);
16653
- const stopped = settlement.then((s) => s?.stop()).catch(() => void 0);
17074
+ const stopped = settlement.then((s) => s?.stop());
16654
17075
  if (this.ownsMintHealthTracker) this.mintHealthTracker?.stop();
16655
17076
  this.undeclareSessionCapability?.();
16656
17077
  this.undeclareSessionCapability = void 0;
@@ -16847,6 +17268,7 @@ var DVMServer = class {
16847
17268
  * `cashu_not_p2pk_locked`).
16848
17269
  */
16849
17270
  async resolveLockPubkeysForReceive() {
17271
+ if (this.opts.disposableCashu && this.opts.lockPubkey) return [toLockPubkey(this.opts.lockPubkey)];
16850
17272
  if (this.opts.cashuMode !== "p2pk-accumulator" || !this.opts.db || !this.opts.dvmId || !this.opts.lockPubkey) {
16851
17273
  return void 0;
16852
17274
  }
@@ -17165,7 +17587,7 @@ var DVMServer = class {
17165
17587
  defaultPaymentMethods() {
17166
17588
  const methods = [];
17167
17589
  if (this.opts.mints?.length) {
17168
- const accumulatorReady = this.opts.cashuMode === "p2pk-accumulator" && !!this.opts.lockPubkey && !!this.opts.db;
17590
+ const accumulatorReady = this.opts.cashuMode === "p2pk-accumulator" && !!this.opts.lockPubkey && (!!this.opts.db || !!this.opts.disposableCashu);
17169
17591
  if (accumulatorReady) methods.push("cashu");
17170
17592
  }
17171
17593
  if (this.opts.x402 && this.x402CurrencySupported()) methods.push("x402");
@@ -17350,7 +17772,7 @@ var DVMServer = class {
17350
17772
  const authHeader = c.req.header("Authorization");
17351
17773
  const { Credential } = await import("mppx");
17352
17774
  const mppCredential = authHeader ? Credential.extractPaymentScheme(authHeader) ?? void 0 : void 0;
17353
- let lockPubkeys;
17775
+ let lockPubkeys = this.opts.disposableCashu && this.opts.lockPubkey ? [toLockPubkey(this.opts.lockPubkey)] : void 0;
17354
17776
  if (this.opts.cashuMode === "p2pk-accumulator" && this.opts.db && this.opts.dvmId && this.opts.lockPubkey) {
17355
17777
  const active = await this.lockPubkeyStateForRead(this.opts.db, this.opts.dvmId);
17356
17778
  lockPubkeys = activePubkeySet(active);
@@ -17368,7 +17790,7 @@ var DVMServer = class {
17368
17790
  x402ExactSettlement: this.x402ExactSettlement,
17369
17791
  x402ExactVersions: x402Support?.exact,
17370
17792
  x402BatchSettlement,
17371
- x402Resource: c.req.url,
17793
+ x402Resource: callerFacingRequestUrl(c.req.url, this.publicOrigin),
17372
17794
  mppCredential,
17373
17795
  mpp: this.opts.mpp,
17374
17796
  resourcePath: c.req.path,
@@ -17384,6 +17806,7 @@ var DVMServer = class {
17384
17806
  consumedCredentialStore: this.consumedCredentialStore,
17385
17807
  cashuMode: this.opts.cashuMode,
17386
17808
  accumulatorDb: this.opts.cashuMode === "p2pk-accumulator" ? this.opts.db : void 0,
17809
+ disposableCashu: this.opts.disposableCashu,
17387
17810
  dvmId: this.opts.dvmId,
17388
17811
  lockPubkeys,
17389
17812
  requestId: c.req.header("X-Cashu-Request-Id"),
@@ -17570,6 +17993,7 @@ var DVMServer = class {
17570
17993
  ...this.opts.mpp.tempoConfiguration && {
17571
17994
  recipient: this.opts.mpp.tempoConfiguration.recipient,
17572
17995
  payment_token: this.opts.mpp.tempoConfiguration.paymentToken,
17996
+ chain_id: this.opts.mpp.tempoConfiguration.chainId,
17573
17997
  ...this.opts.mpp.tempoConfiguration.operator && {
17574
17998
  operator: this.opts.mpp.tempoConfiguration.operator
17575
17999
  },
@@ -17698,7 +18122,8 @@ var DVMServer = class {
17698
18122
  500
17699
18123
  );
17700
18124
  }
17701
- if (err instanceof SignedRequestError) return buildAuthErrorResponse(c, err);
18125
+ if (err instanceof SignedRequestError)
18126
+ return buildAuthErrorResponse(c, err, this.publicOrigin);
17702
18127
  throw err;
17703
18128
  }
17704
18129
  }
@@ -17723,7 +18148,7 @@ var DVMServer = class {
17723
18148
  { onError: () => ({ "error.code": "invalid_quote_data" }) }
17724
18149
  );
17725
18150
  } catch (err) {
17726
- const schemaUrl = infoSchemaUrl(c.req.url);
18151
+ const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
17727
18152
  const { message, hint } = err instanceof z2.ZodError ? humanizeZodError(err, { schemaUrl }) : {
17728
18153
  message: "Quote-input validation failed",
17729
18154
  hint: `Inspect ${schemaUrl} for the expected input schema.`
@@ -17837,7 +18262,7 @@ var DVMServer = class {
17837
18262
  */
17838
18263
  requireCapabilityFromBody(c, capability) {
17839
18264
  const known = this.jobManager.capabilityNames();
17840
- const infoUrl = `${new URL(c.req.url).origin}/v1/info`;
18265
+ const infoUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
17841
18266
  if (typeof capability !== "string" || capability.length === 0) {
17842
18267
  return {
17843
18268
  error: c.json(
@@ -17878,7 +18303,7 @@ var DVMServer = class {
17878
18303
  try {
17879
18304
  return { body: await c.req.json() };
17880
18305
  } catch {
17881
- const infoUrl = `${new URL(c.req.url).origin}/v1/info`;
18306
+ const infoUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
17882
18307
  return {
17883
18308
  error: c.json(
17884
18309
  {
@@ -17913,7 +18338,7 @@ var DVMServer = class {
17913
18338
  * relayed to a human don't change with the status.
17914
18339
  */
17915
18340
  invalidInputResponse(c, err) {
17916
- const schemaUrl = infoSchemaUrl(c.req.url);
18341
+ const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
17917
18342
  const issues = zodIssuesOf(err);
17918
18343
  if (issues) {
17919
18344
  const inputIssues = callerInputIssues(issues);
@@ -18027,7 +18452,8 @@ var DVMServer = class {
18027
18452
  500
18028
18453
  );
18029
18454
  }
18030
- if (error instanceof SignedRequestError) return buildAuthErrorResponse(c, error);
18455
+ if (error instanceof SignedRequestError)
18456
+ return buildAuthErrorResponse(c, error, this.publicOrigin);
18031
18457
  throw error;
18032
18458
  }
18033
18459
  const signedRequestId = requestIdFromEnvelope(body.data);
@@ -18081,6 +18507,7 @@ var DVMServer = class {
18081
18507
  const capabilityResult = this.requireCapabilityFromBody(c, body.capability);
18082
18508
  if ("error" in capabilityResult) return capabilityResult.error;
18083
18509
  const capabilityName = capabilityResult.name;
18510
+ pinDimension("capability", capabilityName);
18084
18511
  const cap = this.descriptor.capabilities[capabilityName];
18085
18512
  let parsedInput;
18086
18513
  let deferredInputError;
@@ -18145,7 +18572,8 @@ var DVMServer = class {
18145
18572
  500
18146
18573
  );
18147
18574
  }
18148
- if (err instanceof SignedRequestError) return buildAuthErrorResponse(c, err);
18575
+ if (err instanceof SignedRequestError)
18576
+ return buildAuthErrorResponse(c, err, this.publicOrigin);
18149
18577
  throw err;
18150
18578
  }
18151
18579
  }
@@ -18378,7 +18806,7 @@ var DVMServer = class {
18378
18806
  } catch (err) {
18379
18807
  if (err instanceof SignedRequestError && err.sub_reason === "replay_detected" && exactAdmission?.replayed) {
18380
18808
  } else if (err instanceof SignedRequestError) {
18381
- return buildAuthErrorResponse(c, err);
18809
+ return buildAuthErrorResponse(c, err, this.publicOrigin);
18382
18810
  } else {
18383
18811
  throw err;
18384
18812
  }
@@ -18936,8 +19364,7 @@ var DVMServer = class {
18936
19364
  summary,
18937
19365
  lastActivityAt: Date.now()
18938
19366
  });
18939
- const baseUrl = new URL(c.req.url);
18940
- const infoUrl = `${baseUrl.origin}/v1/info`;
19367
+ const infoUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
18941
19368
  return c.json(
18942
19369
  {
18943
19370
  error: "unknown_capability",
@@ -18960,7 +19387,7 @@ var DVMServer = class {
18960
19387
  summary: "Persisted auth envelope failed signature re-verification on reactivation",
18961
19388
  lastActivityAt: Date.now()
18962
19389
  }).catch(() => void 0);
18963
- return buildAuthErrorResponse(c, err);
19390
+ return buildAuthErrorResponse(c, err, this.publicOrigin);
18964
19391
  }
18965
19392
  if (err instanceof ReactivationAuthSchemaError) {
18966
19393
  return c.json(
@@ -19077,13 +19504,13 @@ var DVMServer = class {
19077
19504
  if (isStreamableJobStore(store)) {
19078
19505
  const cancelled = await store.cancelJob(id, reason ?? "Job cancelled");
19079
19506
  if (!cancelled) return this.jobNotActiveResponse(c);
19080
- await jm.issueReceiptForStoredJob(id);
19507
+ await jm.finalizeStoredTerminal(id);
19081
19508
  return c.json({ status: "cancelled" });
19082
19509
  }
19083
19510
  record.status = "cancelled";
19084
19511
  record.lastActivityAt = Date.now();
19085
19512
  await jm.store.save(record);
19086
- await jm.issueReceiptForStoredJob(id);
19513
+ await jm.finalizeStoredTerminal(id);
19087
19514
  return c.json({ status: "cancelled" });
19088
19515
  }
19089
19516
  if (!isJobOwner(c, job)) return c.json({ error: "Job not found" }, 404);
@@ -19350,8 +19777,8 @@ var PaymentValidationError = class extends Error {
19350
19777
  }
19351
19778
  detail;
19352
19779
  };
19353
- function buildAuthErrorResponse(c, err) {
19354
- return c.json(authErrorBody(err, { schemaUrl: infoSchemaUrl(c.req.url) }), 401);
19780
+ function buildAuthErrorResponse(c, err, publicOrigin) {
19781
+ return c.json(authErrorBody(err, { schemaUrl: infoSchemaUrl(c.req.url, publicOrigin) }), 401);
19355
19782
  }
19356
19783
  function validateMessageContent(type, content, devMode) {
19357
19784
  if (type === "response") {
@@ -19545,6 +19972,7 @@ export {
19545
19972
  pruneRetired,
19546
19973
  DEFAULT_LOCK_PUBKEY_GRACE_SECONDS,
19547
19974
  activePubkeySet,
19975
+ AccumulatorReplayError,
19548
19976
  initWalletAccumulatorTable,
19549
19977
  insertAccumulatorRows,
19550
19978
  clearAccumulatorForDvm,