@dvmkit/sdk 0.21.1 → 0.22.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 (55) hide show
  1. package/dist/card-charge-journal-SKMBEPRM.js +44 -0
  2. package/dist/{chunk-ALTE7I6C.js → chunk-2WWVI32F.js} +56 -1
  3. package/dist/{chunk-C65P6MGZ.js → chunk-3DGIRHP2.js} +2 -2
  4. package/dist/{chunk-6RJAE332.js → chunk-5NMHZIIU.js} +1 -1
  5. package/dist/{chunk-34H4ZKBM.js → chunk-5YJWAJ4N.js} +1 -1
  6. package/dist/{chunk-2PN4DKZW.js → chunk-CUWSJQGT.js} +1 -1
  7. package/dist/{chunk-A6AOE63A.js → chunk-DLRPKEAM.js} +5 -1
  8. package/dist/{chunk-KRX7VNBG.js → chunk-DY5QIHJA.js} +1 -1
  9. package/dist/{chunk-RX6FT7XA.js → chunk-EHBUAJ5M.js} +1 -1
  10. package/dist/{chunk-7RWE6ELV.js → chunk-EQIP6TFD.js} +1 -1
  11. package/dist/{chunk-OTNHKRHA.js → chunk-GDZXGB43.js} +3 -3
  12. package/dist/{chunk-5RLNRLLB.js → chunk-HEH7YUGE.js} +1 -1
  13. package/dist/{chunk-DJRTYBEE.js → chunk-HVZIITWW.js} +27 -4
  14. package/dist/{chunk-MUMZPUYD.js → chunk-JINZMDNK.js} +1231 -85
  15. package/dist/{chunk-QTKGFG3I.js → chunk-JMTLK2SZ.js} +1 -1
  16. package/dist/{chunk-INYFUPXV.js → chunk-L35WYOH2.js} +1 -1
  17. package/dist/{chunk-MWDUGMN2.js → chunk-LCNRFKP7.js} +2 -2
  18. package/dist/{chunk-CVN2YNMG.js → chunk-QMKGYIBM.js} +1 -1
  19. package/dist/{chunk-I5MXADJS.js → chunk-SEIMPHIP.js} +225 -11
  20. package/dist/{chunk-QCZXKUS2.js → chunk-SQOVLAJ2.js} +1 -1
  21. package/dist/{chunk-72A4PHLD.js → chunk-UVRBD5GR.js} +1 -1
  22. package/dist/{chunk-5RGZN6WQ.js → chunk-XB644VN3.js} +1 -1
  23. package/dist/{chunk-CXRI4N4Y.js → chunk-YNHFJE6U.js} +1 -1
  24. package/dist/{chunk-LY2JYSJS.js → chunk-ZH5CLGO6.js} +1 -1
  25. package/dist/{content-retention-COnj0QKi.d.ts → content-retention-DEvY5vjM.d.ts} +1 -1
  26. package/dist/{credit-ledger-ZCJXJG6R.js → credit-ledger-S76KU53A.js} +11 -3
  27. package/dist/{credit-menu-DbjmA1vK.d.ts → credit-menu-BA2my6hE.d.ts} +229 -8
  28. package/dist/{fx-DfzQ0Lge.d.ts → fx-J5oXbv9A.d.ts} +1 -1
  29. package/dist/index.d.ts +5 -5
  30. package/dist/index.js +4 -4
  31. package/dist/internal/caller.d.ts +10 -9
  32. package/dist/internal/caller.js +11 -11
  33. package/dist/internal/server.d.ts +7 -7
  34. package/dist/internal/server.js +21 -21
  35. package/dist/{job-store-p45j28px.d.ts → job-store-CjPnf0dd.d.ts} +1 -1
  36. package/dist/{lightning-backend-DO7G7VqP.d.ts → lightning-backend-D9Q2KDS-.d.ts} +1 -1
  37. package/dist/{memory-credit-ledger-N4PZKTT5.js → memory-credit-ledger-5KXHOWB2.js} +4 -4
  38. package/dist/{mpp-setup-BLIYBORK.js → mpp-setup-3YZMURZL.js} +3 -3
  39. package/dist/{payout-reporter-5QAHCYIO.js → payout-reporter-KTLBNJJ7.js} +2 -2
  40. package/dist/{postgres-consumed-credential-store-4PCJEZBD.js → postgres-consumed-credential-store-Y336KNIB.js} +1 -1
  41. package/dist/{postgres-job-store-QKN5LGAH.js → postgres-job-store-ESEAN7JL.js} +2 -2
  42. package/dist/{postgres-replay-store-GH5AINQR.js → postgres-replay-store-6SVCZ475.js} +2 -2
  43. package/dist/{pricing-JJYLTTWK.js → pricing-RP5CGW6I.js} +3 -3
  44. package/dist/{revenue-reporter-6XARUEG6.js → revenue-reporter-FD3GDQZS.js} +2 -2
  45. package/dist/server/index.d.ts +20 -11
  46. package/dist/server/index.js +303 -30
  47. package/dist/{step-cache-CcCjpZm1.d.ts → step-cache-DXFftnAc.d.ts} +156 -13
  48. package/dist/{tempo-charge-store-A3CDF7D5.js → tempo-charge-store-SOIRMEFJ.js} +2 -2
  49. package/dist/{tempo-session-store-XJE6GIXN.js → tempo-session-store-UBDFFHDG.js} +3 -3
  50. package/dist/testing/index.d.ts +3 -3
  51. package/dist/testing/index.js +6 -6
  52. package/dist/{usd-BnAFcUai.d.ts → usd-Bc_rva_K.d.ts} +1 -1
  53. package/dist/{x402-XLFD43RF.js → x402-MFBRORHG.js} +5 -5
  54. package/package.json +1 -1
  55. package/dist/{chunk-NUDHO4HV.js → chunk-LABJSMSJ.js} +48 -48
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-XRLKGCAW.js";
6
6
  import {
7
7
  x402EarnedCeiling
8
- } from "./chunk-KRX7VNBG.js";
8
+ } from "./chunk-DY5QIHJA.js";
9
9
  import {
10
10
  createX402FacilitatorAuthHeaders,
11
11
  createX402FacilitatorClient,
@@ -14,18 +14,19 @@ import {
14
14
  verifyWithFacilitator,
15
15
  withPaymentDiagnostics,
16
16
  withPaymentStage
17
- } from "./chunk-C65P6MGZ.js";
17
+ } from "./chunk-3DGIRHP2.js";
18
18
  import {
19
19
  MemoryCreditLedger
20
- } from "./chunk-DJRTYBEE.js";
20
+ } from "./chunk-HVZIITWW.js";
21
21
  import {
22
22
  PostgresJobStore
23
- } from "./chunk-QCZXKUS2.js";
23
+ } from "./chunk-SQOVLAJ2.js";
24
24
  import {
25
25
  CASHU_MIN_LOCKTIME_SECONDS,
26
26
  DEFAULT_CASHU_LOCK_WINDOW_SECONDS,
27
27
  SIGNED_ENVELOPE_FIELDS,
28
28
  SignedRequestError,
29
+ carriesSignedEnvelope,
29
30
  humanizeZodError,
30
31
  humanizeZodIssues,
31
32
  shortestCashuLockWindowSeconds,
@@ -33,7 +34,7 @@ import {
33
34
  signedRequestInput,
34
35
  unsignedCashuLockWindowSeconds,
35
36
  zodIssuesOf
36
- } from "./chunk-A6AOE63A.js";
37
+ } from "./chunk-DLRPKEAM.js";
37
38
  import {
38
39
  MemoryJobStore,
39
40
  UNCAPPABLE_ASK_TOTAL,
@@ -42,7 +43,7 @@ import {
42
43
  isJobRetentionStore,
43
44
  isReceiptIssuingStore,
44
45
  isStreamableJobStore
45
- } from "./chunk-6RJAE332.js";
46
+ } from "./chunk-5NMHZIIU.js";
46
47
  import {
47
48
  ContentHoldError,
48
49
  JobCancelledError,
@@ -83,7 +84,7 @@ import {
83
84
  receiptPubkeyFromSecret,
84
85
  toLockPubkey,
85
86
  verifyChallenge
86
- } from "./chunk-7RWE6ELV.js";
87
+ } from "./chunk-EQIP6TFD.js";
87
88
  import {
88
89
  X402_BATCH_SETTLEMENT_SCHEME,
89
90
  X402_DEFAULT_FACILITATOR,
@@ -102,7 +103,7 @@ import {
102
103
  resolveRpcOverride,
103
104
  usdcToMsats,
104
105
  x402NetworkToCaip2
105
- } from "./chunk-NUDHO4HV.js";
106
+ } from "./chunk-LABJSMSJ.js";
106
107
  import {
107
108
  RAIL_REFUNDABLE,
108
109
  canonicalRequestPath,
@@ -110,7 +111,7 @@ import {
110
111
  } from "./chunk-SXL44PL5.js";
111
112
  import {
112
113
  createConsoleLogger
113
- } from "./chunk-QTKGFG3I.js";
114
+ } from "./chunk-JMTLK2SZ.js";
114
115
  import {
115
116
  StepCache
116
117
  } from "./chunk-BQJIFRAZ.js";
@@ -127,8 +128,9 @@ import {
127
128
  roundUsd,
128
129
  satsToFiat,
129
130
  validateCurrency
130
- } from "./chunk-72A4PHLD.js";
131
+ } from "./chunk-UVRBD5GR.js";
131
132
  import {
133
+ CARD_EXPIRY_DRAIN_PREFIX,
132
134
  CreditLedger,
133
135
  CreditLedgerError,
134
136
  DRAIN_METHODS,
@@ -138,12 +140,14 @@ import {
138
140
  X402_RESOLVED_SETTLEMENT_STATUSES,
139
141
  X402_SPEND_BLOCKING_SETTLEMENT_STATUSES,
140
142
  X402_WEDGED_SETTLEMENT_STATUSES,
143
+ cardExpiryDrainId,
144
+ isCardFundedRail,
141
145
  isDrainMethod,
142
146
  isFundingRail,
143
147
  isNonChannelBitcoinRail,
144
148
  netOwedSats,
145
149
  x402SettlementPending
146
- } from "./chunk-I5MXADJS.js";
150
+ } from "./chunk-SEIMPHIP.js";
147
151
  import {
148
152
  parseStoreValue
149
153
  } from "./chunk-YG7G4DPZ.js";
@@ -162,7 +166,7 @@ import {
162
166
  import {
163
167
  isTransportError,
164
168
  withPostgresRecoveryContext
165
- } from "./chunk-INYFUPXV.js";
169
+ } from "./chunk-L35WYOH2.js";
166
170
  import {
167
171
  DvmError,
168
172
  x402NetworkByCaip2
@@ -180,17 +184,17 @@ import {
180
184
  installHostedTempoCloseGuard,
181
185
  isTempoInsufficientFundsError,
182
186
  resolveTempoNetwork
183
- } from "./chunk-MWDUGMN2.js";
187
+ } from "./chunk-LCNRFKP7.js";
184
188
  import {
185
189
  loggers,
186
190
  pinDimension,
187
191
  withSpan,
188
192
  withTraceContext
189
- } from "./chunk-LY2JYSJS.js";
193
+ } from "./chunk-ZH5CLGO6.js";
190
194
  import {
191
195
  createDiagnosticCapabilityAuthority,
192
196
  diagnosticConsole
193
- } from "./chunk-ALTE7I6C.js";
197
+ } from "./chunk-2WWVI32F.js";
194
198
 
195
199
  // src/sdk/server/lock-pubkey-store.ts
