@devlab.group/agent-commerce 2.4.1 → 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-DTKU3Z2M.js → chunk-3OTCFFFT.js} +37 -4
  6. package/dist/chunk-3OTCFFFT.js.map +1 -0
  7. package/dist/{chunk-QPV3EGJO.js → chunk-4K7FIBJM.js} +25 -4
  8. package/dist/chunk-4K7FIBJM.js.map +1 -0
  9. package/dist/{chunk-CXWC5LWM.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-WZW5ORZH.js → chunk-VUCMBGUR.js} +3 -3
  14. package/dist/{chunk-WZW5ORZH.js.map → chunk-VUCMBGUR.js.map} +1 -1
  15. package/dist/cli/{ap2-GWSP4LYY.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-ITWNSS2B.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-TNMLDPUO.js → chunk-QA2OPHBF.js} +655 -70
  36. package/dist/cli/chunk-QA2OPHBF.js.map +1 -0
  37. package/dist/cli/{chunk-GJIMDMYT.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-PGBRWZVA.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-I2ESL6RS.js → provider-6TKB5ADJ.js} +26 -15
  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-JK4ERPJV.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-7WBFZQXV.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 +20 -10
  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-CXWC5LWM.js.map +0 -1
  84. package/dist/chunk-DTKU3Z2M.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-QPV3EGJO.js.map +0 -1
  88. package/dist/cli/ap2-GWSP4LYY.js.map +0 -1
  89. package/dist/cli/chunk-G2JEY7MB.js.map +0 -1
  90. package/dist/cli/chunk-GJIMDMYT.js.map +0 -1
  91. package/dist/cli/chunk-ITWNSS2B.js +0 -8
  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-TNMLDPUO.js.map +0 -1
  98. package/dist/cli/config-UI4EDG4L.js +0 -11
  99. package/dist/cli/mcp-PGBRWZVA.js.map +0 -1
  100. package/dist/cli/near-5AY3HWYF.js.map +0 -1
  101. package/dist/cli/provider-I2ESL6RS.js.map +0 -1
  102. package/dist/cli/run-JK4ERPJV.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-7WBFZQXV.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/README.md CHANGED
@@ -70,8 +70,8 @@ agent-commerce doctor
70
70
  ```
71
71
 
72
72
  Requires **Node >= 22**. The package includes the `agent-commerce` CLI
73
- (`init`, `validate`, `start`, `doctor`, `demo`, `import openapi`, `version`) and
74
- a library for embedding the gateway.
73
+ (`init`, `validate`, `start`, `doctor`, `reconcile`, `resolve`, `demo`,
74
+ `import openapi`, `version`) and a library for embedding the gateway.
75
75
  The default install excludes optional protocol, payment and authorization
76
76
  peers. The OpenAPI parser is a regular dependency because import is part of the
77
77
  CLI.
@@ -84,9 +84,10 @@ agent-commerce start --config config.yaml --env-file .env
84
84
  Install the optional peers for the features you enable, listed below. Missing
85
85
  startup imports stop the gateway with an error naming the package; missing
86
86
  Solana, NEAR or CDP peers fail provider health checks or payment requests.
87
- `start`, `validate` and `doctor` accept `--env-file <path>` to load environment
88
- variables before reading the config. Variables already set in the environment
89
- take precedence. To embed the gateway in your own process, use the library:
87
+ `start`, `validate`, `doctor`, `reconcile` and `resolve` accept `--env-file <path>` to
88
+ load environment variables before reading the config. Variables already set in
89
+ the environment take precedence. To embed the gateway in your own process, use
90
+ the library:
90
91
 
91
92
  ```ts
92
93
  import { createGateway, loadConfig, receipts } from '@devlab.group/agent-commerce';
@@ -309,12 +310,17 @@ prints the `unsupported` lists for A2A, ACP and AP2 when they are enabled.
309
310
 
310
311
  ### Versioning
311
312
 
312
- Minor releases may break experimental configurations, integrations or types.
313
- This applies to MPP, A2A, ACP, AP2, ERC-8409 quotes and
314
- `agent-commerce import openapi`.
315
- Runtime adapters and providers report `status: experimental` in
316
- `GET /.well-known/agent-commerce`. Pin an exact version and test upgrades
317
- before deploying them. Since 2.1, some supported x402 behavior and types
313
+ Minor releases may break experimental configurations, integrations or types:
314
+ MPP, A2A, ACP, AP2, ERC-8409 quotes, `agent-commerce import openapi`, the
315
+ journal's `Transaction` type, `ReceiptStore` journal methods,
316
+ `PaymentProvider.reconcile` and the `/api/transactions` response shape.
317
+ The `transactionId` fields, `receiptCommitted`, and the flags accepted by
318
+ `reconcile` and `resolve` are supported.
319
+
320
+ Experimental runtime adapters and providers identify themselves with
321
+ `status: experimental` in `GET /.well-known/agent-commerce`. Pin experimental
322
+ integrations to an exact version and test upgrades before deployment.
323
+ Since 2.1, some supported x402 behavior and types
318
324
  differ from 2.0; see [Upgrading from 2.0 to 2.x](docs/migration-2.1.md).
319
325
 
320
326
  ## Payment model
@@ -461,7 +467,7 @@ INFO ACP disabled
461
467
  INFO AP2 disabled
462
468
  PASS Payments x402 v2 (scheme=exact) enabled - LOCAL dev chain (eip155:31337), destination=0x7099…79C8, facilitator=local (in-process), paymentFlow=authorization
463
469
  INFO Payments (MPP) MPP not configured
464
- PASS Storage sqlite schema v3 writable; receipts=2
470
+ PASS Storage sqlite schema v4 writable; receipts=2
465
471
  PASS Protocol versions reported by gateway /.well-known/agent-commerce
466
472
 
467
473
  Score: 7/7 checks passed
@@ -474,6 +480,98 @@ failing offers when health is published; reasons appear in gateway debug logs.
474
480
 
475
481
  Exits non-zero if anything fails. `--json` for machines.
476
482
 
