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