@devlab.group/agent-commerce 1.3.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,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { ZodError, z } from 'zod';
3
- import { existsSync, mkdirSync, accessSync, constants, readFileSync, closeSync, openSync, 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';
@@ -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
 
@@ -337,7 +372,7 @@ var init_networks = __esm({
337
372
 
338
373
  // src/protocols/acp/constants.ts
339
374
  var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS;
340
- var init_constants = __esm({
375
+ var init_constants2 = __esm({
341
376
  "src/protocols/acp/constants.ts"() {
342
377
  ACP_SPEC_VERSION = "2026-04-17";
343
378
  ACP_API_VERSION = "2026-04-17";
@@ -767,8 +802,12 @@ function normalise(raw) {
767
802
  ...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
768
803
  });
769
804
  }
805
+ const ap2 = normaliseAp2(raw.authorization?.ap2);
806
+ if (ap2?.enabled) {
807
+ validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
808
+ }
770
809
  const resources = Object.entries(raw.resources).map(
771
- ([id, entry]) => normaliseResource(id, entry, protocols, x402)
810
+ ([id, entry]) => normaliseResource(id, entry, protocols, x402, ap2)
772
811
  );
773
812
  if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
774
813
  return {
@@ -791,7 +830,8 @@ function normalise(raw) {
791
830
  resources,
792
831
  payments: {
793
832
  ...x402 !== void 0 ? { x402 } : {}
794
- }
833
+ },
834
+ ...ap2 !== void 0 ? { authorization: { ap2 } } : {}
795
835
  };
796
836
  }
797
837
  function validateMountPaths(protocols) {
@@ -814,6 +854,194 @@ function validateMountPaths(protocols) {
814
854
  }
815
855
  }
816
856
  }
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
+ }
817
1045
  function acpInvalid(path2, message, extra = {}) {
818
1046
  return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
819
1047
  }
@@ -953,20 +1181,23 @@ function validateAcpOperationInput(path2, operation, resource) {
953
1181
  }
954
1182
  }
955
1183
  }
956
- function normaliseResource(id, entry, protocols, x402) {
1184
+ function normaliseResource(id, entry, protocols, x402, ap2) {
957
1185
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
958
1186
  const inputProperties = entry.input?.["properties"];
959
- if (inputProperties && typeof inputProperties === "object" && PAYMENT_INPUT_FIELD in inputProperties) {
960
- throw new CommerceError(
961
- "CONFIG_INVALID",
962
- `Resource "${id}" declares an input property "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
963
- {
964
- details: {
965
- path: `resources.${id}.input.properties.${PAYMENT_INPUT_FIELD}`,
966
- 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
+ }
967
1198
  }
968
- }
969
- );
1199
+ );
1200
+ }
970
1201
  }
971
1202
  for (const protocol of entry.expose) {
972
1203
  if (!SUPPORTED_PROTOCOLS.has(protocol)) {
@@ -1055,6 +1286,7 @@ function normaliseResource(id, entry, protocols, x402) {
1055
1286
  }
1056
1287
  }
1057
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);
1058
1290
  return {
1059
1291
  id,
1060
1292
  name: entry.name,
@@ -1075,7 +1307,8 @@ function normaliseResource(id, entry, protocols, x402) {
1075
1307
  },
1076
1308
  pricing,
1077
1309
  exposedVia: entry.expose,
1078
- paymentMethods
1310
+ paymentMethods,
1311
+ ...authorization !== void 0 ? { authorization } : {}
1079
1312
  };
1080
1313
  }
1081
1314
  function defaultClosedObjectSchema(schema) {
@@ -1225,11 +1458,8 @@ function validateInputBindings(id, backend, input) {
1225
1458
  Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
1226
1459
  );
1227
1460
  for (const [location, property] of entries) {
1228
- if (property === PAYMENT_INPUT_FIELD) {
1229
- fail(
1230
- `binds "${location}" to "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
1231
- { location }
1232
- );
1461
+ if (RESERVED_INPUT_FIELDS.includes(property)) {
1462
+ fail(`binds "${location}" to "${property}", which is reserved by the gateway`, { location });
1233
1463
  }
1234
1464
  const other = seen.get(property);
1235
1465
  if (other !== void 0) {
@@ -1358,13 +1588,14 @@ function validateAddress(path2, value) {
1358
1588
  );
1359
1589
  }
1360
1590
  }
1361
- 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, 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;
1362
1592
  var init_schema = __esm({
1363
1593
  "src/config/schema.ts"() {
1594
+ init_constants();
1364
1595
  init_execution();
1365
1596
  init_core();
1366
1597
  init_guardrails();
1367
- init_constants();
1598
+ init_constants2();
1368
1599
  init_env();
1369
1600
  SUPPORTED_CONFIG_VERSION = 1;
1370
1601
  ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
@@ -1504,7 +1735,8 @@ var init_schema = __esm({
1504
1735
  backend: BackendHandlerSchema,
1505
1736
  pricing: PricingSchema,
1506
1737
  expose: z.array(z.string().min(1)).min(1),
1507
- 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()
1508
1740
  }).strict();
1509
1741
  ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
1510
1742
  FacilitatorAuthSchema = z.discriminatedUnion("type", [
@@ -1552,6 +1784,32 @@ var init_schema = __esm({
1552
1784
  PaymentsSchema = z.object({
1553
1785
  x402: X402Schema.optional()
1554
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();
1555
1813
  RawConfigSchema = z.object({
1556
1814
  version: z.literal(SUPPORTED_CONFIG_VERSION),
1557
1815
  merchant: MerchantSchema,
@@ -1559,10 +1817,12 @@ var init_schema = __esm({
1559
1817
  storage: StorageSchema,
1560
1818
  protocols: ProtocolsSchema,
1561
1819
  resources: ResourcesMapSchema,
1562
- payments: PaymentsSchema
1820
+ payments: PaymentsSchema,
1821
+ authorization: AuthorizationSchema.optional()
1563
1822
  }).strict();
1564
1823
  SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
1565
1824
  SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
1825
+ SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
1566
1826
  EMPTY_CLOSED_OBJECT_SCHEMA = {
1567
1827
  type: "object",
1568
1828
  properties: {},
@@ -1746,6 +2006,33 @@ async function execCommand(command, args) {
1746
2006
  });
1747
2007
  }
1748
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
+
1749
2036
  // src/cli/commands/doctor.ts
1750
2037
  init_execution();
1751
2038
  init_core();
@@ -1789,10 +2076,10 @@ var A2A_UNSUPPORTED = [
1789
2076
  ];
1790
2077
 
1791
2078
  // src/cli/commands/doctor.ts
1792
- init_constants();
2079
+ init_constants2();
1793
2080
 
1794
2081
  // src/protocols/acp/descriptor.ts
1795
- init_constants();
2082
+ init_constants2();
1796
2083
  [
1797
2084
  "rest transport",
1798
2085
  "well-known discovery",
@@ -1826,7 +2113,7 @@ var ACP_UNSUPPORTED = [
1826
2113
 
1827
2114
  // src/storage/receipts/store.ts
1828
2115
  init_core();
1829
- var PACKAGE_VERSION = "1.3.0" ;
2116
+ var PACKAGE_VERSION = "1.4.0" ;
1830
2117
 
1831
2118
  // src/storage/sqlite.ts
1832
2119
  init_core();
@@ -1911,12 +2198,14 @@ function receiptToRow(receipt) {
1911
2198
  backend_status: receipt.backendStatus,
1912
2199
  duration_ms: receipt.durationMs !== void 0 ? receipt.durationMs : null,
1913
2200
  protocol: receipt.protocol !== void 0 ? receipt.protocol : null,
1914
- 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
1915
2203
  };
1916
2204
  }
1917
2205
  function rowToReceipt(row) {
1918
2206
  const payment = row.payment_json !== null ? JSON.parse(row.payment_json) : void 0;
1919
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;
1920
2209
  return {
1921
2210
  id: row.id,
1922
2211
  requestId: row.request_id,
@@ -1926,7 +2215,8 @@ function rowToReceipt(row) {
1926
2215
  ...payment !== void 0 ? { payment } : {},
1927
2216
  ...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
1928
2217
  ...row.protocol !== null ? { protocol: row.protocol } : {},
1929
- ...metadata !== void 0 ? { metadata } : {}
2218
+ ...metadata !== void 0 ? { metadata } : {},
2219
+ ...authorization !== void 0 ? { authorization } : {}
1930
2220
  };
1931
2221
  }
1932
2222
  function eventToRow(event) {
@@ -2035,6 +2325,12 @@ var MIGRATIONS = [
2035
2325
  CREATE INDEX IF NOT EXISTS idx_payment_attempts_created_at ON payment_attempts(created_at);
2036
2326
  `);
2037
2327
  }
2328
+ },
2329
+ {
2330
+ version: 2,
2331
+ up(db) {
2332
+ db.exec(`ALTER TABLE receipts ADD COLUMN authorization_json TEXT;`);
2333
+ }
2038
2334
  }
2039
2335
  ];
2040
2336
  var SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
@@ -2072,8 +2368,8 @@ function createSqliteReceiptStore(options) {
2072
2368
  const db = openSqliteDatabase({ path: path2, label: "Receipt database", logger });
2073
2369
  migrate(db);
2074
2370
  const insertReceiptStmt = db.prepare(
2075
- `INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json)
2076
- 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)`
2077
2373
  );
2078
2374
  const getReceiptStmt = db.prepare("SELECT * FROM receipts WHERE id = ?");
2079
2375
  const listReceiptsStmt = db.prepare(
@@ -2588,7 +2884,7 @@ function maskMiddle(value, keep = 6) {
2588
2884
  }
2589
2885
  createRequire(import.meta.url);
2590
2886
  function readVersionReport() {
2591
- const injected = '{"cliVersion":"1.3.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"}]}' ;
2592
2888
  {
2593
2889
  return JSON.parse(injected);
2594
2890
  }
@@ -2650,6 +2946,50 @@ function findX402Mismatch(configured, live) {
2650
2946
  if (diffs.length === 0) return void 0;
2651
2947
  return `${diffs.join("; ")} \u2014 the gateway may be running against an older deployment; restart it or re-run chain:deploy`;
2652
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
+ }
2653
2993
  function acpIdempotencyCheck(idempotency) {
2654
2994
  const retention = `retention ${idempotency.retentionHours}h`;
2655
2995
  if (idempotency.retentionHours < 24) {
@@ -2668,16 +3008,11 @@ function acpIdempotencyCheck(idempotency) {
2668
3008
  }
2669
3009
  const directory = dirname(idempotency.path);
2670
3010
  if (existsSync(idempotency.path)) {
2671
- try {
2672
- accessSync(idempotency.path, constants.W_OK);
2673
- return { name: "ACP idempotency", status: "PASS", detail: `writable, ${retention}` };
2674
- } catch {
2675
- return {
2676
- name: "ACP idempotency",
2677
- status: "FAIL",
2678
- detail: `"${idempotency.path}" exists but is not writable by this user \u2014 the adapter cannot claim idempotency keys`
2679
- };
2680
- }
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
+ };
2681
3016
  }
2682
3017
  if (directory !== "" && directory !== "." && !existsSync(directory)) {
2683
3018
  return {
@@ -2841,6 +3176,37 @@ async function runDoctor(options, deps = {}) {
2841
3176
  detail: ACP_UNSUPPORTED.join(", ")
2842
3177
  });
2843
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
+ }
2844
3210
  const x402 = config?.payments.x402;
2845
3211
  if (x402 === void 0 || !x402.enabled) {
2846
3212
  checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });