@devlab.group/agent-commerce 2.4.0 → 2.5.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 (108) hide show
  1. package/README.md +115 -15
  2. package/dist/ap2.d.ts +24 -12
  3. package/dist/ap2.js +31 -13
  4. package/dist/ap2.js.map +1 -1
  5. package/dist/{chunk-AITMDXKW.js → chunk-3OTCFFFT.js} +37 -4
  6. package/dist/chunk-3OTCFFFT.js.map +1 -0
  7. package/dist/{chunk-4OEXK3YG.js → chunk-4K7FIBJM.js} +25 -4
  8. package/dist/chunk-4K7FIBJM.js.map +1 -0
  9. package/dist/{chunk-WUIAUAA7.js → chunk-FKQK5XZA.js} +652 -68
  10. package/dist/chunk-FKQK5XZA.js.map +1 -0
  11. package/dist/chunk-GA3O2A7R.js +92 -0
  12. package/dist/chunk-GA3O2A7R.js.map +1 -0
  13. package/dist/{chunk-6WACRJVT.js → chunk-VUCMBGUR.js} +3 -3
  14. package/dist/{chunk-6WACRJVT.js.map → chunk-VUCMBGUR.js.map} +1 -1
  15. package/dist/cli/{ap2-QDHPRSTF.js → ap2-YAPLERSC.js} +34 -15
  16. package/dist/cli/ap2-YAPLERSC.js.map +1 -0
  17. package/dist/cli/{chunk-G2JEY7MB.js → chunk-2J2IQQ4X.js} +8 -7
  18. package/dist/cli/chunk-2J2IQQ4X.js.map +1 -0
  19. package/dist/cli/chunk-2UPPX3FJ.js +311 -0
  20. package/dist/cli/chunk-2UPPX3FJ.js.map +1 -0
  21. package/dist/cli/{chunk-WYOGIANL.js → chunk-4H2QLHQI.js} +4 -4
  22. package/dist/cli/{chunk-WYOGIANL.js.map → chunk-4H2QLHQI.js.map} +1 -1
  23. package/dist/cli/{chunk-OZ4LAWBK.js → chunk-5R7DM5LM.js} +25 -4
  24. package/dist/cli/chunk-5R7DM5LM.js.map +1 -0
  25. package/dist/cli/chunk-663ALDZA.js +92 -0
  26. package/dist/cli/chunk-663ALDZA.js.map +1 -0
  27. package/dist/cli/{chunk-QQ3WY5L3.js → chunk-IDY2735M.js} +36 -104
  28. package/dist/cli/chunk-IDY2735M.js.map +1 -0
  29. package/dist/cli/chunk-JFVRG2HH.js +107 -0
  30. package/dist/cli/chunk-JFVRG2HH.js.map +1 -0
  31. package/dist/cli/chunk-M7NXVRU5.js +8 -0
  32. package/dist/cli/{chunk-UXB65XCS.js.map → chunk-M7NXVRU5.js.map} +1 -1
  33. package/dist/cli/chunk-NIU5EMSB.js +119 -0
  34. package/dist/cli/chunk-NIU5EMSB.js.map +1 -0
  35. package/dist/cli/{chunk-AZDWFVDW.js → chunk-QA2OPHBF.js} +655 -70
  36. package/dist/cli/chunk-QA2OPHBF.js.map +1 -0
  37. package/dist/cli/{chunk-DCMW67JM.js → chunk-QLUCNPRT.js} +404 -161
  38. package/dist/cli/chunk-QLUCNPRT.js.map +1 -0
  39. package/dist/cli/chunk-SYQLJXIU.js +105 -0
  40. package/dist/cli/chunk-SYQLJXIU.js.map +1 -0
  41. package/dist/cli/{chunk-3MGAJ5FX.js → chunk-VJ4EO4LE.js} +3 -3
  42. package/dist/cli/{chunk-3MGAJ5FX.js.map → chunk-VJ4EO4LE.js.map} +1 -1
  43. package/dist/cli/{chunk-PIMFPCNZ.js → chunk-Z34UMZBB.js} +234 -85
  44. package/dist/cli/chunk-Z34UMZBB.js.map +1 -0
  45. package/dist/cli/config-BWFMN2ZB.js +12 -0
  46. package/dist/cli/{config-UI4EDG4L.js.map → config-BWFMN2ZB.js.map} +1 -1
  47. package/dist/cli/index.js +258 -11
  48. package/dist/cli/index.js.map +1 -1
  49. package/dist/cli/{mcp-26LJMN4N.js → mcp-KZEMXLHR.js} +5 -4
  50. package/dist/cli/mcp-KZEMXLHR.js.map +1 -0
  51. package/dist/cli/{near-5AY3HWYF.js → near-U6365VFX.js} +15 -4
  52. package/dist/cli/near-U6365VFX.js.map +1 -0
  53. package/dist/cli/payment-providers-SZL36H3Y.js +6 -0
  54. package/dist/cli/payment-providers-SZL36H3Y.js.map +1 -0
  55. package/dist/cli/{provider-Z4OJNAYI.js → provider-6TKB5ADJ.js} +30 -16
  56. package/dist/cli/provider-6TKB5ADJ.js.map +1 -0
  57. package/dist/cli/receipts-ZGLJWMQG.js +8 -0
  58. package/dist/cli/receipts-ZGLJWMQG.js.map +1 -0
  59. package/dist/cli/reconcile-IC22MJD5.js +4 -0
  60. package/dist/cli/reconcile-IC22MJD5.js.map +1 -0
  61. package/dist/cli/{run-TEPYJUHG.js → run-ZTYPQI4D.js} +151 -115
  62. package/dist/cli/run-ZTYPQI4D.js.map +1 -0
  63. package/dist/cli/svm-KDSPDFJG.js +48 -0
  64. package/dist/cli/svm-KDSPDFJG.js.map +1 -0
  65. package/dist/cli/{x402-BMVONZKA.js → x402-WLYBLODI.js} +10 -9
  66. package/dist/cli/x402-WLYBLODI.js.map +1 -0
  67. package/dist/index.d.ts +112 -12
  68. package/dist/index.js +948 -157
  69. package/dist/index.js.map +1 -1
  70. package/dist/mcp.d.ts +1 -2
  71. package/dist/mcp.js +1 -1
  72. package/dist/mpp.js +24 -11
  73. package/dist/mpp.js.map +1 -1
  74. package/dist/{near-D6C42VPE.js → near-FH7HPZV3.js} +15 -4
  75. package/dist/near-FH7HPZV3.js.map +1 -0
  76. package/dist/{protocol-adapter-C9odYl6l.d.ts → protocol-adapter-DEXpdp8m.d.ts} +102 -48
  77. package/dist/{runtime-Cl_BLVZV.d.ts → runtime-Dz2fSO-P.d.ts} +220 -2
  78. package/dist/svm-KNC3PHMS.js +48 -0
  79. package/dist/svm-KNC3PHMS.js.map +1 -0
  80. package/dist/x402.js +5 -5
  81. package/package.json +1 -1
  82. package/dist/authorization-DjAecwuW.d.ts +0 -115
  83. package/dist/chunk-4OEXK3YG.js.map +0 -1
  84. package/dist/chunk-AITMDXKW.js.map +0 -1
  85. package/dist/chunk-KA2AX5FH.js +0 -44
  86. package/dist/chunk-KA2AX5FH.js.map +0 -1
  87. package/dist/chunk-WUIAUAA7.js.map +0 -1
  88. package/dist/cli/ap2-QDHPRSTF.js.map +0 -1
  89. package/dist/cli/chunk-AZDWFVDW.js.map +0 -1
  90. package/dist/cli/chunk-DCMW67JM.js.map +0 -1
  91. package/dist/cli/chunk-G2JEY7MB.js.map +0 -1
  92. package/dist/cli/chunk-OSTCU6ZK.js +0 -44
  93. package/dist/cli/chunk-OSTCU6ZK.js.map +0 -1
  94. package/dist/cli/chunk-OZ4LAWBK.js.map +0 -1
  95. package/dist/cli/chunk-PIMFPCNZ.js.map +0 -1
  96. package/dist/cli/chunk-QQ3WY5L3.js.map +0 -1
  97. package/dist/cli/chunk-UXB65XCS.js +0 -8
  98. package/dist/cli/config-UI4EDG4L.js +0 -11
  99. package/dist/cli/mcp-26LJMN4N.js.map +0 -1
  100. package/dist/cli/near-5AY3HWYF.js.map +0 -1
  101. package/dist/cli/provider-Z4OJNAYI.js.map +0 -1
  102. package/dist/cli/run-TEPYJUHG.js.map +0 -1
  103. package/dist/cli/svm-3MMYFPC7.js +0 -33
  104. package/dist/cli/svm-3MMYFPC7.js.map +0 -1
  105. package/dist/cli/x402-BMVONZKA.js.map +0 -1
  106. package/dist/near-D6C42VPE.js.map +0 -1
  107. package/dist/svm-QEUAUNPQ.js +0 -33
  108. package/dist/svm-QEUAUNPQ.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,18 +1,18 @@