483
+ ### `agent-commerce reconcile`
484
+
485
+ ```bash
486
+ npm run agent-commerce -- reconcile --config config.yaml --dry-run
487
+ ```
488
+
489
+ Resolves attempts whose settlement outcome the gateway did not learn:
490
+ `settlement-uncertain` and legacy `failed` attempts, plus `reserved` attempts in
491
+ the journal at least `--min-age` old (default `15m`). The payment provider
492
+ checks chain evidence and returns:
493
+
494
+ - **settled**: a final transfer matches the authorization. The attempt becomes
495
+ `settled` with its transaction ID.
496
+ - **not settled**: final chain state shows the authorization unused after its
497
+ last valid second on EVM or its `maxBlockHeight` on NEAR. The attempt becomes
498
+ `rejected` with reason `not_settled`. Solana reconciliation does not return
499
+ this result: blockhash expiry alone does not prove nonpayment.
500
+ - **unknown**: the evidence is inconclusive. The report includes a reason and,
501
+ when available, a time to try again. The attempt remains unresolved.
502
+
503
+ The command builds payment providers as `start` does without starting a
504
+ listener. It does not call a facilitator, broadcast, refund or retry delivery.
505
+ Each resolution records `--actor` (default: the OS user) and the evidence.
506
+ `--dry-run` checks evidence without recording resolutions; it still opens and
507
+ initializes the receipt database. `--json` prints the report for machines.
508
+ `--limit` caps payment checks and hold updates separately. Journal entries are
509
+ checked by state: `payment-uncertain`, `payment-pending`, then
510
+ `delivery-started`, newest first within each group. Only eligible payment
511
+ attempts are considered. A capped batch of attempts without journal entries
512
+ follows.
513
+ It exits 0 after a completed run, even if attempts remain unresolved, and 1 if
514
+ the run fails to complete.
515
+
516
+ Each family needs its own evidence:
517
+
518
+ - **EVM**: uses the recorded transaction hash or searches logs for a used nonce
519
+ near the attempt's creation time, within its validity bounds. The log search
520
+ covers at most 200,000 blocks. If no match is found and part of the candidate
521
+ range remains unsearched, it returns `unknown` (`search_window_too_wide`).
522
+ - **Solana**: uses the recorded transaction signature or searches a known
523
+ payer's history near the attempt's creation time. Only successful finalized
524
+ transactions count. A search that reaches its page cap returns `unknown`
525
+ (`history_too_long`). Otherwise, a missing match gives `not_expired` while
526
+ the blockhash is valid at `confirmed` commitment, then `blockhash_expired`.
527
+ - **NEAR**: uses the recorded transaction hash and relayer IDs from the
528
+ facilitator's `/supported`. Both are needed to prove `settled`. Without
529
+ them, final access-key state can still prove `not-settled` after expiry;
530
+ other cases remain `unknown`.
531
+
532
+ Coverage is partial: EVM reconciliation (x402 EIP-3009 and Permit2, and MPP)
533
+ is tested on Anvil, Solana on `solana-test-validator` and NEAR on a local
534
+ sandbox. Public-network validation is pending. EVM and NEAR can report
535
+ `not_final` while a referenced transaction awaits finality.
536
+
537
+ With `authorization.ap2` enabled, reconciliation also updates AP2 holds on
538
+ resolved payments: `settled` consumes the mandate; `rejected` releases it.
539
+ It opens the AP2 replay database for these updates without verifying mandates.
540
+ The report's `holds` list (`--json`) covers pending holds whose payments are
541
+ resolved in the database. Dry runs report those holds without writing
542
+ resolutions or hold changes. Both databases are still opened and initialized.
543
+ Later runs can retry holds left pending after earlier payment resolutions,
544
+ including those recorded by `resolve` while AP2 was disabled.
545
+
546
+ ### `agent-commerce resolve`
547
+
548
+ ```bash
549
+ npm run agent-commerce -- resolve <attemptId> --status settled --reason "transfer seen in explorer" --reference 0x...
550
+ ```
551
+
552
+ Records an operator's verdict on one unresolved payment attempt, including
553
+ one `reconcile` left `unknown`. Only `reserved`, `settlement-uncertain` and
554
+ legacy `failed` attempts can be resolved, once each:
555
+
556
+ - `--status settled` has no expiry check. `--reference` records an unchecked
557
+ operator claim in the resolution; it does not replace the attempt's recorded
558
+ transaction ID.
559
+ - A `reserved` attempt younger than 15 minutes is refused (`too_recent`),
560
+ because the gateway may still be settling it.
561
+ - `--status rejected` is refused while the recorded expiry is in the future.
562
+ Without a valid recorded expiry, `--confirm-expired` is required. This
563
+ includes Solana and NEAR attempts and those written before the journal.
564
+
565
+ `--actor` is a caller-supplied label and defaults to the OS user. The command
566
+ opens the receipt database directly and works with the gateway running or
567
+ stopped. With AP2 enabled, it attempts the same hold update as `reconcile`.
568
+ If the provider is disabled, its call fails, or the hold's journal write fails,
569
+ the command reports the update as pending with a warning. A later `reconcile`
570
+ run can retry it.
571
+ `POST /api/payment-attempts/:id/resolve` applies the same rules over HTTP;
572
+ the admin token controls access without identifying the operator. Exit 0
573
+ means the resolution was written; 1 means a refusal or error.
574
+
477
575
  ### `agent-commerce version`
478
576
 