196
200
  async function initLockPubkeyTable(db) {
@@ -903,6 +907,7 @@ function stablecoinCreditInstrumentAllowed(allowOneShotStablecoin, instrument) {
903
907
  }
904
908
 
905
909
  // src/sdk/server/credit-menu.ts
910
+ var STRIPE_CREDIT_MIN_MICRO = 5e5;
906
911
  function creditMenuConfigured(args) {
907
912
  if (args.runtime === "isolate") return false;
908
913
  if (!args.config) return false;
@@ -951,6 +956,9 @@ async function buildCreditMenu(args) {
951
956
  const funding = args.funding.filter((rail) => {
952
957
  if (rail === "tempo") return tempoMethods.length > 0;
953
958
  if (rail === "x402") return x402Schemes.length > 0;
959
+ if (rail === "stripe") {
960
+ return args.stripeNetworkId !== void 0 && currency === "usd" && bounds.maxMicro >= STRIPE_CREDIT_MIN_MICRO;
961
+ }
954
962
  if (isNonChannelBitcoinRail(rail)) return args.hasAcceptedMint === true;
955
963
  return true;
956
964
  });
@@ -970,7 +978,14 @@ async function buildCreditMenu(args) {
970
978
  // a builder who overrides `paymentMethods` can drop x402 from `funding`
971
979
  // while the rail stays wired, and terms for a rail the menu doesn't offer
972
980
  // are the same misadvertisement in the other direction.
973
- ...funding.includes("x402") && x402Schemes.length > 0 && { x402: { schemes: x402Schemes } }
981
+ ...funding.includes("x402") && x402Schemes.length > 0 && { x402: { schemes: x402Schemes } },
982
+ ...funding.includes("stripe") && args.stripeNetworkId !== void 0 && {
983
+ stripe: {
984
+ network_id: args.stripeNetworkId,
985
+ min_micro: Math.max(bounds.minMicro, STRIPE_CREDIT_MIN_MICRO),
986
+ currency: "usd"
987
+ }
988
+ }
974
989
  };
975
990
  if (funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
976
991
  try {
@@ -1050,7 +1065,8 @@ function toCreditTerms(menu) {
1050
1065
  },
1051
1066
  ...menu.lightning_min_micro !== void 0 && {
1052
1067
  lightning_min_micro: menu.lightning_min_micro
1053
- }
1068
+ },
1069
+ ...menu.stripe && { stripe: { ...menu.stripe } }
1054
1070
  };
1055
1071
  }
1056
1072
  var LIGHTNING_FLOOR_FX_MARGIN = 1.15;
@@ -4256,7 +4272,7 @@ function upfrontPaymentFromDraw(draw) {
4256
4272
  amount_micro: draw.amountMicro,
4257
4273
  currency: value.creditCurrency
4258
4274
  },
4259
- ...rail === "cashu" || rail === "x402" || rail === "tempo" || rail === "lightning" ? { rail } : {},
4275
+ ...rail !== null && isFundingRail(rail) ? { rail } : {},
4260
4276
  ...value.drawNative !== null && value.nativeAsset !== null && {
4261
4277
  native_amount: value.drawNative,
4262
4278
  native_asset: value.nativeAsset
@@ -4331,8 +4347,10 @@ function decimalsFor(methodName) {
4331
4347
  switch (methodName) {
4332
4348
  case "tempo":
4333
4349
  return 6;
4350
+ case "stripe":
4351
+ return 2;
4334
4352
  default:
4335
- throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo')`);
4353
+ throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo' or 'stripe')`);
4336
4354
  }
4337
4355
  }
4338
4356
  function msatsToMethodAmount(msats, methodName, btcUsd) {
@@ -4363,8 +4381,10 @@ function methodNativeAsset(methodName) {
4363
4381
  switch (methodName) {
4364
4382
  case "tempo":
4365
4383
  return "usdc";
4384
+ case "stripe":
4385
+ return "usd-cents";
4366
4386
  default:
4367
- throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo')`);
4387
+ throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo' or 'stripe')`);
4368
4388
  }
4369
4389
  }
4370
4390
  function methodNativeAmountFromDecimal(amountStr, methodName) {
@@ -4824,6 +4844,235 @@ async function verifyUpfrontPayment(opts) {
4824
4844
  }
4825
4845
  return result;
4826
4846
  }
4847
+ async function verifyCardFunding(opts) {
4848
+ return withPaymentDiagnostics(() => verifyCardFundingInScope(opts));
4849
+ }
4850
+ async function verifyCardFundingInScope(opts) {
4851
+ const rail = opts.stripeCredit;
4852
+ const { fundOnly, amountMicro } = opts;
4853
+ const resourcePath = opts.resourcePath ?? "/v1/credit";
4854
+ if (!rail || !opts.creditLedger) {
4855
+ return creditRefusal("payment_invalid", "This DVM does not accept card funding.", {
4856
+ status: 409,
4857
+ hint: "Fund the credit over one of the rails listed in credit.funding on /v1/info."
4858
+ });
4859
+ }
4860
+ if (amountMicro % 1e4 !== 0) {
4861
+ return creditRefusal(
4862
+ "payment_invalid",
4863
+ `A card top-up must be a whole number of cents; ${amountMicro} micro-USD is not.`,
4864
+ { status: 400, hint: "Round fund.amount_micro to a multiple of 10000 (one cent) and sign again." }
4865
+ );
4866
+ }
4867
+ const challenge = async () => [
4868
+ await rail.issueChallenge({
4869
+ amountMicro,
4870
+ resourcePath,
4871
+ expires: new Date(Date.now() + UPFRONT_CHALLENGE_TTL_MS).toISOString()
4872
+ })
4873
+ ];
4874
+ const credential = typeof opts.mppCredential === "string" ? opts.mppCredential : void 0;
4875
+ if (!credential) {
4876
+ return {
4877
+ paidMsats: 0,
4878
+ receivedProofs: [],
4879
+ error: {
4880
+ body: {
4881
+ error: "payment_required",
4882
+ amount_micro: amountMicro,
4883
+ currency: "usd",
4884
+ display: "This top-up needs a card payment approved in Link.",
4885
+ hint: 'Build an MPP credential for the attached method="stripe" challenge from the shared payment token (spt_\u2026) your human approved, set fund.commitment to its SHA-256, and resend this fund request, freshly signed, with the credential in the Authorization header.'
4886
+ },
4887
+ status: 402,
4888
+ mppChallenges: await challenge()
4889
+ }
4890
+ };
4891
+ }
4892
+ if (fundingCommitment(credential) !== fundOnly.commitment) {
4893
+ logCreditRejected("fund_commitment_mismatch", fundOnly.creditId);
4894
+ return creditRefusal(
4895
+ "funding_commitment_mismatch",
4896
+ "fund.commitment does not match the SHA-256 of the attached funding artifact."
4897
+ );
4898
+ }
4899
+ const refusal = crossRailRefusal(await opts.creditLedger.get(fundOnly.creditId), {
4900
+ creditId: fundOnly.creditId,
4901
+ binding: { rail: "stripe" }
4902
+ });
4903
+ if (refusal) return refusal;
4904
+ const charged = await withPaymentStage(
4905
+ "verifier_broadcast",
4906
+ () => rail.charge(credential, { amountMicro, resourcePath })
4907
+ );
4908
+ if (!charged.ok) {
4909
+ logCreditRejected(`stripe_${charged.reason}`, fundOnly.creditId, cardLogFields(charged));
4910
+ return cardChargeRefusal(charged, await challenge());
4911
+ }
4912
+ const ledgerCtx = {
4913
+ ledger: opts.creditLedger,
4914
+ processedPayments: opts.processedPayments,
4915
+ enqueueCreditDeposit: opts.enqueueCreditDeposit,
4916
+ priceFiat: { currency: "usd", amountMicro },
4917
+ // A card charge is valued in cents, never through a sats quote.
4918
+ requiredMsats: 0,
4919
+ callerId: opts.callerPubkey ?? opts.requesterId ?? "anonymous",
4920
+ hostingAdmissionId: opts.hostingAdmissionId,
4921
+ hostingPaymentArtifactHash: opts.hostingPaymentArtifactHash,
4922
+ dvmId: opts.dvmId ?? "",
4923
+ fundOnly,
4924
+ drawBasis: false,
4925
+ ttlMs: opts.creditTtlMs ?? IMPLICIT_CREDIT_TTL_MS
4926
+ };
4927
+ const cents = amountMicro / 1e4;
4928
+ let credited;
4929
+ try {
4930
+ credited = await commitMarkedFunding(ledgerCtx, {
4931
+ rail: "stripe",
4932
+ paymentId: charged.paymentIntentId,
4933
+ paidMsats: 0,
4934
+ fundedMicro: amountMicro,
4935
+ nativeAmount: cents,
4936
+ nativeAsset: "usd-cents"
4937
+ });
4938
+ } catch (err) {
4939
+ const mapped = mapLedgerCommitError(err, void 0, true);
4940
+ if (mapped) {
4941
+ logCredentialRejected("stripe", "upfront", err);
4942
+ return mapped.error?.body.error === "replay_rejected" ? cardReplayRefusal(mapped) : mapped;
4943
+ }
4944
+ logCreditRejected("stripe_booking_failed", fundOnly.creditId, {
4945
+ payment_intent: charged.paymentIntentId
4946
+ });
4947
+ return {
4948
+ paidMsats: 0,
4949
+ receivedProofs: [],
4950
+ error: {
4951
+ body: paymentErrorBody("payment_invalid", {
4952
+ message: "The card was charged but the credit could not be recorded.",
4953
+ display: "Your card was charged, but the credit wasn't recorded yet. Sending the same request again will record it without charging again.",
4954
+ hint: "Resend this exact fund request with the same credential and fund_id (re-sign it if the signature has expired). Do not build a new credential: that would be a second charge.",
4955
+ retryable: true,
4956
+ extra: { rail: "stripe", reason: "booking_failed" }
4957
+ }),
4958
+ status: 503
4959
+ }
4960
+ };
4961
+ }
4962
+ return withDrawBasis(
4963
+ {
4964
+ paidMsats: 0,
4965
+ paymentRail: "stripe",
4966
+ receivedProofs: [],
4967
+ paymentTxHash: charged.paymentIntentId,
4968
+ nativeAmount: cents,
4969
+ nativeAsset: "usd-cents"
4970
+ },
4971
+ ledgerCtx,
4972
+ credited
4973
+ );
4974
+ }
4975
+ function cardLogFields(outcome) {
4976
+ return {
4977
+ message: outcome.message,
4978
+ ...outcome.stripeError?.type && { stripe_error_type: outcome.stripeError.type },
4979
+ ...outcome.stripeError?.code && { stripe_error_code: outcome.stripeError.code },
4980
+ ...outcome.stripeError?.declineCode && { decline_code: outcome.stripeError.declineCode },
4981
+ ...outcome.paymentIntentId && { payment_intent: outcome.paymentIntentId }
4982
+ };
4983
+ }
4984
+ function cardChargeRefusal(outcome, challenges) {
4985
+ const newApproval = "Ask your human to approve a new top-up in Link for at least this amount, then build a credential from the new shared payment token for the challenge attached to this response, and resend the fund request, freshly signed.";
4986
+ const copy = {
4987
+ invalid_credential: {
4988
+ status: 402,
4989
+ display: "The card payment couldn't be verified, so nothing was charged.",
4990
+ hint: "Build a new credential for the challenge attached to this response, from a shared payment token that hasn't been charged, and resend the fund request, freshly signed.",
4991
+ retryable: false,
4992
+ challenges
4993
+ },
4994
+ declined: {
4995
+ status: 402,
4996
+ display: "The card was declined, so nothing was charged and no credit was added.",
4997
+ hint: `${newApproval} A different card is more likely to work.`,
4998
+ retryable: false,
4999
+ challenges
5000
+ },
5001
+ requires_action: {
5002
+ status: 402,
5003
+ display: "The card needs an extra verification step that can't be completed here, so no credit was added.",
5004
+ hint: "Ask your human to approve a new top-up in Link with a card that doesn't require an extra authentication step, then fund again with the new shared payment token.",
5005
+ retryable: false,
5006
+ challenges
5007
+ },
5008
+ token_unusable: {
5009
+ status: 402,
5010
+ display: "The card approval can't be used for this top-up (it was already used, is for a smaller amount, or has lapsed), so nothing was charged.",
5011
+ hint: `If you already sent this shared payment token in an earlier fund request that got no answer, resend that same request (same credential and fund_id) first: it may have been charged, and the resend records it without charging again. Only if no earlier request used this token: ${newApproval.charAt(0).toLowerCase()}${newApproval.slice(1)} An approval can be charged only once, up to the amount it was approved for.`,
5012
+ retryable: false,
5013
+ challenges
5014
+ },
5015
+ // Charged, but not as this top-up: never booked, and a resend would not change it.
5016
+ mismatch: {
5017
+ status: 502,
5018
+ display: "The card processor reported a charge that doesn't match this top-up, so no credit was added.",
5019
+ hint: "Don't retry with this approval. Tell your human the top-up failed and give the provider the payment_intent reference so they can check the charge.",
5020
+ retryable: false,
5021
+ challenges: []
5022
+ },
5023
+ // Nothing reached Stripe and the approval is unused, so the same credential still works.
5024
+ unavailable: {
5025
+ status: 503,
5026
+ display: "The service couldn't record the payment attempt, so this request did not charge the card and no credit was added.",
5027
+ hint: "Resend this exact fund request with the same credential and fund_id (re-sign it if the signature has expired), within five minutes of the original request. This request charged nothing. If an earlier request with this credential got no answer, it may have charged the card; resending records that charge instead of charging again. If resends keep failing, stop and tell your human.",
5028
+ retryable: true,
5029
+ challenges: []
5030
+ },
5031
+ // No fresh challenge: a new challenge would mean a second PaymentIntent.
5032
+ indeterminate: {
5033
+ status: 503,
5034
+ display: "The card processor didn't confirm the payment. The card may or may not have been charged; no credit was added yet.",
5035
+ hint: "Resend this exact fund request with the same credential and fund_id (re-sign it if the signature has expired), within five minutes of the original request. If the earlier attempt was charged and its answer was lost, the resend records that charge instead of charging again. Do not build a new credential from the same approval. If resends keep failing, stop and tell your human; the provider can trace any charge from its records.",
5036
+ retryable: true,
5037
+ challenges: []
5038
+ }
5039
+ }[outcome.reason];
5040
+ return {
5041
+ paidMsats: 0,
5042
+ receivedProofs: [],
5043
+ error: {
5044
+ body: paymentErrorBody("payment_invalid", {
5045
+ message: `Card funding refused (${outcome.reason}): ${outcome.message}.`,
5046
+ display: copy.display,
5047
+ hint: copy.hint,
5048
+ retryable: copy.retryable,
5049
+ extra: {
5050
+ rail: "stripe",
5051
+ reason: outcome.reason,
5052
+ ...outcome.stripeError?.code && { stripe_error_code: outcome.stripeError.code },
5053
+ ...outcome.stripeError?.declineCode && { decline_code: outcome.stripeError.declineCode },
5054
+ ...outcome.reason === "mismatch" && outcome.paymentIntentId && { payment_intent: outcome.paymentIntentId }
5055
+ }
5056
+ }),
5057
+ status: copy.status,
5058
+ ...copy.challenges.length > 0 && { mppChallenges: copy.challenges }
5059
+ }
5060
+ };
5061
+ }
5062
+ function cardReplayRefusal(mapped) {
5063
+ if (!mapped.error) return mapped;
5064
+ return {
5065
+ ...mapped,
5066
+ error: {
5067
+ ...mapped.error,
5068
+ body: {
5069
+ ...mapped.error.body,
5070
+ display: "This card payment was already used to add credit, so it wasn't charged or added again.",
5071
+ hint: "Don't resend this credential. To add more credit, ask your human to approve a new top-up in Link and fund with the new shared payment token under a new fund_id."
5072
+ }
5073
+ }
5074
+ };
5075
+ }
4827
5076
  async function verifyUpfrontPaymentInScope(opts) {
4828
5077
  const {
4829
5078
  cashuToken,
@@ -5180,7 +5429,7 @@ async function verifyUpfrontPaymentInScope(opts) {
5180
5429
  x402Requirements
5181
5430
  });
5182
5431
  }
5183
- const { verifyX402Payment } = await import("./x402-XLFD43RF.js");
5432
+ const { verifyX402Payment } = await import("./x402-MFBRORHG.js");
5184
5433
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
5185
5434
  x402Payment,
5186
5435
  x402Config,
@@ -5872,6 +6121,11 @@ var PAYMENT_ERROR_COPY = {
5872
6121
  hint: "Do not retry, top up, or drain this credit. Pay per call or open a separate prepaid balance.",
5873
6122
  retryable: false
5874
6123
  },
6124
+ credit_closed: {
6125
+ display: "This card-funded balance is closed: either its expired balance was refunded to the card, or a dispute on the card payment froze it. It can't pay for jobs, take a top-up, or be drained.",
6126
+ hint: "Do not retry with this credit_id. Pay per call, or open a new prepaid balance by funding without a credit_id.",
6127
+ retryable: false
6128
+ },
5875
6129
  credit_not_found: {
5876
6130
  display: "No prepaid balance exists at this service under the referenced credit. Nothing was charged.",
5877
6131
  hint: 'Check the credit_id, or open one at POST /v1/credit (op "fund") before drawing on it.',
@@ -6236,7 +6490,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
6236
6490
  snapshot
6237
6491
  });
6238
6492
  }
6239
- const { verifyX402Payment } = await import("./x402-XLFD43RF.js");
6493
+ const { verifyX402Payment } = await import("./x402-MFBRORHG.js");
6240
6494
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
6241
6495
  body.content.x402_payment,
6242
6496
  opts.x402Config,
@@ -6688,7 +6942,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
6688
6942
  try {
6689
6943
  const requiredMsats = job.pendingPaymentMsats ?? 0;
6690
6944
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
6691
- const { verifyX402Payment } = await import("./x402-XLFD43RF.js");
6945
+ const { verifyX402Payment } = await import("./x402-MFBRORHG.js");
6692
6946
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
6693
6947
  body.content.x402_payment,
6694
6948
  opts.x402Config,
@@ -7974,7 +8228,7 @@ function mapLedgerCommitError(err, x402Requirements, paymentCommitted = false) {
7974
8228
  return void 0;
7975
8229
  }
7976
8230
  function creditLedgerErrorResult(err, x402Requirements, funding, paymentCommitted = false) {
7977
- const code = err.code === "insufficient_credit" || err.code === "credit_expired" || err.code === "credit_unbacked" || err.code === "credit_not_found" || err.code === "tempo_channel_closing" || err.code === "settlement_pending" || err.code === "draw_conflict" ? err.code : "payment_invalid";
8231
+ const code = err.code === "insufficient_credit" || err.code === "credit_expired" || err.code === "credit_unbacked" || err.code === "credit_closed" || err.code === "credit_not_found" || err.code === "tempo_channel_closing" || err.code === "settlement_pending" || err.code === "draw_conflict" ? err.code : "payment_invalid";
7978
8232
  if (code === "payment_invalid") logCreditRejected(err.code, void 0);
7979
8233
  const extra = {};
7980
8234
  if (err.details.availableMicro !== void 0) {
@@ -8296,6 +8550,8 @@ function authErrorBody(err, opts = {}) {
8296
8550
  const subReason = err.sub_reason === "schema_invalid" ? "signature_invalid" : err.sub_reason;
8297
8551
  const display = (() => {
8298
8552
  switch (subReason) {
8553
+ case "signature_required":
8554
+ return "This capability requires a signed request";
8299
8555
  case "signature_invalid":
8300
8556
  return "Request signature didn't match the supplied pubkey";
8301
8557
  case "timestamp_drift":
@@ -8310,7 +8566,7 @@ function authErrorBody(err, opts = {}) {
8310
8566
  error: "auth_error",
8311
8567
  sub_reason: subReason,
8312
8568
  display,
8313
- 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."
8569
+ hint: subReason === "signature_required" ? "Run `npx @dvmkit/dvm-cli init`, then use `dvm request` to sign and send the request, or implement secp256k1+BIP-340 Schnorr signing with the domain-bound envelope described at https://dvmkit.com/docs/protocol." : "Regenerate the request with the current timestamp + a fresh nonce; sign with secp256k1+BIP-340 Schnorr over the canonical JSON form of the input."
8314
8570
  };
8315
8571
  }
8316
8572
  function infoSchemaUrl(requestUrl, publicOrigin) {
@@ -8482,6 +8738,401 @@ async function drawFundingReceiptFor(ledger, issuer, funding, currency) {
8482
8738
  return isDrawFundingReceipt(saved) ? saved : void 0;
8483
8739
  }
8484
8740
 
8741
+ // src/sdk/server/stripe-drain-leg.ts
8742
+ import { createHash as createHash7 } from "crypto";
8743
+
8744
+ // src/sdk/server/stripe-refund-api.ts
8745
+ var StripeApiError = class extends Error {
8746
+ status;
8747
+ type;
8748
+ code;
8749
+ constructor(status, codes) {
8750
+ super(status === 0 ? "Stripe could not be reached" : `Stripe answered HTTP ${status}`);
8751
+ this.name = "StripeApiError";
8752
+ this.status = status;
8753
+ if (codes.type) this.type = codes.type;
8754
+ if (codes.code) this.code = codes.code;
8755
+ }
8756
+ /** Stripe looked at the request and refused it; repeating it unchanged will not help. */
8757
+ get rejected() {
8758
+ return this.status >= 400 && this.status < 500 && this.status !== 409 && this.status !== 429;
8759
+ }
8760
+ };
8761
+ function createStripeRefundApi(opts) {
8762
+ const apiBase = opts.apiBase ?? "https://api.stripe.com";
8763
+ const fetchImpl = opts.fetch ?? fetch;
8764
+ const auth = `Basic ${Buffer.from(`${opts.secretKey}:`).toString("base64")}`;
8765
+ const call = async (method, path, args = {}) => {
8766
+ const url = `${apiBase}${path}${args.query ? `?${args.query.toString()}` : ""}`;
8767
+ let response;
8768
+ try {
8769
+ response = await fetchImpl(url, {
8770
+ method,
8771
+ headers: {
8772
+ Authorization: auth,
8773
+ ...args.form && { "Content-Type": "application/x-www-form-urlencoded" },
8774
+ ...args.idempotencyKey && { "Idempotency-Key": args.idempotencyKey }
8775
+ },
8776
+ ...args.form && { body: args.form }
8777
+ });
8778
+ } catch {
8779
+ throw new StripeApiError(0, {});
8780
+ }
8781
+ const body = await response.json().catch(() => ({}));
8782
+ if (!response.ok) {
8783
+ const error = body.error ?? {};
8784
+ throw new StripeApiError(response.status, {
8785
+ type: enumField(error.type),
8786
+ code: enumField(error.code)
8787
+ });
8788
+ }
8789
+ return body;
8790
+ };
8791
+ return {
8792
+ async retrieveCharge(paymentIntentId) {
8793
+ const query = new URLSearchParams([["expand[]", "latest_charge.balance_transaction"]]);
8794
+ const pi = await call("GET", `/v1/payment_intents/${encodeURIComponent(paymentIntentId)}`, {
8795
+ query
8796
+ });
8797
+ const charge = isRecord2(pi.latest_charge) ? pi.latest_charge : void 0;
8798
+ const txn = charge && isRecord2(charge.balance_transaction) ? charge.balance_transaction : void 0;
8799
+ return {
8800
+ paymentIntentId,
8801
+ status: stringOr(pi.status, "unknown"),
8802
+ amount: integerOr(pi.amount, 0),
8803
+ currency: stringOr(pi.currency, ""),
8804
+ amountRefunded: integerOr(charge?.amount_refunded, 0),
8805
+ disputed: charge?.disputed === true,
8806
+ fee: txn && Number.isSafeInteger(txn.fee) && Number.isSafeInteger(txn.amount) && txn.amount > 0 ? { fee: txn.fee, settledAmount: txn.amount } : null
8807
+ };
8808
+ },
8809
+ async listRefunds(paymentIntentId) {
8810
+ const query = new URLSearchParams({ payment_intent: paymentIntentId, limit: "100" });
8811
+ const page = await call("GET", "/v1/refunds", { query });
8812
+ return arrayOf(page.data).map(refundFrom);
8813
+ },
8814
+ async createRefund(args) {
8815
+ const form = new URLSearchParams({
8816
+ payment_intent: args.paymentIntentId,
8817
+ amount: String(args.amount)
8818
+ });
8819
+ for (const [key, value] of Object.entries(args.metadata)) form.set(`metadata[${key}]`, value);
8820
+ return refundFrom(
8821
+ await call("POST", "/v1/refunds", { form, idempotencyKey: args.idempotencyKey })
8822
+ );
8823
+ },
8824
+ async retrieveRefund(refundId) {
8825
+ return refundFrom(await call("GET", `/v1/refunds/${encodeURIComponent(refundId)}`));
8826
+ },
8827
+ async listDisputes(args) {
8828
+ const query = new URLSearchParams({ "created[gte]": String(args.createdGte), limit: "100" });
8829
+ if (args.startingAfter) query.set("starting_after", args.startingAfter);
8830
+ const page = await call("GET", "/v1/disputes", { query });
8831
+ return {
8832
+ disputes: arrayOf(page.data).map((d) => ({
8833
+ id: stringOr(d.id, ""),
8834
+ paymentIntentId: typeof d.payment_intent === "string" ? d.payment_intent : null,
8835
+ status: stringOr(d.status, "unknown"),
8836
+ created: integerOr(d.created, 0)
8837
+ })),
8838
+ hasMore: page.has_more === true
8839
+ };
8840
+ },
8841
+ async listDisputesForPaymentIntent(paymentIntentId) {
8842
+ const query = new URLSearchParams({ payment_intent: paymentIntentId, limit: "100" });
8843
+ const page = await call("GET", "/v1/disputes", { query });
8844
+ return {
8845
+ disputes: arrayOf(page.data).map((d) => ({
8846
+ id: stringOr(d.id, ""),
8847
+ paymentIntentId: typeof d.payment_intent === "string" ? d.payment_intent : null,
8848
+ status: stringOr(d.status, "unknown"),
8849
+ created: integerOr(d.created, 0)
8850
+ })),
8851
+ hasMore: page.has_more === true
8852
+ };
8853
+ },
8854
+ async searchPaymentIntentsByMetadata(key, value) {
8855
+ if (!/^[A-Za-z0-9_-]+$/.test(key) || !/^[A-Za-z0-9_-]+$/.test(value)) return [];
8856
+ const query = new URLSearchParams({ query: `metadata['${key}']:'${value}'`, limit: "20" });
8857
+ const page = await call("GET", "/v1/payment_intents/search", { query });
8858
+ return arrayOf(page.data).map((pi) => ({
8859
+ id: stringOr(pi.id, ""),
8860
+ status: stringOr(pi.status, "unknown"),
8861
+ amount: integerOr(pi.amount, 0),
8862
+ currency: stringOr(pi.currency, ""),
8863
+ created: integerOr(pi.created, 0)
8864
+ }));
8865
+ }
8866
+ };
8867
+ }
8868
+ function refundFrom(body) {
8869
+ const metadata = {};
8870
+ if (isRecord2(body.metadata)) {
8871
+ for (const [key, value] of Object.entries(body.metadata)) {
8872
+ if (typeof value === "string") metadata[key] = value;
8873
+ }
8874
+ }
8875
+ return {
8876
+ id: stringOr(body.id, ""),
8877
+ status: stringOr(body.status, "unknown"),
8878
+ amount: integerOr(body.amount, 0),
8879
+ paymentIntentId: typeof body.payment_intent === "string" ? body.payment_intent : null,
8880
+ metadata
8881
+ };
8882
+ }
8883
+ function enumField(value) {
8884
+ return typeof value === "string" && /^[a-z0-9_]{1,64}$/.test(value) ? value : void 0;
8885
+ }
8886
+ function isRecord2(value) {
8887
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8888
+ }
8889
+ function arrayOf(value) {
8890
+ return Array.isArray(value) ? value.filter(isRecord2) : [];
8891
+ }
8892
+ function stringOr(value, fallback) {
8893
+ return typeof value === "string" ? value : fallback;
8894
+ }
8895
+ function integerOr(value, fallback) {
8896
+ return Number.isSafeInteger(value) ? value : fallback;
8897
+ }
8898
+
8899
+ // src/sdk/server/stripe-drain-leg.ts
8900
+ var MICRO_PER_CENT = 1e4;
8901
+ async function runStripeDrain(deps, drain) {
8902
+ if (drain.method !== "stripe" || drain.status !== "pending") {
8903
+ return { drain, completed: false };
8904
+ }
8905
+ const save = deps.ledger.saveCardRefund?.bind(deps.ledger);
8906
+ if (!save) throw new Error("this credit ledger cannot record card refunds");
8907
+ let plan = drain.cardRefund ?? null;
8908
+ if (!plan) {
8909
+ try {
8910
+ const fresh = await planCardRefund(deps, drain);
8911
+ plan = await save({
8912
+ creditId: drain.creditId,
8913
+ drainId: drain.drainId,
8914
+ plan: fresh,
8915
+ onlyIfUnset: true
8916
+ });
8917
+ } catch (err) {
8918
+ logPending(drain, { stage: "plan", error_code: failureCode(err) });
8919
+ return { drain, completed: false };
8920
+ }
8921
+ if (!plan) return { drain: await reread(deps, drain), completed: false };
8922
+ }
8923
+ plan = structuredClone(plan);
8924
+ for (const entry of plan.entries) {
8925
+ if (entry.amountCents <= 0 || entry.status === "succeeded") continue;
8926
+ const status = await advanceRefund(deps.stripe, drain, entry);
8927
+ if (status !== "succeeded") {
8928
+ logPending(drain, {
8929
+ stage: "refund",
8930
+ payment_intent: entry.paymentIntentId,
8931
+ refund_status: entry.status,
8932
+ error_code: entry.lastError ?? void 0
8933
+ });
8934
+ }
8935
+ }
8936
+ const stored = await save({ creditId: drain.creditId, drainId: drain.drainId, plan });
8937
+ if (stored) plan = stored;
8938
+ if (!refundsFinished(plan)) {
8939
+ return { drain: { ...await reread(deps, drain), cardRefund: plan }, completed: false };
8940
+ }
8941
+ const result = await commitTerminalDrain(
8942
+ deps.drainReport,
8943
+ (tx) => deps.ledger.markDrainSent({
8944
+ creditId: drain.creditId,
8945
+ drainId: drain.drainId,
8946
+ sentRef: cardRefundSentRef(plan),
8947
+ tx
8948
+ })
8949
+ );
8950
+ let record = result.drain;
8951
+ if (!result.replayed && deps.receiptIssuer) {
8952
+ const receipt = deps.receiptIssuer.issueDrain({
8953
+ creditId: record.creditId,
8954
+ drainId: record.drainId,
8955
+ event: "sent",
8956
+ method: record.method,
8957
+ amountMicro: record.amountMicro,
8958
+ balanceAfterMicro: record.balanceAfterMicro,
8959
+ ledgerSeq: record.ledgerSeq,
8960
+ callerPubkey: record.callerPubkey
8961
+ });
8962
+ await deps.ledger.appendDrainReceipt({
8963
+ creditId: record.creditId,
8964
+ drainId: record.drainId,
8965
+ receipt
8966
+ });
8967
+ record = { ...record, receipts: [...record.receipts, receipt] };
8968
+ }
8969
+ return { drain: record, completed: !result.replayed };
8970
+ }
8971
+ function proportionalFeeShare(grossCents, fee) {
8972
+ const share = Math.floor(
8973
+ (2 * grossCents * fee.fee + fee.settledAmount) / (2 * fee.settledAmount)
8974
+ );
8975
+ return Math.min(grossCents, Math.max(0, share));
8976
+ }
8977
+ function cardRefundSummary(plan) {
8978
+ return {
8979
+ grossCents: plan.grossCents,
8980
+ refundedCents: sum(plan.entries.map((entry) => entry.amountCents)),
8981
+ feeShareCents: sum(plan.entries.map((entry) => entry.feeShareCents)),
8982
+ subCentMicro: plan.subCentMicro,
8983
+ refundIds: plan.entries.flatMap((entry) => entry.refundId ? [entry.refundId] : [])
8984
+ };
8985
+ }
8986
+ function usdFromCents(cents) {
8987
+ return `$${(cents / 100).toFixed(2)}`;
8988
+ }
8989
+ async function planCardRefund(deps, drain) {
8990
+ const lots = (await deps.ledger.listFundingLots(drain.creditId)).filter(
8991
+ (lot) => isCardFundedRail(lot.rail) && lot.fundingRef?.startsWith("pi_")
8992
+ );
8993
+ const grossCents = Math.floor(drain.amountMicro / MICRO_PER_CENT);
8994
+ const entries = [];
8995
+ let left = grossCents;
8996
+ for (const lot of lots) {
8997
+ if (left === 0) break;
8998
+ const charge = await deps.stripe.retrieveCharge(lot.fundingRef);
8999
+ assertRefundable(charge);
9000
+ const gross = Math.min(left, charge.amount - charge.amountRefunded);
9001
+ if (gross <= 0) continue;
9002
+ const feeShare = proportionalFeeShare(gross, charge.fee);
9003
+ entries.push({
9004
+ lotId: lot.lotId,
9005
+ paymentIntentId: charge.paymentIntentId,
9006
+ grossCents: gross,
9007
+ feeShareCents: feeShare,
9008
+ amountCents: gross - feeShare,
9009
+ attempt: 1,
9010
+ refundId: null,
9011
+ status: "planned",
9012
+ lastError: null
9013
+ });
9014
+ left -= gross;
9015
+ }
9016
+ if (left > 0) throw new CardRefundPlanError("charges_exhausted");
9017
+ return { grossCents, subCentMicro: drain.amountMicro - grossCents * MICRO_PER_CENT, entries };
9018
+ }
9019
+ var CardRefundPlanError = class extends Error {
9020
+ constructor(code) {
9021
+ super(`card refund cannot be planned: ${code}`);
9022
+ this.code = code;
9023
+ this.name = "CardRefundPlanError";
9024
+ }
9025
+ code;
9026
+ };
9027
+ function assertRefundable(charge) {
9028
+ if (charge.status !== "succeeded" || charge.currency !== "usd") {
9029
+ throw new CardRefundPlanError("charge_not_refundable");
9030
+ }
9031
+ if (!charge.fee) throw new CardRefundPlanError("fee_not_settled");
9032
+ }
9033
+ async function advanceRefund(stripe, drain, entry) {
9034
+ await advanceRefundOnce(stripe, drain, entry);
9035
+ return entry.status;
9036
+ }
9037
+ async function advanceRefundOnce(stripe, drain, entry) {
9038
+ const tag = refundDrainTag(drain);
9039
+ try {
9040
+ if (entry.refundId && isOpenStatus(entry.status)) {
9041
+ const refund2 = await stripe.retrieveRefund(entry.refundId);
9042
+ setStatus(entry, refund2.status);
9043
+ return;
9044
+ }
9045
+ const existing = (await stripe.listRefunds(entry.paymentIntentId)).find(
9046
+ (refund2) => refund2.metadata.dvmkit_drain === tag && refund2.metadata.dvmkit_lot === entry.lotId && refund2.status !== "failed" && refund2.status !== "canceled"
9047
+ );
9048
+ if (existing) {
9049
+ entry.refundId = existing.id;
9050
+ setStatus(entry, existing.status);
9051
+ return;
9052
+ }
9053
+ if (entry.status === "rejected" || entry.status === "failed" || entry.status === "canceled") {
9054
+ entry.attempt += 1;
9055
+ }
9056
+ const refund = await stripe.createRefund({
9057
+ paymentIntentId: entry.paymentIntentId,
9058
+ amount: entry.amountCents,
9059
+ idempotencyKey: refundIdempotencyKey(drain, entry),
9060
+ metadata: { dvmkit_drain: tag, dvmkit_lot: entry.lotId, dvmkit_attempt: String(entry.attempt) }
9061
+ });
9062
+ entry.refundId = refund.id;
9063
+ setStatus(entry, refund.status);
9064
+ } catch (err) {
9065
+ if (err instanceof StripeApiError && err.rejected) entry.status = "rejected";
9066
+ entry.lastError = failureCode(err);
9067
+ }
9068
+ }
9069
+ function setStatus(entry, status) {
9070
+ switch (status) {
9071
+ case "succeeded":
9072
+ case "pending":
9073
+ case "requires_action":
9074
+ case "failed":
9075
+ case "canceled":
9076
+ entry.status = status;
9077
+ entry.lastError = status === "failed" || status === "canceled" ? status : null;
9078
+ return;
9079
+ default:
9080
+ entry.status = "pending";
9081
+ }
9082
+ }
9083
+ function isOpenStatus(status) {
9084
+ return status === "pending" || status === "requires_action" || status === "planned";
9085
+ }
9086
+ function refundsFinished(plan) {
9087
+ return plan.entries.every((entry) => entry.amountCents <= 0 || entry.status === "succeeded");
9088
+ }
9089
+ function refundIdempotencyKey(drain, entry) {
9090
+ const digest = createHash7("sha256").update([drain.creditId, drain.drainId, entry.lotId, String(entry.attempt)].join("\0")).digest("hex");
9091
+ return `dvmkit-card-refund-${digest}`;
9092
+ }
9093
+ function cardRefundSentRef(plan) {
9094
+ const summary = cardRefundSummary(plan);
9095
+ return {
9096
+ refund_ids: summary.refundIds,
9097
+ refunded_cents: summary.refundedCents,
9098
+ fee_share_cents: summary.feeShareCents,
9099
+ sub_cent_micro: summary.subCentMicro,
9100
+ refunds: plan.entries.map((entry) => ({
9101
+ payment_intent: entry.paymentIntentId,
9102
+ refund_id: entry.refundId,
9103
+ gross_cents: entry.grossCents,
9104
+ fee_share_cents: entry.feeShareCents,
9105
+ amount_cents: entry.amountCents
9106
+ }))
9107
+ };
9108
+ }
9109
+ async function reread(deps, drain) {
9110
+ return await deps.ledger.getDrain({ creditId: drain.creditId, drainId: drain.drainId }) ?? drain;
9111
+ }
9112
+ function failureCode(err) {
9113
+ if (err instanceof CardRefundPlanError) return err.code;
9114
+ if (err instanceof StripeApiError) {
9115
+ return err.code ?? err.type ?? (err.status === 0 ? "stripe_unreachable" : `http_${err.status}`);
9116
+ }
9117
+ return "internal_error";
9118
+ }
9119
+ function refundDrainTag(drain) {
9120
+ const tag = `${drain.creditId}:${drain.drainId}`;
9121
+ if (tag.length <= 200) return tag;
9122
+ return `sha256:${createHash7("sha256").update(tag).digest("hex")}`;
9123
+ }
9124
+ function logPending(drain, fields) {
9125
+ diagnosticConsole.warn({
9126
+ event: "card_refund_pending",
9127
+ credit_id: drain.creditId,
9128
+ drain_id: drain.drainId,
9129
+ ...fields
9130
+ });
9131
+ }
9132
+ function sum(values) {
9133
+ return values.reduce((total, value) => total + value, 0);
9134
+ }
9135
+
8485
9136
  // src/sdk/server/tempo-drain-leg.ts
8486
9137
  async function completeTempoChannelDrain(args) {
8487
9138
  if (!args.tx && args.refundedNative !== void 0 && /^\d{1,78}$/.test(String(args.refundedNative))) {
@@ -8627,9 +9278,10 @@ var CREDIT_REQUEST_SCHEMA = z.object({
8627
9278
  /**
8628
9279
  * Lightning names an invoice request; MPP names the charge/session
8629
9280
  * intent before a credential exists, so a bound credit can refuse the
8630
- * wrong instrument before issuing or broadcasting anything.
9281
+ * wrong instrument before issuing or broadcasting anything. Stripe names
9282
+ * a card top-up: its MPP `method="stripe"` challenge is issued only here.
8631
9283
  */
8632
- method: z.enum(["lightning", "tempo"]).optional(),
9284
+ method: z.enum(["lightning", "tempo", "stripe"]).optional(),
8633
9285
  /** Required with `method: "tempo"`; invalid on every other funding rail. */
8634
9286
  intent: z.enum(["charge", "session"]).optional()
8635
9287
  }).optional(),
@@ -8858,6 +9510,9 @@ async function handleDrain(c, deps, args) {
8858
9510
  if (isHttpResponse(completed)) return completed;
8859
9511
  existing = completed;
8860
9512
  }
9513
+ if (existing.method === "stripe" && existing.status === "pending" && deps.stripeCredit) {
9514
+ existing = (await runStripeDrain(stripeLegDeps(deps, deps.stripeCredit), existing)).drain;
9515
+ }
8861
9516
  return respondDrain(c, deps, { record: await deliverIfParked(deps, existing), credit });
8862
9517
  }
8863
9518
  if (credit.tempoChannelId && drain.method !== "tempo") {
@@ -8883,8 +9538,17 @@ async function handleDrain(c, deps, args) {
8883
9538
  }
8884
9539
  const rejected = await burnNonce(c, deps, args.envelope);
8885
9540
  if (rejected) return rejected;
9541
+ if (isCardFundedRail(credit.rail) && drain.method !== "stripe") {
9542
+ return c.json(cardCreditNotDrainable(credit), 409);
9543
+ }
9544
+ if (drain.method === "stripe" && !isCardFundedRail(credit.rail)) {
9545
+ return c.json(cardRefundOfNonCardCredit(credit), 409);
9546
+ }
8886
9547
  const target = validateDrainTarget(drain);
8887
9548
  if ("error" in target) return c.json(target.error, 400);
9549
+ if (target.method === "stripe") {
9550
+ return handleCardRefundDrain(c, deps, { creditId, drainId: drain.drain_id, callerPubkey: args.callerPubkey });
9551
+ }
8888
9552
  const railMismatch = railMismatchRefusal(credit, target.method);
8889
9553
  if (railMismatch) return c.json(railMismatch, 400);
8890
9554
  const tempoChargeDrain = target.method === "tempo" && !credit.tempoChannelId;
@@ -9641,13 +10305,14 @@ async function respondDrain(c, deps, args) {
9641
10305
  ...drainCopy(args.record)
9642
10306
  });
9643
10307
  }
10308
+ var MAX_CARD_REFUND_DRAIN_ID = 128;
9644
10309
  function validateDrainTarget(drain) {
9645
10310
  const refuse = (message, hint) => ({
9646
10311
  error: { error: "invalid_request", message, hint }
9647
10312
  });
9648
10313
  const missingFields = () => refuse(
9649
10314
  "A new drain requires drain.method and drain.payout.",
9650
- 'Send drain: { drain_id, method: "cashu" | "x402" | "tempo", payout: { \u2026 } }. Only a poll of an existing drain_id may omit them.'
10315
+ 'Send drain: { drain_id, method: "cashu" | "x402" | "tempo", payout: { \u2026 } }, or { drain_id, method: "stripe" } for card-funded credit. Only a poll of an existing drain_id may omit them.'
9651
10316
  );
9652
10317
  const method = drain.method;
9653
10318
  if (!method) return missingFields();
@@ -9655,10 +10320,33 @@ function validateDrainTarget(drain) {
9655
10320
  return {
9656
10321
  error: paymentErrorBody("drain_method_unsupported", {
9657
10322
  message: `"${method}" is not a payout rail this service reclaims over.`,
9658
- extra: { supported_methods: [...DRAIN_METHODS], drain_registered: false }
10323
+ // Payout rails only: a card refund is not one, and card credit is pointed at it by its own refusal.
10324
+ extra: { supported_methods: DRAIN_METHODS.filter((m) => m !== "stripe"), drain_registered: false }
9659
10325
  })
9660
10326
  };
9661
10327
  }
10328
+ if (method === "stripe") {
10329
+ const named = Object.values(drain.payout ?? {}).some((value) => value !== void 0 && value !== "");
10330
+ if (named) {
10331
+ return refuse(
10332
+ "stripe drains carry no payout destination: the refund goes back to the card that paid for the credit.",
10333
+ 'Send drain: { drain_id, method: "stripe" } with no payout.'
10334
+ );
10335
+ }
10336
+ if (drain.drain_id.length > MAX_CARD_REFUND_DRAIN_ID) {
10337
+ return refuse(
10338
+ `stripe drain ids are at most ${MAX_CARD_REFUND_DRAIN_ID} characters.`,
10339
+ "Choose a shorter drain_id, such as a UUID."
10340
+ );
10341
+ }
10342
+ if (drain.drain_id.startsWith(CARD_EXPIRY_DRAIN_PREFIX)) {
10343
+ return refuse(
10344
+ `drain ids starting "${CARD_EXPIRY_DRAIN_PREFIX}" are reserved for the refund this service makes when a card balance expires.`,
10345
+ "Choose a drain_id that does not start with that prefix, such as a UUID."
10346
+ );
10347
+ }
10348
+ return { method: "stripe", payout: {} };
10349
+ }
9662
10350
  if (!drain.payout && method !== "tempo") return missingFields();
9663
10351
  switch (method) {
9664
10352
  case "cashu": {
@@ -9687,9 +10375,70 @@ function validateDrainTarget(drain) {
9687
10375
  }
9688
10376
  }
9689
10377
  }
10378
+ function cardCreditNotDrainable(credit) {
10379
+ return {
10380
+ error: "card_credit_not_drainable",
10381
+ message: "This credit was funded by card. Card credit is refunded to the card that paid for it, never paid out on another rail.",
10382
+ display: credit.expired ? "This balance was paid by card and has expired, so it can only go back to that card. It will be refunded there automatically, or you can ask for the refund now." : "This balance was paid by card, so it can only go back to that card.",
10383
+ hint: `Drain it with method "stripe" and no payout ('dvm credit drain <dvm> --method stripe'); no other method is accepted for card-funded credit. The refund goes to the card, less the refund's share of Stripe's card processing fee.`,
10384
+ retryable: false
10385
+ };
10386
+ }
10387
+ function cardRefundOfNonCardCredit(credit) {
10388
+ return {
10389
+ error: "invalid_request",
10390
+ message: `This credit was funded over ${credit.rail ?? "another rail"}, not by card, so it cannot be refunded to a card.`,
10391
+ hint: 'Reclaim it on the rail that funded it; method "stripe" only refunds credit that a card paid for.',
10392
+ retryable: false,
10393
+ ...credit.rail ? { credit_rail: credit.rail } : {}
10394
+ };
10395
+ }
10396
+ async function handleCardRefundDrain(c, deps, args) {
10397
+ const stripeCredit = deps.stripeCredit;
10398
+ if (!stripeCredit) {
10399
+ return c.json(
10400
+ {
10401
+ error: "card_refund_unavailable",
10402
+ message: "This service is not configured for card refunds right now, so nothing was drained.",
10403
+ hint: "Retry later with the same drain_id. Your balance is unchanged and stays yours.",
10404
+ retryable: true
10405
+ },
10406
+ 503
10407
+ );
10408
+ }
10409
+ try {
10410
+ const result = await deps.ledger.requestDrain({
10411
+ creditId: args.creditId,
10412
+ drainId: args.drainId,
10413
+ callerPubkey: args.callerPubkey,
10414
+ method: "stripe",
10415
+ payout: {}
10416
+ });
10417
+ let record = result.drain;
10418
+ if (!result.replayed && deps.receiptIssuer) {
10419
+ record = await countersignDrain(deps, record, "requested");
10420
+ }
10421
+ record = (await runStripeDrain(stripeLegDeps(deps, stripeCredit), record)).drain;
10422
+ return await respondDrain(c, deps, { record, credit: result.credit });
10423
+ } catch (err) {
10424
+ if (err instanceof CreditLedgerError) {
10425
+ return drainLedgerErrorResponse(err, args.creditId, args.drainId);
10426
+ }
10427
+ throw err;
10428
+ }
10429
+ }
10430
+ function stripeLegDeps(deps, stripeCredit) {
10431
+ return {
10432
+ ledger: deps.ledger,
10433
+ drainReport: deps.drainReport,
10434
+ stripe: stripeCredit.refunds,
10435
+ ...deps.receiptIssuer && { receiptIssuer: deps.receiptIssuer }
10436
+ };
10437
+ }
9690
10438
  function railMismatchRefusal(credit, method) {
9691
10439
  if (method !== "x402" && method !== "tempo") return void 0;
9692
10440
  if (!isFundingRail(credit.rail) || credit.rail === method) return void 0;
10441
+ if (isCardFundedRail(credit.rail)) return cardCreditNotDrainable(credit);
9693
10442
  const target = credit.rail === "cashu" || credit.rail === "lightning" ? "--method cashu" : `--method ${credit.rail} --address 0x\u2026`;
9694
10443
  return {
9695
10444
  error: "invalid_request",
@@ -9727,10 +10476,57 @@ function drainBody(record) {
9727
10476
  created_at: record.createdAt,
9728
10477
  ...record.token !== null ? { token: record.token } : {},
9729
10478
  ...record.sentRef !== null ? { sent: record.sentRef } : {},
10479
+ ...record.method === "stripe" ? cardRefundBody(record) : {},
9730
10480
  receipts: record.receipts
9731
10481
  };
9732
10482
  }
10483
+ function cardRefundBody(record) {
10484
+ if (!record.cardRefund) return { refund_ids: [] };
10485
+ const summary = cardRefundSummary(record.cardRefund);
10486
+ return {
10487
+ refund_ids: summary.refundIds,
10488
+ card_refund: {
10489
+ refunded_usd: usdFromCents(summary.refundedCents),
10490
+ stripe_fee_share_usd: usdFromCents(summary.feeShareCents),
10491
+ refunded_cents: summary.refundedCents,
10492
+ stripe_fee_share_cents: summary.feeShareCents,
10493
+ sub_cent_micro: summary.subCentMicro,
10494
+ refunds: record.cardRefund.entries.map((entry) => ({
10495
+ refund_id: entry.refundId,
10496
+ amount_cents: entry.amountCents,
10497
+ fee_share_cents: entry.feeShareCents,
10498
+ status: entry.status
10499
+ }))
10500
+ }
10501
+ };
10502
+ }
10503
+ function cardRefundCopy(record) {
10504
+ const sent = record.status === "sent";
10505
+ if (!record.cardRefund) {
10506
+ return {
10507
+ display: "The refund to your card is approved and the balance is set aside for it, but the card processor hasn't been reached yet.",
10508
+ hint: "Poll this op with the same drain_id; each poll retries the refund. The balance stays owed to the card until it goes through."
10509
+ };
10510
+ }
10511
+ const summary = cardRefundSummary(record.cardRefund);
10512
+ const gross = usdFromCents(summary.grossCents);
10513
+ const fee = summary.feeShareCents > 0 ? ` ${usdFromCents(summary.feeShareCents)} of the ${gross} balance was kept to cover this refund's share of Stripe's card processing fee: Stripe charged that fee when the card paid and does not return it on a refund.` : "";
10514
+ const dust = summary.subCentMicro > 0 ? " Less than one cent of the balance could not be refunded, because cards are refunded in whole cents." : "";
10515
+ if (sent) {
10516
+ return {
10517
+ display: `${usdFromCents(summary.refundedCents)} went back to the card that paid for this credit.${fee}${dust} Card refunds usually show on the statement within 5 to 10 business days.`,
10518
+ hint: "See refund_ids for the Stripe refund references. Nothing else to do."
10519
+ };
10520
+ }
10521
+ return {
10522
+ display: `A refund of ${usdFromCents(summary.refundedCents)} to the card that paid for this credit is under way and the balance is set aside for it.${fee}${dust}`,
10523
+ hint: "Poll this op with the same drain_id; each poll retries any refund the card processor hasn't finished, and it flips to sent once every refund has gone through. No refund is made twice."
10524
+ };
10525
+ }
9733
10526
  function drainCopy(record) {
10527
+ if (record.method === "stripe" && (record.status === "pending" || record.status === "sent")) {
10528
+ return cardRefundCopy(record);
10529
+ }
9734
10530
  switch (record.status) {
9735
10531
  case "pending":
9736
10532
  if (record.method === "cashu") {
@@ -9870,26 +10666,36 @@ async function handleFund(c, deps, args) {
9870
10666
  tempoIntent: fund.method === "tempo" ? fund.intent : void 0
9871
10667
  });
9872
10668
  if (sizing) return sizing;
10669
+ if (fund.method === "stripe" && (!menu.stripe || !deps.stripeCredit)) {
10670
+ return jsonPaymentError("payment_invalid", 409, {
10671
+ message: "This DVM does not offer card funding for prepaid credit.",
10672
+ display: "This service doesn't take card payments for prepaid credit. Nothing was charged.",
10673
+ hint: "Fund the credit over one of the rails listed in credit.funding on /v1/info.",
10674
+ extra: { alternatives: menu.funding.filter((rail) => rail !== "stripe") }
10675
+ });
10676
+ }
9873
10677
  if (fund.method === "tempo" && fund.intent === "charge" && !stablecoinCreditInstrumentAllowed(deps.creditConfig?.allowOneShotStablecoin, "tempo_charge")) {
9874
10678
  return jsonPaymentError("one_shot_credit_not_supported", 409, {
9875
10679
  message: "Tempo charge remains available for per-call payment, but this DVM accepts only reusable Tempo sessions for prepaid credit."
9876
10680
  });
9877
10681
  }
9878
- let requiredMsats;
9879
- try {
9880
- const fx = await deps.fxFetcher.fetch();
9881
- requiredMsats = fiatToSatsCeil(fund.amount_micro / 1e6, fxRateFor(fx, menu.currency)) * 1e3;
9882
- } catch {
9883
- return c.json(
9884
- {
9885
- error: "fx_rate_unavailable",
9886
- message: `Could not price a ${menu.currency} top-up in sats right now.`,
9887
- display: "The service can't work out the exchange rate right now. Nothing was charged.",
9888
- hint: "Retry in a few moments.",
9889
- retryable: true
9890
- },
9891
- 503
9892
- );
10682
+ let requiredMsats = 0;
10683
+ if (fund.method !== "stripe") {
10684
+ try {
10685
+ const fx = await deps.fxFetcher.fetch();
10686
+ requiredMsats = fiatToSatsCeil(fund.amount_micro / 1e6, fxRateFor(fx, menu.currency)) * 1e3;
10687
+ } catch {
10688
+ return c.json(
10689
+ {
10690
+ error: "fx_rate_unavailable",
10691
+ message: `Could not price a ${menu.currency} top-up in sats right now.`,
10692
+ display: "The service can't work out the exchange rate right now. Nothing was charged.",
10693
+ hint: "Retry in a few moments.",
10694
+ retryable: true
10695
+ },
10696
+ 503
10697
+ );
10698
+ }
9893
10699
  }
9894
10700
  if (fund.method === "lightning") {
9895
10701
  return lightningIssue(c, deps, {
@@ -9910,7 +10716,13 @@ async function handleFund(c, deps, args) {
9910
10716
  const result = await loggers.isolate.span(
9911
10717
  "dvm.credit.fund",
9912
10718
  { required_msats: requiredMsats },
9913
- () => verifyUpfrontPayment({
10719
+ () => fund.method === "stripe" ? verifyCardFunding({
10720
+ ...railOpts,
10721
+ stripeCredit: deps.stripeCredit,
10722
+ amountMicro: fund.amount_micro,
10723
+ callerPubkey: args.callerPubkey,
10724
+ fundOnly: { creditId, fundId: fund.fund_id, commitment }
10725
+ }) : verifyUpfrontPayment({
9914
10726
  ...railOpts,
9915
10727
  requiredMsats,
9916
10728
  priceFiat: { currency: menu.currency, amountMicro: fund.amount_micro },
@@ -10194,6 +11006,19 @@ async function checkSizing(deps, args) {
10194
11006
  extra: { min_micro: menu.min_micro, max_micro: menu.max_micro, currency: menu.currency }
10195
11007
  });
10196
11008
  }
11009
+ if (args.rail === "stripe" && menu.stripe && args.amountMicro < menu.stripe.min_micro) {
11010
+ return jsonPaymentError("below_rail_minimum", 400, {
11011
+ message: `Top-up of ${args.amountMicro} micro-${menu.currency.toUpperCase()} is below the ${menu.stripe.min_micro} micro-${menu.currency.toUpperCase()} card minimum.`,
11012
+ display: "Card top-ups start at $0.50. Nothing was charged.",
11013
+ hint: `Ask for a top-up of at least ${menu.stripe.min_micro} micro-USD ($0.50) and sign the fund request again with that amount.`,
11014
+ extra: {
11015
+ rail: "stripe",
11016
+ min_micro: menu.stripe.min_micro,
11017
+ alternatives: menu.funding.filter((rail) => rail !== "stripe"),
11018
+ currency: menu.currency
11019
+ }
11020
+ });
11021
+ }
10197
11022
  const floorSats = deps.lightning?.receive.fundingMinSats;
10198
11023
  if (args.rail === "lightning" && floorSats !== void 0) {
10199
11024
  let fx;
@@ -10221,7 +11046,7 @@ async function checkSizing(deps, args) {
10221
11046
  const held = await deps.ledger.getForCaller(args.callerPubkey);
10222
11047
  const target = await checkTargetCredit(deps, { ...args, held });
10223
11048
  if (target) return target;
10224
- const heldMicro = held.filter((credit) => spendable(credit, menu.currency)).reduce((sum, credit) => sum + credit.availableMicro, 0);
11049
+ const heldMicro = held.filter((credit) => spendable(credit, menu.currency)).reduce((sum2, credit) => sum2 + credit.availableMicro, 0);
10225
11050
  const fundableMicro = menu.max_micro - heldMicro;
10226
11051
  if (args.amountMicro > fundableMicro) {
10227
11052
  return jsonPaymentError("credit_over_max", 400, {
@@ -10244,6 +11069,17 @@ async function checkTargetCredit(deps, args) {
10244
11069
  message: `Credit ${args.creditId} is terminal because its Tempo backing finalized before the DVM was protected.`
10245
11070
  });
10246
11071
  }
11072
+ if (mine.status === "refunded" || mine.status === "disputed") {
11073
+ return jsonPaymentError("credit_closed", 409, {
11074
+ message: mine.status === "refunded" ? `Credit ${args.creditId} is closed: its expired card balance was refunded to the card.` : `Credit ${args.creditId} is frozen: a card payment that funded it is disputed.`
11075
+ });
11076
+ }
11077
+ if (isCardFundedRail(mine.rail) && mine.expiryMs <= Date.now()) {
11078
+ return jsonPaymentError("credit_closed", 409, {
11079
+ message: `Credit ${args.creditId} expired; its card balance is being refunded to the card.`,
11080
+ hint: "Omit credit_id (or use a new fund_id) to open a new prepaid balance. The expired balance comes back to the card on its own."
11081
+ });
11082
+ }
10247
11083
  if (mine.currency !== args.menu.currency) {
10248
11084
  return jsonPaymentError("payment_invalid", 400, {
10249
11085
  message: `Credit ${args.creditId} is denominated in ${mine.currency}, not ${args.menu.currency}.`
@@ -10388,6 +11224,12 @@ var DRAIN_CONFLICT_COPY = {
10388
11224
  retryable: true,
10389
11225
  drainRegistered: false
10390
11226
  },
11227
+ card_refund_pending: {
11228
+ display: "A refund of this prepaid balance to your card is already in progress, so a second one was not started. Nothing changed.",
11229
+ hint: ({ blockingDrainId }) => `Poll drain ${blockingDrainId ?? "the earlier card refund"} (the same drain request again) until it is sent; it returns the balance it covers. Start a new card refund only after that, if any balance is left.`,
11230
+ retryable: true,
11231
+ drainRegistered: false
11232
+ },
10391
11233
  refund_floor: {
10392
11234
  display: "The refund reached the chain, but it paid less than the balance this service had recorded, so nothing was booked here. Your on-chain refund arrived; the difference has to be reconciled by the service operator.",
10393
11235
  hint: ({ creditId, drainId }) => `Report drain ${drainId} on credit ${creditId} to the service operator \u2014 only they can reconcile the difference. Retrying this drain, or requesting a new drain_id, lands here again.`,
@@ -10439,9 +11281,12 @@ function drainLedgerErrorResponse(error, creditId, drainId) {
10439
11281
  return jsonPaymentError("drain_conflict", 409, {
10440
11282
  message: error.message,
10441
11283
  display: copy?.display,
10442
- hint: copy?.hint({ creditId, drainId }),
11284
+ hint: copy?.hint({ creditId, drainId, blockingDrainId: error.details.blockingDrainId }),
10443
11285
  retryable: copy?.retryable,
10444
- extra: { drain_registered: copy?.drainRegistered ?? false }
11286
+ extra: {
11287
+ drain_registered: copy?.drainRegistered ?? false,
11288
+ ...error.details.blockingDrainId && { blocking_drain_id: error.details.blockingDrainId }
11289
+ }
10445
11290
  });
10446
11291
  }
10447
11292
  if (error.code === "settlement_pending") {
@@ -10454,6 +11299,9 @@ function drainLedgerErrorResponse(error, creditId, drainId) {
10454
11299
  ...error.details.blockingDrainId ? { blockingDrainId: error.details.blockingDrainId } : {}
10455
11300
  });
10456
11301
  }
11302
+ if (error.code === "credit_closed") {
11303
+ return jsonPaymentError("credit_closed", 409, { message: error.message });
11304
+ }
10457
11305
  if (error.code === "caller_mismatch" || error.code === "credit_not_found") {
10458
11306
  return jsonPaymentError("credit_not_found", 404, {
10459
11307
  message: `No credit ${creditId} for this caller.`
@@ -10469,6 +11317,7 @@ function mapLedgerCode(err) {
10469
11317
  case "insufficient_credit":
10470
11318
  case "credit_expired":
10471
11319
  case "credit_unbacked":
11320
+ case "credit_closed":
10472
11321
  case "credit_not_found":
10473
11322
  case "tempo_channel_closing":
10474
11323
  case "settlement_pending":
@@ -10493,6 +11342,8 @@ function creditBody(credit, reconciliationPending = false) {
10493
11342
  // Nominal expiry does not itself reclaim ownership; a committed sweep can remove undrawn
10494
11343
  // value.
10495
11344
  drainable: credit.status === "active" && credit.availableMicro > 0,
11345
+ // The drain an expired card balance went back to the card under; poll it like any drain.
11346
+ ...credit.status === "refunded" && { refund_drain_id: cardExpiryDrainId(credit.expiryMs) },
10496
11347
  ...reconciliationPending && { reconciliation_pending: true },
10497
11348
  ledger_seq: credit.lastLedgerSeq
10498
11349
  };
@@ -10909,7 +11760,7 @@ async function handle(c, opts, action) {
10909
11760
  }
10910
11761
 
10911
11762
  // src/sdk/server/hosting-settlement-report-obligations.ts
10912
- import { createHash as createHash7, randomUUID as randomUUID4 } from "crypto";
11763
+ import { createHash as createHash8, randomUUID as randomUUID4 } from "crypto";
10913
11764
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
10914
11765
  import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS3 } from "@x402/evm";
10915
11766
  import { computeChannelId as computeChannelId2 } from "@x402/evm/batch-settlement/client";
@@ -10982,7 +11833,7 @@ var TEMPO_CLOSED_EVENT = TempoSession.Precompile.escrowAbi.find(
10982
11833
  (item) => item.type === "event" && item.name === "ChannelClosed"
10983
11834
  );
10984
11835
  function hash2(value) {
10985
- return createHash7("sha256").update(JSON.stringify(value)).digest("hex");
11836
+ return createHash8("sha256").update(JSON.stringify(value)).digest("hex");
10986
11837
  }
10987
11838
  function hex(value, bytes) {
10988
11839
  return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${bytes * 2}}$`).test(value);
@@ -11800,7 +12651,7 @@ var HostedSettlementReportObligations = class {
11800
12651
  [this.dvmId, this.scope, batch.batch_id]
11801
12652
  );
11802
12653
  if (!sources.rows.length || sources.rows.some((row) => !row.tx_hash || !row.block_hash || row.delta_native === null)) return;
11803
- const claimed = sources.rows.reduce((sum, row) => sum + BigInt(row.delta_native), 0n);
12654
+ const claimed = sources.rows.reduce((sum2, row) => sum2 + BigInt(row.delta_native), 0n);
11804
12655
  const hashes = new Set(sources.rows.map((row) => row.tx_hash));
11805
12656
  await tx.query(
11806
12657
  `UPDATE payout_x402_batches
@@ -11901,7 +12752,7 @@ var HostedSettlementReportObligations = class {
11901
12752
  const batchId = payload.payoutId.slice("x402:batch:".length);
11902
12753
  const settle = rows2.rows.filter((row) => row.operation === "settle" && row.rail === "x402");
11903
12754
  const claims = rows2.rows.filter((row) => row.operation === "claim" || row.operation === "refund");
11904
- if (settle.length !== 1 || !claims.length || claims.length + 1 !== rows2.rows.length || rows2.rows.some((row) => row.original.batchId !== batchId) || BigInt(settle[0].delta_native) !== BigInt(amount) || claims.reduce((sum, row) => sum + BigInt(row.delta_native), 0n) !== BigInt(amount)) {
12755
+ if (settle.length !== 1 || !claims.length || claims.length + 1 !== rows2.rows.length || rows2.rows.some((row) => row.original.batchId !== batchId) || BigInt(settle[0].delta_native) !== BigInt(amount) || claims.reduce((sum2, row) => sum2 + BigInt(row.delta_native), 0n) !== BigInt(amount)) {
11905
12756
  throw new Error("Hosted x402 payout differs from its original finalized batch");
11906
12757
  }
11907
12758
  } else {
@@ -12370,7 +13221,7 @@ var HostingCleanupFence = class {
12370
13221
  };
12371
13222
 
12372
13223
  // src/sdk/server/hosting-channel-evidence.ts
12373
- import { createHash as createHash8 } from "crypto";
13224
+ import { createHash as createHash9 } from "crypto";
12374
13225
  import { Session as TempoSession2 } from "mppx/tempo";
12375
13226
  import { createPublicClient as createPublicClient5, http as http5 } from "viem";
12376
13227
  import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS4 } from "@x402/evm";
@@ -12421,7 +13272,7 @@ async function readHostedChannelSnapshot(query) {
12421
13272
  };
12422
13273
  const x402HistoryComplete = await history(names[6]);
12423
13274
  const tempoHistoryComplete = await history(names[7]);
12424
- const digest = createHash8("sha256").update(JSON.stringify({
13275
+ const digest = createHash9("sha256").update(JSON.stringify({
12425
13276
  present: [...exists].sort(),
12426
13277
  x402,
12427
13278
  x402Tombstones,
@@ -13544,7 +14395,7 @@ function protocolEnvelopeIgnoreFields(schema) {
13544
14395
  }
13545
14396
  function stripProtocolEnvelope(schema, data) {
13546
14397
  const withoutCredit = stripCreditEnvelope(schema, data);
13547
- if (!isRecord2(withoutCredit)) return withoutCredit;
14398
+ if (!isRecord3(withoutCredit)) return withoutCredit;
13548
14399
  const {
13549
14400
  [REQUEST_ID_FIELD]: _requestId,
13550
14401
  [AUTH_STATEMENT_FIELD]: _authStatement,
@@ -13553,13 +14404,13 @@ function stripProtocolEnvelope(schema, data) {
13553
14404
  return builderInput;
13554
14405
  }
13555
14406
  function requestIdFromEnvelope(data) {
13556
- if (!isRecord2(data) || !(REQUEST_ID_FIELD in data)) return void 0;
14407
+ if (!isRecord3(data) || !(REQUEST_ID_FIELD in data)) return void 0;
13557
14408
  return isPublicRequestId(data[REQUEST_ID_FIELD]) ? data[REQUEST_ID_FIELD] : void 0;
13558
14409
  }
13559
14410
  function hasInvalidRequestId(data) {
13560
- return isRecord2(data) && REQUEST_ID_FIELD in data && !isPublicRequestId(data[REQUEST_ID_FIELD]);
14411
+ return isRecord3(data) && REQUEST_ID_FIELD in data && !isPublicRequestId(data[REQUEST_ID_FIELD]);
13561
14412
  }
13562
- function isRecord2(value) {
14413
+ function isRecord3(value) {
13563
14414
  return typeof value === "object" && value !== null && !Array.isArray(value);
13564
14415
  }
13565
14416
 
@@ -13571,9 +14422,9 @@ import { AsyncLocalStorage } from "async_hooks";
13571
14422
  import { randomBytes as randomBytes3 } from "crypto";
13572
14423
 
13573
14424
  // src/sdk/server/instrumented-kv.ts
13574
- import { createHash as createHash9 } from "crypto";
14425
+ import { createHash as createHash10 } from "crypto";
13575
14426
  function hashKey(key) {
13576
- return createHash9("sha256").update(key).digest("hex").slice(0, 16);
14427
+ return createHash10("sha256").update(key).digest("hex").slice(0, 16);
13577
14428
  }
13578
14429
  function instrumentKVStore(inner) {
13579
14430
  return {
@@ -14343,9 +15194,7 @@ function resultHashFor(record) {
14343
15194
  // src/sdk/server/unsigned-request.ts
14344
15195
  function takesUnsignedPath(auth, body) {
14345
15196
  if (!auth?.allowUnsignedPaid) return false;
14346
- const raw = signedRequestInput(body);
14347
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return true;
14348
- return !SIGNED_ENVELOPE_FIELDS.some((field) => field in raw);
15197
+ return !carriesSignedEnvelope(signedRequestInput(body));
14349
15198
  }
14350
15199
  function carriesCreditFields(raw) {
14351
15200
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return false;
@@ -17189,7 +18038,7 @@ var JobManager = class {
17189
18038
  const rail = draw?.railValue.rail;
17190
18039
  const currency = draw?.railValue.creditCurrency;
17191
18040
  if (draw?.status !== "settled" || draw.amountMicro <= 0 || !currency || !rail || !isFundingRail(rail)) return;
17192
- const testFunds = rail === "lightning" ? null : this.opts.paymentMode?.rails[rail].testFunds ?? null;
18041
+ const testFunds = rail === "lightning" || rail === "stripe" ? null : this.opts.paymentMode?.rails[rail].testFunds ?? null;
17193
18042
  try {
17194
18043
  await callback({ jobId: record.id, rail, amountMicro: draw.amountMicro, currency, testFunds });
17195
18044
  } catch (error) {
@@ -18529,7 +19378,7 @@ function isReadUnavailable(error) {
18529
19378
  }
18530
19379
 
18531
19380
  // src/sdk/server/balance-payment.ts
18532
- import { createHash as createHash10 } from "crypto";
19381
+ import { createHash as createHash11 } from "crypto";
18533
19382
  import { z as z2 } from "zod";
18534
19383
  var BALANCE_PAYMENT_SCHEMA = z2.object({
18535
19384
  credit_id: z2.string().min(1).max(256),
@@ -18559,7 +19408,7 @@ async function commitBalancePayment(args) {
18559
19408
  args.auth.recheckSignature(BALANCE_PAYMENT_SCHEMA, args.raw, args.domain);
18560
19409
  const auth = BALANCE_PAYMENT_SCHEMA.parse(args.raw);
18561
19410
  const { pubkey, credit_id, draw_id, payment_id, request_seq, amount_micro, currency } = auth;
18562
- const fingerprint = createHash10("sha256").update(JSON.stringify([
19411
+ const fingerprint = createHash11("sha256").update(JSON.stringify([
18563
19412
  args.domain,
18564
19413
  args.jobId,
18565
19414
  pubkey,
@@ -18683,7 +19532,7 @@ async function commitBalancePayment(args) {
18683
19532
  }
18684
19533
 
18685
19534
  // src/sdk/server/app.ts
18686
- import { createHash as createHash12, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
19535
+ import { createHash as createHash13, randomBytes as randomBytes5, randomUUID as randomUUID6 } from "crypto";
18687
19536
  import { Hono } from "hono";
18688
19537
  import { getCookie } from "hono/cookie";
18689
19538
  import { cors } from "hono/cors";
@@ -18963,7 +19812,7 @@ async function handlePendingDrains(c, opts) {
18963
19812
  const drains = pending.map((d) => {
18964
19813
  const automatic = d.method === "cashu";
18965
19814
  const amountSats = !automatic ? null : d.owedSats ?? netOwedSats(fiatToSatsCeil(d.amountMicro / 1e6, rateFor(d.currency)));
18966
- const settlement = automatic ? "automatic" : d.channelId === null ? "manual" : "channel";
19815
+ const settlement = automatic ? "automatic" : d.method === "stripe" ? "card" : d.channelId === null ? "manual" : "channel";
18967
19816
  return {
18968
19817
  credit_id: d.creditId,
18969
19818
  drain_id: d.drainId,
@@ -18979,17 +19828,19 @@ async function handlePendingDrains(c, opts) {
18979
19828
  amount_native: d.drainedNative,
18980
19829
  settlement,
18981
19830
  channel_id: d.channelId,
18982
- created_at: d.createdAt
19831
+ created_at: d.createdAt,
19832
+ ...d.method === "stripe" && { card_refund: cardRefundStatus(d) }
18983
19833
  };
18984
19834
  });
18985
19835
  const liability = {
18986
- cashu_sats: drains.filter((d) => d.method === "cashu").reduce((sum, d) => sum + (d.amount_sats ?? 0), 0),
19836
+ cashu_sats: drains.filter((d) => d.method === "cashu").reduce((sum2, d) => sum2 + (d.amount_sats ?? 0), 0),
18987
19837
  by_method_micro: DRAIN_METHODS.reduce((acc, method) => {
18988
- acc[method] = pending.filter((d) => d.method === method).reduce((sum, d) => sum + d.amountMicro, 0);
19838
+ acc[method] = pending.filter((d) => d.method === method).reduce((sum2, d) => sum2 + d.amountMicro, 0);
18989
19839
  return acc;
18990
19840
  }, {}),
18991
- manual_micro: drains.filter((d) => d.settlement === "manual").reduce((sum, d) => sum + d.amount_micro, 0),
18992
- channel_micro: drains.filter((d) => d.settlement === "channel").reduce((sum, d) => sum + d.amount_micro, 0)
19841
+ manual_micro: drains.filter((d) => d.settlement === "manual").reduce((sum2, d) => sum2 + d.amount_micro, 0),
19842
+ channel_micro: drains.filter((d) => d.settlement === "channel").reduce((sum2, d) => sum2 + d.amount_micro, 0),
19843
+ card_micro: drains.filter((d) => d.settlement === "card").reduce((sum2, d) => sum2 + d.amount_micro, 0)
18993
19844
  };
18994
19845
  return c.json({ drains, liability, mints: opts.mints?.() ?? [] });
18995
19846
  }
@@ -18999,7 +19850,7 @@ async function handleBitcoinLiability(c, opts) {
18999
19850
  const deposit = await opts.ledger.bitcoinDepositLiability();
19000
19851
  const pending = await opts.ledger.listPendingDrains();
19001
19852
  const promised = pending.filter((d) => d.method === "cashu" && d.status === "pending");
19002
- const drainSats = promised.reduce((sum, d) => sum + (d.owedSats ?? 0), 0);
19853
+ const drainSats = promised.reduce((sum2, d) => sum2 + (d.owedSats ?? 0), 0);
19003
19854
  const drainsUnpriced = promised.filter((d) => d.owedSats === null).length;
19004
19855
  return c.json({
19005
19856
  deposit_liability_sats: deposit.depositSats,
@@ -19336,6 +20187,7 @@ async function handleMarkDrainSent(c, opts) {
19336
20187
  409
19337
20188
  );
19338
20189
  }
20190
+ if (existing?.method === "stripe") return retryCardRefund(c, opts, existing);
19339
20191
  let record;
19340
20192
  let replayed;
19341
20193
  try {
@@ -20748,6 +21600,60 @@ function parseSerializedDleq(raw) {
20748
21600
  if (dleq.r !== void 0 && typeof dleq.r !== "string") return void 0;
20749
21601
  return { e: dleq.e, s: dleq.s, ...dleq.r !== void 0 ? { r: dleq.r } : {} };
20750
21602
  }
21603
+ function cardRefundStatus(drain) {
21604
+ const plan = drain.cardRefund;
21605
+ if (!plan) return { planned: false };
21606
+ const summary = cardRefundSummary(plan);
21607
+ return {
21608
+ planned: true,
21609
+ refunded_cents: summary.refundedCents,
21610
+ fee_share_cents: summary.feeShareCents,
21611
+ sub_cent_micro: summary.subCentMicro,
21612
+ refund_ids: summary.refundIds,
21613
+ refunds: plan.entries.map((entry) => ({
21614
+ payment_intent: entry.paymentIntentId,
21615
+ refund_id: entry.refundId,
21616
+ amount_cents: entry.amountCents,
21617
+ fee_share_cents: entry.feeShareCents,
21618
+ attempt: entry.attempt,
21619
+ status: entry.status,
21620
+ last_error: entry.lastError
21621
+ }))
21622
+ };
21623
+ }
21624
+ async function retryCardRefund(c, opts, drain) {
21625
+ if (drain.status === "sent") {
21626
+ return c.json({ status: "sent", credit_id: drain.creditId, drain_id: drain.drainId, replayed: true });
21627
+ }
21628
+ if (!opts.stripeRefunds) {
21629
+ return c.json(
21630
+ {
21631
+ error: "card_refund_unavailable",
21632
+ message: "This DVM has no Stripe secret key configured, so it cannot refund to the card."
21633
+ },
21634
+ 503
21635
+ );
21636
+ }
21637
+ const { drain: after, completed } = await runStripeDrain(
21638
+ {
21639
+ ledger: opts.ledger,
21640
+ drainReport: { db: opts.db, dvmId: opts.dvmId, enqueueCreditDrain: opts.enqueueCreditDrain },
21641
+ stripe: opts.stripeRefunds,
21642
+ ...opts.receiptIssuer && { receiptIssuer: opts.receiptIssuer }
21643
+ },
21644
+ drain
21645
+ );
21646
+ return c.json(
21647
+ {
21648
+ status: after.status,
21649
+ credit_id: after.creditId,
21650
+ drain_id: after.drainId,
21651
+ replayed: after.status === "sent" && !completed,
21652
+ card_refund: cardRefundStatus(after)
21653
+ },
21654
+ after.status === "sent" ? 200 : 202
21655
+ );
21656
+ }
20751
21657
 
20752
21658
  // src/sdk/server/caller-facing-url.ts
20753
21659
  function resolvePublicOrigin(value) {
@@ -20766,6 +21672,220 @@ function callerFacingRequestUrl(requestUrl, publicOrigin) {
20766
21672
  return `${publicOrigin}${request.pathname}${request.search}`;
20767
21673
  }
20768
21674
 
21675
+ // src/sdk/server/card-refund-sweep.ts
21676
+ var DISPUTE_LOOKBACK_MS = 180 * 24 * 60 * 60 * 1e3;
21677
+ var MAX_DISPUTE_PAGES = 10;
21678
+ var DISPUTE_SETTLED_FOR_MERCHANT = /* @__PURE__ */ new Set(["won", "warning_closed"]);
21679
+ var STRANDED_AFTER_MS = 30 * 60 * 1e3;
21680
+ var STRANDED_SEARCH_WINDOW_MS = 24 * 60 * 60 * 1e3;
21681
+ var STRANDED_ABANDON_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
21682
+ async function runCardRefundSweep(deps, opts = {}) {
21683
+ const nowMs = opts.nowMs ?? Date.now();
21684
+ const limit = opts.limit ?? 200;
21685
+ const result = {
21686
+ frozen: [],
21687
+ unfrozen: [],
21688
+ expiryRefunds: [],
21689
+ completed: [],
21690
+ pending: [],
21691
+ strandedRefunds: [],
21692
+ failedSteps: []
21693
+ };
21694
+ const legDeps = {
21695
+ ledger: deps.ledger,
21696
+ stripe: deps.stripe,
21697
+ drainReport: deps.drainReport,
21698
+ ...deps.receiptIssuer && { receiptIssuer: deps.receiptIssuer }
21699
+ };
21700
+ const step = async (name, run) => {
21701
+ try {
21702
+ await run();
21703
+ } catch {
21704
+ result.failedSteps.push(name);
21705
+ diagnosticConsole.error({ event: "card_refund_sweep_step_failed", stage: name });
21706
+ }
21707
+ };
21708
+ await step("disputes", () => applyDisputes(deps, nowMs, result));
21709
+ const { listExpiredCardCredits, openCardExpiryRefund } = deps.ledger;
21710
+ if (listExpiredCardCredits && openCardExpiryRefund) {
21711
+ await step("expiry", async () => {
21712
+ for (const creditId of await listExpiredCardCredits.call(deps.ledger, { nowMs, limit })) {
21713
+ try {
21714
+ const drain = await openCardExpiryRefund.call(deps.ledger, { creditId, nowMs });
21715
+ if (drain) result.expiryRefunds.push(creditId);
21716
+ } catch {
21717
+ diagnosticConsole.error({ event: "card_refund_expiry_failed", credit_id: creditId });
21718
+ }
21719
+ }
21720
+ });
21721
+ }
21722
+ await step("retry", async () => {
21723
+ const pending = (await deps.ledger.listPendingDrains()).filter(
21724
+ (drain) => drain.method === "stripe" && drain.status === "pending"
21725
+ );
21726
+ for (const drain of pending.slice(0, limit)) {
21727
+ try {
21728
+ const { drain: after } = await runStripeDrain(legDeps, drain);
21729
+ (after.status === "sent" ? result.completed : result.pending).push(
21730
+ `${drain.creditId}:${drain.drainId}`
21731
+ );
21732
+ } catch {
21733
+ result.pending.push(`${drain.creditId}:${drain.drainId}`);
21734
+ }
21735
+ }
21736
+ });
21737
+ const journal = deps.journal;
21738
+ if (journal && deps.ledger.cardFundingBooked) {
21739
+ await step("stranded", async () => {
21740
+ result.strandedRefunds = await refundStranded(deps, journal, nowMs, limit);
21741
+ });
21742
+ }
21743
+ return result;
21744
+ }
21745
+ function startCardRefundSweep(deps, opts = {}) {
21746
+ if (!deps.ledger.saveCardRefund) return void 0;
21747
+ const intervalMs = opts.intervalMs ?? 60 * 60 * 1e3;
21748
+ const bootDelayMs = opts.bootDelayMs ?? 3e4;
21749
+ let stopped = false;
21750
+ const runOnce = () => runCardRefundSweep(deps, { limit: opts.limit });
21751
+ const pass = () => {
21752
+ if (stopped) return;
21753
+ void runOnce().then((summary) => {
21754
+ const moved = summary.frozen.length + summary.unfrozen.length + summary.expiryRefunds.length + summary.completed.length + summary.pending.length + summary.strandedRefunds.length;
21755
+ if (moved === 0) return;
21756
+ diagnosticConsole.log({
21757
+ event: "card_refund_sweep",
21758
+ frozen: summary.frozen.length,
21759
+ unfrozen: summary.unfrozen.length,
21760
+ expiry_refunds: summary.expiryRefunds.length,
21761
+ completed: summary.completed.length,
21762
+ pending: summary.pending.length,
21763
+ stranded_refunds: summary.strandedRefunds.length
21764
+ });
21765
+ }).catch(() => {
21766
+ diagnosticConsole.error({ event: "card_refund_sweep_failed" });
21767
+ });
21768
+ };
21769
+ const bootTimer = bootDelayMs > 0 ? setTimeout(pass, bootDelayMs) : void 0;
21770
+ bootTimer?.unref();
21771
+ const intervalTimer = intervalMs > 0 ? setInterval(pass, intervalMs) : void 0;
21772
+ intervalTimer?.unref();
21773
+ return {
21774
+ stop() {
21775
+ stopped = true;
21776
+ if (bootTimer) clearTimeout(bootTimer);
21777
+ if (intervalTimer) clearInterval(intervalTimer);
21778
+ },
21779
+ runOnce
21780
+ };
21781
+ }
21782
+ async function applyDisputes(deps, nowMs, result) {
21783
+ const freeze = deps.ledger.freezeCardCreditsForPaymentIntent?.bind(deps.ledger);
21784
+ if (!freeze) return;
21785
+ const statusesByIntent = /* @__PURE__ */ new Map();
21786
+ const createdGte = Math.floor((nowMs - DISPUTE_LOOKBACK_MS) / 1e3);
21787
+ let startingAfter;
21788
+ try {
21789
+ for (let page = 0; page < MAX_DISPUTE_PAGES; page++) {
21790
+ const { disputes, hasMore } = await deps.stripe.listDisputes({
21791
+ createdGte,
21792
+ ...startingAfter && { startingAfter }
21793
+ });
21794
+ for (const dispute of disputes) {
21795
+ if (!dispute.paymentIntentId) continue;
21796
+ const statuses = statusesByIntent.get(dispute.paymentIntentId) ?? [];
21797
+ statuses.push(dispute.status);
21798
+ statusesByIntent.set(dispute.paymentIntentId, statuses);
21799
+ }
21800
+ if (!hasMore || disputes.length === 0) break;
21801
+ startingAfter = disputes[disputes.length - 1].id;
21802
+ }
21803
+ } finally {
21804
+ const blocking = new Set(
21805
+ [...statusesByIntent].filter(([, statuses]) => !allSettledForMerchant(statuses)).map(([paymentIntentId]) => paymentIntentId)
21806
+ );
21807
+ for (const paymentIntentId of blocking) {
21808
+ result.frozen.push(...await freeze(paymentIntentId));
21809
+ }
21810
+ for (const paymentIntentId of statusesByIntent.keys()) {
21811
+ if (!blocking.has(paymentIntentId)) result.unfrozen.push(...await unfreezeSettled(deps, paymentIntentId));
21812
+ }
21813
+ }
21814
+ }
21815
+ async function unfreezeSettled(deps, paymentIntentId) {
21816
+ const candidates = deps.ledger.disputedCardCreditsFundedBy?.bind(deps.ledger);
21817
+ const unfreeze = deps.ledger.unfreezeCardCredit?.bind(deps.ledger);
21818
+ if (!candidates || !unfreeze) return [];
21819
+ const unfrozen = [];
21820
+ for (const credit of await candidates(paymentIntentId)) {
21821
+ let settled = true;
21822
+ for (const fundingIntent of credit.paymentIntentIds) {
21823
+ const { disputes, hasMore } = await deps.stripe.listDisputesForPaymentIntent(fundingIntent);
21824
+ if (hasMore || !allSettledForMerchant(disputes.map((dispute) => dispute.status))) {
21825
+ settled = false;
21826
+ break;
21827
+ }
21828
+ }
21829
+ if (settled && await unfreeze(credit.creditId)) unfrozen.push(credit.creditId);
21830
+ }
21831
+ return unfrozen;
21832
+ }
21833
+ function allSettledForMerchant(statuses) {
21834
+ return statuses.every((status) => DISPUTE_SETTLED_FOR_MERCHANT.has(status));
21835
+ }
21836
+ async function refundStranded(deps, journal, nowMs, limit) {
21837
+ const booked = deps.ledger.cardFundingBooked?.bind(deps.ledger);
21838
+ if (!booked) return [];
21839
+ const refunded = [];
21840
+ const attempts = await journal.listUnresolved({ olderThanMs: nowMs - STRANDED_AFTER_MS, limit });
21841
+ for (const attempt of attempts) {
21842
+ try {
21843
+ const intents = await deps.stripe.searchPaymentIntentsByMetadata(
21844
+ "mpp_challenge_id",
21845
+ attempt.challengeId
21846
+ );
21847
+ if (intents.length === 0) {
21848
+ if (nowMs - attempt.createdAt > STRANDED_SEARCH_WINDOW_MS) {
21849
+ await journal.resolve(attempt.challengeId, "not_found");
21850
+ }
21851
+ continue;
21852
+ }
21853
+ let unsettled = false;
21854
+ let refundedHere = 0;
21855
+ for (const intent of intents) {
21856
+ if (intent.status !== "succeeded") {
21857
+ if (intent.status !== "canceled" && intent.status !== "requires_payment_method") {
21858
+ unsettled = true;
21859
+ }
21860
+ continue;
21861
+ }
21862
+ if (await booked(intent.id)) continue;
21863
+ const charge = await deps.stripe.retrieveCharge(intent.id);
21864
+ const owed = charge.amount - charge.amountRefunded;
21865
+ if (owed > 0 && !charge.disputed) {
21866
+ await deps.stripe.createRefund({
21867
+ paymentIntentId: intent.id,
21868
+ amount: owed,
21869
+ idempotencyKey: `dvmkit-stranded-${intent.id}`,
21870
+ metadata: { dvmkit_stranded: "true" }
21871
+ });
21872
+ refunded.push(intent.id);
21873
+ refundedHere += 1;
21874
+ diagnosticConsole.warn({ event: "card_charge_stranded_refunded", payment_intent: intent.id });
21875
+ }
21876
+ }
21877
+ if (!unsettled) {
21878
+ await journal.resolve(attempt.challengeId, refundedHere > 0 ? "refunded" : "settled");
21879
+ } else if (nowMs - attempt.createdAt > STRANDED_ABANDON_AFTER_MS) {
21880
+ diagnosticConsole.error({ event: "card_charge_unsettled", challenge_id: attempt.challengeId });
21881
+ await journal.resolve(attempt.challengeId, "unsettled");
21882
+ }
21883
+ } catch {
21884
+ }
21885
+ }
21886
+ return refunded;
21887
+ }
21888
+
20769
21889
  // src/sdk/server/credit-expiry-sweep.ts
20770
21890
  function startCreditExpirySweep(ledger, opts = {}) {
20771
21891
  const sweepExpiredCredits = ledger.sweepExpiredCredits?.bind(ledger);
@@ -21739,7 +22859,7 @@ initSchemaForm();
21739
22859
  }
21740
22860
 
21741
22861
  // src/sdk/server/request-fingerprint.ts
21742
- import { createHash as createHash11 } from "crypto";
22862
+ import { createHash as createHash12 } from "crypto";
21743
22863
  function requestFingerprint(req, opts) {
21744
22864
  const canonical = canonicalize({
21745
22865
  capability: req.capability,
@@ -21747,7 +22867,7 @@ function requestFingerprint(req, opts) {
21747
22867
  params: req.params ?? {},
21748
22868
  data: opts.stripAuthEnvelope ? stripEnvelope(req.data) : req.data
21749
22869
  });
21750
- return createHash11("sha256").update(canonical).digest("hex");
22870
+ return createHash12("sha256").update(canonical).digest("hex");
21751
22871
  }
21752
22872
  var ENVELOPE_KEYS = /* @__PURE__ */ new Set(["pubkey", "signature", "timestamp", "nonce", "auth_statement"]);
21753
22873
  function stripEnvelope(data) {
@@ -21785,6 +22905,7 @@ async function createDVMServer(descriptor, opts) {
21785
22905
  let payoutReporter;
21786
22906
  let unsubscribeTempoSettlementReadiness;
21787
22907
  let creditExpirySweep;
22908
+ let cardRefundSweep;
21788
22909
  let x402ChannelStorage;
21789
22910
  let x402BatchSettlement;
21790
22911
  let server;
@@ -21817,6 +22938,7 @@ async function createDVMServer(descriptor, opts) {
21817
22938
  await capture(() => payoutReporter?.stop());
21818
22939
  await capture(() => revenueReporter?.stop());
21819
22940
  await capture(() => creditExpirySweep?.stop());
22941
+ await capture(() => cardRefundSweep?.stop());
21820
22942
  await capture(() => unsubscribeTempoSettlementReadiness?.());
21821
22943
  if (settlementStopped) await capture(() => settlementStopped);
21822
22944
  if (x402ChannelStorage) {
@@ -21870,7 +22992,7 @@ async function createDVMServer(descriptor, opts) {
21870
22992
  }
21871
22993
  }
21872
22994
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
21873
- const { RevenueReporter } = await import("./revenue-reporter-6XARUEG6.js");
22995
+ const { RevenueReporter } = await import("./revenue-reporter-FD3GDQZS.js");
21874
22996
  const reporter = new RevenueReporter(
21875
22997
  opts.db,
21876
22998
  platformUrl,
@@ -21880,7 +23002,7 @@ async function createDVMServer(descriptor, opts) {
21880
23002
  revenueReporter = reporter;
21881
23003
  await reporter.init();
21882
23004
  reporter.startRetryLoop();
21883
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-5QAHCYIO.js");
23005
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-KTLBNJJ7.js");
21884
23006
  payoutReporter = new PayoutReporter({
21885
23007
  dvmId,
21886
23008
  db: opts.db,
@@ -22086,6 +23208,18 @@ async function createDVMServer(descriptor, opts) {
22086
23208
  payoutReporter
22087
23209
  });
22088
23210
  await server.jobManager.recoverTerminalCosts();
23211
+ if (opts.stripeCredit) {
23212
+ cardRefundSweep = startCardRefundSweep(
23213
+ {
23214
+ ledger: creditLedger,
23215
+ stripe: opts.stripeCredit.refunds,
23216
+ drainReport: { db: opts.db, dvmId, enqueueCreditDrain },
23217
+ ...server.drainReceiptIssuer && { receiptIssuer: server.drainReceiptIssuer },
23218
+ ...opts.stripeCredit.chargeJournal && { journal: opts.stripeCredit.chargeJournal }
23219
+ },
23220
+ opts.creditExpirySweep
23221
+ );
23222
+ }
22089
23223
  return {
22090
23224
  app: server.app,
22091
23225
  authAudience: server.authAudience,
@@ -22136,6 +23270,10 @@ var DVMServer = class {
22136
23270
  tempoSweepLastAttemptMs = 0;
22137
23271
  receiptIssuer;
22138
23272
  creditLedger;
23273
+ /** Countersigns reclaim events outside a request, such as a card refund the sweep finishes. */
23274
+ get drainReceiptIssuer() {
23275
+ return this.receiptIssuer;
23276
+ }
22139
23277
  processedPayments;
22140
23278
  authAudience;
22141
23279
  /** Lazily constructed when a refreshed facilitator probe gains batch support. */
@@ -22581,6 +23719,7 @@ var DVMServer = class {
22581
23719
  // DVM wired with an in-memory session store resolves to a rail with no
22582
23720
  // close-intent read and the verbs answer 503.
22583
23721
  tempoDrainRail: () => Promise.resolve(this.opts.mpp),
23722
+ ...this.opts.stripeCredit && { stripeRefunds: this.opts.stripeCredit.refunds },
22584
23723
  ...this.opts.hostedSettlementReports ? {
22585
23724
  hostedSettlementReports: { store: this.opts.hostedSettlementReports }
22586
23725
  } : {},
@@ -22822,7 +23961,7 @@ var DVMServer = class {
22822
23961
  const dvmId = this.opts.dvmId ?? `dev:${this.descriptor.name}`;
22823
23962
  return {
22824
23963
  dvm_id: dvmId,
22825
- builder_pubkey: createHash12("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
23964
+ builder_pubkey: createHash13("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
22826
23965
  };
22827
23966
  }
22828
23967
  throw new AuthAudienceError();
@@ -22924,6 +24063,7 @@ var DVMServer = class {
22924
24063
  methods.push("x402");
22925
24064
  }
22926
24065
  if (this.opts.lightningReceive?.available()) methods.push("lightning");
24066
+ if (this.opts.stripeCredit) methods.push("stripe");
22927
24067
  return methods;
22928
24068
  }
22929
24069
  creditMppMethods() {
@@ -23132,6 +24272,7 @@ var DVMServer = class {
23132
24272
  tempoWithheld: mppSplit.withheld,
23133
24273
  x402Schemes: this.creditX402Schemes(),
23134
24274
  lightningFundingMinSats: this.opts.lightningReceive?.fundingMinSats,
24275
+ stripeNetworkId: this.opts.stripeCredit?.networkId,
23135
24276
  // Configured, not advertised: `advertisedMints()` is health
23136
24277
  // filtered, and a rail that comes and goes with a mint's uptime is worse
23137
24278
  // product than one that is honestly absent.
@@ -23183,7 +24324,7 @@ var DVMServer = class {
23183
24324
  const paymentAuthorization = ctx.req.header("Authorization");
23184
24325
  const mppArtifact = paymentAuthorization ? (await import("mppx")).Credential.extractPaymentScheme(paymentAuthorization) : void 0;
23185
24326
  const attachedPayment = ctx.req.header("X-Cashu") ?? ctx.req.header("PAYMENT-SIGNATURE") ?? ctx.req.header("X-PAYMENT") ?? mppArtifact;
23186
- const paymentArtifactHash = attachedPayment ? createHash12("sha256").update(attachedPayment).digest("hex") : void 0;
24327
+ const paymentArtifactHash = attachedPayment ? createHash13("sha256").update(attachedPayment).digest("hex") : void 0;
23187
24328
  hostingFundingArtifactHash = paymentArtifactHash;
23188
24329
  if (!await control.admit(admissionId, workId, {
23189
24330
  creditId,
@@ -23222,7 +24363,8 @@ var DVMServer = class {
23222
24363
  await control.finishAdmission(admissionId, outcome);
23223
24364
  }
23224
24365
  } : void 0,
23225
- tempoSettlementReadiness: this.opts.tempoSettlementReadiness
24366
+ tempoSettlementReadiness: this.opts.tempoSettlementReadiness,
24367
+ stripeCredit: this.opts.stripeCredit
23226
24368
  });
23227
24369
  }
23228
24370
  /**
@@ -23770,7 +24912,7 @@ var DVMServer = class {
23770
24912
  if (err instanceof SignedRequestError) return err;
23771
24913
  throw err;
23772
24914
  }
23773
- return new SignedRequestError("signed request required", "signature_invalid");
24915
+ return new SignedRequestError("signed request required", "signature_required");
23774
24916
  }
23775
24917
  /**
23776
24918
  * On authenticated DVMs, envelope-only parse failures defer to auth
@@ -24043,7 +25185,7 @@ var DVMServer = class {
24043
25185
  const authorization = c.req.header("Authorization");
24044
25186
  const mppArtifact = authorization ? (await import("mppx")).Credential.extractPaymentScheme(authorization) : void 0;
24045
25187
  const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? mppArtifact;
24046
- const hostingPaymentArtifactHash = attachedPayment ? createHash12("sha256").update(attachedPayment).digest("hex") : void 0;
25188
+ const hostingPaymentArtifactHash = attachedPayment ? createHash13("sha256").update(attachedPayment).digest("hex") : void 0;
24047
25189
  if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
24048
25190
  const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
24049
25191
  hostingPaymentArtifactHash
@@ -24367,7 +25509,7 @@ var DVMServer = class {
24367
25509
  }
24368
25510
  const isAnonymous = requesterId === "anonymous" || requesterId === "dev-anonymous";
24369
25511
  const jobToken = isAnonymous ? randomBytes5(32).toString("hex") : void 0;
24370
- const requesterTokenHash = jobToken ? createHash12("sha256").update(jobToken).digest("hex") : void 0;
25512
+ const requesterTokenHash = jobToken ? createHash13("sha256").update(jobToken).digest("hex") : void 0;
24371
25513
  const job = jm.createJob(
24372
25514
  normalisedBody,
24373
25515
  requesterId,
@@ -25225,7 +26367,7 @@ function isJobOwner(c, job) {
25225
26367
  if (job.requesterTokenHash) {
25226
26368
  const token = c.req.header("X-Job-Token");
25227
26369
  if (!token) return false;
25228
- const provided = createHash12("sha256").update(token).digest("hex");
26370
+ const provided = createHash13("sha256").update(token).digest("hex");
25229
26371
  return timingSafeEqualHex(provided, job.requesterTokenHash);
25230
26372
  }
25231
26373
  return c.get("requesterId") === job.requesterId;
@@ -25779,11 +26921,15 @@ export {
25779
26921
  extractCreditEnvelope,
25780
26922
  creditEnvelopeIgnoreFields,
25781
26923
  stripCreditEnvelope,
26924
+ STRIPE_CREDIT_MIN_MICRO,
25782
26925
  buildCreditMenu,
25783
26926
  selectPrimaryCredit,
25784
26927
  toCreditView,
25785
26928
  attachCreditMenu,
25786
26929
  toCreditTerms,
26930
+ fiatMicroToMethodAmount,
26931
+ createStripeRefundApi,
26932
+ callerInputIssues,
25787
26933
  resolvePriceFiat,
25788
26934
  priceFiatMicro,
25789
26935
  resolveFxSnapshot,