@dvmkit/sdk 0.21.2 → 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-MA55LUNR.js → chunk-2WWVI32F.js} +55 -1
  3. package/dist/{chunk-3V6CSTWB.js → chunk-3DGIRHP2.js} +2 -2
  4. package/dist/{chunk-WWKFG6TM.js → chunk-5NMHZIIU.js} +1 -1
  5. package/dist/{chunk-FAT57DAP.js → chunk-5YJWAJ4N.js} +1 -1
  6. package/dist/{chunk-D2MGOQIS.js → chunk-CUWSJQGT.js} +1 -1
  7. package/dist/{chunk-6GZF7VDO.js → chunk-DLRPKEAM.js} +1 -1
  8. package/dist/{chunk-62JG4AF7.js → chunk-DY5QIHJA.js} +1 -1
  9. package/dist/{chunk-LWF4ABYF.js → chunk-EHBUAJ5M.js} +1 -1
  10. package/dist/{chunk-IUFVIIPF.js → chunk-EQIP6TFD.js} +1 -1
  11. package/dist/{chunk-ZBOCNF56.js → chunk-GDZXGB43.js} +3 -3
  12. package/dist/{chunk-2HSWWHXK.js → chunk-HEH7YUGE.js} +1 -1
  13. package/dist/{chunk-B27E7JIR.js → chunk-HVZIITWW.js} +27 -4
  14. package/dist/{chunk-VIAMJ6GS.js → chunk-JINZMDNK.js} +1224 -80
  15. package/dist/{chunk-NRE4NOCY.js → chunk-JMTLK2SZ.js} +1 -1
  16. package/dist/{chunk-LSANCGLP.js → chunk-L35WYOH2.js} +1 -1
  17. package/dist/{chunk-27QB2D3Z.js → chunk-LCNRFKP7.js} +2 -2
  18. package/dist/{chunk-2AGADOX3.js → chunk-QMKGYIBM.js} +1 -1
  19. package/dist/{chunk-JOSNSNS7.js → chunk-SEIMPHIP.js} +225 -11
  20. package/dist/{chunk-Y746QPX7.js → chunk-SQOVLAJ2.js} +1 -1
  21. package/dist/{chunk-XWG54M6T.js → chunk-UVRBD5GR.js} +1 -1
  22. package/dist/{chunk-5RGZN6WQ.js → chunk-XB644VN3.js} +1 -1
  23. package/dist/{chunk-2RZRYMCR.js → chunk-YNHFJE6U.js} +1 -1
  24. package/dist/{chunk-7KNFEDFW.js → chunk-ZH5CLGO6.js} +1 -1
  25. package/dist/{content-retention-efjireLM.d.ts → content-retention-DEvY5vjM.d.ts} +1 -1
  26. package/dist/{credit-ledger-M3EBVMOP.js → credit-ledger-S76KU53A.js} +11 -3
  27. package/dist/{credit-menu-UOOxuxVL.d.ts → credit-menu-BA2my6hE.d.ts} +229 -8
  28. package/dist/{fx-BtxHVj6w.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-DWVkleH3.d.ts → job-store-CjPnf0dd.d.ts} +1 -1
  36. package/dist/{lightning-backend-CTijN35u.d.ts → lightning-backend-D9Q2KDS-.d.ts} +1 -1
  37. package/dist/{memory-credit-ledger-WL2VKLE3.js → memory-credit-ledger-5KXHOWB2.js} +4 -4
  38. package/dist/{mpp-setup-DBYDXISN.js → mpp-setup-3YZMURZL.js} +3 -3
  39. package/dist/{payout-reporter-D5TCIBQA.js → payout-reporter-KTLBNJJ7.js} +2 -2
  40. package/dist/{postgres-consumed-credential-store-IPWPJ6V7.js → postgres-consumed-credential-store-Y336KNIB.js} +1 -1
  41. package/dist/{postgres-job-store-POZJNFJU.js → postgres-job-store-ESEAN7JL.js} +2 -2
  42. package/dist/{postgres-replay-store-JQYAKG47.js → postgres-replay-store-6SVCZ475.js} +2 -2
  43. package/dist/{pricing-FGPVRNTL.js → pricing-RP5CGW6I.js} +3 -3
  44. package/dist/{revenue-reporter-QKCBYFWT.js → revenue-reporter-FD3GDQZS.js} +2 -2
  45. package/dist/server/index.d.ts +20 -11
  46. package/dist/server/index.js +292 -28
  47. package/dist/{step-cache-Bx_ZybWF.d.ts → step-cache-DXFftnAc.d.ts} +154 -12
  48. package/dist/{tempo-charge-store-P7ZBR3GG.js → tempo-charge-store-SOIRMEFJ.js} +2 -2
  49. package/dist/{tempo-session-store-ZVRBAMLG.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-eOAMEO12.d.ts → usd-Bc_rva_K.d.ts} +1 -1
  53. package/dist/{x402-44JZJQ5F.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-62JG4AF7.js";
8
+ } from "./chunk-DY5QIHJA.js";
9
9
  import {
10
10
  createX402FacilitatorAuthHeaders,
11
11
  createX402FacilitatorClient,
@@ -14,13 +14,13 @@ import {
14
14
  verifyWithFacilitator,
15
15
  withPaymentDiagnostics,
16
16
  withPaymentStage
17
- } from "./chunk-3V6CSTWB.js";
17
+ } from "./chunk-3DGIRHP2.js";
18
18
  import {
19
19
  MemoryCreditLedger
20
- } from "./chunk-B27E7JIR.js";
20
+ } from "./chunk-HVZIITWW.js";
21
21
  import {
22
22
  PostgresJobStore
23
- } from "./chunk-Y746QPX7.js";
23
+ } from "./chunk-SQOVLAJ2.js";
24
24
  import {
25
25
  CASHU_MIN_LOCKTIME_SECONDS,
26
26
  DEFAULT_CASHU_LOCK_WINDOW_SECONDS,
@@ -34,7 +34,7 @@ import {
34
34
  signedRequestInput,
35
35
  unsignedCashuLockWindowSeconds,
36
36
  zodIssuesOf
37
- } from "./chunk-6GZF7VDO.js";
37
+ } from "./chunk-DLRPKEAM.js";
38
38
  import {
39
39
  MemoryJobStore,
40
40
  UNCAPPABLE_ASK_TOTAL,
@@ -43,7 +43,7 @@ import {
43
43
  isJobRetentionStore,
44
44
  isReceiptIssuingStore,
45
45
  isStreamableJobStore
46
- } from "./chunk-WWKFG6TM.js";
46
+ } from "./chunk-5NMHZIIU.js";
47
47
  import {
48
48
  ContentHoldError,
49
49
  JobCancelledError,
@@ -84,7 +84,7 @@ import {
84
84
  receiptPubkeyFromSecret,
85
85
  toLockPubkey,
86
86
  verifyChallenge
87
- } from "./chunk-IUFVIIPF.js";
87
+ } from "./chunk-EQIP6TFD.js";
88
88
  import {
89
89
  X402_BATCH_SETTLEMENT_SCHEME,
90
90
  X402_DEFAULT_FACILITATOR,
@@ -103,7 +103,7 @@ import {
103
103
  resolveRpcOverride,
104
104
  usdcToMsats,
105
105
  x402NetworkToCaip2
106
- } from "./chunk-NUDHO4HV.js";
106
+ } from "./chunk-LABJSMSJ.js";
107
107
  import {
108
108
  RAIL_REFUNDABLE,
109
109
  canonicalRequestPath,
@@ -111,7 +111,7 @@ import {
111
111
  } from "./chunk-SXL44PL5.js";
112
112
  import {
113
113
  createConsoleLogger
114
- } from "./chunk-NRE4NOCY.js";
114
+ } from "./chunk-JMTLK2SZ.js";
115
115
  import {
116
116
  StepCache
117
117
  } from "./chunk-BQJIFRAZ.js";