479
577
  ```bash
@@ -493,9 +591,11 @@ reachable by anyone else, know the split:
493
591
  - **Agent routes** (`/api/resources/:id/invoke`, `/mcp`, the A2A mount) are
494
592
  unauthenticated by design - paid resources are protected by payment, not by a
495
593
  password. ACP is the exception: its checkout routes require a bearer token.
496
- - **Operator routes** (`/api/receipts`, `/api/events`, `/api/payment-attempts`)
497
- expose the merchant's commerce ledger: payer addresses, amounts and settlement
498
- hashes. They require `server.adminToken` and return **404** if none is configured.
594
+ - **Operator routes** (`/api/receipts`, `/api/events`, `/api/payment-attempts`,
595
+ `/api/transactions`) expose the merchant's commerce ledger: payer addresses,
596
+ amounts and settlement hashes. `POST /api/payment-attempts/:id/resolve`
597
+ records a resolution. They require `server.adminToken` and return **404** if
598
+ none is configured.
499
599
  - **Browsers** are governed by `server.allowedOrigins`, an explicit allowlist
500
600
  that defaults to empty.
501
601
  - **There is no rate limiting.** A free resource is an unauthenticated proxy to
package/dist/ap2.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { E as EnabledAp2Config } from './types-j7MnUXYN.js';
2
2
  export { A as AP2_CHECKOUT_MANDATE_VCT, a as AP2_CHECKOUT_PROFILE, b as AP2_DEFAULT_CLOCK_SKEW_SECONDS, c as AP2_MAX_CLOCK_SKEW_SECONDS, d as AP2_SIGNING_ALGORITHM, e as AP2_SPEC_VERSION, f as Ap2AuthorizationConfig, g as Ap2Mode, h as Ap2TrustedIssuer, i as Ap2TrustedKey } from './types-j7MnUXYN.js';
3
- import { AuthorizationProvider } from './index.js';
4
- import { Clock, Logger } from './index.js';
3
+ import { AuthorizationProvider, Clock, Logger } from './index.js';
5
4
 
6
5
  /**
7
6
  * A private ES256 key: either a private JWK (the pair of the public one in the
@@ -92,9 +91,9 @@ declare const AP2_REJECTION_REASONS: readonly ["malformed_presentation", "untrus
92
91
  type Ap2RejectionReason = (typeof AP2_REJECTION_REASONS)[number];
93
92
 
94
93
  /**
95
- * `released` is "nothing happened" and the only state a mandate can be
96
- * presented from again. `uncertain` is a settlement whose outcome we never
97
- * learned: not reusable, but findable by an operator reconciling by hand.
94
+ * `released` permits another reservation; other states block reuse.
95
+ * `uncertain` records an unknown settlement outcome and stays blocked until
96
+ * payment resolution is followed by a consume or release update.
98
97
  */
99
98
  type Ap2AuthorizationState = 'reserved' | 'consumed' | 'released' | 'uncertain';
100
99
  /**
@@ -118,20 +117,33 @@ interface Ap2ReservationRequest {
118
117
  type Ap2ReservationResult = {
119
118
  readonly kind: 'reserved';
120
119
  }
121
- /** Already reserved, consumed, or of uncertain outcome. Never re-spendable */
120
+ /** Already reserved, consumed or uncertain; refuse this reservation */
122
121
  | {
123
122
  readonly kind: 'replayed';
124
123
  readonly state: Ap2AuthorizationState;
125
124
  };
125
+ type TransitionResult = boolean | void;
126
126
  interface Ap2ReplayStore {
127
127
  /** Atomically claim a mandate, or report that something already holds it */
128
128
  reserve(request: Ap2ReservationRequest): Ap2ReservationResult;
129
- /** Settlement succeeded: spend it permanently */
130
- consume(reference: string): void;
131
- /** Nothing happened: hand it back so a corrected retry can use it */
132
- release(reference: string): void;
133
- /** Settlement outcome unknown. Not reusable, and flagged for a human */
134
- markUncertain(reference: string): void;
129
+ /**
130
+ * Mark the mandate permanently spent, targeting only the supplied
131
+ * `requestId` when present. Returns true when the matching reservation is
132
+ * consumed, including on a repeated call, and false otherwise.
133
+ */
134
+ consume(reference: string, requestId?: string): TransitionResult;
135
+ /**
136
+ * Release a reservation for reuse. Returns true if it is released or the
137
+ * supplied `requestId` differs from the current holder; a different holder
138
+ * is left unchanged. Returns false otherwise.
139
+ */
140
+ release(reference: string, requestId?: string): TransitionResult;
141
+ /**
142
+ * Record an unknown settlement outcome for operator review and block reuse.
143
+ * Returns true when the matching reservation is uncertain, including on a
144
+ * repeated call, and false otherwise.
145
+ */
146
+ markUncertain(reference: string, requestId?: string): TransitionResult;
135
147
  /** Current state, for tests and diagnostics */
136
148
  stateOf(reference: string): Ap2AuthorizationState | undefined;
137
149
  close(): void;
package/dist/ap2.js CHANGED
@@ -1,6 +1,6 @@
1
- import { AP2_CHECKOUT_MANDATE_VCT, AP2_SIGNING_ALGORITHM, AP2_DIGEST_ALGORITHM, AP2_CHECKOUT_PROFILE, AP2_SPEC_VERSION, AP2_KEY_TYPE, AP2_JWK_CURVE, openSqliteDatabase } from './chunk-WZW5ORZH.js';
2
- export { AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_MAX_CLOCK_SKEW_SECONDS, AP2_SIGNING_ALGORITHM, AP2_SPEC_VERSION } from './chunk-WZW5ORZH.js';
3
- import { systemClock, NOOP_LOGGER, CommerceError, PACKAGE_VERSION } from './chunk-DTKU3Z2M.js';
1
+ import { AP2_CHECKOUT_MANDATE_VCT, AP2_SIGNING_ALGORITHM, AP2_DIGEST_ALGORITHM, AP2_CHECKOUT_PROFILE, AP2_SPEC_VERSION, AP2_KEY_TYPE, AP2_JWK_CURVE, openSqliteDatabase } from './chunk-VUCMBGUR.js';
2
+ export { AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_MAX_CLOCK_SKEW_SECONDS, AP2_SIGNING_ALGORITHM, AP2_SPEC_VERSION } from './chunk-VUCMBGUR.js';
3
+ import { systemClock, NOOP_LOGGER, CommerceError, PACKAGE_VERSION } from './chunk-3OTCFFFT.js';
4
4
  import { isRecord } from './chunk-2CJNA7MS.js';
5
5
  import { SignJWT, importPKCS8, importJWK, jwtVerify, decodeProtectedHeader, decodeJwt } from 'jose';
6
6
  import canonicalize from 'canonicalize';
@@ -276,7 +276,11 @@ function createAp2ReplayStore(options) {
276
276
  );
277
277
  const transition = db.prepare(
278
278
  `UPDATE ap2_authorizations SET state = @state, updated_at = @at
