@devlab.group/agent-commerce 1.2.0 → 1.4.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.
package/dist/cli/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { ZodError, z } from 'zod';
3
- import { existsSync, mkdirSync, accessSync, constants, closeSync, openSync, readFileSync, chmodSync, promises } from 'fs';
3
+ import { existsSync, mkdirSync, readFileSync, accessSync, constants, closeSync, openSync, chmodSync, promises } from 'fs';
4
4
  import * as path from 'path';
5
5
  import { resolve, dirname, basename as basename$1, extname, join } from 'path';
6
6
  import { stringify, parse, Document, YAMLError } from 'yaml';
7
7
  import { CommanderError, Command } from 'commander';
8
8
  import picocolors from 'picocolors';
9
9
  import { randomUUID } from 'crypto';
10
- import Database from 'better-sqlite3';
11
10
  import { createRequire } from 'module';
11
+ import Database from 'better-sqlite3';
12
12
  import { writeFile, stat, readFile, rename, rm } from 'fs/promises';
13
13
  import { validate } from '@scalar/openapi-parser';
14
14
  import * as clack from '@clack/prompts';
@@ -35,7 +35,7 @@ var init_filename = __esm({
35
35
  var PROTOCOL_NAMES;
36
36
  var init_common = __esm({
37
37
  "src/core/domain/common.ts"() {
38
- PROTOCOL_NAMES = ["http", "mcp", "a2a"];
38
+ PROTOCOL_NAMES = ["http", "mcp", "a2a", "acp"];
39
39
  }
40
40
  });
41
41
 
@@ -51,14 +51,6 @@ var init_resource = __esm({
51
51
  }
52
52
  });
53
53
 
54
- // src/core/domain/wire.ts
55
- var PAYMENT_INPUT_FIELD;
56
- var init_wire = __esm({
57
- "src/core/domain/wire.ts"() {
58
- PAYMENT_INPUT_FIELD = "_payment";
59
- }
60
- });
61
-
62
54
  // src/core/errors/codes.ts
63
55
  var COMMERCE_ERROR_HTTP_STATUS, RETRYABLE_ERROR_CODES;
64
56
  var init_codes = __esm({
@@ -72,6 +64,14 @@ var init_codes = __esm({
72
64
  PAYMENT_REPLAYED: 409,
73
65
  PAYMENT_PROVIDER_UNAVAILABLE: 503,
74
66
  PAYMENT_SETTLEMENT_FAILED: 502,
67
+ // 403, not 402: the buyer's money is not the problem. A 402 tells a client
68
+ // "pay and retry", which cannot fix a missing or rejected mandate, and a
69
+ // client that auto-pays on 402 would be charged for a request that was
70
+ // never going to be delivered.
71
+ AUTHORIZATION_REQUIRED: 403,
72
+ AUTHORIZATION_INVALID: 403,
73
+ AUTHORIZATION_REPLAYED: 409,
74
+ AUTHORIZATION_PROVIDER_UNAVAILABLE: 503,
75
75
  BACKEND_TIMEOUT: 504,
76
76
  BACKEND_ERROR: 502,
77
77
  PROTOCOL_UNSUPPORTED: 501,
@@ -81,6 +81,10 @@ var init_codes = __esm({
81
81
  };
82
82
  RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([
83
83
  "PAYMENT_PROVIDER_UNAVAILABLE",
84
+ // Our verifier or our replay store was unreachable, so no verdict on the
85
+ // mandate was ever reached. That is an outage on our side, and the same
86
+ // proof will verify once it clears.
87
+ "AUTHORIZATION_PROVIDER_UNAVAILABLE",
84
88
  "BACKEND_TIMEOUT",
85
89
  // Load shedding is transient by definition: the caller should back off and
86
90
  // try again. Before this code existed the MCP adapter's queue-full path threw
@@ -147,6 +151,20 @@ var init_errors = __esm({
147
151
  }
148
152
  });
149
153
 
154
+ // src/core/domain/wire.ts
155
+ var PAYMENT_INPUT_FIELD, AUTHORIZATION_INPUT_FIELD, RESERVED_INPUT_FIELDS;
156
+ var init_wire = __esm({
157
+ "src/core/domain/wire.ts"() {
158
+ init_errors();
159
+ PAYMENT_INPUT_FIELD = "_payment";
160
+ AUTHORIZATION_INPUT_FIELD = "_authorization";
161
+ RESERVED_INPUT_FIELDS = [
162
+ PAYMENT_INPUT_FIELD,
163
+ AUTHORIZATION_INPUT_FIELD
164
+ ];
165
+ }
166
+ });
167
+
150
168
  // src/core/interfaces/logger.ts
151
169
  var NOOP_LOGGER;
152
170
  var init_logger = __esm({
@@ -204,6 +222,24 @@ var init_core = __esm({
204
222
  }
205
223
  });
206
224
 
225
+ // src/authorization/ap2/constants.ts
226
+ var AP2_SPEC_VERSION, AP2_MODES, AP2_SIGNING_ALGORITHM, AP2_KEY_TYPE, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, AP2_JWK_MEMBERS, AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_MAX_CLOCK_SKEW_SECONDS;
227
+ var init_constants = __esm({
228
+ "src/authorization/ap2/constants.ts"() {
229
+ AP2_SPEC_VERSION = "0.2.0";
230
+ AP2_MODES = ["direct"];
231
+ AP2_SIGNING_ALGORITHM = "ES256";
232
+ AP2_KEY_TYPE = "EC";
233
+ AP2_JWK_CURVE = "P-256";
234
+ AP2_JWK_COORDINATE_BYTES = 32;
235
+ AP2_JWK_MEMBERS = ["kty", "crv", "x", "y", "kid", "alg", "use"];
236
+ AP2_CHECKOUT_MANDATE_VCT = "mandate.checkout.1";
237
+ AP2_CHECKOUT_PROFILE = "agent-commerce/ap2/checkout/v1";
238
+ AP2_DEFAULT_CLOCK_SKEW_SECONDS = 60;
239
+ AP2_MAX_CLOCK_SKEW_SECONDS = 300;
240
+ }
241
+ });
242
+
207
243
  // src/core/execution/backend-http.ts
208
244
  function extractPathParameterNames(url) {
209
245
  return [...url.matchAll(PATH_PARAM_PATTERN)].map((match) => match[1]);
@@ -248,14 +284,13 @@ var init_validation = __esm({
248
284
  "src/core/execution/validation.ts"() {
249
285
  }
250
286
  });
251
-
252
- // src/core/execution/pipeline.ts
253
287
  var init_pipeline = __esm({
254
288
  "src/core/execution/pipeline.ts"() {
255
289
  init_wire();
256
290
  init_errors();
257
291
  init_backend_http();
258
292
  init_validation();
293
+ /* @__PURE__ */ new Set([...RESERVED_INPUT_FIELDS, "__proto__"]);
259
294
  }
260
295
  });
261
296
 
@@ -335,6 +370,30 @@ var init_networks = __esm({
335
370
  }
336
371
  });
337
372
 
373
+ // src/protocols/acp/constants.ts
374
+ var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS;
375
+ var init_constants2 = __esm({
376
+ "src/protocols/acp/constants.ts"() {
377
+ ACP_SPEC_VERSION = "2026-04-17";
378
+ ACP_API_VERSION = "2026-04-17";
379
+ ACP_WELL_KNOWN_PATH = "/.well-known/acp.json";
380
+ ACP_CHECKOUT_OPERATIONS = [
381
+ "createCheckoutSession",
382
+ "updateCheckoutSession",
383
+ "getCheckoutSession",
384
+ "completeCheckoutSession",
385
+ "cancelCheckoutSession"
386
+ ];
387
+ ACP_OPERATION_INPUT_KEYS = {
388
+ createCheckoutSession: ["body"],
389
+ updateCheckoutSession: ["path", "body"],
390
+ getCheckoutSession: ["path"],
391
+ completeCheckoutSession: ["path", "body"],
392
+ cancelCheckoutSession: ["path"]
393
+ };
394
+ }
395
+ });
396
+
338
397
  // src/config/env.ts
339
398
  function substituteEnv(value, env, path2 = "$") {
340
399
  if (typeof value === "string") {
@@ -691,7 +750,8 @@ function normalise(raw) {
691
750
  a2a: {
692
751
  enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
693
752
  mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
694
- }
753
+ },
754
+ acp: normaliseAcp(raw.protocols.acp)
695
755
  };
696
756
  validateMountPaths(protocols);
697
757
  const x402Raw = raw.payments.x402;
@@ -742,9 +802,14 @@ function normalise(raw) {
742
802
  ...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
743
803
  });
744
804
  }
805
+ const ap2 = normaliseAp2(raw.authorization?.ap2);
806
+ if (ap2?.enabled) {
807
+ validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
808
+ }
745
809
  const resources = Object.entries(raw.resources).map(
746
- ([id, entry]) => normaliseResource(id, entry, protocols, x402)
810
+ ([id, entry]) => normaliseResource(id, entry, protocols, x402, ap2)
747
811
  );
812
+ if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
748
813
  return {
749
814
  version: SUPPORTED_CONFIG_VERSION,
750
815
  merchant: {
@@ -765,13 +830,15 @@ function normalise(raw) {
765
830
  resources,
766
831
  payments: {
767
832
  ...x402 !== void 0 ? { x402 } : {}
768
- }
833
+ },
834
+ ...ap2 !== void 0 ? { authorization: { ap2 } } : {}
769
835
  };
770
836
  }
771
837
  function validateMountPaths(protocols) {
772
838
  const mounts = [
773
839
  ["mcp", protocols.mcp],
774
- ["a2a", protocols.a2a]
840
+ ["a2a", protocols.a2a],
841
+ ["acp", protocols.acp]
775
842
  ].filter(([, p]) => p.enabled);
776
843
  for (const [index, [nameA, a]] of mounts.entries()) {
777
844
  for (const [nameB, b] of mounts.slice(index + 1)) {
@@ -787,27 +854,357 @@ function validateMountPaths(protocols) {
787
854
  }
788
855
  }
789
856
  }
790
- function normaliseResource(id, entry, protocols, x402) {
857
+ function ap2Invalid(path2, message, extra = {}) {
858
+ return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
859
+ }
860
+ function normaliseAp2(raw) {
861
+ if (raw === void 0) return void 0;
862
+ if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
863
+ const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
864
+ if (specVersion !== AP2_SPEC_VERSION) {
865
+ throw ap2Invalid(
866
+ "authorization.ap2.specVersion",
867
+ `authorization.ap2.specVersion "${specVersion}" is not supported - this gateway verifies against the tagged AP2 release ${AP2_SPEC_VERSION} only`
868
+ );
869
+ }
870
+ const mode = raw.mode ?? AP2_MODES[0];
871
+ if (!AP2_MODES.includes(mode)) {
872
+ throw ap2Invalid(
873
+ "authorization.ap2.mode",
874
+ `authorization.ap2.mode "${mode}" is not supported. Supported: ${AP2_MODES.join(", ")}. Autonomous mode needs open mandates, agent key binding and constraint evaluation, none of which this release implements.`
875
+ );
876
+ }
877
+ if (raw.replay === void 0) {
878
+ throw ap2Invalid(
879
+ "authorization.ap2.replay",
880
+ "authorization.ap2.replay is required when AP2 is enabled - it names the SQLite file recording which mandates have been spent, and without it a verified mandate could authorise a second settlement"
881
+ );
882
+ }
883
+ const mandateIssuers = normaliseIssuers(
884
+ raw.trust?.mandateIssuers,
885
+ "authorization.ap2.trust.mandateIssuers"
886
+ );
887
+ const checkoutIssuers = normaliseIssuers(
888
+ raw.trust?.checkoutIssuers,
889
+ "authorization.ap2.trust.checkoutIssuers"
890
+ );
891
+ return {
892
+ enabled: true,
893
+ specVersion: AP2_SPEC_VERSION,
894
+ mode,
895
+ trust: { mandateIssuers, checkoutIssuers },
896
+ clockSkewSeconds: toNumber(
897
+ raw.clockSkewSeconds ?? AP2_DEFAULT_CLOCK_SKEW_SECONDS,
898
+ "authorization.ap2.clockSkewSeconds",
899
+ { min: 0, max: AP2_MAX_CLOCK_SKEW_SECONDS }
900
+ ),
901
+ replay: { path: raw.replay.path }
902
+ };
903
+ }
904
+ function normaliseIssuers(raw, path2) {
905
+ if (raw === void 0 || raw.length === 0) {
906
+ throw ap2Invalid(
907
+ path2,
908
+ `${path2} is required when AP2 is enabled and must name at least one issuer - trust is operator-configured and static, so an empty list means no mandate can ever verify`
909
+ );
910
+ }
911
+ const seen = /* @__PURE__ */ new Set();
912
+ return raw.map((entry, index) => {
913
+ const entryPath = `${path2}.${index}`;
914
+ if (seen.has(entry.issuer)) {
915
+ throw ap2Invalid(
916
+ `${entryPath}.issuer`,
917
+ `${entryPath}.issuer "${entry.issuer}" is listed twice - key lookup resolves by issuer, so a second entry for the same one is silently unreachable. Merge the keys into one entry instead (which is also how a rotation overlaps an old and a new key).`,
918
+ { issuer: entry.issuer }
919
+ );
920
+ }
921
+ seen.add(entry.issuer);
922
+ const kids = /* @__PURE__ */ new Set();
923
+ const keys = entry.keys.map((key, keyIndex) => {
924
+ const keyPath = `${entryPath}.keys.${keyIndex}`;
925
+ if (kids.has(key.kid)) {
926
+ throw ap2Invalid(
927
+ `${keyPath}.kid`,
928
+ `${keyPath}.kid "${key.kid}" is listed twice for issuer "${entry.issuer}" - a kid selects exactly one key, so which of the two verifies a signature would be undefined`,
929
+ { issuer: entry.issuer, kid: key.kid }
930
+ );
931
+ }
932
+ kids.add(key.kid);
933
+ return { kid: key.kid, jwk: validateJwk(key.jwk, key.kid, keyPath) };
934
+ });
935
+ return { issuer: entry.issuer, audience: entry.audience, keys };
936
+ });
937
+ }
938
+ function validateJwk(jwk, kid, path2) {
939
+ const fail = (detail) => {
940
+ throw ap2Invalid(`${path2}.jwk`, `${path2}.jwk (kid "${kid}") ${detail}`, { kid });
941
+ };
942
+ for (const member of Object.keys(jwk)) {
943
+ if (AP2_JWK_MEMBERS.includes(member)) continue;
944
+ if (member === "d" || member === "k") {
945
+ fail(
946
+ `carries private key material ("${member}"). The gateway verifies signatures and never produces them; publish only the public half. Treat the pasted key as compromised and rotate it.`
947
+ );
948
+ }
949
+ fail(
950
+ `has unsupported member "${member}". Allowed: ${AP2_JWK_MEMBERS.join(", ")}. Members naming a URL are refused on purpose - keys are configured inline and never fetched.`
951
+ );
952
+ }
953
+ const value = (member) => {
954
+ const raw = jwk[member];
955
+ if (typeof raw !== "string" || raw.length === 0) {
956
+ fail(`must give "${member}" as a non-empty string`);
957
+ }
958
+ return raw;
959
+ };
960
+ if (value("kty") !== AP2_KEY_TYPE) {
961
+ fail(
962
+ `must have kty "${AP2_KEY_TYPE}" (got "${value("kty")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
963
+ );
964
+ }
965
+ if (value("crv") !== AP2_JWK_CURVE) {
966
+ fail(
967
+ `must be on curve "${AP2_JWK_CURVE}" (got "${value("crv")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
968
+ );
969
+ }
970
+ for (const coordinate of ["x", "y"]) {
971
+ const encoded = value(coordinate);
972
+ if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
973
+ fail(`coordinate "${coordinate}" is not base64url (no padding, no "+" or "/")`);
974
+ }
975
+ if (Buffer.from(encoded, "base64url").length !== AP2_JWK_COORDINATE_BYTES) {
976
+ fail(
977
+ `coordinate "${coordinate}" decodes to ${Buffer.from(encoded, "base64url").length} bytes; a ${AP2_JWK_CURVE} coordinate is ${AP2_JWK_COORDINATE_BYTES}`
978
+ );
979
+ }
980
+ }
981
+ if (jwk["alg"] !== void 0 && value("alg") !== AP2_SIGNING_ALGORITHM) {
982
+ fail(`declares alg "${value("alg")}"; only ${AP2_SIGNING_ALGORITHM} is accepted`);
983
+ }
984
+ if (jwk["use"] !== void 0 && value("use") !== "sig") {
985
+ fail(`declares use "${value("use")}"; a verification key must be "sig"`);
986
+ }
987
+ if (jwk["kid"] !== void 0 && value("kid") !== kid) {
988
+ fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
989
+ }
990
+ const normalised = {};
991
+ for (const member of AP2_JWK_MEMBERS) {
992
+ if (jwk[member] !== void 0) normalised[member] = value(member);
993
+ }
994
+ return normalised;
995
+ }
996
+ function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
997
+ if (replayPath === ":memory:") return;
998
+ const others = [["storage.receipts.path", receiptsPath]];
999
+ if (acp.enabled) others.push(["protocols.acp.idempotency.path", acp.idempotency.path]);
1000
+ for (const [otherPath, other] of others) {
1001
+ if (replayPath !== other) continue;
1002
+ throw ap2Invalid(
1003
+ "authorization.ap2.replay.path",
1004
+ `authorization.ap2.replay.path is the same file as ${otherPath} ("${replayPath}") - the AP2 replay store keeps its own schema and must not share a database with another store`
1005
+ );
1006
+ }
1007
+ }
1008
+ function normaliseResourceAuthorization(id, entry, pricing, ap2) {
1009
+ const required = entry.authorization?.required;
1010
+ if (required === void 0) return void 0;
1011
+ const path2 = `resources.${id}.authorization.required`;
1012
+ const methods = [];
1013
+ for (const method of required) {
1014
+ if (!SUPPORTED_AUTHORIZATION_METHODS.has(method)) {
1015
+ throw ap2Invalid(
1016
+ path2,
1017
+ `Resource "${id}" requires unsupported authorization method "${method}". Supported: ${[...SUPPORTED_AUTHORIZATION_METHODS].join(", ")}.`,
1018
+ { resourceId: id, method }
1019
+ );
1020
+ }
1021
+ if (methods.includes(method)) {
1022
+ throw ap2Invalid(path2, `Resource "${id}" lists authorization method "${method}" twice`, {
1023
+ resourceId: id,
1024
+ method
1025
+ });
1026
+ }
1027
+ if (method === "ap2" && (ap2 === void 0 || !ap2.enabled)) {
1028
+ throw ap2Invalid(
1029
+ path2,
1030
+ `Resource "${id}" requires authorization method "ap2", which is not configured or not enabled under authorization.ap2`,
1031
+ { resourceId: id, method }
1032
+ );
1033
+ }
1034
+ methods.push(method);
1035
+ }
1036
+ if (pricing.type !== "fixed") {
1037
+ throw ap2Invalid(
1038
+ path2,
1039
+ `Resource "${id}" requires authorization but its pricing is "${pricing.type}" - authorization proves a purchase was approved and never replaces payment, so it applies to fixed-price paid resources only in this release`,
1040
+ { resourceId: id }
1041
+ );
1042
+ }
1043
+ return { required: methods };
1044
+ }
1045
+ function acpInvalid(path2, message, extra = {}) {
1046
+ return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
1047
+ }
1048
+ function normaliseAcp(raw) {
1049
+ const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
1050
+ if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
1051
+ return { enabled: false, mountPath };
1052
+ }
1053
+ if (raw.auth === void 0) {
1054
+ throw acpInvalid(
1055
+ "protocols.acp.auth",
1056
+ 'protocols.acp.auth is required when ACP is enabled - every ACP checkout endpoint is authenticated, and "bearer" is the only scheme supported in this release'
1057
+ );
1058
+ }
1059
+ if (raw.idempotency === void 0) {
1060
+ throw acpInvalid(
1061
+ "protocols.acp.idempotency",
1062
+ "protocols.acp.idempotency is required when ACP is enabled - it names the SQLite file holding checkout idempotency records, and ACP makes the Idempotency-Key mandatory on every checkout POST"
1063
+ );
1064
+ }
1065
+ const discovery = raw.discovery;
1066
+ return {
1067
+ enabled: true,
1068
+ mountPath,
1069
+ auth: raw.auth,
1070
+ idempotency: {
1071
+ path: raw.idempotency.path,
1072
+ retentionHours: toNumber(
1073
+ raw.idempotency.retentionHours ?? ACP_RETENTION_HOURS,
1074
+ "protocols.acp.idempotency.retentionHours",
1075
+ { min: ACP_RETENTION_HOURS }
1076
+ )
1077
+ },
1078
+ checkout: { operations: normaliseAcpOperations(raw.checkout) },
1079
+ // Built key by key rather than passed through: absent metadata must stay
1080
+ // absent from the discovery document, not appear there as `undefined`.
1081
+ ...discovery !== void 0 ? {
1082
+ discovery: {
1083
+ ...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
1084
+ ...discovery.supportedCurrencies !== void 0 ? { supportedCurrencies: discovery.supportedCurrencies } : {},
1085
+ ...discovery.supportedLocales !== void 0 ? { supportedLocales: discovery.supportedLocales } : {},
1086
+ ...discovery.interventionTypes !== void 0 ? { interventionTypes: discovery.interventionTypes } : {}
1087
+ }
1088
+ } : {}
1089
+ };
1090
+ }
1091
+ function normaliseAcpOperations(checkout) {
1092
+ const configured = checkout?.operations ?? {};
1093
+ for (const key of Object.keys(configured)) {
1094
+ if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
1095
+ throw acpInvalid(
1096
+ `protocols.acp.checkout.operations.${key}`,
1097
+ `protocols.acp.checkout.operations names unknown ACP checkout operation "${key}". Supported: ${ACP_CHECKOUT_OPERATIONS.join(", ")}.`
1098
+ );
1099
+ }
1100
+ }
1101
+ const operations = {};
1102
+ const claimedBy = /* @__PURE__ */ new Map();
1103
+ for (const operation of ACP_CHECKOUT_OPERATIONS) {
1104
+ const path2 = `protocols.acp.checkout.operations.${operation}`;
1105
+ const resourceId = configured[operation];
1106
+ if (resourceId === void 0) {
1107
+ throw acpInvalid(
1108
+ path2,
1109
+ `${path2} is required when ACP is enabled - all five checkout operations must be mapped, since ACP discovery advertises the checkout service as a whole`
1110
+ );
1111
+ }
1112
+ const claimed = claimedBy.get(resourceId);
1113
+ if (claimed !== void 0) {
1114
+ throw acpInvalid(
1115
+ path2,
1116
+ `${path2} maps resource "${resourceId}", which is already mapped to "${claimed}" - each ACP checkout operation needs its own resource`,
1117
+ { resourceId }
1118
+ );
1119
+ }
1120
+ claimedBy.set(resourceId, operation);
1121
+ operations[operation] = resourceId;
1122
+ }
1123
+ return operations;
1124
+ }
1125
+ function validateAcpCheckoutMapping(acp, resources) {
1126
+ const byId = new Map(resources.map((resource) => [resource.id, resource]));
1127
+ for (const operation of ACP_CHECKOUT_OPERATIONS) {
1128
+ const resourceId = acp.checkout.operations[operation];
1129
+ const path2 = `protocols.acp.checkout.operations.${operation}`;
1130
+ const resource = byId.get(resourceId);
1131
+ if (resource === void 0) {
1132
+ throw acpInvalid(
1133
+ path2,
1134
+ `${path2} maps resource "${resourceId}", which is not defined under "resources"`,
1135
+ { resourceId }
1136
+ );
1137
+ }
1138
+ if (!resource.exposedVia.includes("acp")) {
1139
+ throw acpInvalid(
1140
+ path2,
1141
+ `Resource "${resourceId}" implements ACP operation "${operation}" but does not list "acp" in its expose - the mapping says which resource serves the operation, expose says ACP may invoke it, and both are required`,
1142
+ { resourceId }
1143
+ );
1144
+ }
1145
+ if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
1146
+ throw acpInvalid(
1147
+ path2,
1148
+ `Resource "${resourceId}" implements ACP operation "${operation}" and must use pricing.type "free" with no "payments" - ACP checkout carries the merchant's own purchase payment, so the gateway does not also charge for the invocation`,
1149
+ { resourceId }
1150
+ );
1151
+ }
1152
+ validateAcpOperationInput(path2, operation, resource);
1153
+ }
1154
+ }
1155
+ function validateAcpOperationInput(path2, operation, resource) {
1156
+ const schema = resource.inputSchema;
1157
+ if (schema === void 0) return;
1158
+ const keys = ACP_OPERATION_INPUT_KEYS[operation];
1159
+ const properties = schema["properties"];
1160
+ if (schema["additionalProperties"] === false && isPlainObject2(properties)) {
1161
+ for (const key of keys) {
1162
+ if (!Object.hasOwn(properties, key)) {
1163
+ throw acpInvalid(
1164
+ path2,
1165
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter always sends for this operation`,
1166
+ { resourceId: resource.id }
1167
+ );
1168
+ }
1169
+ }
1170
+ }
1171
+ const required = schema["required"];
1172
+ if (Array.isArray(required)) {
1173
+ for (const entry of required) {
1174
+ if (typeof entry === "string" && !keys.includes(entry)) {
1175
+ throw acpInvalid(
1176
+ path2,
1177
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema requires "${entry}", which that operation never supplies (it sends: ${keys.join(", ")})`,
1178
+ { resourceId: resource.id }
1179
+ );
1180
+ }
1181
+ }
1182
+ }
1183
+ }
1184
+ function normaliseResource(id, entry, protocols, x402, ap2) {
791
1185
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
792
1186
  const inputProperties = entry.input?.["properties"];
793
- if (inputProperties && typeof inputProperties === "object" && PAYMENT_INPUT_FIELD in inputProperties) {
794
- throw new CommerceError(
795
- "CONFIG_INVALID",
796
- `Resource "${id}" declares an input property "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
797
- {
798
- details: {
799
- path: `resources.${id}.input.properties.${PAYMENT_INPUT_FIELD}`,
800
- resourceId: id
1187
+ if (inputProperties && typeof inputProperties === "object") {
1188
+ for (const reserved of RESERVED_INPUT_FIELDS) {
1189
+ if (!(reserved in inputProperties)) continue;
1190
+ throw new CommerceError(
1191
+ "CONFIG_INVALID",
1192
+ `Resource "${id}" declares an input property "${reserved}", which is reserved by the gateway`,
1193
+ {
1194
+ details: {
1195
+ path: `resources.${id}.input.properties.${reserved}`,
1196
+ resourceId: id
1197
+ }
801
1198
  }
802
- }
803
- );
1199
+ );
1200
+ }
804
1201
  }
805
1202
  for (const protocol of entry.expose) {
806
1203
  if (!SUPPORTED_PROTOCOLS.has(protocol)) {
807
1204
  const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
808
1205
  throw new CommerceError(
809
1206
  "CONFIG_INVALID",
810
- `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp, a2a.`,
1207
+ `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: ${PROTOCOL_NAMES.join(", ")}.`,
811
1208
  { details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
812
1209
  );
813
1210
  }
@@ -835,6 +1232,13 @@ function normaliseResource(id, entry, protocols, x402) {
835
1232
  { details: { path: `resources.${id}.expose`, resourceId: id } }
836
1233
  );
837
1234
  }
1235
+ if (entry.expose.includes("acp") && !protocols.acp.enabled) {
1236
+ throw new CommerceError(
1237
+ "CONFIG_INVALID",
1238
+ `Resource "${id}" is exposed via "acp" but protocols.acp.enabled is false`,
1239
+ { details: { path: `resources.${id}.expose`, resourceId: id } }
1240
+ );
1241
+ }
838
1242
  if (entry.expose.includes("http") && !protocols.http.enabled) {
839
1243
  throw new CommerceError(
840
1244
  "CONFIG_INVALID",
@@ -882,6 +1286,7 @@ function normaliseResource(id, entry, protocols, x402) {
882
1286
  }
883
1287
  }
884
1288
  const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
1289
+ const authorization = normaliseResourceAuthorization(id, entry, pricing, ap2);
885
1290
  return {
886
1291
  id,
887
1292
  name: entry.name,
@@ -902,7 +1307,8 @@ function normaliseResource(id, entry, protocols, x402) {
902
1307
  },
903
1308
  pricing,
904
1309
  exposedVia: entry.expose,
905
- paymentMethods
1310
+ paymentMethods,
1311
+ ...authorization !== void 0 ? { authorization } : {}
906
1312
  };
907
1313
  }
908
1314
  function defaultClosedObjectSchema(schema) {
@@ -1052,11 +1458,8 @@ function validateInputBindings(id, backend, input) {
1052
1458
  Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
1053
1459
  );
1054
1460
  for (const [location, property] of entries) {
1055
- if (property === PAYMENT_INPUT_FIELD) {
1056
- fail(
1057
- `binds "${location}" to "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
1058
- { location }
1059
- );
1461
+ if (RESERVED_INPUT_FIELDS.includes(property)) {
1462
+ fail(`binds "${location}" to "${property}", which is reserved by the gateway`, { location });
1060
1463
  }
1061
1464
  const other = seen.get(property);
1062
1465
  if (other !== void 0) {
@@ -1185,12 +1588,14 @@ function validateAddress(path2, value) {
1185
1588
  );
1186
1589
  }
1187
1590
  }
1188
- var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, MCP_TOOL_NAME_PATTERN, NumberOrString, BooleanOrString, MerchantSchema, ServerSchema, StorageSchema, RESERVED_GATEWAY_PATHS, MountPathSchema, ProtocolsSchema, DEFAULT_A2A_MOUNT_PATH, BackendMethodSchema, BackendInputBindingsSchema, BackendHandlerSchema, PricingSchema, JsonSchemaValueSchema, ResourceEntrySchema, ResourcesMapSchema, FacilitatorAuthSchema, FacilitatorSchema, X402Schema, PaymentsSchema, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
1591
+ var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, MCP_TOOL_NAME_PATTERN, NumberOrString, BooleanOrString, MerchantSchema, ServerSchema, StorageSchema, RESERVED_GATEWAY_PATHS, MountPathSchema, AcpAuthSchema, AcpIdempotencySchema, AcpCheckoutSchema, AcpDiscoverySchema, ProtocolsSchema, DEFAULT_A2A_MOUNT_PATH, DEFAULT_ACP_MOUNT_PATH, ACP_RETENTION_HOURS, BackendMethodSchema, BackendInputBindingsSchema, BackendHandlerSchema, PricingSchema, JsonSchemaValueSchema, ResourceEntrySchema, ResourcesMapSchema, FacilitatorAuthSchema, FacilitatorSchema, X402Schema, PaymentsSchema, Ap2KeySchema, Ap2IssuerSchema, Ap2Schema, AuthorizationSchema, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, SUPPORTED_AUTHORIZATION_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
1189
1592
  var init_schema = __esm({
1190
1593
  "src/config/schema.ts"() {
1594
+ init_constants();
1191
1595
  init_execution();
1192
1596
  init_core();
1193
1597
  init_guardrails();
1598
+ init_constants2();
1194
1599
  init_env();
1195
1600
  SUPPORTED_CONFIG_VERSION = 1;
1196
1601
  ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
@@ -1235,6 +1640,8 @@ var init_schema = __esm({
1235
1640
  // Fixed by the A2A specification, so it is the adapter's to serve and never
1236
1641
  // a configurable mount's to claim.
1237
1642
  "/.well-known/agent-card.json",
1643
+ // Likewise fixed by the ACP specification.
1644
+ ACP_WELL_KNOWN_PATH,
1238
1645
  "/api/resources",
1239
1646
  "/api/resources/:id/invoke",
1240
1647
  "/api/receipts",
@@ -1256,6 +1663,15 @@ var init_schema = __esm({
1256
1663
  message: `must not collide with a route the gateway already serves (${RESERVED_GATEWAY_PATHS.join(", ")}); pick a dedicated prefix such as "/mcp".`
1257
1664
  }
1258
1665
  );
1666
+ AcpAuthSchema = z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict();
1667
+ AcpIdempotencySchema = z.object({ path: z.string().min(1), retentionHours: NumberOrString.optional() }).strict();
1668
+ AcpCheckoutSchema = z.object({ operations: z.record(z.string().min(1)) }).strict();
1669
+ AcpDiscoverySchema = z.object({
1670
+ documentationUrl: z.string().min(1).optional(),
1671
+ supportedCurrencies: z.array(z.string().min(1)).optional(),
1672
+ supportedLocales: z.array(z.string().min(1)).optional(),
1673
+ interventionTypes: z.array(z.string().min(1)).optional()
1674
+ }).strict();
1259
1675
  ProtocolsSchema = z.object({
1260
1676
  http: z.object({ enabled: BooleanOrString }).strict(),
1261
1677
  mcp: z.object({
@@ -1267,9 +1683,23 @@ var init_schema = __esm({
1267
1683
  a2a: z.object({
1268
1684
  enabled: BooleanOrString,
1269
1685
  mountPath: MountPathSchema.optional()
1686
+ }).strict().optional(),
1687
+ // Likewise optional and off by default. The sub-blocks are optional here
1688
+ // and required by the business pass only when ACP is enabled, so a
1689
+ // disabled placeholder block stays writable and an enabled one gets a
1690
+ // message naming the piece it is missing.
1691
+ acp: z.object({
1692
+ enabled: BooleanOrString,
1693
+ mountPath: MountPathSchema.optional(),
1694
+ auth: AcpAuthSchema.optional(),
1695
+ idempotency: AcpIdempotencySchema.optional(),
1696
+ checkout: AcpCheckoutSchema.optional(),
1697
+ discovery: AcpDiscoverySchema.optional()
1270
1698
  }).strict().optional()
1271
1699
  }).strict();
1272
1700
  DEFAULT_A2A_MOUNT_PATH = "/a2a";
1701
+ DEFAULT_ACP_MOUNT_PATH = "/acp";
1702
+ ACP_RETENTION_HOURS = 24;
1273
1703
  BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
1274
1704
  BackendInputBindingsSchema = z.object({
1275
1705
  path: z.string().min(1).optional(),
@@ -1305,7 +1735,8 @@ var init_schema = __esm({
1305
1735
  backend: BackendHandlerSchema,
1306
1736
  pricing: PricingSchema,
1307
1737
  expose: z.array(z.string().min(1)).min(1),
1308
- payments: z.array(z.string().min(1)).optional()
1738
+ payments: z.array(z.string().min(1)).optional(),
1739
+ authorization: z.object({ required: z.array(z.string().min(1)).min(1) }).strict().optional()
1309
1740
  }).strict();
1310
1741
  ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
1311
1742
  FacilitatorAuthSchema = z.discriminatedUnion("type", [
@@ -1353,6 +1784,32 @@ var init_schema = __esm({
1353
1784
  PaymentsSchema = z.object({
1354
1785
  x402: X402Schema.optional()
1355
1786
  }).strict();
1787
+ Ap2KeySchema = z.object({
1788
+ kid: z.string().min(1),
1789
+ jwk: z.record(z.string(), z.unknown())
1790
+ }).strict();
1791
+ Ap2IssuerSchema = z.object({
1792
+ issuer: z.string().min(1),
1793
+ /**
1794
+ * Required, not defaulted. Without it, a mandate minted for another
1795
+ * merchant would verify here, and there is no value worth guessing for
1796
+ * something that decides that.
1797
+ */
1798
+ audience: z.string().min(1),
1799
+ keys: z.array(Ap2KeySchema).min(1)
1800
+ }).strict();
1801
+ Ap2Schema = z.object({
1802
+ enabled: BooleanOrString,
1803
+ specVersion: z.string().min(1).optional(),
1804
+ mode: z.string().min(1).optional(),
1805
+ trust: z.object({
1806
+ mandateIssuers: z.array(Ap2IssuerSchema).optional(),
1807
+ checkoutIssuers: z.array(Ap2IssuerSchema).optional()
1808
+ }).strict().optional(),
1809
+ clockSkewSeconds: NumberOrString.optional(),
1810
+ replay: z.object({ path: z.string().min(1) }).strict().optional()
1811
+ }).strict();
1812
+ AuthorizationSchema = z.object({ ap2: Ap2Schema.optional() }).strict();
1356
1813
  RawConfigSchema = z.object({
1357
1814
  version: z.literal(SUPPORTED_CONFIG_VERSION),
1358
1815
  merchant: MerchantSchema,
@@ -1360,10 +1817,12 @@ var init_schema = __esm({
1360
1817
  storage: StorageSchema,
1361
1818
  protocols: ProtocolsSchema,
1362
1819
  resources: ResourcesMapSchema,
1363
- payments: PaymentsSchema
1820
+ payments: PaymentsSchema,
1821
+ authorization: AuthorizationSchema.optional()
1364
1822
  }).strict();
1365
1823
  SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
1366
1824
  SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
1825
+ SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
1367
1826
  EMPTY_CLOSED_OBJECT_SCHEMA = {
1368
1827
  type: "object",
1369
1828
  properties: {},
@@ -1547,6 +2006,33 @@ async function execCommand(command, args) {
1547
2006
  });
1548
2007
  }
1549
2008
 
2009
+ // src/cli/commands/doctor.ts
2010
+ init_constants();
2011
+
2012
+ // src/authorization/ap2/descriptor.ts
2013
+ init_constants();
2014
+ var AP2_UNSUPPORTED = [
2015
+ // Mandate kinds. Open mandates carry spending constraints nothing here
2016
+ // evaluates, so accepting one would tell a buyer their limits were checked.
2017
+ "autonomous mode",
2018
+ "open checkout mandates (mandate.checkout.open.1)",
2019
+ "intent mandates",
2020
+ "cart mandates",
2021
+ "spending constraint evaluation",
2022
+ "cnf-bound agent keys",
2023
+ // Key handling. Every key is written into config by an operator
2024
+ "JWKS and any key discovery by URL (jku, x5u)",
2025
+ "issuer metadata fetching",
2026
+ "key rotation without a config change",
2027
+ // Algorithms
2028
+ "signature algorithms other than ES256",
2029
+ "digest algorithms other than sha-256",
2030
+ // Roles this gateway does not play
2031
+ "mandate issuance",
2032
+ "merchant checkout JWT issuance",
2033
+ "AP2 over the ACP checkout adapter"
2034
+ ];
2035
+
1550
2036
  // src/cli/commands/doctor.ts
1551
2037
  init_execution();
1552
2038
  init_core();
@@ -1589,9 +2075,94 @@ var A2A_UNSUPPORTED = [
1589
2075
  "artifact types beyond Agent Commerce outcome data"
1590
2076
  ];
1591
2077
 
2078
+ // src/cli/commands/doctor.ts
2079
+ init_constants2();
2080
+
2081
+ // src/protocols/acp/descriptor.ts
2082
+ init_constants2();
2083
+ [
2084
+ "rest transport",
2085
+ "well-known discovery",
2086
+ "bearer authentication",
2087
+ "API-Version negotiation",
2088
+ "checkout service",
2089
+ ...ACP_CHECKOUT_OPERATIONS
2090
+ ];
2091
+ var ACP_UNSUPPORTED = [
2092
+ // Services this seller does not implement, named as ACP names them. The
2093
+ // discovery document advertises `checkout` alone, and these are the reason.
2094
+ "carts service",
2095
+ "feed service",
2096
+ "standalone orders service",
2097
+ "delegate_payment",
2098
+ "delegate_authentication",
2099
+ // Bindings and delivery directions.
2100
+ "ACP MCP transport binding",
2101
+ "webhooks",
2102
+ "outbound merchant-to-agent delivery",
2103
+ "ACP client role",
2104
+ // Request authentication beyond the bearer token.
2105
+ "Signature header verification",
2106
+ "Timestamp replay-window verification",
2107
+ // Extensions and versioning.
2108
+ "discount extension",
2109
+ "general ACP extension framework",
2110
+ "multiple ACP API versions",
2111
+ "seller-backed payment handler integration"
2112
+ ];
2113
+
1592
2114
  // src/storage/receipts/store.ts
1593
2115
  init_core();
1594
- var PACKAGE_VERSION = "1.2.0" ;
2116
+ var PACKAGE_VERSION = "1.4.0" ;
2117
+
2118
+ // src/storage/sqlite.ts
2119
+ init_core();
2120
+ function openSqliteDatabase(options) {
2121
+ const { path: path2, label, logger } = options;
2122
+ const isFileBacked = path2 !== ":memory:";
2123
+ if (isFileBacked) {
2124
+ const dir = dirname(path2);
2125
+ if (dir !== "" && dir !== "." && !existsSync(dir)) {
2126
+ mkdirSync(dir, { recursive: true, mode: 448 });
2127
+ }
2128
+ }
2129
+ if (isFileBacked && existsSync(path2)) {
2130
+ try {
2131
+ accessSync(path2, constants.W_OK);
2132
+ } catch {
2133
+ throw new CommerceError(
2134
+ "STORAGE_ERROR",
2135
+ `${label} "${path2}" exists but is not writable by this process (uid ${typeof process.getuid === "function" ? process.getuid() : "unknown"}). If this is a container that recently changed user, an existing volume may still be owned by the previous one - recreate it (docker compose down -v) or fix its ownership.`,
2136
+ { details: { path: path2 } }
2137
+ );
2138
+ }
2139
+ }
2140
+ if (isFileBacked) {
2141
+ try {
2142
+ closeSync(openSync(path2, "a", 384));
2143
+ } catch {
2144
+ }
2145
+ }
2146
+ const db = new Database(path2);
2147
+ db.pragma("journal_mode = WAL");
2148
+ db.pragma("foreign_keys = ON");
2149
+ if (isFileBacked) restrictDatabasePermissions(path2, label, logger);
2150
+ return db;
2151
+ }
2152
+ function restrictDatabasePermissions(path2, label, logger) {
2153
+ for (const file of [path2, `${path2}-wal`, `${path2}-shm`]) {
2154
+ try {
2155
+ chmodSync(file, 384);
2156
+ } catch (error) {
2157
+ const code = error.code;
2158
+ if (code === "ENOENT") continue;
2159
+ logger.warn(
2160
+ { file, label, error: code ?? "unknown" },
2161
+ "could not restrict database permissions to owner-only"
2162
+ );
2163
+ }
2164
+ }
2165
+ }
1595
2166
 
1596
2167
  // src/storage/receipts/redact.ts
1597
2168
  var SECRET_KEY_PATTERN = /private[-_ ]?key|secret|password|mnemonic|seed[-_ ]?phrase|api[-_ ]?key|authoriz(e|ation)|^auth$|auth[-_]?header|payment[-_ ]?proof|^proof$|x-payment|signature|signed[-_ ]?message|token|bearer|credential|cookie|session|jwt/i;
@@ -1627,12 +2198,14 @@ function receiptToRow(receipt) {
1627
2198
  backend_status: receipt.backendStatus,
1628
2199
  duration_ms: receipt.durationMs !== void 0 ? receipt.durationMs : null,
1629
2200
  protocol: receipt.protocol !== void 0 ? receipt.protocol : null,
1630
- metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null
2201
+ metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
2202
+ authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
1631
2203
  };
1632
2204
  }
1633
2205
  function rowToReceipt(row) {
1634
2206
  const payment = row.payment_json !== null ? JSON.parse(row.payment_json) : void 0;
1635
2207
  const metadata = row.metadata_json !== null ? JSON.parse(row.metadata_json) : void 0;
2208
+ const authorization = row.authorization_json !== null ? JSON.parse(row.authorization_json) : void 0;
1636
2209
  return {
1637
2210
  id: row.id,
1638
2211
  requestId: row.request_id,
@@ -1642,7 +2215,8 @@ function rowToReceipt(row) {
1642
2215
  ...payment !== void 0 ? { payment } : {},
1643
2216
  ...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
1644
2217
  ...row.protocol !== null ? { protocol: row.protocol } : {},
1645
- ...metadata !== void 0 ? { metadata } : {}
2218
+ ...metadata !== void 0 ? { metadata } : {},
2219
+ ...authorization !== void 0 ? { authorization } : {}
1646
2220
  };
1647
2221
  }
1648
2222
  function eventToRow(event) {
@@ -1751,6 +2325,12 @@ var MIGRATIONS = [
1751
2325
  CREATE INDEX IF NOT EXISTS idx_payment_attempts_created_at ON payment_attempts(created_at);
1752
2326
  `);
1753
2327
  }
2328
+ },
2329
+ {
2330
+ version: 2,
2331
+ up(db) {
2332
+ db.exec(`ALTER TABLE receipts ADD COLUMN authorization_json TEXT;`);
2333
+ }
1754
2334
  }
1755
2335
  ];
1756
2336
  var SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
@@ -1779,57 +2359,17 @@ var defaultIds = {
1779
2359
  function isUniqueConstraintOn(err, column) {
1780
2360
  return err instanceof Error && "code" in err && err.code === "SQLITE_CONSTRAINT_UNIQUE" && err.message.includes(column);
1781
2361
  }
1782
- function restrictDatabasePermissions(path2, logger) {
1783
- for (const file of [path2, `${path2}-wal`, `${path2}-shm`]) {
1784
- try {
1785
- chmodSync(file, 384);
1786
- } catch (error) {
1787
- const code = error.code;
1788
- if (code === "ENOENT") continue;
1789
- logger.warn(
1790
- { file, error: code ?? "unknown" },
1791
- "could not restrict receipt database permissions to owner-only"
1792
- );
1793
- }
1794
- }
1795
- }
1796
2362
  function createSqliteReceiptStore(options) {
1797
2363
  const logger = options.logger ?? NOOP_LOGGER;
1798
2364
  const clock = options.clock ?? systemClock;
1799
2365
  const ids = options.ids ?? defaultIds;
1800
2366
  const path2 = options.path;
1801
2367
  const isFileBacked = path2 !== ":memory:";
1802
- if (isFileBacked) {
1803
- const dir = dirname(path2);
1804
- if (dir !== "" && dir !== "." && !existsSync(dir)) {
1805
- mkdirSync(dir, { recursive: true, mode: 448 });
1806
- }
1807
- }
1808
- if (isFileBacked && existsSync(path2)) {
1809
- try {
1810
- accessSync(path2, constants.W_OK);
1811
- } catch {
1812
- throw new CommerceError(
1813
- "STORAGE_ERROR",
1814
- `Receipt database "${path2}" exists but is not writable by this process (uid ${typeof process.getuid === "function" ? process.getuid() : "unknown"}). If this is a container that recently changed user, an existing volume may still be owned by the previous one \u2014 recreate it (docker compose down -v) or fix its ownership.`,
1815
- { details: { path: path2 } }
1816
- );
1817
- }
1818
- }
1819
- if (isFileBacked) {
1820
- try {
1821
- closeSync(openSync(path2, "a", 384));
1822
- } catch {
1823
- }
1824
- }
1825
- const db = new Database(path2);
1826
- db.pragma("journal_mode = WAL");
1827
- db.pragma("foreign_keys = ON");
2368
+ const db = openSqliteDatabase({ path: path2, label: "Receipt database", logger });
1828
2369
  migrate(db);
1829
- if (isFileBacked) restrictDatabasePermissions(path2, logger);
1830
2370
  const insertReceiptStmt = db.prepare(
1831
- `INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json)
1832
- VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json)`
2371
+ `INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json, authorization_json)
2372
+ VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json, @authorization_json)`
1833
2373
  );
1834
2374
  const getReceiptStmt = db.prepare("SELECT * FROM receipts WHERE id = ?");
1835
2375
  const listReceiptsStmt = db.prepare(
@@ -2344,7 +2884,7 @@ function maskMiddle(value, keep = 6) {
2344
2884
  }
2345
2885
  createRequire(import.meta.url);
2346
2886
  function readVersionReport() {
2347
- const injected = '{"cliVersion":"1.2.0","pinned":[{"name":"@coinbase/x402","version":"2.1.0","via":"@devlab.group/agent-commerce"},{"name":"@modelcontextprotocol/sdk","version":"1.30.0","via":"@devlab.group/agent-commerce"},{"name":"@x402/core","version":"2.23.0","via":"@devlab.group/agent-commerce"},{"name":"@x402/evm","version":"2.23.0","via":"@devlab.group/agent-commerce"},{"name":"viem","version":"2.55.18","via":"@devlab.group/agent-commerce"},{"name":"better-sqlite3","version":"13.0.3","via":"@devlab.group/agent-commerce"},{"name":"fastify","version":"5.12.0","via":"@devlab.group/agent-commerce"},{"name":"pino","version":"10.3.1","via":"@devlab.group/agent-commerce"},{"name":"zod","version":"3.25.76","via":"@devlab.group/agent-commerce"}]}' ;
2887
+ const injected = '{"cliVersion":"1.4.0","pinned":[{"name":"@coinbase/x402","version":"2.1.0","via":"@devlab.group/agent-commerce"},{"name":"@modelcontextprotocol/sdk","version":"1.30.0","via":"@devlab.group/agent-commerce"},{"name":"@x402/core","version":"2.23.0","via":"@devlab.group/agent-commerce"},{"name":"@x402/evm","version":"2.23.0","via":"@devlab.group/agent-commerce"},{"name":"viem","version":"2.55.18","via":"@devlab.group/agent-commerce"},{"name":"better-sqlite3","version":"13.0.3","via":"@devlab.group/agent-commerce"},{"name":"fastify","version":"5.12.0","via":"@devlab.group/agent-commerce"},{"name":"pino","version":"10.3.1","via":"@devlab.group/agent-commerce"},{"name":"zod","version":"3.25.76","via":"@devlab.group/agent-commerce"}]}' ;
2348
2888
  {
2349
2889
  return JSON.parse(injected);
2350
2890
  }
@@ -2406,6 +2946,112 @@ function findX402Mismatch(configured, live) {
2406
2946
  if (diffs.length === 0) return void 0;
2407
2947
  return `${diffs.join("; ")} \u2014 the gateway may be running against an older deployment; restart it or re-run chain:deploy`;
2408
2948
  }
2949
+ function describeIssuers(label, issuers) {
2950
+ const described = issuers.map(
2951
+ (entry) => `${entry.issuer} (${entry.keys.length} key${entry.keys.length === 1 ? "" : "s"})`
2952
+ ).join(", ");
2953
+ return `${label} issuers: ${described}`;
2954
+ }
2955
+ function isWritableFile(path2) {
2956
+ try {
2957
+ accessSync(path2, constants.W_OK);
2958
+ return true;
2959
+ } catch {
2960
+ return false;
2961
+ }
2962
+ }
2963
+ function ap2ReplayCheck(path2) {
2964
+ const name = "AP2 replay store";
2965
+ if (path2 === ":memory:") {
2966
+ return {
2967
+ name,
2968
+ status: "WARN",
2969
+ detail: "in-memory store - every spent mandate is forgotten on restart, so one could authorize a second purchase; use a file path in production"
2970
+ };
2971
+ }
2972
+ if (existsSync(path2)) {
2973
+ return isWritableFile(path2) ? { name, status: "PASS", detail: `writable at "${path2}"` } : {
2974
+ name,
2975
+ status: "FAIL",
2976
+ detail: `"${path2}" exists but is not writable by this user - no mandate could be recorded as spent`
2977
+ };
2978
+ }
2979
+ const directory = dirname(path2);
2980
+ if (directory !== "" && directory !== "." && !existsSync(directory)) {
2981
+ return {
2982
+ name,
2983
+ status: "WARN",
2984
+ detail: `no store yet at "${path2}" and its directory does not exist - it is created when the gateway starts, provided that path is writable`
2985
+ };
2986
+ }
2987
+ return {
2988
+ name,
2989
+ status: "WARN",
2990
+ detail: `no store yet at "${path2}" - it is created the first time the gateway starts`
2991
+ };
2992
+ }
2993
+ function acpIdempotencyCheck(idempotency) {
2994
+ const retention = `retention ${idempotency.retentionHours}h`;
2995
+ if (idempotency.retentionHours < 24) {
2996
+ return {
2997
+ name: "ACP idempotency",
2998
+ status: "FAIL",
2999
+ detail: `${retention} is below the 24h ACP minimum \u2014 a replayed key could run a checkout twice`
3000
+ };
3001
+ }
3002
+ if (idempotency.path === ":memory:") {
3003
+ return {
3004
+ name: "ACP idempotency",
3005
+ status: "WARN",
3006
+ detail: `in-memory store, ${retention} \u2014 replay protection is lost on every restart; use a file path in production`
3007
+ };
3008
+ }
3009
+ const directory = dirname(idempotency.path);
3010
+ if (existsSync(idempotency.path)) {
3011
+ return isWritableFile(idempotency.path) ? { name: "ACP idempotency", status: "PASS", detail: `writable, ${retention}` } : {
3012
+ name: "ACP idempotency",
3013
+ status: "FAIL",
3014
+ detail: `"${idempotency.path}" exists but is not writable by this user \u2014 the adapter cannot claim idempotency keys`
3015
+ };
3016
+ }
3017
+ if (directory !== "" && directory !== "." && !existsSync(directory)) {
3018
+ return {
3019
+ name: "ACP idempotency",
3020
+ status: "WARN",
3021
+ detail: `no store yet at "${idempotency.path}" and its directory does not exist \u2014 it is created when the gateway starts, provided that path is writable`
3022
+ };
3023
+ }
3024
+ return {
3025
+ name: "ACP idempotency",
3026
+ status: "WARN",
3027
+ detail: `no store yet at "${idempotency.path}" \u2014 it is created the first time the gateway starts (${retention})`
3028
+ };
3029
+ }
3030
+ function acpMappingCheck(config, operations) {
3031
+ const byId = new Map(config.resources.map((resource) => [resource.id, resource]));
3032
+ const problems = [];
3033
+ for (const [operation, resourceId] of Object.entries(operations)) {
3034
+ const resource = byId.get(resourceId);
3035
+ if (resource === void 0) {
3036
+ problems.push(`${operation} -> "${resourceId}" does not exist`);
3037
+ continue;
3038
+ }
3039
+ if (!resource.exposedVia.includes("acp")) {
3040
+ problems.push(`${operation} -> "${resourceId}" is not exposed via acp`);
3041
+ }
3042
+ if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
3043
+ problems.push(`${operation} -> "${resourceId}" is not free to invoke`);
3044
+ }
3045
+ }
3046
+ if (problems.length > 0) {
3047
+ return { name: "ACP checkout mapping", status: "FAIL", detail: problems.join("; ") };
3048
+ }
3049
+ return {
3050
+ name: "ACP checkout mapping",
3051
+ status: "PASS",
3052
+ detail: `5 operations mapped to free acp-exposed resources (${Object.values(operations).join(", ")})`
3053
+ };
3054
+ }
2409
3055
  async function runDoctor(options, deps = {}) {
2410
3056
  const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
2411
3057
  const loadConfig2 = deps.loadConfig ?? loadConfigDynamic;
@@ -2487,7 +3133,7 @@ async function runDoctor(options, deps = {}) {
2487
3133
  checks.push({
2488
3134
  name: "Protocols",
2489
3135
  status: "PASS",
2490
- detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"} a2a=${config.protocols.a2a.enabled ? `on (${a2aMountPath})` : "off"}`
3136
+ detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"} a2a=${config.protocols.a2a.enabled ? `on (${a2aMountPath})` : "off"} acp=${config.protocols.acp.enabled ? `on (${config.protocols.acp.mountPath})` : "off"}`
2491
3137
  });
2492
3138
  }
2493
3139
  if (config === void 0) {
@@ -2506,6 +3152,61 @@ async function runDoctor(options, deps = {}) {
2506
3152
  detail: A2A_UNSUPPORTED.join(", ")
2507
3153
  });
2508
3154
  }
3155
+ if (config === void 0) {
3156
+ checks.push({ name: "ACP", status: "WARN", detail: "skipped \u2014 config invalid" });
3157
+ } else if (!config.protocols.acp.enabled) {
3158
+ checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
3159
+ } else {
3160
+ const acp = config.protocols.acp;
3161
+ checks.push({
3162
+ name: "ACP",
3163
+ status: "PASS",
3164
+ detail: `experimental \xB7 spec ${ACP_SPEC_VERSION} \xB7 API-Version ${ACP_API_VERSION} \xB7 service checkout \xB7 mount ${acp.mountPath} \xB7 discovery ${ACP_WELL_KNOWN_PATH}`
3165
+ });
3166
+ checks.push({
3167
+ name: "ACP auth",
3168
+ status: "PASS",
3169
+ detail: `bearer token configured (${acp.auth.token.length} characters, not shown)`
3170
+ });
3171
+ checks.push(acpIdempotencyCheck(acp.idempotency));
3172
+ checks.push(acpMappingCheck(config, acp.checkout.operations));
3173
+ checks.push({
3174
+ name: "ACP unsupported",
3175
+ status: "INFO",
3176
+ detail: ACP_UNSUPPORTED.join(", ")
3177
+ });
3178
+ }
3179
+ const ap2 = config?.authorization?.ap2;
3180
+ if (config === void 0) {
3181
+ checks.push({ name: "AP2", status: "WARN", detail: "skipped \u2014 config invalid" });
3182
+ } else if (ap2 === void 0 || !ap2.enabled) {
3183
+ checks.push({ name: "AP2", status: "INFO", detail: "disabled" });
3184
+ } else {
3185
+ checks.push({
3186
+ name: "AP2",
3187
+ status: "PASS",
3188
+ detail: `experimental \xB7 spec ${AP2_SPEC_VERSION} \xB7 mode ${ap2.mode} \xB7 ${AP2_CHECKOUT_MANDATE_VCT} \xB7 profile ${AP2_CHECKOUT_PROFILE} \xB7 clock skew ${ap2.clockSkewSeconds}s`
3189
+ });
3190
+ checks.push({
3191
+ name: "AP2 trust",
3192
+ status: "PASS",
3193
+ detail: `${describeIssuers("mandate", ap2.trust.mandateIssuers)} \xB7 ${describeIssuers("checkout", ap2.trust.checkoutIssuers)}`
3194
+ });
3195
+ checks.push(ap2ReplayCheck(ap2.replay.path));
3196
+ const gated = config.resources.filter(
3197
+ (resource) => resource.authorization?.required.includes("ap2")
3198
+ );
3199
+ checks.push({
3200
+ name: "AP2 resources",
3201
+ status: gated.length > 0 ? "PASS" : "WARN",
3202
+ detail: gated.length > 0 ? gated.map((resource) => resource.id).join(", ") : "AP2 is enabled but no resource requires it \u2014 every purchase settles without a mandate"
3203
+ });
3204
+ checks.push({
3205
+ name: "AP2 unsupported",
3206
+ status: "INFO",
3207
+ detail: AP2_UNSUPPORTED.join(", ")
3208
+ });
3209
+ }
2509
3210
  const x402 = config?.payments.x402;
2510
3211
  if (x402 === void 0 || !x402.enabled) {
2511
3212
  checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
@@ -3801,7 +4502,7 @@ async function runValidate(options, io, deps = {}) {
3801
4502
  io.stdout(` merchant: ${config.merchant.name} (${config.merchant.id})`);
3802
4503
  io.stdout(` resources: ${config.resources.length}`);
3803
4504
  io.stdout(
3804
- ` protocols: http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? "on" : "off"} a2a=${config.protocols.a2a.enabled ? "on" : "off"}`
4505
+ ` protocols: http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? "on" : "off"} a2a=${config.protocols.a2a.enabled ? "on" : "off"} acp=${config.protocols.acp.enabled ? "on" : "off"}`
3805
4506
  );
3806
4507
  io.stdout(` payments: x402=${config.payments.x402?.enabled === true ? "on" : "off"}`);
3807
4508
  return 0;