@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/README.md +23 -18
- package/dist/{chunk-EIACECVM.js → chunk-N3DNLXGC.js} +4 -4
- package/dist/chunk-N3DNLXGC.js.map +1 -0
- package/dist/{chunk-5TKNTWMC.js → chunk-TBZA6Z5B.js} +3 -3
- package/dist/{chunk-5TKNTWMC.js.map → chunk-TBZA6Z5B.js.map} +1 -1
- package/dist/cli/index.js +387 -52
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +153 -6
- package/dist/index.js +5359 -201
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{protocol-adapter-BAKGnuZ4.d.ts → protocol-adapter-DTFGWGXq.d.ts} +2 -2
- package/dist/{runtime-Bqg9qW3t.d.ts → runtime-Plk5Peqi.d.ts} +3 -3
- package/dist/x402.d.ts +1 -1
- package/dist/x402.js +3 -3
- package/package.json +3 -1
- package/dist/chunk-EIACECVM.js.map +0 -1
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,
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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;
|