279
- WHERE reference = @reference AND state = 'reserved'`
279
+ WHERE reference = @reference AND state IN ('reserved', 'uncertain')
280
+ AND (@request_id IS NULL OR request_id = @request_id)`
281
+ );
282
+ const selectHolder = db.prepare(
283
+ "SELECT state, request_id FROM ap2_authorizations WHERE reference = ?"
280
284
  );
281
285
  const reserveTx = db.transaction((request) => {
282
286
  const at = new Date(now()).toISOString();
@@ -303,22 +307,32 @@ function createAp2ReplayStore(options) {
303
307
  });
304
308
  return { kind: "reserved" };
305
309
  });
306
- const move = (reference, state) => {
307
- transition.run({ reference, state, at: new Date(now()).toISOString() });
310
+ const move = (reference, state, requestId) => {
311
+ const { changes } = transition.run({
312
+ reference,
313
+ state,
314
+ request_id: requestId ?? null,
315
+ at: new Date(now()).toISOString()
316
+ });
317
+ if (changes > 0) return true;
318
+ const row = selectHolder.get(reference);
319
+ if (row === void 0) return false;
320
+ if (requestId !== void 0 && row.request_id !== requestId) return state === "released";
321
+ return row.state === state;
308
322
  };
309
323
  let closed = false;
310
324
  return {
311
325
  reserve(request) {
312
326
  return reserveTx(request);
313
327
  },
314
- consume(reference) {
315
- move(reference, "consumed");
328
+ consume(reference, requestId) {
329
+ return move(reference, "consumed", requestId);
316
330
  },
317
- release(reference) {
318
- move(reference, "released");
331
+ release(reference, requestId) {
332
+ return move(reference, "released", requestId);
319
333
  },
320
- markUncertain(reference) {
321
- move(reference, "uncertain");
334
+ markUncertain(reference, requestId) {
335
+ return move(reference, "uncertain", requestId);
322
336
  },
323
337
  stateOf(reference) {
324
338
  return selectByReference.get(reference)?.state;
@@ -670,11 +684,15 @@ function createAp2AuthorizationProvider(options) {
670
684
  };
671
685
  }
672
686
  const finalize = (action, reservationId, context) => {
687
+ let applied;
673
688
  try {
674
- replay[action](reservationId);
689
+ applied = replay[action](reservationId, context.requestId);
675
690
  } catch (cause) {
676
691
  throw ap2Unavailable(`replay store could not record ${action}`, { ...context, cause });
677
692
  }
693
+ if (applied === false) {
694
+ throw ap2Unavailable(`replay store holds no matching reservation to ${action}`, context);
695
+ }
678
696
  };
679
697
  return {
680
698
  name: "ap2",