@@ -128,8 +128,9 @@ import {
128
128
  roundUsd,
129
129
  satsToFiat,
130
130
  validateCurrency
131
- } from "./chunk-XWG54M6T.js";
131
+ } from "./chunk-UVRBD5GR.js";
132
132
  import {
133
+ CARD_EXPIRY_DRAIN_PREFIX,
133
134
  CreditLedger,
134
135
  CreditLedgerError,
135
136
  DRAIN_METHODS,
@@ -139,12 +140,14 @@ import {
139
140
  X402_RESOLVED_SETTLEMENT_STATUSES,
140
141
  X402_SPEND_BLOCKING_SETTLEMENT_STATUSES,
141
142
  X402_WEDGED_SETTLEMENT_STATUSES,
143
+ cardExpiryDrainId,
144
+ isCardFundedRail,
142
145
  isDrainMethod,
143
146
  isFundingRail,
144
147
  isNonChannelBitcoinRail,
145
148
  netOwedSats,
146
149
  x402SettlementPending
147
- } from "./chunk-JOSNSNS7.js";
150
+ } from "./chunk-SEIMPHIP.js";
148
151
  import {
149
152
  parseStoreValue
150
153
  } from "./chunk-YG7G4DPZ.js";
@@ -163,7 +166,7 @@ import {
163
166
  import {
164
167
  isTransportError,
165
168
  withPostgresRecoveryContext
166
- } from "./chunk-LSANCGLP.js";
169
+ } from "./chunk-L35WYOH2.js";
167
170
  import {
168
171
  DvmError,
169
172
  x402NetworkByCaip2
@@ -181,17 +184,17 @@ import {
181
184
  installHostedTempoCloseGuard,
182
185
  isTempoInsufficientFundsError,
183
186
  resolveTempoNetwork
184
- } from "./chunk-27QB2D3Z.js";
187
+ } from "./chunk-LCNRFKP7.js";
185
188
  import {
186
189
  loggers,
187
190
  pinDimension,
188
191
  withSpan,
189
192
  withTraceContext
190
- } from "./chunk-7KNFEDFW.js";
193
+ } from "./chunk-ZH5CLGO6.js";
191
194
  import {
192
195
  createDiagnosticCapabilityAuthority,
193
196
  diagnosticConsole
194
- } from "./chunk-MA55LUNR.js";
197
+ } from "./chunk-2WWVI32F.js";
195
198
 
196
199
  // src/sdk/server/lock-pubkey-store.ts
197
200
  async function initLockPubkeyTable(db) {
@@ -904,6 +907,7 @@ function stablecoinCreditInstrumentAllowed(allowOneShotStablecoin, instrument) {
904
907
  }
905
908
 
906
909
  // src/sdk/server/credit-menu.ts
910
+ var STRIPE_CREDIT_MIN_MICRO = 5e5;
907
911
  function creditMenuConfigured(args) {
908
912
  if (args.runtime === "isolate") return false;
909
913
  if (!args.config) return false;
@@ -952,6 +956,9 @@ async function buildCreditMenu(args) {
952
956
  const funding = args.funding.filter((rail) => {
953
957
  if (rail === "tempo") return tempoMethods.length > 0;
954
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
+ }
955
962
  if (isNonChannelBitcoinRail(rail)) return args.hasAcceptedMint === true;
956
963
  return true;
957
964
  });
@@ -971,7 +978,14 @@ async function buildCreditMenu(args) {
971
978
  // a builder who overrides `paymentMethods` can drop x402 from `funding`
972
979
  // while the rail stays wired, and terms for a rail the menu doesn't offer
973
980
  // are the same misadvertisement in the other direction.
974
- ...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
+ }
975
989
  };
976
990
  if (funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
977
991
  try {
@@ -1051,7 +1065,8 @@ function toCreditTerms(menu) {
1051
1065
  },
1052
1066
  ...menu.lightning_min_micro !== void 0 && {
1053
1067
  lightning_min_micro: menu.lightning_min_micro
1054
- }
1068
+ },
1069
+ ...menu.stripe && { stripe: { ...menu.stripe } }
1055
1070
  };
1056
1071
  }
