@devlab.group/agent-commerce 1.4.0 → 1.4.1

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.
package/dist/index.d.ts CHANGED
@@ -4,8 +4,8 @@ export { f as AuthorizationMethodName, g as BackendMethod, h as DEFAULT_BACKEND_
4
4
  import { X as X402FacilitatorConfig } from './guardrails-Dmwi63KB.js';
5
5
  import { a as AuthorizationRequirement, b as AuthorizationSubmission, A as AuthorizationProvider } from './authorization-CPDcpJVa.js';
6
6
  export { c as AuthorizationFinalizeContext, d as AuthorizationRecord, e as AuthorizationVerification, f as AuthorizationVerificationContext } from './authorization-CPDcpJVa.js';
7
- import { D as DeliveredOutcome, P as PaymentRequiredOutcome, C as CommerceEvent, a as PaymentAttempt, b as CommerceReceipt, E as ExecutionPipeline, c as ProtocolAdapter, H as HttpProtocolAdapter, A as AdapterHttpRoute, d as ProtocolAdapterContext } from './protocol-adapter-CaMZj6Wm.js';
8
- export { e as COMMERCE_EVENT_TYPES, f as CanonicalRequest, g as CommerceEventType, h as EventSink, i as ExecutionOutcome, j as isHttpProtocolAdapter } from './protocol-adapter-CaMZj6Wm.js';
7
+ import { D as DeliveredOutcome, P as PaymentRequiredOutcome, C as CommerceEvent, a as PaymentAttempt, b as CommerceReceipt, E as ExecutionPipeline, c as ProtocolAdapter, H as HttpProtocolAdapter, A as AdapterHttpRoute, d as ProtocolAdapterContext } from './protocol-adapter-Bebmggf_.js';
8
+ export { e as COMMERCE_EVENT_TYPES, f as CanonicalRequest, g as CommerceEventType, h as EventSink, i as ExecutionOutcome, j as isHttpProtocolAdapter } from './protocol-adapter-Bebmggf_.js';
9
9
  import { FastifyInstance } from 'fastify';
10
10
  import { IncomingMessage, ServerResponse } from 'node:http';
11
11
 
@@ -303,6 +303,16 @@ interface BackendRequest {
303
303
  readonly resourceId: string;
304
304
  /** Validated resource input. */
305
305
  readonly input: unknown;
306
+ /**
307
+ * Names the *operation*, so the merchant can recognise a repeat of it.
308
+ *
309
+ * Not `requestId`, which is fresh per call and would make every retry look
310
+ * like new work. An adapter sets this only when it can derive a value that
311
+ * survives a client retry, a reconnect and a gateway restart; the HTTP
312
+ * executor forwards it as the `Idempotency-Key` request header. Absent
313
+ * means the protocol has no such notion, and the merchant sees no header.
314
+ */
315
+ readonly idempotencyKey?: string;
306
316
  }
307
317
  interface BackendResponse {
308
318
  readonly status: number;
@@ -630,8 +640,14 @@ declare class AcpProtocolAdapter implements HttpProtocolAdapter {
630
640
  private readonly token;
631
641
  private readonly operations;
632
642
  private readonly idempotencyOptions;
633
- /** Derived once: the token is fixed, and only its digest may be persisted. */
634
- private readonly identityHash;
643
+ /**
644
+ * What an idempotency claim is scoped to, from `context.publicBaseUrl`.
645
+ *
646
+ * Not readonly because, unlike the token, it is known only once the adapter
647
+ * starts. Worth that: scoping by the credential meant a rotation freed
648
+ * every outstanding claim.
649
+ */
650
+ private deployment;
635
651
  private readonly discoveryMetadata;
636
652
  private idempotency;
637
653
  private resources;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { 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, openSqliteDatabase, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-7NKWFGBY.js';
2
- import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-IUR3Q7DE.js';
3
- import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER } from './chunk-ZDVJ7EVL.js';
4
- 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_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-ZDVJ7EVL.js';
1
+ import { 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, openSqliteDatabase, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-GF6KHHKL.js';
2
+ import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-NWIQLJNU.js';
3
+ import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER } from './chunk-ZEGFLRGV.js';
4
+ 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_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-ZEGFLRGV.js';
5
5
  import { promises, accessSync, constants } from 'fs';
6
6
  import * as path from 'path';
7
7
  import { parse, YAMLError } from 'yaml';
@@ -85,6 +85,7 @@ var DEFAULT_CONFIG_FILENAME = "config.yaml";
85
85
 
86
86
  // src/core/execution/backend-http.ts
87
87
  var MAX_BODY_SNIPPET_LENGTH = 512;
88
+ var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
88
89
  var PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
89
90
  var MAX_RESPONSE_BODY_BYTES = 1024 * 1024;
90
91
  var HttpBackendExecutor = class {
@@ -126,6 +127,10 @@ var HttpBackendExecutor = class {
126
127
  if (error instanceof CommerceError) throw error;
127
128
  }
128
129
  const headers = { ...handler.headers ?? {} };
130
+ if (request.idempotencyKey !== void 0) {
131
+ deleteHeader(headers, IDEMPOTENCY_KEY_HEADER);
132
+ headers[IDEMPOTENCY_KEY_HEADER] = request.idempotencyKey;
133
+ }
129
134
  let body;
130
135
  checkQueryCollision(target, parts.query, context);
131
136
  for (const [key, value] of Object.entries(parts.query)) {
@@ -334,6 +339,12 @@ function hasHeader(headers, name) {
334
339
  const target = name.toLowerCase();
335
340
  return Object.keys(headers).some((key) => key.toLowerCase() === target);
336
341
  }
342
+ function deleteHeader(headers, name) {
343
+ const target = name.toLowerCase();
344
+ for (const key of Object.keys(headers)) {
345
+ if (key.toLowerCase() === target) delete headers[key];
346
+ }
347
+ }
337
348
  function headersToRecord(headers) {
338
349
  const record = {};
339
350
  headers.forEach((value, key) => {
@@ -924,17 +935,16 @@ function createExecutionPipeline(options) {
924
935
  verification
925
936
  });
926
937
  } catch (error) {
927
- const uncertainTxHash = uncertainSettlementTxHash(error);
928
- await hold?.finalize(uncertainTxHash !== void 0 ? "markUncertain" : "release");
938
+ const txHash = settlementTxHash(error);
939
+ await hold?.finalize("markUncertain");
929
940
  await safePersist(
930
- () => options.store.updatePaymentAttempt(
931
- uncertainTxHash !== void 0 ? { replayKey, status: "settlement-uncertain", externalReference: uncertainTxHash } : {
932
- replayKey,
933
- status: "failed",
934
- ...error instanceof Error ? { rejectionReason: error.message } : {}
935
- }
936
- ),
937
- uncertainTxHash !== void 0 ? "updatePaymentAttempt(settlement-uncertain)" : "updatePaymentAttempt(failed)",
941
+ () => options.store.updatePaymentAttempt({
942
+ replayKey,
943
+ status: "settlement-uncertain",
944
+ ...txHash !== void 0 ? { externalReference: txHash } : {},
945
+ ...error instanceof Error ? { rejectionReason: error.message } : {}
946
+ }),
947
+ "updatePaymentAttempt(settlement-uncertain)",
938
948
  request.requestId
939
949
  );
940
950
  await safeEmit(
@@ -945,19 +955,21 @@ function createExecutionPipeline(options) {
945
955
  adapter: request.protocol,
946
956
  paymentProvider: provider.name,
947
957
  status: "error",
948
- data: uncertainTxHash !== void 0 ? { reason: "settlement-uncertain", transactionHash: uncertainTxHash } : { reason: "settlement-threw" }
958
+ data: {
959
+ reason: "settlement-uncertain",
960
+ ...txHash !== void 0 ? { transactionHash: txHash } : {}
961
+ }
949
962
  })
950
963
  );
951
- throw new CommerceError(
952
- "PAYMENT_SETTLEMENT_FAILED",
953
- uncertainTxHash !== void 0 ? "Settlement could not be confirmed" : "Payment settlement failed",
954
- {
955
- requestId: request.requestId,
956
- resourceId: resource.id,
957
- cause: error,
958
- ...uncertainTxHash !== void 0 ? { details: { settlementUncertain: true, transactionHash: uncertainTxHash } } : {}
964
+ throw new CommerceError("PAYMENT_SETTLEMENT_FAILED", "Settlement could not be confirmed", {
965
+ requestId: request.requestId,
966
+ resourceId: resource.id,
967
+ cause: error,
968
+ details: {
969
+ settlementUncertain: true,
970
+ ...txHash !== void 0 ? { transactionHash: txHash } : {}
959
971
  }
960
- );
972
+ });
961
973
  }
962
974
  if (settlement.status !== "settled") {
963
975
  await hold?.finalize("release");
@@ -1018,7 +1030,8 @@ function createExecutionPipeline(options) {
1018
1030
  backendResponse = await options.backend.call(resource.handler, {
1019
1031
  requestId: request.requestId,
1020
1032
  resourceId: resource.id,
1021
- input: validInput
1033
+ input: validInput,
1034
+ ...request.idempotencyKey !== void 0 ? { idempotencyKey: request.idempotencyKey } : {}
1022
1035
  });
1023
1036
  } catch (error) {
1024
1037
  const commerceError = toCommerceError(error, "BACKEND_ERROR", "Backend call failed");
@@ -1124,9 +1137,8 @@ function backendErrorStatus(error) {
1124
1137
  const status = error.details?.["status"];
1125
1138
  return typeof status === "number" ? status : 0;
1126
1139
  }
1127
- function uncertainSettlementTxHash(error) {
1128
- if (!isCommerceError(error) || error.code !== "PAYMENT_PROVIDER_UNAVAILABLE") return void 0;
1129
- const hash = error.details?.["transactionHash"];
1140
+ function settlementTxHash(error) {
1141
+ const hash = isCommerceError(error) ? error.details?.["transactionHash"] : void 0;
1130
1142
  return typeof hash === "string" ? hash : void 0;
1131
1143
  }
1132
1144
  function resolveAuthorizationProviders(providers, resource, requestId) {
@@ -3334,13 +3346,14 @@ async function createGateway(options) {
3334
3346
 
3335
3347
  // src/protocols/acp/checkout-mapping.ts
3336
3348
  function toCanonicalRequest(options) {
3337
- const { request, resourceId, requestId, receivedAt } = options;
3349
+ const { request, resourceId, requestId, receivedAt, idempotencyKey } = options;
3338
3350
  return {
3339
3351
  requestId,
3340
3352
  resourceId,
3341
3353
  input: canonicalInput(request),
3342
3354
  protocol: "acp",
3343
- receivedAt
3355
+ receivedAt,
3356
+ ...idempotencyKey !== void 0 ? { idempotencyKey } : {}
3344
3357
  };
3345
3358
  }
3346
3359
  function canonicalInput(request) {
@@ -3448,8 +3461,8 @@ function writeAcpJson(res, status, body, headers = {}) {
3448
3461
  function writeAcpFailure(res, failure, headers = {}) {
3449
3462
  writeAcpJson(res, failure.status, failure.error, headers);
3450
3463
  }
3451
- function identityHash(token) {
3452
- return sha256(`acp-auth:${token}`);
3464
+ function operationKey(scope) {
3465
+ return sha256(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
3453
3466
  }
3454
3467
  function requestFingerprint(body) {
3455
3468
  return sha256(canonicalize(body));
@@ -3468,6 +3481,7 @@ function sha256(value) {
3468
3481
  }
3469
3482
 
3470
3483
  // src/protocols/acp/idempotency/store.ts
3484
+ var SCHEMA_VERSION = 2;
3471
3485
  var ACP_MAX_IDEMPOTENCY_KEY_LENGTH = 255;
3472
3486
  function createAcpIdempotencyStore(options) {
3473
3487
  const logger = options.logger ?? NOOP_LOGGER;
@@ -3478,33 +3492,39 @@ function createAcpIdempotencyStore(options) {
3478
3492
  label: "ACP idempotency database",
3479
3493
  logger
3480
3494
  });
3481
- migrate(db);
3495
+ migrate(db, logger);
3482
3496
  const selectStmt = db.prepare(
3483
3497
  `SELECT state, fingerprint, status, body_json FROM acp_idempotency
3484
- WHERE identity_hash = ? AND endpoint = ? AND idempotency_key = ?`
3498
+ WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
3485
3499
  );
3486
3500
  const insertStmt = db.prepare(
3487
3501
  `INSERT INTO acp_idempotency
3488
- (identity_hash, endpoint, idempotency_key, fingerprint, state, created_at, expires_at)
3489
- VALUES (@identity_hash, @endpoint, @idempotency_key, @fingerprint, 'in_flight', @created_at, @expires_at)`
3502
+ (deployment, endpoint, idempotency_key, fingerprint, state, created_at, expires_at)
3503
+ VALUES (@deployment, @endpoint, @idempotency_key, @fingerprint, 'in_flight', @created_at, @expires_at)`
3490
3504
  );
3491
3505
  const completeStmt = db.prepare(
3492
3506
  `UPDATE acp_idempotency SET state = 'completed', status = @status, body_json = @body_json
3493
- WHERE identity_hash = @identity_hash AND endpoint = @endpoint AND idempotency_key = @idempotency_key`
3507
+ WHERE deployment = @deployment AND endpoint = @endpoint AND idempotency_key = @idempotency_key`
3494
3508
  );
3495
3509
  const deleteStmt = db.prepare(
3496
3510
  `DELETE FROM acp_idempotency
3497
- WHERE identity_hash = ? AND endpoint = ? AND idempotency_key = ?`
3511
+ WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
3512
+ );
3513
+ const unresolvedStmt = db.prepare(
3514
+ `UPDATE acp_idempotency SET state = 'unresolved'
3515
+ WHERE deployment = @deployment AND endpoint = @endpoint AND idempotency_key = @idempotency_key`
3516
+ );
3517
+ const expireStmt = db.prepare(
3518
+ "DELETE FROM acp_idempotency WHERE expires_at <= ? AND state = 'completed'"
3498
3519
  );
3499
- const expireStmt = db.prepare("DELETE FROM acp_idempotency WHERE expires_at <= ?");
3500
3520
  const claimTx = db.transaction(
3501
3521
  (scope, fingerprint) => {
3502
3522
  const at = now();
3503
3523
  expireStmt.run(new Date(at).toISOString());
3504
- const existing = selectStmt.get(scope.identityHash, scope.endpoint, scope.key);
3524
+ const existing = selectStmt.get(scope.deployment, scope.endpoint, scope.key);
3505
3525
  if (existing === void 0) {
3506
3526
  insertStmt.run({
3507
- identity_hash: scope.identityHash,
3527
+ deployment: scope.deployment,
3508
3528
  endpoint: scope.endpoint,
3509
3529
  idempotency_key: scope.key,
3510
3530
  fingerprint,
@@ -3514,6 +3534,7 @@ function createAcpIdempotencyStore(options) {
3514
3534
  return { kind: "reserved" };
3515
3535
  }
3516
3536
  if (existing.fingerprint !== fingerprint) return { kind: "conflict" };
3537
+ if (existing.state === "unresolved") return { kind: "unresolved" };
3517
3538
  if (existing.state !== "completed") return { kind: "in-flight" };
3518
3539
  return {
3519
3540
  kind: "replay",
@@ -3529,15 +3550,22 @@ function createAcpIdempotencyStore(options) {
3529
3550
  },
3530
3551
  complete(scope, response) {
3531
3552
  completeStmt.run({
3532
- identity_hash: scope.identityHash,
3553
+ deployment: scope.deployment,
3533
3554
  endpoint: scope.endpoint,
3534
3555
  idempotency_key: scope.key,
3535
3556
  status: response.status,
3536
3557
  body_json: JSON.stringify(response.body ?? null)
3537
3558
  });
3538
3559
  },
3560
+ markUnresolved(scope) {
3561
+ unresolvedStmt.run({
3562
+ deployment: scope.deployment,
3563
+ endpoint: scope.endpoint,
3564
+ idempotency_key: scope.key
3565
+ });
3566
+ },
3539
3567
  release(scope) {
3540
- deleteStmt.run(scope.identityHash, scope.endpoint, scope.key);
3568
+ deleteStmt.run(scope.deployment, scope.endpoint, scope.key);
3541
3569
  },
3542
3570
  close() {
3543
3571
  if (closed) return;
@@ -3546,13 +3574,23 @@ function createAcpIdempotencyStore(options) {
3546
3574
  }
3547
3575
  };
3548
3576
  }
3549
- function migrate(db) {
3577
+ function migrate(db, logger) {
3550
3578
  const current = db.pragma("user_version", { simple: true });
3551
- if (current >= 1) return;
3579
+ if (current >= SCHEMA_VERSION) return;
3552
3580
  const apply = db.transaction(() => {
3581
+ if (current === 1) {
3582
+ const carried = db.prepare("SELECT COUNT(*) AS count FROM acp_idempotency WHERE state != 'completed'").get();
3583
+ if (carried.count > 0) {
3584
+ logger.warn(
3585
+ { unresolved: carried.count },
3586
+ "acp idempotency: schema v1 -> v2 discards claims keyed by the old bearer-token digest; reconcile these operations against the merchant before trusting a retry"
3587
+ );
3588
+ }
3589
+ db.exec("DROP TABLE acp_idempotency");
3590
+ }
3553
3591
  db.exec(`
3554
3592
  CREATE TABLE IF NOT EXISTS acp_idempotency (
3555
- identity_hash TEXT NOT NULL,
3593
+ deployment TEXT NOT NULL,
3556
3594
  endpoint TEXT NOT NULL,
3557
3595
  idempotency_key TEXT NOT NULL,
3558
3596
  fingerprint TEXT NOT NULL,
@@ -3561,12 +3599,12 @@ function migrate(db) {
3561
3599
  body_json TEXT,
3562
3600
  created_at TEXT NOT NULL,
3563
3601
  expires_at TEXT NOT NULL,
3564
- PRIMARY KEY (identity_hash, endpoint, idempotency_key)
3602
+ PRIMARY KEY (deployment, endpoint, idempotency_key)
3565
3603
  );
3566
3604
  CREATE INDEX IF NOT EXISTS idx_acp_idempotency_expires_at
3567
3605
  ON acp_idempotency(expires_at);
3568
3606
  `);
3569
- db.pragma("user_version = 1");
3607
+ db.pragma(`user_version = ${SCHEMA_VERSION}`);
3570
3608
  });
3571
3609
  apply();
3572
3610
  }
@@ -7974,6 +8012,28 @@ function asResponse(failure) {
7974
8012
 
7975
8013
  // src/protocols/acp/adapter.ts
7976
8014
  var DISCOVERY_CACHE_CONTROL = "public, max-age=3600";
8015
+ var PRE_BACKEND_ERROR_CODES = /* @__PURE__ */ new Set([
8016
+ "CONFIG_INVALID",
8017
+ "RESOURCE_NOT_FOUND",
8018
+ "INPUT_INVALID",
8019
+ "PROTOCOL_UNSUPPORTED",
8020
+ "GATEWAY_BUSY",
8021
+ "PAYMENT_REQUIRED",
8022
+ "PAYMENT_INVALID",
8023
+ "PAYMENT_REPLAYED",
8024
+ "PAYMENT_PROVIDER_UNAVAILABLE",
8025
+ "PAYMENT_SETTLEMENT_FAILED",
8026
+ "AUTHORIZATION_REQUIRED",
8027
+ "AUTHORIZATION_INVALID",
8028
+ "AUTHORIZATION_REPLAYED",
8029
+ "AUTHORIZATION_PROVIDER_UNAVAILABLE"
8030
+ ]);
8031
+ function reachedFor(code) {
8032
+ return PRE_BACKEND_ERROR_CODES.has(code) ? "no" : "unknown";
8033
+ }
8034
+ function notReached(failure) {
8035
+ return { response: asResponse(failure), reached: "no" };
8036
+ }
7977
8037
  var AcpProtocolAdapter = class {
7978
8038
  name = "acp";
7979
8039
  mountPath;
@@ -7982,8 +8042,14 @@ var AcpProtocolAdapter = class {
7982
8042
  token;
7983
8043
  operations;
7984
8044
  idempotencyOptions;
7985
- /** Derived once: the token is fixed, and only its digest may be persisted. */
7986
- identityHash;
8045
+ /**
8046
+ * What an idempotency claim is scoped to, from `context.publicBaseUrl`.
8047
+ *
8048
+ * Not readonly because, unlike the token, it is known only once the adapter
8049
+ * starts. Worth that: scoping by the credential meant a rotation freed
8050
+ * every outstanding claim.
8051
+ */
8052
+ deployment;
7987
8053
  discoveryMetadata;
7988
8054
  idempotency;
7989
8055
  resources = /* @__PURE__ */ new Map();
@@ -7998,7 +8064,6 @@ var AcpProtocolAdapter = class {
7998
8064
  this.token = options.token;
7999
8065
  this.operations = options.operations;
8000
8066
  this.idempotencyOptions = options.idempotency;
8001
- this.identityHash = identityHash(options.token);
8002
8067
  this.discoveryMetadata = options.discovery;
8003
8068
  this.descriptor = buildDescriptor(PACKAGE_VERSION);
8004
8069
  this.additionalHttpRoutes = [
@@ -8042,6 +8107,7 @@ var AcpProtocolAdapter = class {
8042
8107
  resolved.set(operation, resource);
8043
8108
  }
8044
8109
  this.resources = resolved;
8110
+ this.deployment = context.publicBaseUrl;
8045
8111
  this.idempotency = createAcpIdempotencyStore({
8046
8112
  path: this.idempotencyOptions.path,
8047
8113
  retentionHours: this.idempotencyOptions.retentionHours,
@@ -8130,22 +8196,33 @@ var AcpProtocolAdapter = class {
8130
8196
  */
8131
8197
  async dispatch(request) {
8132
8198
  const store = this.idempotency;
8199
+ const deployment = this.deployment;
8133
8200
  const key = request.idempotencyKey;
8134
- if (store === void 0 || key === void 0) return this.runCheckout(request);
8135
- const scope = {
8136
- identityHash: this.identityHash,
8137
- endpoint: request.route.path,
8138
- key
8139
- };
8201
+ if (store === void 0 || deployment === void 0 || key === void 0) {
8202
+ return (await this.runCheckout(request)).response;
8203
+ }
8204
+ const scope = { deployment, endpoint: request.route.path, key };
8140
8205
  const claim = store.claim(scope, requestFingerprint(request.body));
8141
8206
  switch (claim.kind) {
8142
8207
  case "in-flight":
8208
+ return {
8209
+ ...asResponse(
8210
+ acpFailure(
8211
+ 409,
8212
+ "invalid_request",
8213
+ "idempotency_in_flight",
8214
+ "A request with this Idempotency-Key is still being processed."
8215
+ )
8216
+ ),
8217
+ retryAfterSeconds: ACP_IN_FLIGHT_RETRY_AFTER_SECONDS
8218
+ };
8219
+ case "unresolved":
8143
8220
  return asResponse(
8144
8221
  acpFailure(
8145
8222
  409,
8146
- "invalid_request",
8147
- "idempotency_in_flight",
8148
- "A request with this Idempotency-Key is still being processed."
8223
+ "processing_error",
8224
+ "idempotency_unresolved",
8225
+ "An earlier request with this Idempotency-Key reached the merchant and its outcome is unknown. Check the operation with the merchant before retrying."
8149
8226
  )
8150
8227
  );
8151
8228
  case "conflict":
@@ -8160,16 +8237,21 @@ var AcpProtocolAdapter = class {
8160
8237
  case "replay":
8161
8238
  return { status: claim.status, body: claim.body, replayed: true };
8162
8239
  }
8163
- let response;
8240
+ let outcome;
8164
8241
  try {
8165
- response = await this.runCheckout(request);
8242
+ outcome = await this.runCheckout(request, operationKey(scope));
8166
8243
  } catch (err) {
8167
- store.release(scope);
8244
+ store.markUnresolved(scope);
8168
8245
  throw err;
8169
8246
  }
8170
- if (response.status >= 500) store.release(scope);
8171
- else store.complete(scope, response);
8172
- return response;
8247
+ if (outcome.response.status < 500) {
8248
+ store.complete(scope, outcome.response);
8249
+ } else if (outcome.reached === "no") {
8250
+ store.release(scope);
8251
+ } else {
8252
+ store.markUnresolved(scope);
8253
+ }
8254
+ return outcome.response;
8173
8255
  }
8174
8256
  /**
8175
8257
  * One accepted request, one `pipeline.execute()`.
@@ -8178,11 +8260,11 @@ var AcpProtocolAdapter = class {
8178
8260
  * merchant backend: the adapter builds a canonical request and reads back
8179
8261
  * what the pipeline decided.
8180
8262
  */
8181
- async runCheckout(request) {
8263
+ async runCheckout(request, idempotencyKey) {
8182
8264
  const context = this.context;
8183
8265
  const resource = this.resources.get(request.route.operation);
8184
8266
  if (context === void 0 || resource === void 0) {
8185
- return asResponse(
8267
+ return notReached(
8186
8268
  acpFailure(
8187
8269
  503,
8188
8270
  "service_unavailable",
@@ -8195,7 +8277,8 @@ var AcpProtocolAdapter = class {
8195
8277
  request,
8196
8278
  resourceId: resource.id,
8197
8279
  requestId: context.ids.next("acp"),
8198
- receivedAt: context.clock.nowIso()
8280
+ receivedAt: context.clock.nowIso(),
8281
+ ...idempotencyKey !== void 0 ? { idempotencyKey } : {}
8199
8282
  });
8200
8283
  try {
8201
8284
  const outcome = await context.pipeline.execute(canonical);
@@ -8204,7 +8287,7 @@ var AcpProtocolAdapter = class {
8204
8287
  { resourceId: resource.id, requestId: canonical.requestId },
8205
8288
  "acp adapter: mapped checkout resource returned payment-required - it must be priced free"
8206
8289
  );
8207
- return asResponse(
8290
+ return notReached(
8208
8291
  acpFailure(
8209
8292
  500,
8210
8293
  "processing_error",
@@ -8220,14 +8303,22 @@ var AcpProtocolAdapter = class {
8220
8303
  "acp adapter: refusing to forward a non-conformant merchant response"
8221
8304
  );
8222
8305
  }
8223
- return mapped.response;
8306
+ return { response: mapped.response, reached: "yes" };
8224
8307
  } catch (err) {
8225
8308
  const error = toCommerceError(err);
8226
8309
  context.logger.warn(
8227
- { resourceId: resource.id, requestId: canonical.requestId, err: error.toInfo() },
8310
+ {
8311
+ resourceId: resource.id,
8312
+ requestId: canonical.requestId,
8313
+ reached: reachedFor(error.code),
8314
+ err: error.toInfo()
8315
+ },
8228
8316
  "acp adapter: checkout execution failed"
8229
8317
  );
8230
- return asResponse(mapCommerceErrorToAcp(error, request.route.operation));
8318
+ return {
8319
+ response: asResponse(mapCommerceErrorToAcp(error, request.route.operation)),
8320
+ reached: reachedFor(error.code)
8321
+ };
8231
8322
  }
8232
8323
  }
8233
8324
  /**
@@ -8240,7 +8331,7 @@ var AcpProtocolAdapter = class {
8240
8331
  ...request.requestId !== void 0 ? { [ACP_REQUEST_ID_HEADER]: request.requestId } : {},
8241
8332
  ...request.idempotencyKey !== void 0 ? { [ACP_IDEMPOTENCY_KEY_HEADER]: request.idempotencyKey } : {},
8242
8333
  ...replayed ? { [ACP_IDEMPOTENT_REPLAYED_HEADER]: "true" } : {},
8243
- ...response?.status === 409 ? { "retry-after": String(ACP_IN_FLIGHT_RETRY_AFTER_SECONDS) } : {}
8334
+ ...response?.retryAfterSeconds !== void 0 ? { "retry-after": String(response.retryAfterSeconds) } : {}
8244
8335
  };
8245
8336
  }
8246
8337
  fail(res, err, message) {
@@ -8267,6 +8358,7 @@ var AcpProtocolAdapter = class {
8267
8358
  this.resources = /* @__PURE__ */ new Map();
8268
8359
  this.idempotency?.close();
8269
8360
  this.idempotency = void 0;
8361
+ this.deployment = void 0;
8270
8362
  this.context = void 0;
8271
8363
  }
8272
8364
  };
@@ -8443,7 +8535,7 @@ var MIGRATIONS = [
8443
8535
  }
8444
8536
  }
8445
8537
  ];
8446
- var SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
8538
+ var SCHEMA_VERSION2 = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
8447
8539
  function migrate2(db) {
8448
8540
  const current = db.pragma("user_version", { simple: true });
8449
8541
  for (const migration of MIGRATIONS) {
@@ -8529,17 +8621,17 @@ function createSqliteReceiptStore(options) {
8529
8621
  name: "sqlite-receipt-store",
8530
8622
  kind: "storage",
8531
8623
  implementationVersion: PACKAGE_VERSION,
8532
- supportedSpec: `sqlite-schema-v${SCHEMA_VERSION}`,
8624
+ supportedSpec: `sqlite-schema-v${SCHEMA_VERSION2}`,
8533
8625
  capabilities: ["receipts", "events", "payment-attempts"],
8534
8626
  status: "stable"
8535
8627
  };
8536
8628
  const store = {
8537
8629
  async init() {
8538
8630
  const version = db.pragma("user_version", { simple: true });
8539
- if (version !== SCHEMA_VERSION) {
8631
+ if (version !== SCHEMA_VERSION2) {
8540
8632
  throw new CommerceError(
8541
8633
  "STORAGE_ERROR",
8542
- `Receipt store schema version mismatch (found ${version}, expected ${SCHEMA_VERSION})`
8634
+ `Receipt store schema version mismatch (found ${version}, expected ${SCHEMA_VERSION2})`
8543
8635
  );
8544
8636
  }
8545
8637
  },
@@ -8673,9 +8765,9 @@ function createSqliteReceiptStore(options) {
8673
8765
  }
8674
8766
  try {
8675
8767
  const version = db.pragma("user_version", { simple: true });
8676
- if (version !== SCHEMA_VERSION) {
8768
+ if (version !== SCHEMA_VERSION2) {
8677
8769
  logger.warn(
8678
- { found: version, expected: SCHEMA_VERSION },
8770
+ { found: version, expected: SCHEMA_VERSION2 },
8679
8771
  "receipt-store: health check failed (schema version mismatch)"
8680
8772
  );
8681
8773
  return {
@@ -8687,7 +8779,7 @@ function createSqliteReceiptStore(options) {
8687
8779
  }
8688
8780
  return {
8689
8781
  status: "pass",
8690
- detail: `sqlite schema v${SCHEMA_VERSION} writable`,
8782
+ detail: `sqlite schema v${SCHEMA_VERSION2} writable`,
8691
8783
  checkedAt: clock.nowIso(),
8692
8784
  durationMs: clock.monotonicMs() - startedAt
8693
8785
  };