@devlab.group/agent-commerce 1.2.0 → 1.3.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, accessSync, constants, readFileSync, 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
 
@@ -335,6 +335,30 @@ var init_networks = __esm({
335
335
  }
336
336
  });
337
337
 
338
+ // src/protocols/acp/constants.ts
339
+ var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS;
340
+ var init_constants = __esm({
341
+ "src/protocols/acp/constants.ts"() {
342
+ ACP_SPEC_VERSION = "2026-04-17";
343
+ ACP_API_VERSION = "2026-04-17";
344
+ ACP_WELL_KNOWN_PATH = "/.well-known/acp.json";
345
+ ACP_CHECKOUT_OPERATIONS = [
346
+ "createCheckoutSession",
347
+ "updateCheckoutSession",
348
+ "getCheckoutSession",
349
+ "completeCheckoutSession",
350
+ "cancelCheckoutSession"
351
+ ];
352
+ ACP_OPERATION_INPUT_KEYS = {
353
+ createCheckoutSession: ["body"],
354
+ updateCheckoutSession: ["path", "body"],
355
+ getCheckoutSession: ["path"],
356
+ completeCheckoutSession: ["path", "body"],
357
+ cancelCheckoutSession: ["path"]
358
+ };
359
+ }
360
+ });
361
+
338
362
  // src/config/env.ts