1057
1072
  var LIGHTNING_FLOOR_FX_MARGIN = 1.15;
@@ -4257,7 +4272,7 @@ function upfrontPaymentFromDraw(draw) {
4257
4272
  amount_micro: draw.amountMicro,
4258
4273
  currency: value.creditCurrency
4259
4274
  },
4260
- ...rail === "cashu" || rail === "x402" || rail === "tempo" || rail === "lightning" ? { rail } : {},
4275
+ ...rail !== null && isFundingRail(rail) ? { rail } : {},
4261
4276
  ...value.drawNative !== null && value.nativeAsset !== null && {
4262
4277
  native_amount: value.drawNative,
4263
4278
  native_asset: value.nativeAsset
@@ -4332,8 +4347,10 @@ function decimalsFor(methodName) {
4332
4347
  switch (methodName) {
4333
4348
  case "tempo":
4334
4349
  return 6;
4350
+ case "stripe":
4351
+ return 2;
4335
4352
  default:
4336
- throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo')`);
4353
+ throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo' or 'stripe')`);
4337
4354
  }
4338
4355
  }
4339
4356
  function msatsToMethodAmount(msats, methodName, btcUsd) {
@@ -4364,8 +4381,10 @@ function methodNativeAsset(methodName) {
4364
4381
  switch (methodName) {
4365
4382
  case "tempo":
4366
4383
  return "usdc";
4384
+ case "stripe":
4385
+ return "usd-cents";
4367
4386
  default:
4368
- throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo')`);
4387
+ throw new Error(`mpp-units: unknown method '${methodName}' (expected 'tempo' or 'stripe')`);
4369
4388
  }
4370
4389
  }
4371
4390
  function methodNativeAmountFromDecimal(amountStr, methodName) {
@@ -4825,6 +4844,235 @@ async function verifyUpfrontPayment(opts) {
4825
4844
  }
4826
4845
  return result;
4827
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
+ }
4828
5076
  async function verifyUpfrontPaymentInScope(opts) {
4829
5077
  const {
4830
5078
  cashuToken,
@@ -5181,7 +5429,7 @@ async function verifyUpfrontPaymentInScope(opts) {
5181
5429
  x402Requirements
5182
5430
  });
5183
5431
  }
5184
- const { verifyX402Payment } = await import("./x402-44JZJQ5F.js");
5432
+ const { verifyX402Payment } = await import("./x402-MFBRORHG.js");
5185
5433
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
5186
5434
  x402Payment,
5187
5435
  x402Config,
@@ -5873,6 +6121,11 @@ var PAYMENT_ERROR_COPY = {
5873
6121
  hint: "Do not retry, top up, or drain this credit. Pay per call or open a separate prepaid balance.",
5874
6122
  retryable: false
5875
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
+ },
5876
6129
  credit_not_found: {
5877
6130
  display: "No prepaid balance exists at this service under the referenced credit. Nothing was charged.",
5878
6131
  hint: 'Check the credit_id, or open one at POST /v1/credit (op "fund") before drawing on it.',
@@ -6237,7 +6490,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
6237
6490
  snapshot
6238
6491
  });
6239
6492
  }
6240
- const { verifyX402Payment } = await import("./x402-44JZJQ5F.js");
6493
+ const { verifyX402Payment } = await import("./x402-MFBRORHG.js");
6241
6494
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
6242
6495
  body.content.x402_payment,
6243
6496
  opts.x402Config,