1
- import { openSqliteDatabase, AP2_SPEC_VERSION, AP2_MODES, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_JWK_MEMBERS, AP2_KEY_TYPE, AP2_SIGNING_ALGORITHM, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-6WACRJVT.js';
1
+ import { openSqliteDatabase, AP2_SPEC_VERSION, AP2_MODES, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_JWK_MEMBERS, AP2_KEY_TYPE, AP2_SIGNING_ALGORITHM, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-VUCMBGUR.js';
2
2
  import { readCappedBody, createBearerCheck, MCP_TOOL_NAME_PATTERN, settlementResponse, joinUrl, settlementResponseFromDetails, settlementFailure, x402ErrorCode, x402RefusalStatus } from './chunk-ES3V6ECS.js';
3
3
  import { MPP_DEFAULT_NETWORK, isMppNetwork, MPP_NETWORKS, MPP_DEFAULT_PAYMENT_FLOW, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPP_PROFILE } from './chunk-6Z2H53CG.js';
4
4
  import { contentDigest, CONTENT_DIGEST_METADATA_KEY, mppProblem, mppProblemDetail } from './chunk-5ZYXBCDX.js';
5
- import { X402_PAYMENT_FLOWS, validateCustomNetworks, findX402Network, X402_DEFAULT_PAYMENT_FLOW, resolveX402Deployment, SETTLE_AFTER_BACKEND_METADATA_KEY, resolveDeploymentMode, requireX402Network, resolveX402Option, isEvmAddress, isEvmZeroAddress, x402OfferName, REUSE_REJECTED_AUTHORIZATION_METADATA_KEY } from './chunk-4OEXK3YG.js';
6
- import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_METHOD_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, isCommerceError, describeError, isHttpProtocolAdapter, toLogInfo, extractReservedInputFields, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, toErrorEnvelope, DELIVERY_SUMMARY_META_KEY, toDeliverySummary, PAYMENT_INPUT_FIELD, AUTHORIZATION_INPUT_FIELD, parseAuthorizationSubmission } from './chunk-AITMDXKW.js';
7
- export { AUTHORIZATION_HEADER, AUTHORIZATION_INPUT_FIELD, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, MAX_AUTHORIZATION_HEADER_BYTES, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_METHOD_NAMES, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PROTOCOL_NAMES, RESERVED_INPUT_FIELDS, RETRYABLE_ERROR_CODES, extractReservedInputFields, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, parseAuthorizationHeader, parseAuthorizationSubmission, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-AITMDXKW.js';
5
+ import { X402_PAYMENT_FLOWS, validateCustomNetworks, findX402Network, X402_DEFAULT_PAYMENT_FLOW, resolveX402Deployment, resolveDeploymentMode, requireX402Network, resolveX402Option, isEvmAddress, isEvmZeroAddress, SETTLE_AFTER_BACKEND_METADATA_KEY, x402OfferName, REUSE_REJECTED_AUTHORIZATION_METADATA_KEY, canonicalJson } from './chunk-4K7FIBJM.js';
6
+ import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_METHOD_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, CommerceError, systemClock, NOOP_LOGGER, transactionState, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, isCommerceError, describeError, isHttpProtocolAdapter, toLogInfo, extractReservedInputFields, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, toErrorEnvelope, DELIVERY_SUMMARY_META_KEY, toDeliverySummary, PAYMENT_INPUT_FIELD, AUTHORIZATION_INPUT_FIELD, parseAuthorizationSubmission, RECONCILIATION_METADATA_KEY, TRANSACTION_STATES, redactedErrorText } from './chunk-3OTCFFFT.js';
7
+ export { AUTHORIZATION_HEADER, AUTHORIZATION_INPUT_FIELD, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, MAX_AUTHORIZATION_HEADER_BYTES, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_METHOD_NAMES, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PROTOCOL_NAMES, RECONCILIATION_METADATA_KEY, RESERVED_INPUT_FIELDS, RETRYABLE_ERROR_CODES, TRANSACTION_STATES, extractReservedInputFields, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, parseAuthorizationHeader, parseAuthorizationSubmission, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope, transactionState } from './chunk-3OTCFFFT.js';
8
8
  import { isRecord } from './chunk-2CJNA7MS.js';
9
9
  import { promises, accessSync, constants } from 'fs';
10
10
  import * as path from 'path';
11
11
  import { parse, YAMLError } from 'yaml';
12
12
  import { z } from 'zod';
13
+ import { randomUUID, createHash } from 'crypto';
13
14
  import { isDeepStrictEqual } from 'util';
14
15
  import Fastify from 'fastify';
15
- import { randomUUID, createHash } from 'crypto';
16
16
  import { createRequire } from 'module';
17
17
  import pino from 'pino';
18
18
  import { Transform } from 'stream';
@@ -1169,7 +1169,7 @@ var TRAVERSAL_PATH_VALUES = /* @__PURE__ */ new Set(["", ".", ".."]);
1169
1169
  function applyPathTemplate(template, input, context) {
1170
1170
  const remaining = { ...input };
1171
1171
  let missing;
1172
- let invalid2;
1172
+ let invalid3;
1173
1173
  const url = template.replace(PATH_PARAM_PATTERN, (_match, key) => {
1174
1174
  if (!Object.hasOwn(remaining, key)) {
1175
1175
  missing = key;
@@ -1179,7 +1179,7 @@ function applyPathTemplate(template, input, context) {
1179
1179
  delete remaining[key];
1180
1180
  const raw = stringifyPrimitive(value);
1181
1181
  if (TRAVERSAL_PATH_VALUES.has(raw)) {
1182
- invalid2 = key;
1182
+ invalid3 = key;
1183
1183
  return "";
1184
1184
  }
1185
1185
  return encodeURIComponent(raw);
@@ -1190,10 +1190,10 @@ function applyPathTemplate(template, input, context) {
1190
1190
  details: { field: missing }
1191
1191
  });
1192
1192
  }
1193
- if (invalid2 !== void 0) {
1194
- throw new CommerceError("INPUT_INVALID", `Path parameter "${invalid2}" is not a valid value`, {
1193
+ if (invalid3 !== void 0) {
1194
+ throw new CommerceError("INPUT_INVALID", `Path parameter "${invalid3}" is not a valid value`, {
1195
1195
  ...context,
1196
- details: { field: invalid2 }
1196
+ details: { field: invalid3 }
1197
1197
  });
1198
1198
  }
1199
1199
  return { url, remaining };
@@ -1446,6 +1446,56 @@ function safeStringify(value) {
1446
1446
  function isAbortError(error) {
1447
1447
  return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
1448
1448
  }
1449
+ function createFlow(request, transactionId) {
1450
+ return { request, transactionId, persistenceFailures: [], journaled: false };
1451
+ }
1452
+ function failuresOf(flow) {
1453
+ return flow.persistenceFailures.length > 0 ? { persistenceFailures: [...flow.persistenceFailures] } : {};
1454
+ }
1455
+ function sha256(text) {
1456
+ return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
1457
+ }
1458
+ function inputDigest(input) {
1459
+ const json = canonicalJson(input);
1460
+ return json !== void 0 ? sha256(json) : void 0;
1461
+ }
1462
+ function configDigest(resource, requirement) {
1463
+ const terms = {
1464
+ resourceId: resource.id,
1465
+ pricing: resource.pricing,
1466
+ paymentMethods: resource.paymentMethods,
1467
+ authorization: resource.authorization?.required ?? [],
1468
+ requirement: {
1469
+ provider: requirement.provider,
1470
+ amount: requirement.amount,
1471
+ currency: requirement.currency,
1472
+ destination: requirement.destination ?? null,
1473
+ network: requirement.network ?? null,
1474
+ asset: requirement.asset ?? null,
1475
+ settleAfterBackend: requirement.metadata?.[SETTLE_AFTER_BACKEND_METADATA_KEY] === true
1476
+ }
1477
+ };
1478
+ return sha256(canonicalJson(terms) ?? JSON.stringify(terms));
1479
+ }
1480
+ function newTransaction(flow, resource, requirement, verification, validInput, authorization) {
1481
+ const raw = verification.metadata?.[RECONCILIATION_METADATA_KEY];
1482
+ const locator = isRecord(raw) ? raw : void 0;
1483
+ const expiresAt = typeof locator?.["expiresAt"] === "string" ? locator["expiresAt"] : void 0;
1484
+ const network = verification.network ?? requirement.network;
1485
+ const asset = verification.asset ?? requirement.asset;
1486
+ const digest = inputDigest(validInput);
1487
+ return {
1488
+ id: flow.transactionId,
1489
+ protocol: flow.request.protocol,
1490
+ ...network !== void 0 ? { network } : {},
1491
+ ...asset !== void 0 ? { asset } : {},
1492
+ ...locator !== void 0 ? { locator } : {},
1493
+ ...expiresAt !== void 0 ? { expiresAt } : {},
1494
+ ...authorization !== void 0 ? { authorization } : {},
1495
+ ...digest !== void 0 ? { inputDigest: digest } : {},
1496
+ configDigest: configDigest(resource, requirement)
1497
+ };
1498
+ }
1449
1499
  var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
1450
1500
  "object",
1451
1501
  "string",
@@ -1588,16 +1638,15 @@ function resolveAuthorizationProviders(providers, resource, requestId) {
1588
1638
  }
1589
1639
  function createAuthorizeAndReserve(ctx) {
1590
1640
  const { options, safeEmit, buildEvent } = ctx;
1591
- async function authorizeAndReserve(request, resource, providers, requirement, input) {
1641
+ async function authorizeAndReserve(flow, request, resource, providers, requirement, input) {
1592
1642
  if (providers.length === 0) return void 0;
1593
1643
  const submission = request.authorization;
1594
1644
  const provider = providers.find((candidate) => candidate.name === submission?.method);
1595
1645
  const unmet = providers.filter((candidate) => candidate !== provider).map((c) => c.name);
1596
1646
  if (submission === void 0 || provider === void 0 || unmet.length > 0) {
1597
1647
  await safeEmit(
1598
- buildEvent({
1648
+ buildEvent(flow, {
1599
1649
  type: "authorization.rejected",
1600
- requestId: request.requestId,
1601
1650
  resourceId: resource.id,
1602
1651
  adapter: request.protocol,
1603
1652
  status: "error",
@@ -1634,9 +1683,8 @@ function createAuthorizeAndReserve(ctx) {
1634
1683
  }
1635
1684
  );
1636
1685
  await safeEmit(
1637
- buildEvent({
1686
+ buildEvent(flow, {
1638
1687
  type: "authorization.rejected",
1639
- requestId: request.requestId,
1640
1688
  resourceId: resource.id,
1641
1689
  adapter: request.protocol,
1642
1690
  status: "error",
@@ -1646,9 +1694,8 @@ function createAuthorizeAndReserve(ctx) {
1646
1694
  throw mapped;
1647
1695
  }
1648
1696
  await safeEmit(
1649
- buildEvent({
1697
+ buildEvent(flow, {
1650
1698
  type: "authorization.verified",
1651
- requestId: request.requestId,
1652
1699
  resourceId: resource.id,
1653
1700
  adapter: request.protocol,
1654
1701
  status: "ok",
@@ -1662,14 +1709,17 @@ function createAuthorizeAndReserve(ctx) {
1662
1709
  reference: verification.reference,
1663
1710
  ...verification.metadata !== void 0 ? { metadata: verification.metadata } : {}
1664
1711
  },
1712
+ reservationId: verification.reservationId,
1665
1713
  async finalize(action) {
1666
1714
  try {
1667
1715
  await provider[action](verification.reservationId, context);
1716
+ return true;
1668
1717
  } catch (error) {
1669
1718
  options.logger.error(
1670
1719
  { err: describeError(error), requestId: request.requestId, action },
1671
1720
  "Authorization finalization failed; the reservation stays reserved"
1672
1721
  );
1722
+ return false;
1673
1723
  }
1674
1724
  }
1675
1725
  };
@@ -1710,7 +1760,7 @@ function replayError(error, requirement) {
1710
1760
  // src/core/execution/pipeline/admission.ts
1711
1761
  function createPaymentAdmission(ctx, authorizeAndReserve) {
1712
1762
  const { options, safeEmit, buildEvent } = ctx;
1713
- return async function admitPayment(request, resource, pricing, validInput) {
1763
+ return async function admitPayment(flow, request, resource, pricing, validInput) {
1714
1764
  const provider = pickProvider(options.paymentProviders, resource.paymentMethods);
1715
1765
  if (!provider) {
1716
1766
  throw new CommerceError(
@@ -1749,9 +1799,8 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1749
1799
  }
1750
1800
  if (!request.payment) {
1751
1801
  await safeEmit(
1752
- buildEvent({
1802
+ buildEvent(flow, {
1753
1803
  type: "payment.required",
1754
- requestId: request.requestId,
1755
1804
  resourceId: resource.id,
1756
1805
  adapter: request.protocol,
1757
1806
  paymentProvider: provider.name
@@ -1760,6 +1809,7 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1760
1809
  return {
1761
1810
  kind: "payment-required",
1762
1811
  requestId: request.requestId,
1812
+ transactionId: flow.transactionId,
1763
1813
  resourceId: resource.id,
1764
1814
  requirement,
1765
1815
  ...authProviders.length > 0 ? { authorization: authProviders.map((p) => p.requirement) } : {}
@@ -1790,9 +1840,8 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1790
1840
  ...retryChallenge(requirement)
1791
1841
  });
1792
1842
  await safeEmit(
1793
- buildEvent({
1843
+ buildEvent(flow, {
1794
1844
  type: "payment.rejected",
1795
- requestId: request.requestId,
1796
1845
  resourceId: resource.id,
1797
1846
  adapter: request.protocol,
1798
1847
  paymentProvider: provider.name,
@@ -1804,9 +1853,8 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1804
1853
  }
1805
1854
  if (verification.status !== "verified") {
1806
1855
  await safeEmit(
1807
- buildEvent({
1856
+ buildEvent(flow, {
1808
1857
  type: "payment.rejected",
1809
- requestId: request.requestId,
1810
1858
  resourceId: resource.id,
1811
1859
  adapter: request.protocol,
1812
1860
  paymentProvider: provider.name,
@@ -1826,9 +1874,8 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1826
1874
  }
1827
1875
  if (!verification.replayKey) {
1828
1876
  await safeEmit(
1829
- buildEvent({
1877
+ buildEvent(flow, {
1830
1878
  type: "payment.rejected",
1831
- requestId: request.requestId,
1832
1879
  resourceId: resource.id,
1833
1880
  adapter: request.protocol,
1834
1881
  paymentProvider: provider.name,
@@ -1843,6 +1890,7 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1843
1890
  }
1844
1891
  const replayKey = verification.replayKey;
1845
1892
  const hold = await authorizeAndReserve(
1893
+ flow,
1846
1894
  request,
1847
1895
  resource,
1848
1896
  authProviders,
@@ -1850,7 +1898,7 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1850
1898
  validInput
1851
1899
  );
1852
1900
  try {
1853
- await options.store.reservePaymentAttempt({
1901
+ const attempt = await options.store.reservePaymentAttempt({
1854
1902
  requestId: request.requestId,
1855
1903
  resourceId: resource.id,
1856
1904
  provider: provider.name,
@@ -1859,8 +1907,17 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1859
1907
  currency: verification.currency,
1860
1908
  ...verification.payer !== void 0 ? { payer: verification.payer } : {},
1861
1909
  ...verification.payee !== void 0 ? { payee: verification.payee } : {},
1862
- ...requirement.metadata?.[REUSE_REJECTED_AUTHORIZATION_METADATA_KEY] === true ? { reclaimRejected: true } : {}
1910
+ ...requirement.metadata?.[REUSE_REJECTED_AUTHORIZATION_METADATA_KEY] === true ? { reclaimRejected: true } : {},
1911
+ transaction: newTransaction(
1912
+ flow,
1913
+ resource,
1914
+ requirement,
1915
+ verification,
1916
+ validInput,
1917
+ hold !== void 0 ? { ...hold.record, reservationId: hold.reservationId } : void 0
1918
+ )
1863
1919
  });
1920
+ flow.journaled = attempt.transactionId === flow.transactionId && options.store.recordTransactionStep !== void 0;
1864
1921
  } catch (error) {
1865
1922
  const mapped = toCommerceError(
1866
1923
  error,
@@ -1869,9 +1926,8 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1869
1926
  );
1870
1927
  await hold?.finalize("release");
1871
1928
  await safeEmit(
1872
- buildEvent({
1929
+ buildEvent(flow, {
1873
1930
  type: "payment.rejected",
1874
- requestId: request.requestId,
1875
1931
  resourceId: resource.id,
1876
1932
  adapter: request.protocol,
1877
1933
  paymentProvider: provider.name,
@@ -1882,9 +1938,8 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1882
1938
  throw replayError(mapped, requirement);
1883
1939
  }
1884
1940
  await safeEmit(
1885
- buildEvent({
1941
+ buildEvent(flow, {
1886
1942
  type: "payment.verified",
1887
- requestId: request.requestId,
1888
1943
  resourceId: resource.id,
1889
1944
  adapter: request.protocol,
1890
1945
  paymentProvider: provider.name,
@@ -1894,6 +1949,7 @@ function createPaymentAdmission(ctx, authorizeAndReserve) {
1894
1949
  return {
1895
1950
  kind: "admitted",
1896
1951
  settleArgs: {
1952
+ flow,
1897
1953
  request,
1898
1954
  resource,
1899
1955
  provider,
@@ -1915,6 +1971,11 @@ function pickProvider(providers, methods) {
1915
1971
  }
1916
1972
 
1917
1973
  // src/core/execution/pipeline/context.ts
1974
+ var HOLD_STATES = {
1975
+ consume: "consumed",
1976
+ release: "released",
1977
+ markUncertain: "uncertain"
1978
+ };
1918
1979
  function createPipelineContext(options) {
1919
1980
  async function safeEmit(event) {
1920
1981
  try {
@@ -1926,27 +1987,73 @@ function createPipelineContext(options) {
1926
1987
  );
1927
1988
  }
1928
1989
  }
1929
- async function safePersist(op, label, requestId) {
1990
+ async function safePersist(flow, op, label, failure) {
1930
1991
  try {
1931
1992
  await op();
1993
+ return true;
1932
1994
  } catch (error) {
1933
1995
  options.logger.error(
1934
- { err: describeError(error), requestId },
1996
+ {
1997
+ err: describeError(error),
1998
+ requestId: flow.request.requestId,
1999
+ transactionId: flow.transactionId
2000
+ },
1935
2001
  `Persistence failed: ${label}`
1936
2002
  );
2003
+ if (failure !== void 0 && !flow.persistenceFailures.includes(failure)) {
2004
+ flow.persistenceFailures.push(failure);
2005
+ }
2006
+ return false;
1937
2007
  }
1938
2008
  }
1939
- function buildEvent(partial) {
1940
- return { id: options.ids.next("evt"), at: options.clock.nowIso(), ...partial };
2009
+ async function recordStep(flow, step) {
2010
+ const record = options.store.recordTransactionStep;
2011
+ if (!flow.journaled || record === void 0) return;
2012
+ const full = {
2013
+ ...step,
2014
+ transactionId: flow.transactionId,
2015
+ at: options.clock.nowIso()
2016
+ };
2017
+ await safePersist(
2018
+ flow,
2019
+ () => record.call(options.store, full),
2020
+ `recordTransactionStep(${step.kind})`,
2021
+ "journal"
2022
+ );
2023
+ }
2024
+ async function finalizeHold(flow, hold, action) {
2025
+ if (hold !== void 0 && await hold.finalize(action)) {
2026
+ await recordStep(flow, { kind: "hold", hold: HOLD_STATES[action] });
2027
+ }
1941
2028
  }
1942
- return { options, safeEmit, safePersist, buildEvent };
2029
+ function buildEvent(flow, partial) {
2030
+ return {
2031
+ id: options.ids.next("evt"),
2032
+ at: options.clock.nowIso(),
2033
+ requestId: flow.request.requestId,
2034
+ transactionId: flow.transactionId,
2035
+ ...partial
2036
+ };
2037
+ }
2038
+ return { options, safeEmit, safePersist, recordStep, finalizeHold, buildEvent };
1943
2039
  }
1944
2040
 
1945
2041
  // src/core/execution/pipeline/settlement.ts
1946
2042
  function createSettlement(ctx) {
1947
- const { options, safeEmit, safePersist, buildEvent } = ctx;
2043
+ const { options, safeEmit, safePersist, recordStep, finalizeHold, buildEvent } = ctx;
1948
2044
  async function settle(args) {
1949
- const { request, resource, provider, requirement, payment, verification, replayKey, hold } = args;
2045
+ const {
2046
+ flow,
2047
+ request,
2048
+ resource,
2049
+ provider,
2050
+ requirement,
2051
+ payment,
2052
+ verification,
2053
+ replayKey,
2054
+ hold
2055
+ } = args;
2056
+ await recordStep(flow, { kind: "settling" });
1950
2057
  let settlement;
1951
2058
  try {
1952
2059
  settlement = await provider.settle({
@@ -1959,25 +2066,29 @@ function createSettlement(ctx) {
1959
2066
  } catch (error) {
1960
2067
  const notSubmitted = notSubmittedReason(error);
1961
2068
  if (notSubmitted !== void 0) {
1962
- await hold?.finalize("release");
2069
+ await finalizeHold(flow, hold, "release");
1963
2070
  await safePersist(
2071
+ flow,
1964
2072
  () => options.store.updatePaymentAttempt({
1965
2073
  replayKey,
1966
2074
  status: "rejected",
1967
2075
  rejectionReason: notSubmitted
1968
2076
  }),
1969
2077
  "updatePaymentAttempt(not-submitted)",
1970
- request.requestId
2078
+ "payment-attempt"
1971
2079
  );
1972
2080
  await safeEmit(
1973
- buildEvent({
2081
+ buildEvent(flow, {
1974
2082
  type: "payment.rejected",
1975
- requestId: request.requestId,
1976
2083
  resourceId: resource.id,
1977
2084
  adapter: request.protocol,
1978
2085
  paymentProvider: provider.name,
1979
2086
  status: "error",
1980
- data: { reason: "settlement-not-submitted", providerReason: notSubmitted }
2087
+ data: {
2088
+ reason: "settlement-not-submitted",
2089
+ providerReason: notSubmitted,
2090
+ ...failuresOf(flow)
2091
+ }
1981
2092
  })
1982
2093
  );
1983
2094
  throw new CommerceError(
@@ -1987,8 +2098,9 @@ function createSettlement(ctx) {
1987
2098
  );
1988
2099
  }
1989
2100
  const txHash = settlementTxHash(error);
1990
- await hold?.finalize("markUncertain");
2101
+ await finalizeHold(flow, hold, "markUncertain");
1991
2102
  await safePersist(
2103
+ flow,
1992
2104
  () => options.store.updatePaymentAttempt({
1993
2105
  replayKey,
1994
2106
  status: "settlement-uncertain",
@@ -1996,19 +2108,19 @@ function createSettlement(ctx) {
1996
2108
  ...error instanceof Error ? { rejectionReason: error.message } : {}
1997
2109
  }),
1998
2110
  "updatePaymentAttempt(settlement-uncertain)",
1999
- request.requestId
2111
+ "payment-attempt"
2000
2112
  );
2001
2113
  await safeEmit(
2002
- buildEvent({
2114
+ buildEvent(flow, {
2003
2115
  type: "payment.rejected",
2004
- requestId: request.requestId,
2005
2116
  resourceId: resource.id,
2006
2117
  adapter: request.protocol,
2007
2118
  paymentProvider: provider.name,
2008
2119
  status: "error",
2009
2120
  data: {
2010
2121
  reason: "settlement-uncertain",
2011
- ...txHash !== void 0 ? { transactionHash: txHash } : {}
2122
+ ...txHash !== void 0 ? { transactionHash: txHash } : {},
2123
+ ...failuresOf(flow)
2012
2124
  }
2013
2125
  })
2014
2126
  );
@@ -2024,8 +2136,9 @@ function createSettlement(ctx) {
2024
2136
  });
2025
2137
  }
2026
2138
  if (settlement.status !== "settled") {
2027
- await hold?.finalize("release");
2139
+ await finalizeHold(flow, hold, "release");
2028
2140
  await safePersist(
2141
+ flow,
2029
2142
  () => options.store.updatePaymentAttempt({
2030
2143
  replayKey,
2031
2144
  status: "rejected",
@@ -2033,17 +2146,19 @@ function createSettlement(ctx) {
2033
2146
  ...settlement.externalReference !== void 0 ? { externalReference: settlement.externalReference } : {}
2034
2147
  }),
2035
2148
  "updatePaymentAttempt(rejected)",
2036
- request.requestId
2149
+ "payment-attempt"
2037
2150
  );
2038
2151
  await safeEmit(
2039
- buildEvent({
2152
+ buildEvent(flow, {
2040
2153
  type: "payment.rejected",
2041
- requestId: request.requestId,
2042
2154
  resourceId: resource.id,
2043
2155
  adapter: request.protocol,
2044
2156
  paymentProvider: provider.name,
2045
2157
  status: "error",
2046
- data: settlement.rejectionReason ? { reason: settlement.rejectionReason } : {}
2158
+ data: {
2159
+ ...settlement.rejectionReason ? { reason: settlement.rejectionReason } : {},
2160
+ ...failuresOf(flow)
2161
+ }
2047
2162
  })
2048
2163
  );
2049
2164
  const rejectionDetails = retryChallenge(requirement, settlement.rejectionReason).details;
@@ -2061,20 +2176,20 @@ function createSettlement(ctx) {
2061
2176
  }
2062
2177
  );
2063
2178
  }
2064
- await hold?.finalize("consume");
2179
+ await finalizeHold(flow, hold, "consume");
2065
2180
  await safePersist(
2181
+ flow,
2066
2182
  () => options.store.updatePaymentAttempt({
2067
2183
  replayKey,
2068
2184
  status: "settled",
2069
2185
  ...settlement.externalReference !== void 0 ? { externalReference: settlement.externalReference } : {}
2070
2186
  }),
2071
2187
  "updatePaymentAttempt(settled)",
2072
- request.requestId
2188
+ "payment-attempt"
2073
2189
  );
2074
2190
  await safeEmit(
2075
- buildEvent({
2191
+ buildEvent(flow, {
2076
2192
  type: "payment.settled",
2077
- requestId: request.requestId,
2078
2193
  resourceId: resource.id,
2079
2194
  adapter: request.protocol,
2080
2195
  paymentProvider: provider.name,
@@ -2084,26 +2199,26 @@ function createSettlement(ctx) {
2084
2199
  return settlement;
2085
2200
  }
2086
2201
  async function abandonSettlement(args, backendErrorCode) {
2087
- const { request, resource, provider, replayKey, hold } = args;
2088
- await hold?.finalize("release");
2202
+ const { flow, request, resource, provider, replayKey, hold } = args;
2203
+ await finalizeHold(flow, hold, "release");
2089
2204
  await safePersist(
2205
+ flow,
2090
2206
  () => options.store.updatePaymentAttempt({
2091
2207
  replayKey,
2092
2208
  status: "rejected",
2093
2209
  rejectionReason: "backend_failed"
2094
2210
  }),
2095
2211
  "updatePaymentAttempt(backend-failed)",
2096
- request.requestId
2212
+ "payment-attempt"
2097
2213
  );
2098
2214
  await safeEmit(
2099
- buildEvent({
2215
+ buildEvent(flow, {
2100
2216
  type: "payment.rejected",
2101
- requestId: request.requestId,
2102
2217
  resourceId: resource.id,
2103
2218
  adapter: request.protocol,
2104
2219
  paymentProvider: provider.name,
2105
2220
  status: "error",
2106
- data: { reason: "backend-failed", backendErrorCode }
2221
+ data: { reason: "backend-failed", backendErrorCode, ...failuresOf(flow) }
2107
2222
  })
2108
2223
  );
2109
2224
  }
@@ -2140,7 +2255,7 @@ function createExecutionPipeline(options) {
2140
2255
  return compiled;
2141
2256
  }
2142
2257
  const ctx = createPipelineContext(options);
2143
- const { safeEmit, safePersist, buildEvent } = ctx;
2258
+ const { safeEmit, safePersist, recordStep, buildEvent } = ctx;
2144
2259
  const { settle, abandonSettlement } = createSettlement(ctx);
2145
2260
  const admitPayment = createPaymentAdmission(ctx, createAuthorizeAndReserve(ctx));
2146
2261
  async function execute(request) {
@@ -2156,10 +2271,10 @@ function createExecutionPipeline(options) {
2156
2271
  }
2157
2272
  );
2158
2273
  }
2274
+ const flow = createFlow(request, options.ids.next("txn"));
2159
2275
  await safeEmit(
2160
- buildEvent({
2276
+ buildEvent(flow, {
2161
2277
  type: "resource.requested",
2162
- requestId: request.requestId,
2163
2278
  resourceId: resource.id,
2164
2279
  adapter: request.protocol
2165
2280
  })
@@ -2204,20 +2319,34 @@ function createExecutionPipeline(options) {
2204
2319
  }
2205
2320
  );
2206
2321
  }
2207
- let paymentResult;
2208
- let authorization;
2209
- let deferredSettlement;
2322
+ let settleArgs;
2210
2323
  if (resource.pricing.type === "fixed") {
2211
- const admission = await admitPayment(request, resource, resource.pricing, validInput);
2324
+ const admission = await admitPayment(flow, request, resource, resource.pricing, validInput);
2212
2325
  if (admission.kind === "payment-required") return admission;
2213
- const { settleArgs } = admission;
2214
- authorization = settleArgs.hold?.record;
2326
+ settleArgs = admission.settleArgs;
2327
+ }
2328
+ try {
2329
+ return await settleAndDeliver(flow, resource, validInput, settleArgs, pipelineStart);
2330
+ } finally {
2331
+ await recordStep(flow, { kind: "completed", ...failuresOf(flow) });
2332
+ }
2333
+ }
2334
+ async function settleAndDeliver(flow, resource, validInput, settleArgs, pipelineStart) {
2335
+ const { request } = flow;
2336
+ const authorization = settleArgs?.hold?.record;
2337
+ let paymentResult;
2338
+ let deferredSettlement;
2339
+ if (settleArgs !== void 0) {
2215
2340
  if (settleArgs.requirement.metadata?.[SETTLE_AFTER_BACKEND_METADATA_KEY] === true) {
2216
2341
  deferredSettlement = settleArgs;
2217
2342
  } else {
2218
2343
  paymentResult = await settle(settleArgs);
2219
2344
  }
2220
2345
  }
2346
+ await recordStep(flow, {
2347
+ kind: "backend-dispatched",
2348
+ deadlineAt: backendDeadline(options.clock.now(), resource)
2349
+ });
2221
2350
  const backendStart = options.clock.monotonicMs();
2222
2351
  let backendResponse;
2223
2352
  try {
@@ -2230,18 +2359,23 @@ function createExecutionPipeline(options) {
2230
2359
  });
2231
2360
  } catch (error) {
2232
2361
  const commerceError = toCommerceError(error, "BACKEND_ERROR", "Backend call failed");
2233
- await safeEmit(
2234
- buildEvent({
2362
+ await recordStep(flow, {
2363
+ kind: "backend-result",
2364
+ status: backendErrorStatus(commerceError),
2365
+ errorCode: commerceError.code
2366
+ });
2367
+ const backendFailed = (data = {}) => safeEmit(
2368
+ buildEvent(flow, {
2235
2369
  type: "backend.failed",
2236
- requestId: request.requestId,
2237
2370
  resourceId: resource.id,
2238
2371
  adapter: request.protocol,
2239
2372
  status: "error",
2240
2373
  durationMs: Math.round(options.clock.monotonicMs() - backendStart),
2241
- data: { code: commerceError.code }
2374
+ data: { code: commerceError.code, ...data }
2242
2375
  })
2243
2376
  );
2244
2377
  if (deferredSettlement !== void 0) {
2378
+ await backendFailed();
2245
2379
  await abandonSettlement(deferredSettlement, commerceError.code);
2246
2380
  throw commerceError;
2247
2381
  }
@@ -2255,13 +2389,11 @@ function createExecutionPipeline(options) {
2255
2389
  protocol: request.protocol,
2256
2390
  payment: paymentResult,
2257
2391
  ...authorization !== void 0 ? { authorization } : {},
2258
- metadata: { delivered: false, backendErrorCode: commerceError.code }
2392
+ metadata: { delivered: false, backendErrorCode: commerceError.code },
2393
+ transactionId: flow.transactionId
2259
2394
  };
2260
- await safePersist(
2261
- () => options.store.saveReceipt(failedReceipt),
2262
- "saveReceipt(backend-failed-after-settlement)",
2263
- request.requestId
2264
- );
2395
+ await saveReceipt(flow, failedReceipt, "saveReceipt(backend-failed-after-settlement)");
2396
+ await backendFailed(failuresOf(flow));
2265
2397
  throw new CommerceError(
2266
2398
  "BACKEND_ERROR",
2267
2399
  "Payment settled but the backend call failed; the payment was not refunded automatically",
@@ -2285,12 +2417,13 @@ function createExecutionPipeline(options) {
2285
2417
  }
2286
2418
  );
2287
2419
  }
2420
+ await backendFailed();
2288
2421
  throw commerceError;
2289
2422
  }
2423
+ await recordStep(flow, { kind: "backend-result", status: backendResponse.status });
2290
2424
  await safeEmit(
2291
- buildEvent({
2425
+ buildEvent(flow, {
2292
2426
  type: "backend.called",
2293
- requestId: request.requestId,
2294
2427
  resourceId: resource.id,
2295
2428
  adapter: request.protocol,
2296
2429
  status: "ok",
@@ -2310,34 +2443,48 @@ function createExecutionPipeline(options) {
2310
2443
  durationMs: backendResponse.durationMs,
2311
2444
  protocol: request.protocol,
2312
2445
  ...paymentResult !== void 0 ? { payment: paymentResult } : {},
2313
- ...authorization !== void 0 ? { authorization } : {}
2446
+ ...authorization !== void 0 ? { authorization } : {},
2447
+ transactionId: flow.transactionId
2314
2448
  };
2315
- await safePersist(() => options.store.saveReceipt(receipt), "saveReceipt", request.requestId);
2449
+ await saveReceipt(flow, receipt, "saveReceipt");
2316
2450
  const totalDurationMs = Math.round(options.clock.monotonicMs() - pipelineStart);
2451
+ const failures = failuresOf(flow);
2317
2452
  await safeEmit(
2318
- buildEvent({
2453
+ buildEvent(flow, {
2319
2454
  type: "resource.delivered",
2320
- requestId: request.requestId,
2321
2455
  resourceId: resource.id,
2322
2456
  adapter: request.protocol,
2323
2457
  status: "ok",
2324
- durationMs: totalDurationMs
2458
+ durationMs: totalDurationMs,
2459
+ ...failures.persistenceFailures !== void 0 ? { data: failures } : {}
2325
2460
  })
2326
2461
  );
2327
2462
  return {
2328
2463
  kind: "delivered",
2329
2464
  requestId: request.requestId,
2465
+ transactionId: flow.transactionId,
2330
2466
  resourceId: resource.id,
2331
2467
  backendStatus: backendResponse.status,
2332
2468
  body: backendResponse.body,
2333
2469
  headers: backendResponse.headers,
2334
2470
  ...paymentResult !== void 0 ? { payment: paymentResult } : {},
2335
2471
  receipt,
2336
- durationMs: totalDurationMs
2472
+ durationMs: totalDurationMs,
2473
+ ...failures
2337
2474
  };
2338
2475
  }
2476
+ async function saveReceipt(flow, receipt, label) {
2477
+ if (await safePersist(flow, () => options.store.saveReceipt(receipt), label, "receipt")) {
2478
+ await recordStep(flow, { kind: "receipt", receiptId: receipt.id });
2479
+ }
2480
+ }
2339
2481
  return { execute };
2340
2482
  }
2483
+ var BACKEND_DEADLINE_MARGIN_MS = 5e3;
2484
+ function backendDeadline(dispatchedAt, resource) {
2485
+ const timeoutMs = resource.handler.timeoutMs ?? DEFAULT_BACKEND_TIMEOUT_MS;
2486
+ return new Date(dispatchedAt.getTime() + timeoutMs + BACKEND_DEADLINE_MARGIN_MS).toISOString();
2487
+ }
2341
2488
  function backendErrorStatus(error) {
2342
2489
  const status = error.details?.["status"];
2343
2490
  return typeof status === "number" ? status : 0;
@@ -3468,6 +3615,168 @@ function wrapPino(instance) {
3468
3615
  return logger;
3469
3616
  }
3470
3617
 
3618
+ // src/core/execution/reconcile.ts
3619
+ var DEFAULT_RECONCILE_MIN_AGE_MS = 15 * 60 * 1e3;
3620
+ function journalOf(store) {
3621
+ const {
3622
+ listTransactions,
3623
+ getTransaction,
3624
+ getPaymentAttempt,
3625
+ resolvePaymentAttempt,
3626
+ recordTransactionStep
3627
+ } = store;
3628
+ if (!listTransactions || !getTransaction || !getPaymentAttempt || !resolvePaymentAttempt || !recordTransactionStep) {
3629
+ throw new CommerceError("STORAGE_ERROR", "The receipt store has no transaction journal");
3630
+ }
3631
+ return {
3632
+ listTransactions: listTransactions.bind(store),
3633
+ getTransaction: getTransaction.bind(store),
3634
+ getPaymentAttempt: getPaymentAttempt.bind(store),
3635
+ resolvePaymentAttempt: resolvePaymentAttempt.bind(store),
3636
+ recordTransactionStep: recordTransactionStep.bind(store)
3637
+ };
3638
+ }
3639
+ var REFUSAL_MESSAGES = {
3640
+ already_resolved: "The payment attempt already has a resolution",
3641
+ not_resolvable: "Only reserved, settlement-uncertain and failed payment attempts can be resolved",
3642
+ too_recent: "The payment attempt is reserved and may still be in flight; retry once it is 15 minutes old",
3643
+ not_expired: "The recorded authorization expiry is still in the future",
3644
+ expiry_unknown: "No expiry is recorded for this authorization; confirm that it has expired"
3645
+ };
3646
+ var MAX_ACTOR_LENGTH = 256;
3647
+ var MAX_REASON_LENGTH = 1024;
3648
+ var MAX_REFERENCE_LENGTH = 256;
3649
+ var RESOLVABLE = [
3650
+ "reserved",
3651
+ "settlement-uncertain",
3652
+ "failed"
3653
+ ];
3654
+ async function resolveManually(options) {
3655
+ const { clock, logger, attemptId, status, externalReference } = options;
3656
+ if (status !== "settled" && status !== "rejected") {
3657
+ throw invalid("status", 'status must be "settled" or "rejected"');
3658
+ }
3659
+ const actor = requiredText(options.actor, "actor", MAX_ACTOR_LENGTH);
3660
+ const reason = requiredText(options.reason, "reason", MAX_REASON_LENGTH);
3661
+ if (externalReference !== void 0) {
3662
+ requiredText(externalReference, "externalReference", MAX_REFERENCE_LENGTH);
3663
+ }
3664
+ const journal = journalOf(options.store);
3665
+ const attempt = await journal.getPaymentAttempt(attemptId);
3666
+ if (attempt === void 0) {
3667
+ throw new CommerceError("RESOURCE_NOT_FOUND", `Payment attempt "${attemptId}" was not found`);
3668
+ }
3669
+ const refuse = (refusal, current2 = attempt) => ({
3670
+ resolved: false,
3671
+ refusal,
3672
+ message: REFUSAL_MESSAGES[refusal],
3673
+ attempt: current2
3674
+ });
3675
+ if (attempt.resolution !== void 0) return refuse("already_resolved");
3676
+ if (!RESOLVABLE.includes(attempt.status)) return refuse("not_resolvable");
3677
+ if (attempt.status === "reserved" && !(clock.now().getTime() - Date.parse(attempt.createdAt) >= DEFAULT_RECONCILE_MIN_AGE_MS)) {
3678
+ return refuse("too_recent");
3679
+ }
3680
+ const transaction = attempt.transactionId !== void 0 ? await journal.getTransaction(attempt.transactionId) : void 0;
3681
+ if (status === "rejected") {
3682
+ const expiresAt = Date.parse(transaction?.payment.expiresAt ?? "");
3683
+ if (expiresAt > clock.now().getTime()) return refuse("not_expired");
3684
+ if (Number.isNaN(expiresAt) && options.confirmExpired !== true) {
3685
+ return refuse("expiry_unknown");
3686
+ }
3687
+ }
3688
+ const written = await journal.resolvePaymentAttempt({
3689
+ attemptId,
3690
+ expectedStatus: attempt.status,
3691
+ status,
3692
+ ...status === "rejected" ? { rejectionReason: "not_settled" } : {},
3693
+ resolution: {
3694
+ method: "manual",
3695
+ actor,
3696
+ reason,
3697
+ at: clock.nowIso(),
3698
+ ...externalReference !== void 0 ? { externalReference } : {}
3699
+ }
3700
+ });
3701
+ const current = await journal.getPaymentAttempt(attemptId) ?? attempt;
3702
+ if (!written) {
3703
+ return refuse(
3704
+ current.resolution !== void 0 ? "already_resolved" : "not_resolvable",
3705
+ current
3706
+ );
3707
+ }
3708
+ const resolved = transaction !== void 0 ? await journal.getTransaction(transaction.id) : void 0;
3709
+ const target = resolved !== void 0 ? holdTarget(resolved) : void 0;
3710
+ const hold = resolved !== void 0 && target !== void 0 ? await alignHold(resolved, target, {
3711
+ journal,
3712
+ authorizationProviders: options.authorizationProviders,
3713
+ clock,
3714
+ logger
3715
+ }) : void 0;
3716
+ return { resolved: true, attempt: current, ...hold !== void 0 ? { hold } : {} };
3717
+ }
3718
+ function requiredText(value, field, max) {
3719
+ if (value.trim() === "" || value.length > max) {
3720
+ throw invalid(field, `${field} must be non-empty and at most ${max} characters`);
3721
+ }
3722
+ return value;
3723
+ }
3724
+ function invalid(field, message) {
3725
+ return new CommerceError("INPUT_INVALID", message, { details: { field } });
3726
+ }
3727
+ function holdTarget(transaction) {
3728
+ const hold = transaction.authorization?.hold;
3729
+ if (hold !== "reserved" && hold !== "uncertain") return void 0;
3730
+ if (transaction.payment.status === "settled") return "consumed";
3731
+ if (transaction.payment.status === "rejected") return "released";
3732
+ return void 0;
3733
+ }
3734
+ function holdEntry(transaction, hold, written, reason) {
3735
+ return {
3736
+ transactionId: transaction.id,
3737
+ attemptId: transaction.payment.attemptId,
3738
+ method: transaction.authorization?.method ?? "",
3739
+ hold,
3740
+ written,
3741
+ ...reason !== void 0 ? { reason } : {}
3742
+ };
3743
+ }
3744
+ async function alignHold(transaction, target, deps) {
3745
+ const authorization = transaction.authorization;
3746
+ const provider = deps.authorizationProviders.find(
3747
+ (candidate) => candidate.name === authorization?.method
3748
+ );
3749
+ if (authorization === void 0 || provider === void 0) {
3750
+ return holdEntry(transaction, target, false, "provider_not_enabled");
3751
+ }
3752
+ const context = { requestId: transaction.requestId, resourceId: transaction.resourceId };
3753
+ try {
3754
+ if (target === "consumed") await provider.consume(authorization.reservationId, context);
3755
+ else await provider.release(authorization.reservationId, context);
3756
+ } catch (err) {
3757
+ deps.logger?.warn(
3758
+ { transactionId: transaction.id, method: provider.name, err: redactedErrorText(err) },
3759
+ "reconcile: authorization provider threw"
3760
+ );
3761
+ return holdEntry(transaction, target, false, "provider_error");
3762
+ }
3763
+ try {
3764
+ await deps.journal.recordTransactionStep({
3765
+ transactionId: transaction.id,
3766
+ at: deps.clock.nowIso(),
3767
+ kind: "hold",
3768
+ hold: target
3769
+ });
3770
+ } catch (err) {
3771
+ deps.logger?.warn(
3772
+ { transactionId: transaction.id, err: redactedErrorText(err) },
3773
+ "reconcile: could not record the hold update"
3774
+ );
3775
+ return holdEntry(transaction, target, false, "journal_error");
3776
+ }
3777
+ return holdEntry(transaction, target, true);
3778
+ }
3779
+
3471
3780
  // src/gateway/public-resource.ts
3472
3781
  function toPublicResource(resource) {
3473
3782
  return {
@@ -3721,6 +4030,15 @@ function buildWellKnownDocument(options) {
3721
4030
 
3722
4031
  // src/gateway/routes.ts
3723
4032
  var X402_V1_PAYMENT_HEADER = "x-payment";
4033
+ var RESOLVE_BODY_LIMIT_BYTES = 4096;
4034
+ var RESOLVE_FIELDS = [
4035
+ "status",
4036
+ "actor",
4037
+ "reason",
4038
+ "externalReference",
4039
+ "confirmExpired"
4040
+ ];
4041
+ var TRANSACTION_EVENT_LIMIT = 50;
3724
4042
  function registerRoutes(options) {
3725
4043
  const { server } = options;
3726
4044
  const readinessProbe = createReadinessProbe({
@@ -3788,6 +4106,113 @@ function registerRoutes(options) {
3788
4106
  )
3789
4107
  )
3790
4108
  );
4109
+ const { store, clock } = options;
4110
+ const journal = () => journalOf(store);
4111
+ const withAge = (transaction) => ({
4112
+ ...transaction,
4113
+ ageSeconds: Math.max(
4114
+ 0,
4115
+ Math.floor((clock.now().getTime() - Date.parse(transaction.createdAt)) / 1e3)
4116
+ )
4117
+ });
4118
+ server.get(
4119
+ "/api/transactions",
4120
+ { onRequest: tokenHook },
4121
+ operatorHandler(async (request) => {
4122
+ const query = request.query;
4123
+ const limit = parseLimit(query["limit"]);
4124
+ const state = parseText(query["state"]);
4125
+ if (state !== void 0 && !TRANSACTION_STATES.includes(state)) {
4126
+ throw new CommerceError("INPUT_INVALID", `Unknown transaction state "${state}"`, {
4127
+ details: { field: "state" }
4128
+ });
4129
+ }
4130
+ const requestId = parseText(query["requestId"]);
4131
+ const cursor = parseText(query["cursor"]);
4132
+ const page = await journal().listTransactions({
4133
+ ...limit !== void 0 ? { limit } : {},
4134
+ ...parseFlag(query["unresolved"], "unresolved") ? { unresolved: true } : {},
4135
+ ...state !== void 0 ? { state } : {},
4136
+ ...requestId !== void 0 ? { requestId } : {},
4137
+ ...cursor !== void 0 ? { cursor } : {}
4138
+ });
4139
+ return {
4140
+ transactions: page.transactions.map(withAge),
4141
+ ...page.nextCursor !== void 0 ? { nextCursor: page.nextCursor } : {}
4142
+ };
4143
+ })
4144
+ );
4145
+ server.get(
4146
+ "/api/transactions/:id",
4147
+ { onRequest: tokenHook },
4148
+ operatorHandler(async (request) => {
4149
+ const id = request.params.id;
4150
+ const transaction = await journal().getTransaction(id);
4151
+ if (transaction === void 0) {
4152
+ throw new CommerceError("RESOURCE_NOT_FOUND", `Transaction "${id}" was not found`);
4153
+ }
4154
+ const [attempt, receipt, events] = await Promise.all([
4155
+ journal().getPaymentAttempt(transaction.payment.attemptId),
4156
+ transaction.receiptId !== void 0 ? store.getReceipt(transaction.receiptId) : void 0,
4157
+ store.listEvents({ requestId: transaction.requestId, limit: TRANSACTION_EVENT_LIMIT })
4158
+ ]);
4159
+ return {
4160
+ transaction: withAge(transaction),
4161
+ ...attempt !== void 0 ? { attempt } : {},
4162
+ ...receipt !== void 0 ? { receipt } : {},
4163
+ events
4164
+ };
4165
+ })
4166
+ );
4167
+ server.post(
4168
+ "/api/payment-attempts/:id/resolve",
4169
+ { onRequest: tokenHook, bodyLimit: RESOLVE_BODY_LIMIT_BYTES },
4170
+ operatorHandler(async (request) => {
4171
+ const result = await resolveManually({
4172
+ store,
4173
+ authorizationProviders: options.authorizationProviders,
4174
+ clock,
4175
+ logger: options.logger,
4176
+ attemptId: request.params.id,
4177
+ ...parseResolveBody(request.body)
4178
+ });
4179
+ if (!result.resolved) {
4180
+ throw new CommerceError("INPUT_INVALID", result.message, {
4181
+ httpStatus: 409,
4182
+ details: { reason: result.refusal, attemptStatus: result.attempt.status }
4183
+ });
4184
+ }
4185
+ return {
4186
+ attempt: result.attempt,
4187
+ ...result.hold !== void 0 ? { hold: result.hold } : {}
4188
+ };
4189
+ })
4190
+ );
4191
+ }
4192
+ function parseResolveBody(body) {
4193
+ if (!isRecord(body)) {
4194
+ throw new CommerceError("INPUT_INVALID", "The request body must be a JSON object");
4195
+ }
4196
+ const field = (name, type, required) => {
4197
+ const value = body[name];
4198
+ if (value === void 0 && required || value !== void 0 && typeof value !== type) {
4199
+ throw new CommerceError("INPUT_INVALID", `Field "${name}" must be a ${type}`, {
4200
+ details: { field: name }
4201
+ });
4202
+ }
4203
+ };
4204
+ const unknownField = Object.keys(body).find((name) => !RESOLVE_FIELDS.includes(name));
4205
+ if (unknownField !== void 0) {
4206
+ throw new CommerceError("INPUT_INVALID", `Unknown field "${unknownField}"`, {
4207
+ details: { field: unknownField }
4208
+ });
4209
+ }
4210
+ field("status", "string", true);
4211
+ field("actor", "string", true);
4212
+ field("reason", "string", true);
4213
+ field("externalReference", "string", false);
4214
+ field("confirmExpired", "boolean", false);
4215
+ return body;
3791
4216
  }
3792
4217
  var bodyDigests = /* @__PURE__ */ new WeakMap();
3793
4218
  function digestingStream(request) {
@@ -3803,13 +4228,10 @@ function digestingStream(request) {
3803
4228
  }
3804
4229
  });
3805
4230
  }
3806
- function ledgerHandler(key, list) {
4231
+ function operatorHandler(handle) {
3807
4232
  return async (request, reply) => {
3808
4233
  try {
3809
- const query = request.query;
3810
- const limit = parseLimit(query["limit"]);
3811
- const page = await list(limit !== void 0 ? { limit } : {}, query);
3812
- return { [key]: page };
4234
+ return await handle(request, reply);
3813
4235
  } catch (error) {
3814
4236
  const commerceError = toCommerceError(error);
3815
4237
  reply.status(commerceError.httpStatus).send(toErrorEnvelope(commerceError));
@@ -3817,6 +4239,14 @@ function ledgerHandler(key, list) {
3817
4239
  }
3818
4240
  };
3819
4241
  }
4242
+ function ledgerHandler(key, list) {
4243
+ return operatorHandler(async (request) => {
4244
+ const query = request.query;
4245
+ const limit = parseLimit(query["limit"]);
4246
+ const page = await list(limit !== void 0 ? { limit } : {}, query);
4247
+ return { [key]: page };
4248
+ });
4249
+ }
3820
4250
  async function handleInvoke(request, reply, options) {
3821
4251
  const resourceId = request.params.id;
3822
4252
  let paymentMethod;
@@ -3997,6 +4427,10 @@ function parseLimit(value) {
3997
4427
  }
3998
4428
  return n;
3999
4429
  }
4430
+ function parseText(value) {
4431
+ if (Array.isArray(value)) return parseText(value[0]);
4432
+ return typeof value === "string" && value !== "" ? value : void 0;
4433
+ }
4000
4434
  function parseFlag(value, field) {
4001
4435
  if (Array.isArray(value)) return parseFlag(value[0], field);
4002
4436
  if (value === void 0 || value === "false") return false;
@@ -4258,21 +4692,20 @@ var A2A_CALL_SHAPE = 'one data part {"resource": "<skill id>", "input": {...}}';
4258
4692
  function userRoleProblem(role) {
4259
4693
  return role === A2A_USER_ROLE ? void 0 : `Unsupported message role "${String(role)}": only ${A2A_USER_ROLE} is accepted.`;
4260
4694
  }
4261
- var UnknownRecord = z.record(z.string(), z.unknown());
4262
4695
  var MessageSchema = z.object({
4263
4696
  role: z.string(),
4264
4697
  messageId: z.string().optional(),
4265
- parts: z.array(UnknownRecord),
4698
+ parts: z.array(z.unknown()),
4266
4699
  taskId: z.string().optional(),
4267
4700
  contextId: z.string().optional(),
4268
- referenceTaskIds: z.array(z.string()).optional()
4701
+ referenceTaskIds: z.array(z.unknown()).optional()
4269
4702
  });
4270
4703
  var ParamsSchema = z.object({
4271
4704
  message: MessageSchema,
4272
4705
  taskId: z.string().optional(),
4273
4706
  contextId: z.string().optional()
4274
4707
  });
4275
- function invalid(message) {
4708
+ function invalid2(message) {
4276
4709
  return new CommerceError("INPUT_INVALID", message);
4277
4710
  }
4278
4711
  function unsupported(message, a2aErrorCode) {
@@ -4313,7 +4746,7 @@ function assertSupportedPart(part) {
4313
4746
  );
4314
4747
  }
4315
4748
  if (!("data" in part)) {
4316
- throw invalid('Message part carries no "data": send a structured data part.');
4749
+ throw invalid2('Message part carries no "data": send a structured data part.');
4317
4750
  }
4318
4751
  const mediaType = part["mediaType"];
4319
4752
  if (mediaType !== void 0 && mediaType !== A2A_JSON_MEDIA_TYPE) {
@@ -4326,21 +4759,21 @@ function assertSupportedPart(part) {
4326
4759
  function parseInvocation(rawParams) {
4327
4760
  const parsed = ParamsSchema.safeParse(rawParams);
4328
4761
  if (!parsed.success) {
4329
- throw invalid(
4762
+ throw invalid2(
4330
4763
  'Request must carry a "message" with a "role" and a "parts" array of structured data parts.'
4331
4764
  );
4332
4765
  }
4333
4766
  const params = parsed.data;
4334
4767
  const message = params.message;
4335
4768
  if (message.messageId === void 0 || message.messageId.length === 0) {
4336
- throw invalid('Message must carry a non-empty "messageId".');
4769
+ throw invalid2('Message must carry a non-empty "messageId".');
4337
4770
  }
4338
4771
  const messageId = message.messageId;
4339
4772
  assertNoContinuation(params);
4340
4773
  const roleProblem = userRoleProblem(message.role);
4341
- if (roleProblem !== void 0) throw invalid(roleProblem);
4774
+ if (roleProblem !== void 0) throw invalid2(roleProblem);
4342
4775
  if (message.parts.length === 0) {
4343
- throw invalid("Message carries no parts: send exactly one structured data part.");
4776
+ throw invalid2("Message carries no parts: send exactly one structured data part.");
4344
4777
  }
4345
4778
  if (message.parts.length > 1) {
4346
4779
  throw unsupported(
@@ -4348,22 +4781,23 @@ function parseInvocation(rawParams) {
4348
4781
  );
4349
4782
  }
4350
4783
  const part = message.parts[0];
4351
- if (part === void 0) throw invalid("Message carries no parts.");
4784
+ if (part === void 0) throw invalid2("Message carries no parts.");
4785
+ if (!isRecord(part)) throw invalid2("Message part must be a JSON object.");
4352
4786
  assertSupportedPart(part);
4353
4787
  const data = part["data"];
4354
4788
  if (!isRecord(data)) {
4355
- throw invalid('Message part "data" must be a JSON object.');
4789
+ throw invalid2('Message part "data" must be a JSON object.');
4356
4790
  }
4357
4791
  const resourceId = data["resource"];
4358
4792
  if (typeof resourceId !== "string") {
4359
- throw invalid('Message part data must carry a "resource" string naming a canonical resource.');
4793
+ throw invalid2('Message part data must carry a "resource" string naming a canonical resource.');
4360
4794
  }
4361
4795
  if (resourceId.length === 0) {
4362
- throw invalid('Message part data "resource" must not be empty.');
4796
+ throw invalid2('Message part data "resource" must not be empty.');
4363
4797
  }
4364
4798
  const rawInput = data["input"];
4365
4799
  if (rawInput !== void 0 && !isRecord(rawInput)) {
4366
- throw invalid('Message part data "input" must be a JSON object.');
4800
+ throw invalid2('Message part data "input" must be a JSON object.');
4367
4801
  }
4368
4802
  return { resourceId, input: rawInput ?? {}, messageId };
4369
4803
  }
@@ -5273,10 +5707,10 @@ function writeAcpFailure(res, failure, headers = {}) {
5273
5707
  writeAcpJson(res, failure.status, failure.error, headers);
5274
5708
  }
5275
5709
  function operationKey(scope) {
5276
- return sha256(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
5710
+ return sha2562(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
5277
5711
  }
5278
5712
  function requestFingerprint(body) {
5279
- return sha256(canonicalize(body));
5713
+ return sha2562(canonicalize(body));
5280
5714
  }
5281
5715
  function canonicalize(value) {
5282
5716
  if (value === null) return "null";
@@ -5287,7 +5721,7 @@ function canonicalize(value) {
5287
5721
  }
5288
5722
  return JSON.stringify(value) ?? "null";
5289
5723
  }
5290
- function sha256(value) {
5724
+ function sha2562(value) {
5291
5725
  return createHash("sha256").update(value, "utf8").digest("hex");
5292
5726
  }
5293
5727
 
@@ -10002,12 +10436,12 @@ var AcpProtocolAdapter = class {
10002
10436
  mountPath: this.mountPath,
10003
10437
  ...this.discoveryMetadata !== void 0 ? { metadata: this.discoveryMetadata } : {}
10004
10438
  });
10005
- const invalid2 = validateAcpDocument("discoveryResponse", document);
10006
- if (invalid2 !== void 0) {
10439
+ const invalid3 = validateAcpDocument("discoveryResponse", document);
10440
+ if (invalid3 !== void 0) {
10007
10441
  throw new CommerceError(
10008
10442
  "CONFIG_INVALID",
10009
- `ACP discovery document does not conform to the ${this.descriptor.supportedSpec} snapshot at "${invalid2.path ?? "$"}" (${invalid2.code}) - check protocols.acp.discovery`,
10010
- { details: { path: invalid2.path ?? "$", code: invalid2.code } }
10443
+ `ACP discovery document does not conform to the ${this.descriptor.supportedSpec} snapshot at "${invalid3.path ?? "$"}" (${invalid3.code}) - check protocols.acp.discovery`,
10444
+ { details: { path: invalid3.path ?? "$", code: invalid3.code } }
10011
10445
  );
10012
10446
  }
10013
10447
  const exposed = new Map(
@@ -10314,7 +10748,8 @@ function receiptToRow(receipt) {
10314
10748
  duration_ms: receipt.durationMs ?? null,
10315
10749
  protocol: receipt.protocol ?? null,
10316
10750
  metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
10317
- authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
10751
+ authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null,
10752
+ transaction_id: receipt.transactionId ?? null
10318
10753
  };
10319
10754
  }
10320
10755
  function rowToReceipt(row) {
@@ -10331,7 +10766,8 @@ function rowToReceipt(row) {
10331
10766
  ...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
10332
10767
  ...row.protocol !== null ? { protocol: row.protocol } : {},
10333
10768
  ...metadata !== void 0 ? { metadata } : {},
10334
- ...authorization !== void 0 ? { authorization } : {}
10769
+ ...authorization !== void 0 ? { authorization } : {},
10770
+ ...row.transaction_id !== null ? { transactionId: row.transaction_id } : {}
10335
10771
  };
10336
10772
  }
10337
10773
  function eventToRow(event) {
@@ -10345,7 +10781,8 @@ function eventToRow(event) {
10345
10781
  payment_provider: event.paymentProvider ?? null,
10346
10782
  duration_ms: event.durationMs ?? null,
10347
10783
  status: event.status ?? null,
10348
- data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null
10784
+ data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null,
10785
+ transaction_id: event.transactionId ?? null
10349
10786
  };
10350
10787
  }
10351
10788
  function rowToEvent(row) {
@@ -10360,7 +10797,8 @@ function rowToEvent(row) {
10360
10797
  ...row.payment_provider !== null ? { paymentProvider: row.payment_provider } : {},
10361
10798
  ...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
10362
10799
  ...row.status !== null ? { status: row.status } : {},
10363
- ...data !== void 0 ? { data } : {}
10800
+ ...data !== void 0 ? { data } : {},
10801
+ ...row.transaction_id !== null ? { transactionId: row.transaction_id } : {}
10364
10802
  };
10365
10803
  }
10366
10804
  function rowToPaymentAttempt(row) {
@@ -10378,7 +10816,61 @@ function rowToPaymentAttempt(row) {
10378
10816
  ...row.external_reference !== null ? { externalReference: row.external_reference } : {},
10379
10817
  ...row.rejection_reason !== null ? { rejectionReason: row.rejection_reason } : {},
10380
10818
  createdAt: row.created_at,
10381
- updatedAt: row.updated_at
10819
+ updatedAt: row.updated_at,
10820
+ ...row.transaction_id !== null ? { transactionId: row.transaction_id } : {},
10821
+ ...row.resolution_json !== null ? { resolution: JSON.parse(row.resolution_json) } : {}
10822
+ };
10823
+ }
10824
+ function rowToTransaction(row, now) {
10825
+ const status = row.payment_status;
10826
+ const backend = row.backend_dispatched_at !== null && row.backend_deadline_at !== null ? {
10827
+ dispatchedAt: row.backend_dispatched_at,
10828
+ deadlineAt: row.backend_deadline_at,
10829
+ ...row.backend_status !== null ? { status: row.backend_status } : {},
10830
+ ...row.backend_error_code !== null ? { errorCode: row.backend_error_code } : {}
10831
+ } : void 0;
10832
+ const completedAt = row.completed_at ?? void 0;
10833
+ const authorization = row.authorization_json !== null && row.reservation_id !== null ? {
10834
+ ...JSON.parse(row.authorization_json),
10835
+ reservationId: row.reservation_id
10836
+ } : void 0;
10837
+ return {
10838
+ id: row.id,
10839
+ requestId: row.request_id,
10840
+ resourceId: row.resource_id,
10841
+ protocol: row.protocol,
10842
+ state: transactionState(
10843
+ {
10844
+ payment: { status },
10845
+ ...backend !== void 0 ? { backend } : {},
10846
+ ...completedAt !== void 0 ? { completedAt } : {}
10847
+ },
10848
+ now
10849
+ ),
10850
+ payment: {
10851
+ provider: row.provider,
10852
+ attemptId: row.attempt_id,
10853
+ replayKey: row.replay_key,
10854
+ status,
10855
+ amount: row.payment_amount,
10856
+ currency: row.payment_currency,
10857
+ ...row.network !== null ? { network: row.network } : {},
10858
+ ...row.asset !== null ? { asset: row.asset } : {},
10859
+ ...row.payment_payee !== null ? { payee: row.payment_payee } : {},
10860
+ ...row.payment_external_reference !== null ? { externalReference: row.payment_external_reference } : {},
10861
+ ...row.locator_json !== null ? { locator: JSON.parse(row.locator_json) } : {},
10862
+ ...row.settling_at !== null ? { settlingAt: row.settling_at } : {},
10863
+ ...row.expires_at !== null ? { expiresAt: row.expires_at } : {}
10864
+ },
10865
+ ...authorization !== void 0 ? { authorization } : {},
10866
+ ...backend !== void 0 ? { backend } : {},
10867
+ ...row.receipt_id !== null ? { receiptId: row.receipt_id } : {},
10868
+ ...row.input_digest !== null ? { inputDigest: row.input_digest } : {},
10869
+ configDigest: row.config_digest,
10870
+ ...row.persistence_failures_json !== null ? { persistenceFailures: JSON.parse(row.persistence_failures_json) } : {},
10871
+ createdAt: row.created_at,
10872
+ updatedAt: row.updated_at,
10873
+ ...completedAt !== void 0 ? { completedAt } : {}
10382
10874
  };
10383
10875
  }
10384
10876
 
@@ -10473,13 +10965,60 @@ var MIGRATIONS = [
10473
10965
  CREATE INDEX IF NOT EXISTS idx_payment_attempt_history_request_id ON payment_attempt_history(request_id);
10474
10966
  `);
10475
10967
  }
10968
+ },
10969
+ {
10970
+ version: 4,
10971
+ up(db) {
10972
+ db.exec(`
10973
+ CREATE TABLE IF NOT EXISTS transactions (
10974
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
10975
+ id TEXT NOT NULL UNIQUE,
10976
+ request_id TEXT NOT NULL,
10977
+ resource_id TEXT NOT NULL,
10978
+ protocol TEXT NOT NULL,
10979
+ state TEXT NOT NULL,
10980
+ provider TEXT NOT NULL,
10981
+ attempt_id TEXT NOT NULL,
10982
+ replay_key TEXT NOT NULL,
10983
+ network TEXT,
10984
+ asset TEXT,
10985
+ authorization_json TEXT,
10986
+ reservation_id TEXT,
10987
+ locator_json TEXT,
10988
+ expires_at TEXT,
10989
+ input_digest TEXT,
10990
+ config_digest TEXT NOT NULL,
10991
+ settling_at TEXT,
10992
+ backend_dispatched_at TEXT,
10993
+ backend_deadline_at TEXT,
10994
+ backend_status INTEGER,
10995
+ backend_error_code TEXT,
10996
+ receipt_id TEXT,
10997
+ persistence_failures_json TEXT,
10998
+ completed_at TEXT,
10999
+ created_at TEXT NOT NULL,
11000
+ updated_at TEXT NOT NULL
11001
+ );
11002
+ CREATE INDEX IF NOT EXISTS idx_transactions_state_seq ON transactions(state, seq);
11003
+ CREATE INDEX IF NOT EXISTS idx_transactions_request_id ON transactions(request_id);
11004
+ CREATE INDEX IF NOT EXISTS idx_transactions_replay_key ON transactions(replay_key);
11005
+
11006
+ ALTER TABLE receipts ADD COLUMN transaction_id TEXT;
11007
+ ALTER TABLE events ADD COLUMN transaction_id TEXT;
11008
+ ALTER TABLE payment_attempts ADD COLUMN transaction_id TEXT;
11009
+ ALTER TABLE payment_attempts ADD COLUMN resolution_json TEXT;
11010
+ ALTER TABLE payment_attempt_history ADD COLUMN transaction_id TEXT;
11011
+ ALTER TABLE payment_attempt_history ADD COLUMN resolution_json TEXT;
11012
+ CREATE INDEX IF NOT EXISTS idx_payment_attempt_history_id ON payment_attempt_history(id);
11013
+ `);
11014
+ }
10476
11015
  }
10477
11016
  ];
10478
11017
  var SCHEMA_VERSION2 = MIGRATIONS.at(-1)?.version ?? 0;
10479
- function migrate2(db) {
11018
+ function migrate2(db, target = SCHEMA_VERSION2) {
10480
11019
  const current = db.pragma("user_version", { simple: true });
10481
11020
  for (const migration of MIGRATIONS) {
10482
- if (migration.version <= current) continue;
11021
+ if (migration.version <= current || migration.version > target) continue;
10483
11022
  const apply = db.transaction(() => {
10484
11023
  migration.up(db);
10485
11024
  db.pragma(`user_version = ${migration.version}`);
@@ -10497,6 +11036,22 @@ function clampListLimit(limit) {
10497
11036
  }
10498
11037
  var UNDELIVERED = "backend_status NOT BETWEEN 200 AND 299";
10499
11038
  var UNCERTAIN = "status IN ('settlement-uncertain', 'failed')";
11039
+ var TRANSACTION_SELECT = `
11040
+ SELECT t.*,
11041
+ COALESCE(a.status, h.status) AS payment_status,
11042
+ COALESCE(a.amount, h.amount) AS payment_amount,
11043
+ COALESCE(a.currency, h.currency) AS payment_currency,
11044
+ COALESCE(a.payee, h.payee) AS payment_payee,
11045
+ COALESCE(a.external_reference, h.external_reference) AS payment_external_reference
11046
+ FROM transactions t
11047
+ LEFT JOIN payment_attempts a ON a.id = t.attempt_id
11048
+ LEFT JOIN payment_attempt_history h ON a.id IS NULL AND h.seq = (
11049
+ SELECT MAX(seq) FROM payment_attempt_history
11050
+ WHERE replay_key = t.replay_key AND id = t.attempt_id
11051
+ )`;
11052
+ var TIME_DEPENDENT = "t.backend_deadline_at IS NOT NULL AND t.completed_at IS NULL";
11053
+ var RESOLVED_STATES = ["delivered", "payment-failed"];
11054
+ var CURSOR_PATTERN = /^[1-9][0-9]{0,15}$/;
10500
11055
  var defaultIds = {
10501
11056
  next: (prefix) => prefix !== void 0 ? `${prefix}_${randomUUID()}` : randomUUID()
10502
11057
  };
@@ -10512,8 +11067,8 @@ function createSqliteReceiptStore(options) {
10512
11067
  const db = openSqliteDatabase({ path: path2, label: "Receipt database", logger });
10513
11068
  migrate2(db);
10514
11069
  const insertReceiptStmt = db.prepare(
10515
- `INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json, authorization_json)
10516
- VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json, @authorization_json)`
11070
+ `INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json, authorization_json, transaction_id)
11071
+ VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json, @authorization_json, @transaction_id)`
10517
11072
  );
10518
11073
  const getReceiptStmt = db.prepare("SELECT * FROM receipts WHERE id = ?");
10519
11074
  const listReceiptsStmt = db.prepare(
@@ -10535,8 +11090,8 @@ function createSqliteReceiptStore(options) {
10535
11090
  `SELECT COUNT(*) AS count FROM receipts WHERE ${UNDELIVERED}`
10536
11091
  );
10537
11092
  const insertEventStmt = db.prepare(
10538
- `INSERT INTO events (id, type, request_id, resource_id, at, adapter, payment_provider, duration_ms, status, data_json)
10539
- VALUES (@id, @type, @request_id, @resource_id, @at, @adapter, @payment_provider, @duration_ms, @status, @data_json)`
11093
+ `INSERT INTO events (id, type, request_id, resource_id, at, adapter, payment_provider, duration_ms, status, data_json, transaction_id)
11094
+ VALUES (@id, @type, @request_id, @resource_id, @at, @adapter, @payment_provider, @duration_ms, @status, @data_json, @transaction_id)`
10540
11095
  );
10541
11096
  const listEventsStmt = db.prepare(
10542
11097
  "SELECT * FROM events ORDER BY at DESC, seq DESC LIMIT ?"
@@ -10545,13 +11100,13 @@ function createSqliteReceiptStore(options) {
10545
11100
  "SELECT * FROM events WHERE request_id = ? ORDER BY at DESC, seq DESC LIMIT ?"
10546
11101
  );
10547
11102
  const insertPaymentAttemptStmt = db.prepare(
10548
- `INSERT INTO payment_attempts (id, request_id, resource_id, provider, replay_key, status, amount, currency, payer, payee, external_reference, rejection_reason, created_at, updated_at)
10549
- VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)
11103
+ `INSERT INTO payment_attempts (id, request_id, resource_id, provider, replay_key, status, amount, currency, payer, payee, external_reference, rejection_reason, created_at, updated_at, transaction_id)
11104
+ VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at, @transaction_id)
10550
11105
  RETURNING *`
10551
11106
  );
10552
11107
  const archiveRejectedAttemptStmt = db.prepare(
10553
- `INSERT INTO payment_attempt_history (id, request_id, resource_id, provider, replay_key, status, amount, currency, payer, payee, external_reference, rejection_reason, created_at, updated_at, superseded_at)
10554
- SELECT id, request_id, resource_id, provider, replay_key, status, amount, currency, payer, payee, external_reference, rejection_reason, created_at, updated_at, @created_at
11108
+ `INSERT INTO payment_attempt_history (id, request_id, resource_id, provider, replay_key, status, amount, currency, payer, payee, external_reference, rejection_reason, created_at, updated_at, superseded_at, transaction_id, resolution_json)
11109
+ SELECT id, request_id, resource_id, provider, replay_key, status, amount, currency, payer, payee, external_reference, rejection_reason, created_at, updated_at, @created_at, transaction_id, resolution_json
10555
11110
  FROM payment_attempts
10556
11111
  WHERE replay_key = @replay_key AND provider = @provider AND status = 'rejected'`
10557
11112
  );
@@ -10559,7 +11114,8 @@ function createSqliteReceiptStore(options) {
10559
11114
  `UPDATE payment_attempts SET id = @id, request_id = @request_id, resource_id = @resource_id,
10560
11115
  status = 'reserved', amount = @amount, currency = @currency,
10561
11116
  payer = @payer, payee = @payee, external_reference = NULL, rejection_reason = NULL,
10562
- created_at = @created_at, updated_at = @updated_at
11117
+ created_at = @created_at, updated_at = @updated_at,
11118
+ transaction_id = @transaction_id, resolution_json = NULL
10563
11119
  WHERE replay_key = @replay_key AND provider = @provider AND status = 'rejected'
10564
11120
  RETURNING *`
10565
11121
  );
@@ -10568,7 +11124,7 @@ function createSqliteReceiptStore(options) {
10568
11124
  external_reference = COALESCE(@external_reference, external_reference),
10569
11125
  rejection_reason = COALESCE(@rejection_reason, rejection_reason),
10570
11126
  updated_at = @updated_at
10571
- WHERE replay_key = @replay_key`
11127
+ WHERE replay_key = @replay_key AND resolution_json IS NULL`
10572
11128
  );
10573
11129
  const listPaymentAttemptsStmt = db.prepare(
10574
11130
  "SELECT * FROM payment_attempts ORDER BY created_at DESC, seq DESC LIMIT ?"
@@ -10585,13 +11141,176 @@ function createSqliteReceiptStore(options) {
10585
11141
  const countUncertainPaymentAttemptsStmt = db.prepare(
10586
11142
  `SELECT COUNT(*) AS count FROM payment_attempts WHERE ${UNCERTAIN}`
10587
11143
  );
10588
- const paymentAttemptStatusStmt = db.prepare(
10589
- "SELECT status FROM payment_attempts WHERE replay_key = ?"
11144
+ const paymentAttemptStatusStmt = db.prepare("SELECT id, status, resolution_json FROM payment_attempts WHERE replay_key = ?");
11145
+ const insertTransactionStmt = db.prepare(
11146
+ `INSERT INTO transactions (id, request_id, resource_id, protocol, state, provider, attempt_id, replay_key, network, asset, authorization_json, reservation_id, locator_json, expires_at, input_digest, config_digest, created_at, updated_at)
11147
+ VALUES (@id, @request_id, @resource_id, @protocol, @state, @provider, @attempt_id, @replay_key, @network, @asset, @authorization_json, @reservation_id, @locator_json, @expires_at, @input_digest, @config_digest, @created_at, @created_at)`
10590
11148
  );
10591
- const reclaimRejectedAttempt = db.transaction(
10592
- (params) => {
10593
- archiveRejectedAttemptStmt.run(params);
10594
- return reclaimPaymentAttemptStmt.get(params);
11149
+ const getTransactionStmt = db.prepare(
11150
+ `${TRANSACTION_SELECT} WHERE t.id = ?`
11151
+ );
11152
+ const transactionsOfAttemptStmt = db.prepare(
11153
+ `SELECT t.id FROM payment_attempts a
11154
+ JOIN transactions t ON t.replay_key = a.replay_key AND t.attempt_id = a.id
11155
+ WHERE a.replay_key = ?`
11156
+ );
11157
+ const transactionsOfAttemptIdStmt = db.prepare(
11158
+ `SELECT t.id FROM payment_attempts a
11159
+ JOIN transactions t ON t.replay_key = a.replay_key AND t.attempt_id = a.id
11160
+ WHERE a.id = ?`
11161
+ );
11162
+ const setTransactionStateStmt = db.prepare("UPDATE transactions SET state = ? WHERE id = ?");
11163
+ const stepStmts = {
11164
+ settling: db.prepare(
11165
+ "UPDATE transactions SET settling_at = @at, updated_at = @at WHERE id = @id"
11166
+ ),
11167
+ // A transaction without an authorization has no hold to record
11168
+ hold: db.prepare(
11169
+ `UPDATE transactions SET authorization_json = json_set(authorization_json, '$.hold', @hold), updated_at = @at
11170
+ WHERE id = @id AND authorization_json IS NOT NULL`
11171
+ ),
11172
+ "backend-dispatched": db.prepare(
11173
+ `UPDATE transactions SET backend_dispatched_at = @at, backend_deadline_at = @deadline_at, updated_at = @at
11174
+ WHERE id = @id`
11175
+ ),
11176
+ // Preserve a backend result even when the dispatch step was not saved:
11177
+ // use the result time for missing dispatch and deadline timestamps
11178
+ "backend-result": db.prepare(
11179
+ `UPDATE transactions SET backend_status = @status, backend_error_code = @error_code,
11180
+ backend_dispatched_at = COALESCE(backend_dispatched_at, @at),
11181
+ backend_deadline_at = COALESCE(backend_deadline_at, @at), updated_at = @at
11182
+ WHERE id = @id`
11183
+ ),
11184
+ receipt: db.prepare(
11185
+ "UPDATE transactions SET receipt_id = @receipt_id, updated_at = @at WHERE id = @id"
11186
+ ),
11187
+ completed: db.prepare(
11188
+ `UPDATE transactions SET completed_at = @at, persistence_failures_json = @persistence_failures_json, updated_at = @at
11189
+ WHERE id = @id`
11190
+ )
11191
+ };
11192
+ const getPaymentAttemptStmt = db.prepare(
11193
+ "SELECT * FROM payment_attempts WHERE id = ?"
11194
+ );
11195
+ const getArchivedPaymentAttemptStmt = db.prepare(
11196
+ "SELECT * FROM payment_attempt_history WHERE id = ? ORDER BY seq DESC LIMIT 1"
11197
+ );
11198
+ const resolvePaymentAttemptStmt = db.prepare(
11199
+ `UPDATE payment_attempts SET status = @status,
11200
+ rejection_reason = COALESCE(@rejection_reason, rejection_reason),
11201
+ external_reference = COALESCE(@external_reference, external_reference),
11202
+ resolution_json = @resolution_json, updated_at = @updated_at
11203
+ WHERE id = @id AND status = @expected_status AND resolution_json IS NULL`
11204
+ );
11205
+ function refreshTransactionState(id, now) {
11206
+ const row = getTransactionStmt.get(id);
11207
+ if (row !== void 0) setTransactionStateStmt.run(rowToTransaction(row, now).state, id);
11208
+ }
11209
+ const reserve = db.transaction(
11210
+ (reservation, params) => {
11211
+ let row;
11212
+ if (reservation.reclaimRejected === true) {
11213
+ archiveRejectedAttemptStmt.run(params);
11214
+ row = reclaimPaymentAttemptStmt.get(params);
11215
+ }
11216
+ row ??= insertPaymentAttemptStmt.get({
11217
+ ...params,
11218
+ status: "reserved",
11219
+ external_reference: null,
11220
+ rejection_reason: null
11221
+ });
11222
+ const txn = reservation.transaction;
11223
+ if (txn !== void 0) {
11224
+ const { reservationId, ...authorization } = txn.authorization ?? {};
11225
+ insertTransactionStmt.run({
11226
+ id: txn.id,
11227
+ request_id: reservation.requestId,
11228
+ resource_id: reservation.resourceId,
11229
+ protocol: txn.protocol,
11230
+ state: transactionState({ payment: { status: "reserved" } }, row.created_at),
11231
+ provider: reservation.provider,
11232
+ attempt_id: row.id,
11233
+ replay_key: reservation.replayKey,
11234
+ network: txn.network ?? null,
11235
+ asset: txn.asset ?? null,
11236
+ authorization_json: reservationId !== void 0 ? JSON.stringify(redact({ ...authorization, hold: "reserved" })) : null,
11237
+ reservation_id: reservationId ?? null,
11238
+ locator_json: txn.locator !== void 0 ? JSON.stringify(redact(txn.locator)) : null,
11239
+ expires_at: txn.expiresAt ?? null,
11240
+ input_digest: txn.inputDigest ?? null,
11241
+ config_digest: txn.configDigest,
11242
+ created_at: row.created_at
11243
+ });
11244
+ }
11245
+ return row;
11246
+ }
11247
+ );
11248
+ const updateAttempt = db.transaction((update, now) => {
11249
+ const { changes } = updatePaymentAttemptStmt.run({
11250
+ replay_key: update.replayKey,
11251
+ status: update.status,
11252
+ external_reference: update.externalReference ?? null,
11253
+ rejection_reason: update.rejectionReason ?? null,
11254
+ updated_at: now
11255
+ });
11256
+ for (const { id } of transactionsOfAttemptStmt.all(update.replayKey)) {
11257
+ refreshTransactionState(id, now);
11258
+ }
11259
+ return changes;
11260
+ });
11261
+ const recordStep = db.transaction((step) => {
11262
+ const base = { id: step.transactionId, at: step.at };
11263
+ let changes;
11264
+ switch (step.kind) {
11265
+ case "settling":
11266
+ changes = stepStmts.settling.run(base).changes;
11267
+ break;
11268
+ case "hold":
11269
+ changes = stepStmts.hold.run({ ...base, hold: step.hold }).changes;
11270
+ break;
11271
+ case "backend-dispatched":
11272
+ changes = stepStmts["backend-dispatched"].run({
11273
+ ...base,
11274
+ deadline_at: step.deadlineAt
11275
+ }).changes;
11276
+ break;
11277
+ case "backend-result":
11278
+ changes = stepStmts["backend-result"].run({
11279
+ ...base,
11280
+ status: step.status,
11281
+ error_code: step.errorCode ?? null
11282
+ }).changes;
11283
+ break;
11284
+ case "receipt":
11285
+ changes = stepStmts.receipt.run({ ...base, receipt_id: step.receiptId }).changes;
11286
+ break;
11287
+ case "completed":
11288
+ changes = stepStmts.completed.run({
11289
+ ...base,
11290
+ persistence_failures_json: step.persistenceFailures !== void 0 && step.persistenceFailures.length > 0 ? JSON.stringify(step.persistenceFailures) : null
11291
+ }).changes;
11292
+ break;
11293
+ }
11294
+ if (changes > 0) refreshTransactionState(step.transactionId, step.at);
11295
+ return changes;
11296
+ });
11297
+ const resolveAttempt = db.transaction(
11298
+ (request, now) => {
11299
+ const { changes } = resolvePaymentAttemptStmt.run({
11300
+ id: request.attemptId,
11301
+ expected_status: request.expectedStatus,
11302
+ status: request.status,
11303
+ rejection_reason: request.rejectionReason ?? null,
11304
+ // An operator's reference is unverified; it stays in resolution_json only
11305
+ external_reference: request.resolution.method === "reconcile" ? request.resolution.externalReference ?? null : null,
11306
+ resolution_json: JSON.stringify(redact(request.resolution)),
11307
+ updated_at: now
11308
+ });
11309
+ if (changes === 0) return false;
11310
+ for (const { id } of transactionsOfAttemptIdStmt.all(request.attemptId)) {
11311
+ refreshTransactionState(id, now);
11312
+ }
11313
+ return true;
10595
11314
  }
10596
11315
  );
10597
11316
  let closed = false;
@@ -10600,7 +11319,7 @@ function createSqliteReceiptStore(options) {
10600
11319
  kind: "storage",
10601
11320
  implementationVersion: PACKAGE_VERSION,
10602
11321
  supportedSpec: `sqlite-schema-v${SCHEMA_VERSION2}`,
10603
- capabilities: ["receipts", "events", "payment-attempts"],
11322
+ capabilities: ["receipts", "events", "payment-attempts", "transactions"],
10604
11323
  status: "stable"
10605
11324
  };
10606
11325
  const store = {
@@ -10640,17 +11359,12 @@ function createSqliteReceiptStore(options) {
10640
11359
  payer: reservation.payer ?? null,
10641
11360
  payee: reservation.payee ?? null,
10642
11361
  created_at: now,
10643
- updated_at: now
11362
+ updated_at: now,
11363
+ transaction_id: reservation.transaction?.id ?? null
10644
11364
  };
10645
11365
  let row;
10646
11366
  try {
10647
- row = (reservation.reclaimRejected === true ? reclaimRejectedAttempt.immediate(params) : void 0) ?? // RETURNING on a successful insert always yields the inserted row
10648
- insertPaymentAttemptStmt.get({
10649
- ...params,
10650
- status: "reserved",
10651
- external_reference: null,
10652
- rejection_reason: null
10653
- });
11367
+ row = reserve.immediate(reservation, params);
10654
11368
  } catch (err) {
10655
11369
  if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
10656
11370
  const existing = paymentAttemptStatusStmt.get(reservation.replayKey);
@@ -10670,14 +11384,23 @@ function createSqliteReceiptStore(options) {
10670
11384
  },
10671
11385
  async updatePaymentAttempt(update) {
10672
11386
  try {
10673
- const result = updatePaymentAttemptStmt.run({
10674
- replay_key: update.replayKey,
10675
- status: update.status,
10676
- external_reference: update.externalReference ?? null,
10677
- rejection_reason: update.rejectionReason ?? null,
10678
- updated_at: clock.nowIso()
10679
- });
10680
- if (result.changes === 0) {
11387
+ if (updateAttempt(update, clock.nowIso()) === 0) {
11388
+ const existing = paymentAttemptStatusStmt.get(update.replayKey);
11389
+ if (existing?.resolution_json != null) {
11390
+ logger.warn(
11391
+ {
11392
+ attemptId: existing.id,
11393
+ resolvedStatus: existing.status,
11394
+ status: update.status,
11395
+ ...update.externalReference !== void 0 ? { externalReference: update.externalReference } : {}
11396
+ },
11397
+ "receipt-store: payment attempt already resolved; update not applied"
11398
+ );
11399
+ throw new CommerceError(
11400
+ "STORAGE_ERROR",
11401
+ `Payment attempt ${existing.id} is already resolved; the update was not applied`
11402
+ );
11403
+ }
10681
11404
  throw new CommerceError(
10682
11405
  "STORAGE_ERROR",
10683
11406
  `No payment attempt found for replayKey=${update.replayKey}`
@@ -10729,6 +11452,74 @@ function createSqliteReceiptStore(options) {
10729
11452
  const rows = listOptions.requestId !== void 0 ? (uncertain ? listUncertainPaymentAttemptsByRequestStmt : listPaymentAttemptsByRequestStmt).all(listOptions.requestId, limit) : (uncertain ? listUncertainPaymentAttemptsStmt : listPaymentAttemptsStmt).all(limit);
10730
11453
  return rows.map(rowToPaymentAttempt);
10731
11454
  },
11455
+ async recordTransactionStep(step) {
11456
+ try {
11457
+ if (recordStep(step) === 0) {
11458
+ throw new CommerceError(
11459
+ "STORAGE_ERROR",
11460
+ step.kind === "hold" ? `No transaction with an authorization hold found for transactionId=${step.transactionId}` : `No transaction found for transactionId=${step.transactionId}`
11461
+ );
11462
+ }
11463
+ } catch (err) {
11464
+ throw toCommerceError(err, "STORAGE_ERROR", "Failed to record transaction step");
11465
+ }
11466
+ },
11467
+ async getTransaction(id) {
11468
+ const row = getTransactionStmt.get(id);
11469
+ return row !== void 0 ? rowToTransaction(row, clock.nowIso()) : void 0;
11470
+ },
11471
+ async listTransactions(listOptions = {}) {
11472
+ const { cursor, state, unresolved, pendingHold, requestId } = listOptions;
11473
+ if (cursor !== void 0 && !CURSOR_PATTERN.test(cursor)) {
11474
+ throw new CommerceError("INPUT_INVALID", "Malformed transaction list cursor");
11475
+ }
11476
+ const limit = clampListLimit(listOptions.limit);
11477
+ const where = [];
11478
+ const params = [];
11479
+ if (cursor !== void 0) {
11480
+ where.push("t.seq < ?");
11481
+ params.push(Number(cursor));
11482
+ }
11483
+ if (requestId !== void 0) {
11484
+ where.push("t.request_id = ?");
11485
+ params.push(requestId);
11486
+ }
11487
+ if (state !== void 0) {
11488
+ where.push(`(t.state = ? OR (${TIME_DEPENDENT}))`);
11489
+ params.push(state);
11490
+ }
11491
+ if (pendingHold === true) {
11492
+ where.push("json_extract(t.authorization_json, '$.hold') IN ('reserved', 'uncertain')");
11493
+ }
11494
+ if (unresolved === true) {
11495
+ where.push(`t.state NOT IN (${RESOLVED_STATES.map(() => "?").join(", ")})`);
11496
+ params.push(...RESOLVED_STATES);
11497
+ }
11498
+ const rows = db.prepare(
11499
+ `${TRANSACTION_SELECT}${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY t.seq DESC LIMIT ?`
11500
+ ).all(...params, limit);
11501
+ const now = clock.nowIso();
11502
+ const resolved = RESOLVED_STATES;
11503
+ const transactions = rows.map((row) => rowToTransaction(row, now)).filter(
11504
+ (t) => (state === void 0 || t.state === state) && (unresolved !== true || !resolved.includes(t.state))
11505
+ );
11506
+ const last = rows.at(-1);
11507
+ return {
11508
+ transactions,
11509
+ ...rows.length === limit && last !== void 0 ? { nextCursor: String(last.seq) } : {}
11510
+ };
11511
+ },
11512
+ async getPaymentAttempt(id) {
11513
+ const row = getPaymentAttemptStmt.get(id) ?? getArchivedPaymentAttemptStmt.get(id);
11514
+ return row !== void 0 ? rowToPaymentAttempt(row) : void 0;
11515
+ },
11516
+ async resolvePaymentAttempt(request) {
11517
+ try {
11518
+ return resolveAttempt(request, clock.nowIso());
11519
+ } catch (err) {
11520
+ throw toCommerceError(err, "STORAGE_ERROR", "Failed to resolve payment attempt");
11521
+ }
11522
+ },
10732
11523
  descriptor,
10733
11524
  async health() {
10734
11525
  const startedAt = clock.monotonicMs();