339
363
  function substituteEnv(value, env, path2 = "$") {
340
364
  if (typeof value === "string") {
@@ -691,7 +715,8 @@ function normalise(raw) {
691
715
  a2a: {
692
716
  enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
693
717
  mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
694
- }
718
+ },
719
+ acp: normaliseAcp(raw.protocols.acp)
695
720
  };
696
721
  validateMountPaths(protocols);
697
722
  const x402Raw = raw.payments.x402;
@@ -745,6 +770,7 @@ function normalise(raw) {
745
770
  const resources = Object.entries(raw.resources).map(
746
771
  ([id, entry]) => normaliseResource(id, entry, protocols, x402)
747
772
  );
773
+ if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
748
774
  return {
749
775
  version: SUPPORTED_CONFIG_VERSION,
750
776
  merchant: {
@@ -771,7 +797,8 @@ function normalise(raw) {
771
797
  function validateMountPaths(protocols) {
772
798
  const mounts = [
773
799
  ["mcp", protocols.mcp],
774
- ["a2a", protocols.a2a]
800
+ ["a2a", protocols.a2a],
801
+ ["acp", protocols.acp]
775
802
  ].filter(([, p]) => p.enabled);
776
803
  for (const [index, [nameA, a]] of mounts.entries()) {
777
804
  for (const [nameB, b] of mounts.slice(index + 1)) {
@@ -787,6 +814,145 @@ function validateMountPaths(protocols) {
787
814
  }
788
815
  }
789
816
  }
817
+ function acpInvalid(path2, message, extra = {}) {
818
+ return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
819
+ }
820
+ function normaliseAcp(raw) {
821
+ const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
822
+ if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
823
+ return { enabled: false, mountPath };
824
+ }
825
+ if (raw.auth === void 0) {
826
+ throw acpInvalid(
827
+ "protocols.acp.auth",
828
+ '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'
829
+ );
830
+ }
831
+ if (raw.idempotency === void 0) {
832
+ throw acpInvalid(
833
+ "protocols.acp.idempotency",
834
+ "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"
835
+ );
836
+ }
837
+ const discovery = raw.discovery;
838
+ return {
839
+ enabled: true,
840
+ mountPath,
841
+ auth: raw.auth,
842
+ idempotency: {
843
+ path: raw.idempotency.path,
844
+ retentionHours: toNumber(
845
+ raw.idempotency.retentionHours ?? ACP_RETENTION_HOURS,
846
+ "protocols.acp.idempotency.retentionHours",
847
+ { min: ACP_RETENTION_HOURS }
848
+ )
849
+ },
850
+ checkout: { operations: normaliseAcpOperations(raw.checkout) },
851
+ // Built key by key rather than passed through: absent metadata must stay
852
+ // absent from the discovery document, not appear there as `undefined`.
853
+ ...discovery !== void 0 ? {
854
+ discovery: {
855
+ ...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
856
+ ...discovery.supportedCurrencies !== void 0 ? { supportedCurrencies: discovery.supportedCurrencies } : {},
857
+ ...discovery.supportedLocales !== void 0 ? { supportedLocales: discovery.supportedLocales } : {},
858
+ ...discovery.interventionTypes !== void 0 ? { interventionTypes: discovery.interventionTypes } : {}
859
+ }
860
+ } : {}
861
+ };
862
+ }
863
+ function normaliseAcpOperations(checkout) {
864
+ const configured = checkout?.operations ?? {};
865
+ for (const key of Object.keys(configured)) {
866
+ if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
867
+ throw acpInvalid(
868
+ `protocols.acp.checkout.operations.${key}`,
869
+ `protocols.acp.checkout.operations names unknown ACP checkout operation "${key}". Supported: ${ACP_CHECKOUT_OPERATIONS.join(", ")}.`
870
+ );
871
+ }
872
+ }
873
+ const operations = {};
874
+ const claimedBy = /* @__PURE__ */ new Map();
875
+ for (const operation of ACP_CHECKOUT_OPERATIONS) {
876
+ const path2 = `protocols.acp.checkout.operations.${operation}`;
877
+ const resourceId = configured[operation];
878
+ if (resourceId === void 0) {
879
+ throw acpInvalid(
880
+ path2,
881
+ `${path2} is required when ACP is enabled - all five checkout operations must be mapped, since ACP discovery advertises the checkout service as a whole`
882
+ );
883
+ }
884
+ const claimed = claimedBy.get(resourceId);
885
+ if (claimed !== void 0) {
886
+ throw acpInvalid(
887
+ path2,
888
+ `${path2} maps resource "${resourceId}", which is already mapped to "${claimed}" - each ACP checkout operation needs its own resource`,
889
+ { resourceId }
890
+ );
891
+ }
892
+ claimedBy.set(resourceId, operation);
893
+ operations[operation] = resourceId;
894
+ }
895
+ return operations;
896
+ }
897
+ function validateAcpCheckoutMapping(acp, resources) {
898
+ const byId = new Map(resources.map((resource) => [resource.id, resource]));
899
+ for (const operation of ACP_CHECKOUT_OPERATIONS) {
900
+ const resourceId = acp.checkout.operations[operation];
901
+ const path2 = `protocols.acp.checkout.operations.${operation}`;
902
+ const resource = byId.get(resourceId);
903
+ if (resource === void 0) {
904
+ throw acpInvalid(
905
+ path2,
906
+ `${path2} maps resource "${resourceId}", which is not defined under "resources"`,
907
+ { resourceId }
908
+ );
909
+ }
910
+ if (!resource.exposedVia.includes("acp")) {
911
+ throw acpInvalid(
912
+ path2,
913
+ `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`,
914
+ { resourceId }
915
+ );
916
+ }
917
+ if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
918
+ throw acpInvalid(
919
+ path2,
920
+ `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`,
921
+ { resourceId }
922
+ );
923
+ }
924
+ validateAcpOperationInput(path2, operation, resource);
925
+ }
926
+ }
927
+ function validateAcpOperationInput(path2, operation, resource) {
928
+ const schema = resource.inputSchema;
929
+ if (schema === void 0) return;
930
+ const keys = ACP_OPERATION_INPUT_KEYS[operation];
931
+ const properties = schema["properties"];
932
+ if (schema["additionalProperties"] === false && isPlainObject2(properties)) {
933
+ for (const key of keys) {
934
+ if (!Object.hasOwn(properties, key)) {
935
+ throw acpInvalid(
936
+ path2,
937
+ `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`,
938
+ { resourceId: resource.id }
939
+ );
940
+ }
941
+ }
942
+ }
943
+ const required = schema["required"];
944
+ if (Array.isArray(required)) {
945
+ for (const entry of required) {
946
+ if (typeof entry === "string" && !keys.includes(entry)) {
947
+ throw acpInvalid(
948
+ path2,
949
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema requires "${entry}", which that operation never supplies (it sends: ${keys.join(", ")})`,
950
+ { resourceId: resource.id }
951
+ );
952
+ }
953
+ }
954
+ }
955
+ }
790
956
  function normaliseResource(id, entry, protocols, x402) {
791
957
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
792
958
  const inputProperties = entry.input?.["properties"];
@@ -807,7 +973,7 @@ function normaliseResource(id, entry, protocols, x402) {
807
973
  const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
808
974
  throw new CommerceError(
809
975
  "CONFIG_INVALID",
810
- `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp, a2a.`,
976
+ `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: ${PROTOCOL_NAMES.join(", ")}.`,
811
977
  { details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
812
978
  );
813
979
  }
@@ -835,6 +1001,13 @@ function normaliseResource(id, entry, protocols, x402) {
835
1001
  { details: { path: `resources.${id}.expose`, resourceId: id } }
836
1002
  );
837
1003
  }
1004
+ if (entry.expose.includes("acp") && !protocols.acp.enabled) {
1005
+ throw new CommerceError(
1006
+ "CONFIG_INVALID",
1007
+ `Resource "${id}" is exposed via "acp" but protocols.acp.enabled is false`,
1008
+ { details: { path: `resources.${id}.expose`, resourceId: id } }
1009
+ );
1010
+ }
838
1011
  if (entry.expose.includes("http") && !protocols.http.enabled) {
839
1012
  throw new CommerceError(
840
1013
  "CONFIG_INVALID",
@@ -1185,12 +1358,13 @@ function validateAddress(path2, value) {
1185
1358
  );
1186
1359
  }
1187
1360
  }
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;
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;
1189
1362
  var init_schema = __esm({
1190
1363
  "src/config/schema.ts"() {
1191
1364
  init_execution();
1192
1365
  init_core();
1193
1366
  init_guardrails();
1367
+ init_constants();
1194
1368
  init_env();
1195
1369
  SUPPORTED_CONFIG_VERSION = 1;
1196
1370
  ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
@@ -1235,6 +1409,8 @@ var init_schema = __esm({
1235
1409
  // Fixed by the A2A specification, so it is the adapter's to serve and never
1236
1410
  // a configurable mount's to claim.
1237
1411
  "/.well-known/agent-card.json",
1412
+ // Likewise fixed by the ACP specification.
1413
+ ACP_WELL_KNOWN_PATH,
1238
1414
  "/api/resources",
1239
1415
  "/api/resources/:id/invoke",
1240
1416
  "/api/receipts",
@@ -1256,6 +1432,15 @@ var init_schema = __esm({
1256
1432
  message: `must not collide with a route the gateway already serves (${RESERVED_GATEWAY_PATHS.join(", ")}); pick a dedicated prefix such as "/mcp".`
1257
1433
  }
1258
1434
  );
1435
+ AcpAuthSchema = z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict();
1436
+ AcpIdempotencySchema = z.object({ path: z.string().min(1), retentionHours: NumberOrString.optional() }).strict();
1437
+ AcpCheckoutSchema = z.object({ operations: z.record(z.string().min(1)) }).strict();
1438
+ AcpDiscoverySchema = z.object({
1439
+ documentationUrl: z.string().min(1).optional(),
1440
+ supportedCurrencies: z.array(z.string().min(1)).optional(),
1441
+ supportedLocales: z.array(z.string().min(1)).optional(),
1442
+ interventionTypes: z.array(z.string().min(1)).optional()
1443
+ }).strict();
1259
1444
  ProtocolsSchema = z.object({
1260
1445
  http: z.object({ enabled: BooleanOrString }).strict(),
1261
1446
  mcp: z.object({
@@ -1267,9 +1452,23 @@ var init_schema = __esm({
1267
1452
  a2a: z.object({
1268
1453
  enabled: BooleanOrString,
1269
1454
  mountPath: MountPathSchema.optional()
1455
+ }).strict().optional(),
1456
+ // Likewise optional and off by default. The sub-blocks are optional here
1457
+ // and required by the business pass only when ACP is enabled, so a
1458
+ // disabled placeholder block stays writable and an enabled one gets a
1459
+ // message naming the piece it is missing.
1460
+ acp: z.object({
1461
+ enabled: BooleanOrString,
1462
+ mountPath: MountPathSchema.optional(),
1463
+ auth: AcpAuthSchema.optional(),
1464
+ idempotency: AcpIdempotencySchema.optional(),
1465
+ checkout: AcpCheckoutSchema.optional(),
1466
+ discovery: AcpDiscoverySchema.optional()
1270
1467
  }).strict().optional()
1271
1468
  }).strict();
1272
1469
  DEFAULT_A2A_MOUNT_PATH = "/a2a";
1470
+ DEFAULT_ACP_MOUNT_PATH = "/acp";
1471
+ ACP_RETENTION_HOURS = 24;
1273
1472
  BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
1274
1473
  BackendInputBindingsSchema = z.object({
1275
1474
  path: z.string().min(1).optional(),
@@ -1589,9 +1788,94 @@ var A2A_UNSUPPORTED = [
1589
1788
  "artifact types beyond Agent Commerce outcome data"
1590
1789
  ];
1591
1790
 
1791
+ // src/cli/commands/doctor.ts
1792
+ init_constants();
1793
+
1794
+ // src/protocols/acp/descriptor.ts
1795
+ init_constants();
1796
+ [
1797
+ "rest transport",
1798
+ "well-known discovery",
1799
+ "bearer authentication",
1800
+ "API-Version negotiation",
1801
+ "checkout service",
1802
+ ...ACP_CHECKOUT_OPERATIONS
1803
+ ];
1804
+ var ACP_UNSUPPORTED = [
1805
+ // Services this seller does not implement, named as ACP names them. The
1806
+ // discovery document advertises `checkout` alone, and these are the reason.
1807
+ "carts service",
1808
+ "feed service",
1809
+ "standalone orders service",
1810
+ "delegate_payment",
1811
+ "delegate_authentication",
1812
+ // Bindings and delivery directions.
1813
+ "ACP MCP transport binding",
1814
+ "webhooks",
1815
+ "outbound merchant-to-agent delivery",
1816
+ "ACP client role",
1817
+ // Request authentication beyond the bearer token.
1818
+ "Signature header verification",
1819
+ "Timestamp replay-window verification",
1820
+ // Extensions and versioning.
1821
+ "discount extension",
1822
+ "general ACP extension framework",
1823
+ "multiple ACP API versions",
1824
+ "seller-backed payment handler integration"
1825
+ ];
1826
+
1592
1827
  // src/storage/receipts/store.ts
1593
1828
  init_core();
1594
- var PACKAGE_VERSION = "1.2.0" ;
1829
+ var PACKAGE_VERSION = "1.3.0" ;
1830
+
1831
+ // src/storage/sqlite.ts
1832
+ init_core();
1833
+ function openSqliteDatabase(options) {
1834
+ const { path: path2, label, logger } = options;
1835
+ const isFileBacked = path2 !== ":memory:";
1836
+ if (isFileBacked) {
1837
+ const dir = dirname(path2);
1838
+ if (dir !== "" && dir !== "." && !existsSync(dir)) {
1839
+ mkdirSync(dir, { recursive: true, mode: 448 });
1840
+ }
1841
+ }
1842
+ if (isFileBacked && existsSync(path2)) {
1843
+ try {
1844
+ accessSync(path2, constants.W_OK);
1845
+ } catch {
1846
+ throw new CommerceError(
1847
+ "STORAGE_ERROR",
1848
+ `${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.`,
1849
+ { details: { path: path2 } }
1850
+ );
1851
+ }
1852
+ }
1853
+ if (isFileBacked) {
1854
+ try {
1855
+ closeSync(openSync(path2, "a", 384));
1856
+ } catch {
1857
+ }
1858
+ }
1859
+ const db = new Database(path2);
1860
+ db.pragma("journal_mode = WAL");
1861
+ db.pragma("foreign_keys = ON");
1862
+ if (isFileBacked) restrictDatabasePermissions(path2, label, logger);
1863
+ return db;
1864
+ }
1865
+ function restrictDatabasePermissions(path2, label, logger) {
1866
+ for (const file of [path2, `${path2}-wal`, `${path2}-shm`]) {
1867
+ try {
1868
+ chmodSync(file, 384);
1869
+ } catch (error) {
1870
+ const code = error.code;
1871
+ if (code === "ENOENT") continue;
1872
+ logger.warn(
1873
+ { file, label, error: code ?? "unknown" },
1874
+ "could not restrict database permissions to owner-only"
1875
+ );
1876
+ }
1877
+ }
1878
+ }
1595
1879
 
1596
1880
  // src/storage/receipts/redact.ts
1597
1881
  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;
@@ -1779,54 +2063,14 @@ var defaultIds = {
1779
2063
  function isUniqueConstraintOn(err, column) {
1780
2064
  return err instanceof Error && "code" in err && err.code === "SQLITE_CONSTRAINT_UNIQUE" && err.message.includes(column);
1781
2065
  }
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
2066
  function createSqliteReceiptStore(options) {
1797
2067
  const logger = options.logger ?? NOOP_LOGGER;
1798
2068
  const clock = options.clock ?? systemClock;
1799
2069
  const ids = options.ids ?? defaultIds;
1800
2070
  const path2 = options.path;
1801
2071
  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");
2072
+ const db = openSqliteDatabase({ path: path2, label: "Receipt database", logger });
1828
2073
  migrate(db);
1829
- if (isFileBacked) restrictDatabasePermissions(path2, logger);
1830
2074
  const insertReceiptStmt = db.prepare(
1831
2075
  `INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json)
1832
2076
  VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json)`
@@ -2344,7 +2588,7 @@ function maskMiddle(value, keep = 6) {
2344
2588
  }
2345
2589
  createRequire(import.meta.url);
2346
2590
  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"}]}' ;
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"}]}' ;
2348
2592
  {
2349
2593
  return JSON.parse(injected);
2350
2594
  }
@@ -2406,6 +2650,73 @@ function findX402Mismatch(configured, live) {
2406
2650
  if (diffs.length === 0) return void 0;
2407
2651
  return `${diffs.join("; ")} \u2014 the gateway may be running against an older deployment; restart it or re-run chain:deploy`;
2408
2652
  }
2653
+ function acpIdempotencyCheck(idempotency) {
2654
+ const retention = `retention ${idempotency.retentionHours}h`;
2655
+ if (idempotency.retentionHours < 24) {
2656
+ return {
2657
+ name: "ACP idempotency",
2658
+ status: "FAIL",
2659
+ detail: `${retention} is below the 24h ACP minimum \u2014 a replayed key could run a checkout twice`
2660
+ };
2661
+ }
2662
+ if (idempotency.path === ":memory:") {
2663
+ return {
2664
+ name: "ACP idempotency",
2665
+ status: "WARN",
2666
+ detail: `in-memory store, ${retention} \u2014 replay protection is lost on every restart; use a file path in production`
2667
+ };
2668
+ }
2669
+ const directory = dirname(idempotency.path);
2670
+ 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
+ }
2681
+ }
2682
+ if (directory !== "" && directory !== "." && !existsSync(directory)) {
2683
+ return {
2684
+ name: "ACP idempotency",
2685
+ status: "WARN",
2686
+ 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`
2687
+ };
2688
+ }
2689
+ return {
2690
+ name: "ACP idempotency",
2691
+ status: "WARN",
2692
+ detail: `no store yet at "${idempotency.path}" \u2014 it is created the first time the gateway starts (${retention})`
2693
+ };
2694
+ }
2695
+ function acpMappingCheck(config, operations) {
2696
+ const byId = new Map(config.resources.map((resource) => [resource.id, resource]));
2697
+ const problems = [];
2698
+ for (const [operation, resourceId] of Object.entries(operations)) {
2699
+ const resource = byId.get(resourceId);
2700
+ if (resource === void 0) {
2701
+ problems.push(`${operation} -> "${resourceId}" does not exist`);
2702
+ continue;
2703
+ }
2704
+ if (!resource.exposedVia.includes("acp")) {
2705
+ problems.push(`${operation} -> "${resourceId}" is not exposed via acp`);
2706
+ }
2707
+ if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
2708
+ problems.push(`${operation} -> "${resourceId}" is not free to invoke`);
2709
+ }
2710
+ }
2711
+ if (problems.length > 0) {
2712
+ return { name: "ACP checkout mapping", status: "FAIL", detail: problems.join("; ") };
2713
+ }
2714
+ return {
2715
+ name: "ACP checkout mapping",
2716
+ status: "PASS",
2717
+ detail: `5 operations mapped to free acp-exposed resources (${Object.values(operations).join(", ")})`
2718
+ };
2719
+ }
2409
2720
  async function runDoctor(options, deps = {}) {
2410
2721
  const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
2411
2722
  const loadConfig2 = deps.loadConfig ?? loadConfigDynamic;
@@ -2487,7 +2798,7 @@ async function runDoctor(options, deps = {}) {
2487
2798
  checks.push({
2488
2799
  name: "Protocols",
2489
2800
  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"}`
2801
+ 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
2802
  });
2492
2803
  }
2493
2804
  if (config === void 0) {
@@ -2506,6 +2817,30 @@ async function runDoctor(options, deps = {}) {
2506
2817
  detail: A2A_UNSUPPORTED.join(", ")
2507
2818
  });
2508
2819
  }
2820
+ if (config === void 0) {
2821
+ checks.push({ name: "ACP", status: "WARN", detail: "skipped \u2014 config invalid" });
2822
+ } else if (!config.protocols.acp.enabled) {
2823
+ checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
2824
+ } else {
2825
+ const acp = config.protocols.acp;
2826
+ checks.push({
2827
+ name: "ACP",
2828
+ status: "PASS",
2829
+ 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}`
2830
+ });
2831
+ checks.push({
2832
+ name: "ACP auth",
2833
+ status: "PASS",
2834
+ detail: `bearer token configured (${acp.auth.token.length} characters, not shown)`
2835
+ });
2836
+ checks.push(acpIdempotencyCheck(acp.idempotency));
2837
+ checks.push(acpMappingCheck(config, acp.checkout.operations));
2838
+ checks.push({
2839
+ name: "ACP unsupported",
2840
+ status: "INFO",
2841
+ detail: ACP_UNSUPPORTED.join(", ")
2842
+ });
2843
+ }
2509
2844
  const x402 = config?.payments.x402;
2510
2845
  if (x402 === void 0 || !x402.enabled) {
2511
2846
  checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
@@ -3801,7 +4136,7 @@ async function runValidate(options, io, deps = {}) {
3801
4136
  io.stdout(` merchant: ${config.merchant.name} (${config.merchant.id})`);
3802
4137
  io.stdout(` resources: ${config.resources.length}`);
3803
4138
  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"}`
4139
+ ` 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
4140
  );
3806
4141
  io.stdout(` payments: x402=${config.payments.x402?.enabled === true ? "on" : "off"}`);
3807
4142
  return 0;