@@ -6689,7 +6942,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
6689
6942
  try {
6690
6943
  const requiredMsats = job.pendingPaymentMsats ?? 0;
6691
6944
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
6692
- const { verifyX402Payment } = await import("./x402-44JZJQ5F.js");
6945
+ const { verifyX402Payment } = await import("./x402-MFBRORHG.js");
6693
6946
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
6694
6947
  body.content.x402_payment,
6695
6948
  opts.x402Config,
@@ -7975,7 +8228,7 @@ function mapLedgerCommitError(err, x402Requirements, paymentCommitted = false) {
7975
8228
  return void 0;
7976
8229
  }
7977
8230
  function creditLedgerErrorResult(err, x402Requirements, funding, paymentCommitted = false) {
7978
- 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";
7979
8232
  if (code === "payment_invalid") logCreditRejected(err.code, void 0);
7980
8233
  const extra = {};
7981
8234
  if (err.details.availableMicro !== void 0) {
@@ -8485,6 +8738,401 @@ async function drawFundingReceiptFor(ledger, issuer, funding, currency) {
8485
8738
  return isDrawFundingReceipt(saved) ? saved : void 0;
8486
8739
  }
8487
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
+
8488
9136
  // src/sdk/server/tempo-drain-leg.ts
8489
9137
  async function completeTempoChannelDrain(args) {
8490
9138
  if (!args.tx && args.refundedNative !== void 0 && /^\d{1,78}$/.test(String(args.refundedNative))) {
@@ -8630,9 +9278,10 @@ var CREDIT_REQUEST_SCHEMA = z.object({
8630
9278
  /**
8631
9279
  * Lightning names an invoice request; MPP names the charge/session
8632
9280
  * intent before a credential exists, so a bound credit can refuse the
8633
- * 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.
8634
9283
  */
8635
- method: z.enum(["lightning", "tempo"]).optional(),
9284
+ method: z.enum(["lightning", "tempo", "stripe"]).optional(),
8636
9285
  /** Required with `method: "tempo"`; invalid on every other funding rail. */
8637
9286
  intent: z.enum(["charge", "session"]).optional()
8638
9287
  }).optional(),
@@ -8861,6 +9510,9 @@ async function handleDrain(c, deps, args) {
8861
9510
  if (isHttpResponse(completed)) return completed;
8862
9511
  existing = completed;
8863
9512
  }
9513
+ if (existing.method === "stripe" && existing.status === "pending" && deps.stripeCredit) {
9514
+ existing = (await runStripeDrain(stripeLegDeps(deps, deps.stripeCredit), existing)).drain;
9515
+ }
8864
9516
  return respondDrain(c, deps, { record: await deliverIfParked(deps, existing), credit });
8865
9517
  }
8866
9518
  if (credit.tempoChannelId && drain.method !== "tempo") {
@@ -8886,8 +9538,17 @@ async function handleDrain(c, deps, args) {
8886
9538
  }
8887
9539
  const rejected = await burnNonce(c, deps, args.envelope);
8888
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
+ }
8889
9547
  const target = validateDrainTarget(drain);
8890
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
+ }
8891
9552
  const railMismatch = railMismatchRefusal(credit, target.method);
8892
9553
  if (railMismatch) return c.json(railMismatch, 400);
8893
9554
  const tempoChargeDrain = target.method === "tempo" && !credit.tempoChannelId;
@@ -9644,13 +10305,14 @@ async function respondDrain(c, deps, args) {
9644
10305
  ...drainCopy(args.record)
9645
10306
  });
9646
10307
  }
10308
+ var MAX_CARD_REFUND_DRAIN_ID = 128;
9647
10309
  function validateDrainTarget(drain) {
9648
10310
  const refuse = (message, hint) => ({
9649
10311
  error: { error: "invalid_request", message, hint }
9650
10312
  });
9651
10313
  const missingFields = () => refuse(
9652
10314
  "A new drain requires drain.method and drain.payout.",
9653
- '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.'
9654
10316
  );
9655
10317
  const method = drain.method;
9656
10318
  if (!method) return missingFields();
@@ -9658,10 +10320,33 @@ function validateDrainTarget(drain) {
9658
10320
  return {
9659
10321
  error: paymentErrorBody("drain_method_unsupported", {
9660
10322
  message: `"${method}" is not a payout rail this service reclaims over.`,
9661
- 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 }
9662
10325
  })
9663
10326
  };
9664
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
+ }
9665
10350
  if (!drain.payout && method !== "tempo") return missingFields();
9666
10351
  switch (method) {
9667
10352
  case "cashu": {
@@ -9690,9 +10375,70 @@ function validateDrainTarget(drain) {
9690
10375
  }
9691
10376
  }
9692
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
+ }
9693
10438
  function railMismatchRefusal(credit, method) {
9694
10439
  if (method !== "x402" && method !== "tempo") return void 0;
9695
10440
  if (!isFundingRail(credit.rail) || credit.rail === method) return void 0;
10441
+ if (isCardFundedRail(credit.rail)) return cardCreditNotDrainable(credit);
9696
10442
  const target = credit.rail === "cashu" || credit.rail === "lightning" ? "--method cashu" : `--method ${credit.rail} --address 0x\u2026`;
9697
10443
  return {
9698
10444
  error: "invalid_request",
@@ -9730,10 +10476,57 @@ function drainBody(record) {
9730
10476
  created_at: record.createdAt,
9731
10477
  ...record.token !== null ? { token: record.token } : {},
9732
10478
  ...record.sentRef !== null ? { sent: record.sentRef } : {},
10479
+ ...record.method === "stripe" ? cardRefundBody(record) : {},
9733
10480
  receipts: record.receipts
9734
10481
  };
9735
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
+ }
9736
10526
  function drainCopy(record) {
10527
+ if (record.method === "stripe" && (record.status === "pending" || record.status === "sent")) {
10528
+ return cardRefundCopy(record);
10529
+ }
9737
10530
  switch (record.status) {
9738
10531
  case "pending":
9739
10532
  if (record.method === "cashu") {
@@ -9873,26 +10666,36 @@ async function handleFund(c, deps, args) {
9873
10666
  tempoIntent: fund.method === "tempo" ? fund.intent : void 0
9874
10667
  });
9875
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
+ }
9876
10677
  if (fund.method === "tempo" && fund.intent === "charge" && !stablecoinCreditInstrumentAllowed(deps.creditConfig?.allowOneShotStablecoin, "tempo_charge")) {
9877
10678
  return jsonPaymentError("one_shot_credit_not_supported", 409, {
9878
10679
  message: "Tempo charge remains available for per-call payment, but this DVM accepts only reusable Tempo sessions for prepaid credit."
9879
10680
  });
9880
10681
  }
9881
- let requiredMsats;
9882
- try {
9883
- const fx = await deps.fxFetcher.fetch();
9884
- requiredMsats = fiatToSatsCeil(fund.amount_micro / 1e6, fxRateFor(fx, menu.currency)) * 1e3;
9885
- } catch {
9886
- return c.json(
9887
- {
9888
- error: "fx_rate_unavailable",
9889
- message: `Could not price a ${menu.currency} top-up in sats right now.`,
9890
- display: "The service can't work out the exchange rate right now. Nothing was charged.",
9891
- hint: "Retry in a few moments.",
9892
- retryable: true
9893
- },
9894
- 503
9895
- );
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
+ }
9896
10699
  }
9897
10700
  if (fund.method === "lightning") {
9898
10701
  return lightningIssue(c, deps, {
@@ -9913,7 +10716,13 @@ async function handleFund(c, deps, args) {
9913
10716
  const result = await loggers.isolate.span(
9914
10717
  "dvm.credit.fund",
9915
10718
  { required_msats: requiredMsats },
9916
- () => 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({
9917
10726
  ...railOpts,
9918
10727
  requiredMsats,
9919
10728
  priceFiat: { currency: menu.currency, amountMicro: fund.amount_micro },
@@ -10197,6 +11006,19 @@ async function checkSizing(deps, args) {
10197
11006
  extra: { min_micro: menu.min_micro, max_micro: menu.max_micro, currency: menu.currency }
10198
11007
  });
10199
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
+ }
10200
11022
  const floorSats = deps.lightning?.receive.fundingMinSats;
10201
11023
  if (args.rail === "lightning" && floorSats !== void 0) {
10202
11024
  let fx;
@@ -10224,7 +11046,7 @@ async function checkSizing(deps, args) {
10224
11046
  const held = await deps.ledger.getForCaller(args.callerPubkey);
10225
11047
  const target = await checkTargetCredit(deps, { ...args, held });
10226
11048
  if (target) return target;
10227
- 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);
10228
11050
  const fundableMicro = menu.max_micro - heldMicro;
10229
11051
  if (args.amountMicro > fundableMicro) {
10230
11052
  return jsonPaymentError("credit_over_max", 400, {
@@ -10247,6 +11069,17 @@ async function checkTargetCredit(deps, args) {
10247
11069
  message: `Credit ${args.creditId} is terminal because its Tempo backing finalized before the DVM was protected.`
10248
11070
  });
10249
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
+ }
10250
11083
  if (mine.currency !== args.menu.currency) {
10251
11084
  return jsonPaymentError("payment_invalid", 400, {
10252
11085
  message: `Credit ${args.creditId} is denominated in ${mine.currency}, not ${args.menu.currency}.`
@@ -10391,6 +11224,12 @@ var DRAIN_CONFLICT_COPY = {
10391
11224
  retryable: true,
10392
11225
  drainRegistered: false
10393
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
+ },
10394
11233
  refund_floor: {
10395
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.",
10396
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.`,
@@ -10442,9 +11281,12 @@ function drainLedgerErrorResponse(error, creditId, drainId) {
10442
11281
  return jsonPaymentError("drain_conflict", 409, {
10443
11282
  message: error.message,
10444
11283
  display: copy?.display,
10445
- hint: copy?.hint({ creditId, drainId }),
11284
+ hint: copy?.hint({ creditId, drainId, blockingDrainId: error.details.blockingDrainId }),
10446
11285
  retryable: copy?.retryable,
10447
- 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
+ }
10448
11290
  });
10449
11291
  }
10450
11292
  if (error.code === "settlement_pending") {
@@ -10457,6 +11299,9 @@ function drainLedgerErrorResponse(error, creditId, drainId) {
10457
11299
  ...error.details.blockingDrainId ? { blockingDrainId: error.details.blockingDrainId } : {}
10458
11300
  });
10459
11301
  }
11302
+ if (error.code === "credit_closed") {
11303
+ return jsonPaymentError("credit_closed", 409, { message: error.message });
11304
+ }
10460
11305
  if (error.code === "caller_mismatch" || error.code === "credit_not_found") {
10461
11306
  return jsonPaymentError("credit_not_found", 404, {
10462
11307
  message: `No credit ${creditId} for this caller.`
@@ -10472,6 +11317,7 @@ function mapLedgerCode(err) {
10472
11317
  case "insufficient_credit":
10473
11318
  case "credit_expired":
10474
11319
  case "credit_unbacked":
11320
+ case "credit_closed":
10475
11321
  case "credit_not_found":
10476
11322
  case "tempo_channel_closing":
10477
11323
  case "settlement_pending":
@@ -10496,6 +11342,8 @@ function creditBody(credit, reconciliationPending = false) {
10496
11342
  // Nominal expiry does not itself reclaim ownership; a committed sweep can remove undrawn
10497
11343
  // value.
10498
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) },
10499
11347
  ...reconciliationPending && { reconciliation_pending: true },
10500
11348
  ledger_seq: credit.lastLedgerSeq
10501
11349
  };
@@ -10912,7 +11760,7 @@ async function handle(c, opts, action) {
10912
11760
  }
10913
11761
 
10914
11762
  // src/sdk/server/hosting-settlement-report-obligations.ts
10915
- import { createHash as createHash7, randomUUID as randomUUID4 } from "crypto";
11763
+ import { createHash as createHash8, randomUUID as randomUUID4 } from "crypto";
10916
11764
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
10917
11765
  import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS3 } from "@x402/evm";
10918
11766
  import { computeChannelId as computeChannelId2 } from "@x402/evm/batch-settlement/client";
@@ -10985,7 +11833,7 @@ var TEMPO_CLOSED_EVENT = TempoSession.Precompile.escrowAbi.find(
10985
11833
  (item) => item.type === "event" && item.name === "ChannelClosed"
10986
11834
  );
10987
11835
  function hash2(value) {
10988
- return createHash7("sha256").update(JSON.stringify(value)).digest("hex");
11836
+ return createHash8("sha256").update(JSON.stringify(value)).digest("hex");
10989
11837
  }
10990
11838
  function hex(value, bytes) {
10991
11839
  return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${bytes * 2}}$`).test(value);
@@ -11803,7 +12651,7 @@ var HostedSettlementReportObligations = class {
11803
12651
  [this.dvmId, this.scope, batch.batch_id]
11804
12652
  );
11805
12653
  if (!sources.rows.length || sources.rows.some((row) => !row.tx_hash || !row.block_hash || row.delta_native === null)) return;
11806
- 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);
11807
12655
  const hashes = new Set(sources.rows.map((row) => row.tx_hash));
11808
12656
  await tx.query(
11809
12657
  `UPDATE payout_x402_batches
@@ -11904,7 +12752,7 @@ var HostedSettlementReportObligations = class {
11904
12752
  const batchId = payload.payoutId.slice("x402:batch:".length);
11905
12753
  const settle = rows2.rows.filter((row) => row.operation === "settle" && row.rail === "x402");
11906
12754
  const claims = rows2.rows.filter((row) => row.operation === "claim" || row.operation === "refund");
11907
- 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)) {
11908
12756
  throw new Error("Hosted x402 payout differs from its original finalized batch");
11909
12757
  }
11910
12758
  } else {
@@ -12373,7 +13221,7 @@ var HostingCleanupFence = class {
12373
13221
  };
12374
13222
 
12375
13223
  // src/sdk/server/hosting-channel-evidence.ts
12376
- import { createHash as createHash8 } from "crypto";
13224
+ import { createHash as createHash9 } from "crypto";
12377
13225
  import { Session as TempoSession2 } from "mppx/tempo";
12378
13226
  import { createPublicClient as createPublicClient5, http as http5 } from "viem";
12379
13227
  import { BATCH_SETTLEMENT_ADDRESS as BATCH_SETTLEMENT_ADDRESS4 } from "@x402/evm";
@@ -12424,7 +13272,7 @@ async function readHostedChannelSnapshot(query) {
12424
13272
  };
12425
13273
  const x402HistoryComplete = await history(names[6]);
12426
13274
  const tempoHistoryComplete = await history(names[7]);
12427
- const digest = createHash8("sha256").update(JSON.stringify({
13275
+ const digest = createHash9("sha256").update(JSON.stringify({
12428
13276
  present: [...exists].sort(),
12429
13277
  x402,
12430
13278
  x402Tombstones,
@@ -13547,7 +14395,7 @@ function protocolEnvelopeIgnoreFields(schema) {
13547
14395
  }
13548
14396
  function stripProtocolEnvelope(schema, data) {
13549
14397
  const withoutCredit = stripCreditEnvelope(schema, data);
13550
- if (!isRecord2(withoutCredit)) return withoutCredit;
14398
+ if (!isRecord3(withoutCredit)) return withoutCredit;
13551
14399
  const {
13552
14400
  [REQUEST_ID_FIELD]: _requestId,
13553
14401
  [AUTH_STATEMENT_FIELD]: _authStatement,
@@ -13556,13 +14404,13 @@ function stripProtocolEnvelope(schema, data) {
13556
14404
  return builderInput;
13557
14405
  }
13558
14406
  function requestIdFromEnvelope(data) {
13559
- if (!isRecord2(data) || !(REQUEST_ID_FIELD in data)) return void 0;
14407
+ if (!isRecord3(data) || !(REQUEST_ID_FIELD in data)) return void 0;
13560
14408
  return isPublicRequestId(data[REQUEST_ID_FIELD]) ? data[REQUEST_ID_FIELD] : void 0;
13561
14409
  }
13562
14410
  function hasInvalidRequestId(data) {
13563
- 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]);
13564
14412
  }
13565
- function isRecord2(value) {
14413
+ function isRecord3(value) {
13566
14414
  return typeof value === "object" && value !== null && !Array.isArray(value);
13567
14415
  }
13568
14416
 
@@ -13574,9 +14422,9 @@ import { AsyncLocalStorage } from "async_hooks";
13574
14422
  import { randomBytes as randomBytes3 } from "crypto";
13575
14423
 
13576
14424
  // src/sdk/server/instrumented-kv.ts
13577
- import { createHash as createHash9 } from "crypto";
14425
+ import { createHash as createHash10 } from "crypto";
13578
14426
  function hashKey(key) {
13579
- return createHash9("sha256").update(key).digest("hex").slice(0, 16);
14427
+ return createHash10("sha256").update(key).digest("hex").slice(0, 16);
13580
14428
  }
13581
14429
  function instrumentKVStore(inner) {
13582
14430
  return {
@@ -17190,7 +18038,7 @@ var JobManager = class {
17190
18038
  const rail = draw?.railValue.rail;
17191
18039
  const currency = draw?.railValue.creditCurrency;
17192
18040
  if (draw?.status !== "settled" || draw.amountMicro <= 0 || !currency || !rail || !isFundingRail(rail)) return;
17193
- 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;
17194
18042
  try {
17195
18043
  await callback({ jobId: record.id, rail, amountMicro: draw.amountMicro, currency, testFunds });
17196
18044
  } catch (error) {
@@ -18530,7 +19378,7 @@ function isReadUnavailable(error) {
18530
19378
  }
18531
19379
 
18532
19380
  // src/sdk/server/balance-payment.ts
18533
- import { createHash as createHash10 } from "crypto";
19381
+ import { createHash as createHash11 } from "crypto";
18534
19382
  import { z as z2 } from "zod";
18535
19383
  var BALANCE_PAYMENT_SCHEMA = z2.object({
18536
19384
  credit_id: z2.string().min(1).max(256),
@@ -18560,7 +19408,7 @@ async function commitBalancePayment(args) {
18560
19408
  args.auth.recheckSignature(BALANCE_PAYMENT_SCHEMA, args.raw, args.domain);
18561
19409
  const auth = BALANCE_PAYMENT_SCHEMA.parse(args.raw);
18562
19410
  const { pubkey, credit_id, draw_id, payment_id, request_seq, amount_micro, currency } = auth;
18563
- const fingerprint = createHash10("sha256").update(JSON.stringify([
19411
+ const fingerprint = createHash11("sha256").update(JSON.stringify([
18564
19412
  args.domain,
18565
19413
  args.jobId,
18566
19414
  pubkey,
@@ -18684,7 +19532,7 @@ async function commitBalancePayment(args) {
18684
19532
  }
18685
19533
 
18686
19534
  // src/sdk/server/app.ts
18687
- 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";
18688
19536
  import { Hono } from "hono";
18689
19537
  import { getCookie } from "hono/cookie";
18690
19538
  import { cors } from "hono/cors";
@@ -18964,7 +19812,7 @@ async function handlePendingDrains(c, opts) {
18964
19812
  const drains = pending.map((d) => {
18965
19813
  const automatic = d.method === "cashu";
18966
19814
  const amountSats = !automatic ? null : d.owedSats ?? netOwedSats(fiatToSatsCeil(d.amountMicro / 1e6, rateFor(d.currency)));
18967
- const settlement = automatic ? "automatic" : d.channelId === null ? "manual" : "channel";
19815
+ const settlement = automatic ? "automatic" : d.method === "stripe" ? "card" : d.channelId === null ? "manual" : "channel";
18968
19816
  return {
18969
19817
  credit_id: d.creditId,
18970
19818
  drain_id: d.drainId,
@@ -18980,17 +19828,19 @@ async function handlePendingDrains(c, opts) {
18980
19828
  amount_native: d.drainedNative,
18981
19829
  settlement,
18982
19830
  channel_id: d.channelId,
18983
- created_at: d.createdAt
19831
+ created_at: d.createdAt,
19832
+ ...d.method === "stripe" && { card_refund: cardRefundStatus(d) }
18984
19833
  };
18985
19834
  });
18986
19835
  const liability = {
18987
- 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),
18988
19837
  by_method_micro: DRAIN_METHODS.reduce((acc, method) => {
18989
- 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);
18990
19839
  return acc;
18991
19840
  }, {}),
18992
- manual_micro: drains.filter((d) => d.settlement === "manual").reduce((sum, d) => sum + d.amount_micro, 0),
18993
- 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)
18994
19844
  };
18995
19845
  return c.json({ drains, liability, mints: opts.mints?.() ?? [] });
18996
19846
  }
@@ -19000,7 +19850,7 @@ async function handleBitcoinLiability(c, opts) {
19000
19850
  const deposit = await opts.ledger.bitcoinDepositLiability();
19001
19851
  const pending = await opts.ledger.listPendingDrains();
19002
19852
  const promised = pending.filter((d) => d.method === "cashu" && d.status === "pending");
19003
- const drainSats = promised.reduce((sum, d) => sum + (d.owedSats ?? 0), 0);
19853
+ const drainSats = promised.reduce((sum2, d) => sum2 + (d.owedSats ?? 0), 0);
19004
19854
  const drainsUnpriced = promised.filter((d) => d.owedSats === null).length;
19005
19855
  return c.json({
19006
19856
  deposit_liability_sats: deposit.depositSats,
@@ -19337,6 +20187,7 @@ async function handleMarkDrainSent(c, opts) {
19337
20187
  409
19338
20188
  );
19339
20189
  }
20190
+ if (existing?.method === "stripe") return retryCardRefund(c, opts, existing);
19340
20191
  let record;
19341
20192
  let replayed;
19342
20193
  try {
@@ -20749,6 +21600,60 @@ function parseSerializedDleq(raw) {
20749
21600
  if (dleq.r !== void 0 && typeof dleq.r !== "string") return void 0;
20750
21601
  return { e: dleq.e, s: dleq.s, ...dleq.r !== void 0 ? { r: dleq.r } : {} };
20751
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
+ }
20752
21657
 
20753
21658
  // src/sdk/server/caller-facing-url.ts
20754
21659
  function resolvePublicOrigin(value) {
@@ -20767,6 +21672,220 @@ function callerFacingRequestUrl(requestUrl, publicOrigin) {
20767
21672
  return `${publicOrigin}${request.pathname}${request.search}`;
20768
21673
  }
20769
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
+
20770
21889
  // src/sdk/server/credit-expiry-sweep.ts
20771
21890
  function startCreditExpirySweep(ledger, opts = {}) {
20772
21891
  const sweepExpiredCredits = ledger.sweepExpiredCredits?.bind(ledger);
@@ -21740,7 +22859,7 @@ initSchemaForm();
21740
22859
  }
21741
22860
 
21742
22861
  // src/sdk/server/request-fingerprint.ts
21743
- import { createHash as createHash11 } from "crypto";
22862
+ import { createHash as createHash12 } from "crypto";
21744
22863
  function requestFingerprint(req, opts) {
21745
22864
  const canonical = canonicalize({
21746
22865
  capability: req.capability,
@@ -21748,7 +22867,7 @@ function requestFingerprint(req, opts) {
21748
22867
  params: req.params ?? {},
21749
22868
  data: opts.stripAuthEnvelope ? stripEnvelope(req.data) : req.data
21750
22869
  });
21751
- return createHash11("sha256").update(canonical).digest("hex");
22870
+ return createHash12("sha256").update(canonical).digest("hex");
21752
22871
  }
21753
22872
  var ENVELOPE_KEYS = /* @__PURE__ */ new Set(["pubkey", "signature", "timestamp", "nonce", "auth_statement"]);
21754
22873
  function stripEnvelope(data) {
@@ -21786,6 +22905,7 @@ async function createDVMServer(descriptor, opts) {
21786
22905
  let payoutReporter;
21787
22906
  let unsubscribeTempoSettlementReadiness;
21788
22907
  let creditExpirySweep;
22908
+ let cardRefundSweep;
21789
22909
  let x402ChannelStorage;
21790
22910
  let x402BatchSettlement;
21791
22911
  let server;
@@ -21818,6 +22938,7 @@ async function createDVMServer(descriptor, opts) {
21818
22938
  await capture(() => payoutReporter?.stop());
21819
22939
  await capture(() => revenueReporter?.stop());
21820
22940
  await capture(() => creditExpirySweep?.stop());
22941
+ await capture(() => cardRefundSweep?.stop());
21821
22942
  await capture(() => unsubscribeTempoSettlementReadiness?.());
21822
22943
  if (settlementStopped) await capture(() => settlementStopped);
21823
22944
  if (x402ChannelStorage) {
@@ -21871,7 +22992,7 @@ async function createDVMServer(descriptor, opts) {
21871
22992
  }
21872
22993
  }
21873
22994
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
21874
- const { RevenueReporter } = await import("./revenue-reporter-QKCBYFWT.js");
22995
+ const { RevenueReporter } = await import("./revenue-reporter-FD3GDQZS.js");
21875
22996
  const reporter = new RevenueReporter(
21876
22997
  opts.db,
21877
22998
  platformUrl,
@@ -21881,7 +23002,7 @@ async function createDVMServer(descriptor, opts) {
21881
23002
  revenueReporter = reporter;
21882
23003
  await reporter.init();
21883
23004
  reporter.startRetryLoop();
21884
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-D5TCIBQA.js");
23005
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-KTLBNJJ7.js");
21885
23006
  payoutReporter = new PayoutReporter({
21886
23007
  dvmId,
21887
23008
  db: opts.db,
@@ -22087,6 +23208,18 @@ async function createDVMServer(descriptor, opts) {
22087
23208
  payoutReporter
22088
23209
  });
22089
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
+ }
22090
23223
  return {
22091
23224
  app: server.app,
22092
23225
  authAudience: server.authAudience,
@@ -22137,6 +23270,10 @@ var DVMServer = class {
22137
23270
  tempoSweepLastAttemptMs = 0;
22138
23271
  receiptIssuer;
22139
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
+ }
22140
23277
  processedPayments;
22141
23278
  authAudience;
22142
23279
  /** Lazily constructed when a refreshed facilitator probe gains batch support. */
@@ -22582,6 +23719,7 @@ var DVMServer = class {
22582
23719
  // DVM wired with an in-memory session store resolves to a rail with no
22583
23720
  // close-intent read and the verbs answer 503.
22584
23721
  tempoDrainRail: () => Promise.resolve(this.opts.mpp),
23722
+ ...this.opts.stripeCredit && { stripeRefunds: this.opts.stripeCredit.refunds },
22585
23723
  ...this.opts.hostedSettlementReports ? {
22586
23724
  hostedSettlementReports: { store: this.opts.hostedSettlementReports }
22587
23725
  } : {},
@@ -22823,7 +23961,7 @@ var DVMServer = class {
22823
23961
  const dvmId = this.opts.dvmId ?? `dev:${this.descriptor.name}`;
22824
23962
  return {
22825
23963
  dvm_id: dvmId,
22826
- builder_pubkey: createHash12("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
23964
+ builder_pubkey: createHash13("sha256").update(`dvmkit-dev:${dvmId}`).digest("hex")
22827
23965
  };
22828
23966
  }
22829
23967
  throw new AuthAudienceError();
@@ -22925,6 +24063,7 @@ var DVMServer = class {
22925
24063
  methods.push("x402");
22926
24064
  }
22927
24065
  if (this.opts.lightningReceive?.available()) methods.push("lightning");
24066
+ if (this.opts.stripeCredit) methods.push("stripe");
22928
24067
  return methods;
22929
24068
  }
22930
24069
  creditMppMethods() {
@@ -23133,6 +24272,7 @@ var DVMServer = class {
23133
24272
  tempoWithheld: mppSplit.withheld,
23134
24273
  x402Schemes: this.creditX402Schemes(),
23135
24274
  lightningFundingMinSats: this.opts.lightningReceive?.fundingMinSats,
24275
+ stripeNetworkId: this.opts.stripeCredit?.networkId,
23136
24276
  // Configured, not advertised: `advertisedMints()` is health
23137
24277
  // filtered, and a rail that comes and goes with a mint's uptime is worse
23138
24278
  // product than one that is honestly absent.
@@ -23184,7 +24324,7 @@ var DVMServer = class {
23184
24324
  const paymentAuthorization = ctx.req.header("Authorization");
23185
24325
  const mppArtifact = paymentAuthorization ? (await import("mppx")).Credential.extractPaymentScheme(paymentAuthorization) : void 0;
23186
24326
  const attachedPayment = ctx.req.header("X-Cashu") ?? ctx.req.header("PAYMENT-SIGNATURE") ?? ctx.req.header("X-PAYMENT") ?? mppArtifact;
23187
- const paymentArtifactHash = attachedPayment ? createHash12("sha256").update(attachedPayment).digest("hex") : void 0;
24327
+ const paymentArtifactHash = attachedPayment ? createHash13("sha256").update(attachedPayment).digest("hex") : void 0;
23188
24328
  hostingFundingArtifactHash = paymentArtifactHash;
23189
24329
  if (!await control.admit(admissionId, workId, {
23190
24330
  creditId,
@@ -23223,7 +24363,8 @@ var DVMServer = class {
23223
24363
  await control.finishAdmission(admissionId, outcome);
23224
24364
  }
23225
24365
  } : void 0,
23226
- tempoSettlementReadiness: this.opts.tempoSettlementReadiness
24366
+ tempoSettlementReadiness: this.opts.tempoSettlementReadiness,
24367
+ stripeCredit: this.opts.stripeCredit
23227
24368
  });
23228
24369
  }
23229
24370
  /**
@@ -24044,7 +25185,7 @@ var DVMServer = class {
24044
25185
  const authorization = c.req.header("Authorization");
24045
25186
  const mppArtifact = authorization ? (await import("mppx")).Credential.extractPaymentScheme(authorization) : void 0;
24046
25187
  const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? mppArtifact;
24047
- const hostingPaymentArtifactHash = attachedPayment ? createHash12("sha256").update(attachedPayment).digest("hex") : void 0;
25188
+ const hostingPaymentArtifactHash = attachedPayment ? createHash13("sha256").update(attachedPayment).digest("hex") : void 0;
24048
25189
  if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
24049
25190
  const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
24050
25191
  hostingPaymentArtifactHash
@@ -24368,7 +25509,7 @@ var DVMServer = class {
24368
25509
  }
24369
25510
  const isAnonymous = requesterId === "anonymous" || requesterId === "dev-anonymous";
24370
25511
  const jobToken = isAnonymous ? randomBytes5(32).toString("hex") : void 0;
24371
- const requesterTokenHash = jobToken ? createHash12("sha256").update(jobToken).digest("hex") : void 0;
25512
+ const requesterTokenHash = jobToken ? createHash13("sha256").update(jobToken).digest("hex") : void 0;
24372
25513
  const job = jm.createJob(
24373
25514
  normalisedBody,
24374
25515
  requesterId,
@@ -25226,7 +26367,7 @@ function isJobOwner(c, job) {
25226
26367
  if (job.requesterTokenHash) {
25227
26368
  const token = c.req.header("X-Job-Token");
25228
26369
  if (!token) return false;
25229
- const provided = createHash12("sha256").update(token).digest("hex");
26370
+ const provided = createHash13("sha256").update(token).digest("hex");
25230
26371
  return timingSafeEqualHex(provided, job.requesterTokenHash);
25231
26372
  }
25232
26373
  return c.get("requesterId") === job.requesterId;
@@ -25780,11 +26921,14 @@ export {
25780
26921
  extractCreditEnvelope,
25781
26922
  creditEnvelopeIgnoreFields,
25782
26923
  stripCreditEnvelope,
26924
+ STRIPE_CREDIT_MIN_MICRO,
25783
26925
  buildCreditMenu,
25784
26926
  selectPrimaryCredit,
25785
26927
  toCreditView,
25786
26928
  attachCreditMenu,
25787
26929
  toCreditTerms,
26930
+ fiatMicroToMethodAmount,
26931
+ createStripeRefundApi,
25788
26932
  callerInputIssues,
25789
26933
  resolvePriceFiat,
25790
26934
  priceFiatMicro,