@devlab.group/agent-commerce 1.5.0 → 1.6.1
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 +73 -71
- package/dist/ap2.d.ts +20 -23
- package/dist/ap2.js +53 -75
- package/dist/ap2.js.map +1 -1
- package/dist/authorization-BpbWGGrf.d.ts +115 -0
- package/dist/chunk-AFI2CEZS.js +10 -0
- package/dist/chunk-AFI2CEZS.js.map +1 -0
- package/dist/{chunk-PBYN7EK2.js → chunk-CBJIT7QP.js} +10 -9
- package/dist/chunk-CBJIT7QP.js.map +1 -0
- package/dist/{chunk-NKNXVDHN.js → chunk-JKEKKXRJ.js} +45 -66
- package/dist/chunk-JKEKKXRJ.js.map +1 -0
- package/dist/{chunk-5GEWKNIH.js → chunk-N5KRK7YA.js} +63 -27
- package/dist/chunk-N5KRK7YA.js.map +1 -0
- package/dist/{chunk-6D2EZ5ME.js → chunk-V2V2E7HX.js} +2 -2
- package/dist/{chunk-6D2EZ5ME.js.map → chunk-V2V2E7HX.js.map} +1 -1
- package/dist/{chunk-US622BKF.js → chunk-VKDXMXHL.js} +5 -8
- package/dist/chunk-VKDXMXHL.js.map +1 -0
- package/dist/cli/index.js +501 -494
- package/dist/cli/index.js.map +1 -1
- package/dist/{constants-6mypC60h.d.ts → constants-Bkq04FRZ.d.ts} +1 -3
- package/dist/guardrails-71XwtXlT.d.ts +31 -0
- package/dist/index.d.ts +230 -265
- package/dist/index.js +1050 -607
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +6 -59
- package/dist/mcp.js +61 -53
- package/dist/mcp.js.map +1 -1
- package/dist/mpp.d.ts +4 -4
- package/dist/mpp.js +21 -19
- package/dist/mpp.js.map +1 -1
- package/dist/protocol-adapter-DTkrXTev.d.ts +238 -0
- package/dist/runtime-CiYbgGpi.d.ts +303 -0
- package/dist/{types-CJtwLu1c.d.ts → types-O6lg3Q5V.d.ts} +17 -23
- package/dist/x402.d.ts +43 -75
- package/dist/x402.js +10 -9
- package/dist/x402.js.map +1 -1
- package/package.json +7 -5
- package/dist/authorization-DA7mjy_O.d.ts +0 -123
- package/dist/chunk-5GEWKNIH.js.map +0 -1
- package/dist/chunk-NKNXVDHN.js.map +0 -1
- package/dist/chunk-PBYN7EK2.js.map +0 -1
- package/dist/chunk-US622BKF.js.map +0 -1
- package/dist/guardrails-Dmwi63KB.d.ts +0 -38
- package/dist/protocol-adapter-APRjs1E2.d.ts +0 -273
- package/dist/runtime-DC5XQ9Cc.d.ts +0 -351
package/dist/cli/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { isDeepStrictEqual } from 'util';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { existsSync, mkdirSync, readFileSync, accessSync, constants, closeSync, openSync, chmodSync, promises } from 'fs';
|
|
4
5
|
import * as path from 'path';
|
|
5
|
-
import {
|
|
6
|
+
import { dirname, resolve, basename, extname, win32, join } from 'path';
|
|
6
7
|
import { stringify, parse, Document, YAMLError } from 'yaml';
|
|
7
8
|
import { CommanderError, Command } from 'commander';
|
|
8
9
|
import picocolors from 'picocolors';
|
|
9
10
|
import { randomUUID } from 'crypto';
|
|
10
|
-
import
|
|
11
|
+
import 'module';
|
|
11
12
|
import Database from 'better-sqlite3';
|
|
12
13
|
import { writeFile, stat, rename, rm, open } from 'fs/promises';
|
|
13
14
|
import { validate } from '@scalar/openapi-parser';
|
|
@@ -32,11 +33,22 @@ var init_filename = __esm({
|
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
// src/core/domain/common.ts
|
|
35
|
-
var PROTOCOL_NAMES, PAYMENT_METHOD_NAMES;
|
|
36
|
+
var PROTOCOL_NAME_KEYS, PROTOCOL_NAMES, PAYMENT_METHOD_NAME_KEYS, PAYMENT_METHOD_NAMES;
|
|
36
37
|
var init_common = __esm({
|
|
37
38
|
"src/core/domain/common.ts"() {
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
PROTOCOL_NAME_KEYS = {
|
|
40
|
+
http: true,
|
|
41
|
+
mcp: true,
|
|
42
|
+
a2a: true,
|
|
43
|
+
acp: true
|
|
44
|
+
};
|
|
45
|
+
PROTOCOL_NAMES = Object.keys(
|
|
46
|
+
PROTOCOL_NAME_KEYS
|
|
47
|
+
);
|
|
48
|
+
PAYMENT_METHOD_NAME_KEYS = { x402: true, mpp: true };
|
|
49
|
+
PAYMENT_METHOD_NAMES = Object.keys(
|
|
50
|
+
PAYMENT_METHOD_NAME_KEYS
|
|
51
|
+
);
|
|
40
52
|
}
|
|
41
53
|
});
|
|
42
54
|
|
|
@@ -65,10 +77,9 @@ var init_codes = __esm({
|
|
|
65
77
|
PAYMENT_REPLAYED: 409,
|
|
66
78
|
PAYMENT_PROVIDER_UNAVAILABLE: 503,
|
|
67
79
|
PAYMENT_SETTLEMENT_FAILED: 502,
|
|
68
|
-
// 403, not 402:
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// never going to be delivered.
|
|
80
|
+
// 403, not 402: a 402 tells a client to pay and retry, which cannot fix a
|
|
81
|
+
// missing or rejected mandate, and a client that auto-pays on 402 would be
|
|
82
|
+
// charged for a request that was never going to be delivered
|
|
72
83
|
AUTHORIZATION_REQUIRED: 403,
|
|
73
84
|
AUTHORIZATION_INVALID: 403,
|
|
74
85
|
AUTHORIZATION_REPLAYED: 409,
|
|
@@ -82,17 +93,13 @@ var init_codes = __esm({
|
|
|
82
93
|
};
|
|
83
94
|
RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
84
95
|
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// proof will verify once it clears.
|
|
96
|
+
// The verifier or replay store was unreachable, so the mandate got no
|
|
97
|
+
// verdict. The same proof can verify once the outage clears.
|
|
88
98
|
"AUTHORIZATION_PROVIDER_UNAVAILABLE",
|
|
89
99
|
"BACKEND_TIMEOUT",
|
|
90
|
-
// Load shedding is transient
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// keyed on the machine-readable flag (which exists precisely so clients need
|
|
94
|
-
// not parse prose) would read a momentary throttle as a permanent capability
|
|
95
|
-
// gap and stop calling the tool.
|
|
100
|
+
// Load shedding is transient: the caller should back off and try again.
|
|
101
|
+
// Clients act on this flag, and leaving it out would make a momentary
|
|
102
|
+
// throttle look like a permanent failure.
|
|
96
103
|
"GATEWAY_BUSY"
|
|
97
104
|
]);
|
|
98
105
|
}
|
|
@@ -128,7 +135,7 @@ var init_commerce_error = __esm({
|
|
|
128
135
|
if (options.resourceId !== void 0) this.resourceId = options.resourceId;
|
|
129
136
|
Error.captureStackTrace?.(this, _CommerceError);
|
|
130
137
|
}
|
|
131
|
-
/** Non-secret, client-safe representation. */
|
|
138
|
+
/** Non-secret, client-safe representation. `cause` is never included */
|
|
132
139
|
toInfo() {
|
|
133
140
|
return {
|
|
134
141
|
code: this.code,
|
|
@@ -152,11 +159,21 @@ var init_errors = __esm({
|
|
|
152
159
|
}
|
|
153
160
|
});
|
|
154
161
|
|
|
162
|
+
// src/core/is-record.ts
|
|
163
|
+
function isRecord(value) {
|
|
164
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
165
|
+
}
|
|
166
|
+
var init_is_record = __esm({
|
|
167
|
+
"src/core/is-record.ts"() {
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
155
171
|
// src/core/domain/wire.ts
|
|
156
172
|
var PAYMENT_INPUT_FIELD, AUTHORIZATION_INPUT_FIELD, RESERVED_INPUT_FIELDS;
|
|
157
173
|
var init_wire = __esm({
|
|
158
174
|
"src/core/domain/wire.ts"() {
|
|
159
175
|
init_errors();
|
|
176
|
+
init_is_record();
|
|
160
177
|
PAYMENT_INPUT_FIELD = "_payment";
|
|
161
178
|
AUTHORIZATION_INPUT_FIELD = "_authorization";
|
|
162
179
|
RESERVED_INPUT_FIELDS = [
|
|
@@ -258,17 +275,10 @@ var init_backend_http = __esm({
|
|
|
258
275
|
init_resource();
|
|
259
276
|
init_errors();
|
|
260
277
|
init_logger();
|
|
278
|
+
init_is_record();
|
|
261
279
|
PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
|
|
262
280
|
}
|
|
263
281
|
});
|
|
264
|
-
|
|
265
|
-
// src/core/execution/event-bus.ts
|
|
266
|
-
var init_event_bus = __esm({
|
|
267
|
-
"src/core/execution/event-bus.ts"() {
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// src/core/execution/validation.ts
|
|
272
282
|
function typeList(raw) {
|
|
273
283
|
if (typeof raw === "string") return [raw];
|
|
274
284
|
if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
|
|
@@ -283,15 +293,17 @@ function isObjectSchemaNode(schema) {
|
|
|
283
293
|
}
|
|
284
294
|
var init_validation = __esm({
|
|
285
295
|
"src/core/execution/validation.ts"() {
|
|
296
|
+
init_is_record();
|
|
286
297
|
}
|
|
287
298
|
});
|
|
288
299
|
var init_pipeline = __esm({
|
|
289
300
|
"src/core/execution/pipeline.ts"() {
|
|
290
301
|
init_wire();
|
|
291
302
|
init_errors();
|
|
303
|
+
init_is_record();
|
|
292
304
|
init_backend_http();
|
|
293
305
|
init_validation();
|
|
294
|
-
|
|
306
|
+
[...RESERVED_INPUT_FIELDS, "__proto__"];
|
|
295
307
|
}
|
|
296
308
|
});
|
|
297
309
|
|
|
@@ -302,13 +314,20 @@ var init_registry = __esm({
|
|
|
302
314
|
}
|
|
303
315
|
});
|
|
304
316
|
|
|
317
|
+
// src/core/execution/store-event-sink.ts
|
|
318
|
+
var init_store_event_sink = __esm({
|
|
319
|
+
"src/core/execution/store-event-sink.ts"() {
|
|
320
|
+
init_errors();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
305
324
|
// src/core/execution/index.ts
|
|
306
325
|
var init_execution = __esm({
|
|
307
326
|
"src/core/execution/index.ts"() {
|
|
308
327
|
init_backend_http();
|
|
309
|
-
init_event_bus();
|
|
310
328
|
init_pipeline();
|
|
311
329
|
init_registry();
|
|
330
|
+
init_store_event_sink();
|
|
312
331
|
init_validation();
|
|
313
332
|
}
|
|
314
333
|
});
|
|
@@ -395,12 +414,12 @@ var init_networks = __esm({
|
|
|
395
414
|
];
|
|
396
415
|
BY_ID = new Map(PROFILES.map((profile) => [profile.id, profile]));
|
|
397
416
|
SUPPORTED_NETWORK_IDS = PROFILES.map((p) => p.id);
|
|
398
|
-
LIVE_MAINNET_BANNER = "LIVE MAINNET MODE
|
|
417
|
+
LIVE_MAINNET_BANNER = "LIVE MAINNET MODE - REAL FUNDS";
|
|
399
418
|
}
|
|
400
419
|
});
|
|
401
420
|
|
|
402
421
|
// src/protocols/acp/constants.ts
|
|
403
|
-
var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS;
|
|
422
|
+
var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS, ACP_OPERATION_OPTIONAL_INPUT_KEYS;
|
|
404
423
|
var init_constants3 = __esm({
|
|
405
424
|
"src/protocols/acp/constants.ts"() {
|
|
406
425
|
ACP_SPEC_VERSION = "2026-04-17";
|
|
@@ -420,6 +439,9 @@ var init_constants3 = __esm({
|
|
|
420
439
|
completeCheckoutSession: ["path", "body"],
|
|
421
440
|
cancelCheckoutSession: ["path"]
|
|
422
441
|
};
|
|
442
|
+
ACP_OPERATION_OPTIONAL_INPUT_KEYS = {
|
|
443
|
+
cancelCheckoutSession: ["body"]
|
|
444
|
+
};
|
|
423
445
|
}
|
|
424
446
|
});
|
|
425
447
|
|
|
@@ -431,7 +453,7 @@ function substituteEnv(value, env, path2 = "$") {
|
|
|
431
453
|
if (Array.isArray(value)) {
|
|
432
454
|
return value.map((entry, index) => substituteEnv(entry, env, `${path2}[${index}]`));
|
|
433
455
|
}
|
|
434
|
-
if (
|
|
456
|
+
if (isRecord(value)) {
|
|
435
457
|
const result = {};
|
|
436
458
|
for (const [key, entry] of Object.entries(value)) {
|
|
437
459
|
result[key] = substituteEnv(entry, env, `${path2}.${key}`);
|
|
@@ -448,16 +470,15 @@ function assertTemplateIsSupported(value, path2) {
|
|
|
448
470
|
if (NESTED_PLACEHOLDER.test(value)) {
|
|
449
471
|
throw new CommerceError(
|
|
450
472
|
"CONFIG_INVALID",
|
|
451
|
-
`Configuration value at "${path2}" nests placeholders (e.g. "\${A:-\${B}}"), which is not supported
|
|
473
|
+
`Configuration value at "${path2}" nests placeholders (e.g. "\${A:-\${B}}"), which is not supported: the inner one is not resolved and its closing brace ends up in the value. Use a plain default, or set the variable.`,
|
|
452
474
|
{ details: { path: path2 } }
|
|
453
475
|
);
|
|
454
476
|
}
|
|
455
|
-
BRACE_TOKEN.lastIndex = 0;
|
|
456
477
|
for (const [token] of value.matchAll(BRACE_TOKEN)) {
|
|
457
478
|
if (SUPPORTED_TOKEN.test(token)) continue;
|
|
458
479
|
throw new CommerceError(
|
|
459
480
|
"CONFIG_INVALID",
|
|
460
|
-
`Configuration value at "${path2}" contains "${token}", which is not a supported placeholder. Only "\${VAR}" and "\${VAR:-default}" are
|
|
481
|
+
`Configuration value at "${path2}" contains "${token}", which is not a supported placeholder. Only "\${VAR}" and "\${VAR:-default}" are recognized; shell forms such as "\${VAR-default}", "\${VAR:=default}" and "\${VAR:?message}" would load as literal text.`,
|
|
461
482
|
{ details: { path: path2, token } }
|
|
462
483
|
);
|
|
463
484
|
}
|
|
@@ -482,13 +503,11 @@ function substituteOnce(value, env, path2) {
|
|
|
482
503
|
}
|
|
483
504
|
);
|
|
484
505
|
}
|
|
485
|
-
function isPlainObject(value) {
|
|
486
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
487
|
-
}
|
|
488
506
|
var PLACEHOLDER_PATTERN, BRACE_TOKEN, SUPPORTED_TOKEN, NESTED_PLACEHOLDER;
|
|
489
507
|
var init_env = __esm({
|
|
490
508
|
"src/config/env.ts"() {
|
|
491
509
|
init_core();
|
|
510
|
+
init_is_record();
|
|
492
511
|
PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
|
|
493
512
|
BRACE_TOKEN = /\$\{[^{}]*\}/g;
|
|
494
513
|
SUPPORTED_TOKEN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}$/;
|
|
@@ -526,7 +545,7 @@ function isWellKnownDevAddress(address) {
|
|
|
526
545
|
}
|
|
527
546
|
function isLikelyLocalOrPrivateHost(hostname) {
|
|
528
547
|
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
529
|
-
if (h === "localhost" || h === "
|
|
548
|
+
if (h === "localhost" || h === "::1") return true;
|
|
530
549
|
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
531
550
|
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
532
551
|
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
@@ -573,7 +592,7 @@ function resolveX402Deployment(input) {
|
|
|
573
592
|
assertFacilitatorUrlIsSafe(input.facilitator.url, mode, at);
|
|
574
593
|
if (mode === "mainnet" && input.facilitator.auth.type === "none" && input.allowUnauthenticatedFacilitator !== true) {
|
|
575
594
|
throw invalid(
|
|
576
|
-
`${at}: facilitator ${describeOrigin(input.facilitator.url)}
|
|
595
|
+
`${at}: no credential is configured for facilitator ${describeOrigin(input.facilitator.url)} on mainnet. Set ${at}.allowUnauthenticatedFacilitator: true to accept anonymous access and the facilitator's limits, or configure facilitator.auth.`,
|
|
577
596
|
`${at}.allowUnauthenticatedFacilitator`
|
|
578
597
|
);
|
|
579
598
|
}
|
|
@@ -640,7 +659,7 @@ function assertFacilitatorUrlIsSafe(url, mode, at) {
|
|
|
640
659
|
}
|
|
641
660
|
if (mode !== "mainnet" && isLikelyLocalOrPrivateHost(parsed.hostname)) return;
|
|
642
661
|
throw invalid(
|
|
643
|
-
`${at}: facilitator ${describeOrigin(url)} is reached over plain HTTP. Payment
|
|
662
|
+
`${at}: facilitator ${describeOrigin(url)} is reached over plain HTTP. Payment authorizations and settlement results would travel unencrypted. Use https, or point at a local/private host on a non-mainnet deployment.`,
|
|
644
663
|
`${at}.facilitator.url`
|
|
645
664
|
);
|
|
646
665
|
}
|
|
@@ -674,8 +693,20 @@ var init_guardrails = __esm({
|
|
|
674
693
|
init_networks();
|
|
675
694
|
}
|
|
676
695
|
});
|
|
696
|
+
|
|
697
|
+
// src/protocols/mcp/constants.ts
|
|
698
|
+
var MCP_TOOL_NAME_CHARS, MCP_TOOL_NAME_MAX_LENGTH, MCP_TOOL_NAME_PATTERN;
|
|
699
|
+
var init_constants4 = __esm({
|
|
700
|
+
"src/protocols/mcp/constants.ts"() {
|
|
701
|
+
MCP_TOOL_NAME_CHARS = "A-Za-z0-9._-";
|
|
702
|
+
MCP_TOOL_NAME_MAX_LENGTH = 128;
|
|
703
|
+
MCP_TOOL_NAME_PATTERN = new RegExp(
|
|
704
|
+
`^[${MCP_TOOL_NAME_CHARS}]{1,${MCP_TOOL_NAME_MAX_LENGTH}}$`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
677
708
|
function parseConfig(raw, env) {
|
|
678
|
-
if (
|
|
709
|
+
if (!isRecord(raw)) {
|
|
679
710
|
throw new CommerceError(
|
|
680
711
|
"CONFIG_INVALID",
|
|
681
712
|
'Configuration root must be a mapping (object) at "$"',
|
|
@@ -687,7 +718,7 @@ function parseConfig(raw, env) {
|
|
|
687
718
|
checkVersion(raw);
|
|
688
719
|
const substituted = substituteEnv(raw, env);
|
|
689
720
|
const parsed = parseWithZod(RawConfigSchema, substituted);
|
|
690
|
-
return
|
|
721
|
+
return normalize(parsed);
|
|
691
722
|
}
|
|
692
723
|
function checkVersion(raw) {
|
|
693
724
|
if (!("version" in raw)) {
|
|
@@ -695,12 +726,12 @@ function checkVersion(raw) {
|
|
|
695
726
|
details: { path: "version" }
|
|
696
727
|
});
|
|
697
728
|
}
|
|
698
|
-
const
|
|
699
|
-
const version = typeof rawVersion === "string" ? Number(rawVersion) : rawVersion;
|
|
729
|
+
const version = raw["version"];
|
|
700
730
|
if (version !== SUPPORTED_CONFIG_VERSION) {
|
|
731
|
+
const hint = typeof version === "string" ? ", written as a number without quotes" : "";
|
|
701
732
|
throw new CommerceError(
|
|
702
733
|
"CONFIG_INVALID",
|
|
703
|
-
`Unsupported config version "${String(
|
|
734
|
+
`Unsupported config version "${String(version)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}${hint}`,
|
|
704
735
|
{ details: { path: "version", supported: SUPPORTED_CONFIG_VERSION } }
|
|
705
736
|
);
|
|
706
737
|
}
|
|
@@ -771,7 +802,7 @@ function toBoolean(value, path2) {
|
|
|
771
802
|
{ details: { path: path2 } }
|
|
772
803
|
);
|
|
773
804
|
}
|
|
774
|
-
function
|
|
805
|
+
function normalize(raw) {
|
|
775
806
|
const protocols = {
|
|
776
807
|
http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
|
|
777
808
|
mcp: {
|
|
@@ -782,7 +813,7 @@ function normalise(raw) {
|
|
|
782
813
|
enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
|
|
783
814
|
mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
|
|
784
815
|
},
|
|
785
|
-
acp:
|
|
816
|
+
acp: normalizeAcp(raw.protocols.acp)
|
|
786
817
|
};
|
|
787
818
|
validateMountPaths(protocols);
|
|
788
819
|
const x402Raw = raw.payments.x402;
|
|
@@ -805,7 +836,7 @@ function normalise(raw) {
|
|
|
805
836
|
min: 1
|
|
806
837
|
}
|
|
807
838
|
),
|
|
808
|
-
facilitator:
|
|
839
|
+
facilitator: normalizeFacilitator(x402Raw.facilitator),
|
|
809
840
|
...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
|
|
810
841
|
...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
811
842
|
allowUnauthenticatedFacilitator: toBoolean(
|
|
@@ -828,13 +859,13 @@ function normalise(raw) {
|
|
|
828
859
|
...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
|
|
829
860
|
});
|
|
830
861
|
}
|
|
831
|
-
const mpp =
|
|
832
|
-
const ap2 =
|
|
862
|
+
const mpp = normalizeMpp(raw.payments.mpp);
|
|
863
|
+
const ap2 = normalizeAp2(raw.authorization?.ap2);
|
|
833
864
|
if (ap2?.enabled) {
|
|
834
865
|
validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
|
|
835
866
|
}
|
|
836
867
|
const resources = Object.entries(raw.resources).map(
|
|
837
|
-
([id, entry]) =>
|
|
868
|
+
([id, entry]) => normalizeResource(id, entry, protocols, x402, mpp, ap2)
|
|
838
869
|
);
|
|
839
870
|
if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
|
|
840
871
|
return {
|
|
@@ -844,8 +875,7 @@ function normalise(raw) {
|
|
|
844
875
|
name: raw.merchant.name,
|
|
845
876
|
publicBaseUrl: raw.merchant.publicBaseUrl
|
|
846
877
|
},
|
|
847
|
-
//
|
|
848
|
-
// convention, useful for tests and ephemeral dev/demo instances.
|
|
878
|
+
// Port 0 lets the OS pick a free port, for tests and throwaway instances
|
|
849
879
|
server: {
|
|
850
880
|
port: toNumber(raw.server.port, "server.port", { min: 0, max: 65535 }),
|
|
851
881
|
host: raw.server.host,
|
|
@@ -885,7 +915,7 @@ function validateMountPaths(protocols) {
|
|
|
885
915
|
function ap2Invalid(path2, message, extra = {}) {
|
|
886
916
|
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
|
|
887
917
|
}
|
|
888
|
-
function
|
|
918
|
+
function normalizeAp2(raw) {
|
|
889
919
|
if (raw === void 0) return void 0;
|
|
890
920
|
if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
|
|
891
921
|
const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
|
|
@@ -905,14 +935,14 @@ function normaliseAp2(raw) {
|
|
|
905
935
|
if (raw.replay === void 0) {
|
|
906
936
|
throw ap2Invalid(
|
|
907
937
|
"authorization.ap2.replay",
|
|
908
|
-
"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
|
|
938
|
+
"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 authorize a second settlement"
|
|
909
939
|
);
|
|
910
940
|
}
|
|
911
|
-
const mandateIssuers =
|
|
941
|
+
const mandateIssuers = normalizeIssuers(
|
|
912
942
|
raw.trust?.mandateIssuers,
|
|
913
943
|
"authorization.ap2.trust.mandateIssuers"
|
|
914
944
|
);
|
|
915
|
-
const checkoutIssuers =
|
|
945
|
+
const checkoutIssuers = normalizeIssuers(
|
|
916
946
|
raw.trust?.checkoutIssuers,
|
|
917
947
|
"authorization.ap2.trust.checkoutIssuers"
|
|
918
948
|
);
|
|
@@ -929,7 +959,7 @@ function normaliseAp2(raw) {
|
|
|
929
959
|
replay: { path: raw.replay.path }
|
|
930
960
|
};
|
|
931
961
|
}
|
|
932
|
-
function
|
|
962
|
+
function normalizeIssuers(raw, path2) {
|
|
933
963
|
if (raw === void 0 || raw.length === 0) {
|
|
934
964
|
throw ap2Invalid(
|
|
935
965
|
path2,
|
|
@@ -1015,11 +1045,11 @@ function validateJwk(jwk, kid, path2) {
|
|
|
1015
1045
|
if (jwk["kid"] !== void 0 && value("kid") !== kid) {
|
|
1016
1046
|
fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
|
|
1017
1047
|
}
|
|
1018
|
-
const
|
|
1048
|
+
const normalized = {};
|
|
1019
1049
|
for (const member of AP2_JWK_MEMBERS) {
|
|
1020
|
-
if (jwk[member] !== void 0)
|
|
1050
|
+
if (jwk[member] !== void 0) normalized[member] = value(member);
|
|
1021
1051
|
}
|
|
1022
|
-
return
|
|
1052
|
+
return normalized;
|
|
1023
1053
|
}
|
|
1024
1054
|
function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
|
|
1025
1055
|
if (replayPath === ":memory:") return;
|
|
@@ -1033,7 +1063,7 @@ function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
|
|
|
1033
1063
|
);
|
|
1034
1064
|
}
|
|
1035
1065
|
}
|
|
1036
|
-
function
|
|
1066
|
+
function normalizeResourceAuthorization(id, entry, pricing, ap2) {
|
|
1037
1067
|
const required = entry.authorization?.required;
|
|
1038
1068
|
if (required === void 0) return void 0;
|
|
1039
1069
|
const path2 = `resources.${id}.authorization.required`;
|
|
@@ -1073,7 +1103,7 @@ function normaliseResourceAuthorization(id, entry, pricing, ap2) {
|
|
|
1073
1103
|
function acpInvalid(path2, message, extra = {}) {
|
|
1074
1104
|
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
|
|
1075
1105
|
}
|
|
1076
|
-
function
|
|
1106
|
+
function normalizeAcp(raw) {
|
|
1077
1107
|
const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
|
|
1078
1108
|
if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
|
|
1079
1109
|
return { enabled: false, mountPath };
|
|
@@ -1103,9 +1133,8 @@ function normaliseAcp(raw) {
|
|
|
1103
1133
|
{ min: ACP_RETENTION_HOURS }
|
|
1104
1134
|
)
|
|
1105
1135
|
},
|
|
1106
|
-
checkout: { operations:
|
|
1107
|
-
// Built key by key
|
|
1108
|
-
// absent from the discovery document, not appear there as `undefined`.
|
|
1136
|
+
checkout: { operations: normalizeAcpOperations(raw.checkout) },
|
|
1137
|
+
// Built key by key so absent metadata stays absent rather than `undefined`
|
|
1109
1138
|
...discovery !== void 0 ? {
|
|
1110
1139
|
discovery: {
|
|
1111
1140
|
...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
|
|
@@ -1116,7 +1145,7 @@ function normaliseAcp(raw) {
|
|
|
1116
1145
|
} : {}
|
|
1117
1146
|
};
|
|
1118
1147
|
}
|
|
1119
|
-
function
|
|
1148
|
+
function normalizeAcpOperations(checkout) {
|
|
1120
1149
|
const configured = checkout?.operations ?? {};
|
|
1121
1150
|
for (const key of Object.keys(configured)) {
|
|
1122
1151
|
if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
|
|
@@ -1184,13 +1213,15 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
1184
1213
|
const schema = resource.inputSchema;
|
|
1185
1214
|
if (schema === void 0) return;
|
|
1186
1215
|
const keys = ACP_OPERATION_INPUT_KEYS[operation];
|
|
1216
|
+
const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
|
|
1187
1217
|
const properties = schema["properties"];
|
|
1188
|
-
if (schema["additionalProperties"] === false &&
|
|
1189
|
-
for (const key of keys) {
|
|
1218
|
+
if (schema["additionalProperties"] === false && isRecord(properties)) {
|
|
1219
|
+
for (const key of [...keys, ...optionalKeys]) {
|
|
1190
1220
|
if (!Object.hasOwn(properties, key)) {
|
|
1221
|
+
const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
|
|
1191
1222
|
throw acpInvalid(
|
|
1192
1223
|
path2,
|
|
1193
|
-
`Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter
|
|
1224
|
+
`Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter ${when} for this operation`,
|
|
1194
1225
|
{ resourceId: resource.id }
|
|
1195
1226
|
);
|
|
1196
1227
|
}
|
|
@@ -1202,17 +1233,17 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
1202
1233
|
if (typeof entry === "string" && !keys.includes(entry)) {
|
|
1203
1234
|
throw acpInvalid(
|
|
1204
1235
|
path2,
|
|
1205
|
-
`Resource "${resource.id}" implements ACP operation "${operation}" but its input schema requires "${entry}", which that operation
|
|
1236
|
+
`Resource "${resource.id}" implements ACP operation "${operation}" but its input schema requires "${entry}", which that operation does not always supply (it always sends: ${keys.join(", ")})`,
|
|
1206
1237
|
{ resourceId: resource.id }
|
|
1207
1238
|
);
|
|
1208
1239
|
}
|
|
1209
1240
|
}
|
|
1210
1241
|
}
|
|
1211
1242
|
}
|
|
1212
|
-
function
|
|
1243
|
+
function normalizeFacilitator(raw) {
|
|
1213
1244
|
return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
|
|
1214
1245
|
}
|
|
1215
|
-
function
|
|
1246
|
+
function normalizeMpp(raw) {
|
|
1216
1247
|
if (raw === void 0) return void 0;
|
|
1217
1248
|
const network = raw.network ?? MPP_DEFAULT_NETWORK;
|
|
1218
1249
|
if (!isMppNetwork(network)) {
|
|
@@ -1239,7 +1270,7 @@ function normaliseMpp(raw) {
|
|
|
1239
1270
|
{ min: 1 }
|
|
1240
1271
|
)
|
|
1241
1272
|
} : {},
|
|
1242
|
-
facilitator:
|
|
1273
|
+
facilitator: normalizeFacilitator(raw.facilitator),
|
|
1243
1274
|
...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
|
|
1244
1275
|
...raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
1245
1276
|
allowUnauthenticatedFacilitator: toBoolean(
|
|
@@ -1276,7 +1307,7 @@ function normaliseMpp(raw) {
|
|
|
1276
1307
|
});
|
|
1277
1308
|
return mpp;
|
|
1278
1309
|
}
|
|
1279
|
-
function
|
|
1310
|
+
function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
|
|
1280
1311
|
if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
|
|
1281
1312
|
const inputProperties = entry.input?.["properties"];
|
|
1282
1313
|
if (inputProperties && typeof inputProperties === "object") {
|
|
@@ -1342,6 +1373,7 @@ function normaliseResource(id, entry, protocols, x402, mpp, ap2) {
|
|
|
1342
1373
|
);
|
|
1343
1374
|
}
|
|
1344
1375
|
validateBackendUrl(id, entry.backend.url);
|
|
1376
|
+
validateBackendHeaders(id, entry.backend.headers);
|
|
1345
1377
|
const pathScope = validateInputBindings(id, entry.backend, entry.input);
|
|
1346
1378
|
validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
|
|
1347
1379
|
if (entry.pricing.type === "dynamic") {
|
|
@@ -1392,7 +1424,7 @@ function normaliseResource(id, entry, protocols, x402, mpp, ap2) {
|
|
|
1392
1424
|
}
|
|
1393
1425
|
}
|
|
1394
1426
|
const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
|
|
1395
|
-
const authorization =
|
|
1427
|
+
const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
|
|
1396
1428
|
return {
|
|
1397
1429
|
id,
|
|
1398
1430
|
name: entry.name,
|
|
@@ -1422,11 +1454,11 @@ function defaultClosedObjectSchema(schema) {
|
|
|
1422
1454
|
const result = { ...schema };
|
|
1423
1455
|
if (isObjectSchema) {
|
|
1424
1456
|
const properties = schema["properties"];
|
|
1425
|
-
if (
|
|
1457
|
+
if (isRecord(properties)) {
|
|
1426
1458
|
result["properties"] = Object.fromEntries(
|
|
1427
1459
|
Object.entries(properties).map(([key, sub]) => [
|
|
1428
1460
|
key,
|
|
1429
|
-
|
|
1461
|
+
isRecord(sub) ? defaultClosedObjectSchema(sub) : sub
|
|
1430
1462
|
])
|
|
1431
1463
|
);
|
|
1432
1464
|
}
|
|
@@ -1435,18 +1467,15 @@ function defaultClosedObjectSchema(schema) {
|
|
|
1435
1467
|
}
|
|
1436
1468
|
}
|
|
1437
1469
|
const items = schema["items"];
|
|
1438
|
-
if (
|
|
1470
|
+
if (isRecord(items)) {
|
|
1439
1471
|
result["items"] = defaultClosedObjectSchema(items);
|
|
1440
1472
|
}
|
|
1441
1473
|
const additional = schema["additionalProperties"];
|
|
1442
|
-
if (
|
|
1474
|
+
if (isRecord(additional)) {
|
|
1443
1475
|
result["additionalProperties"] = defaultClosedObjectSchema(additional);
|
|
1444
1476
|
}
|
|
1445
1477
|
return result;
|
|
1446
1478
|
}
|
|
1447
|
-
function isPlainObject2(value) {
|
|
1448
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1449
|
-
}
|
|
1450
1479
|
function excludesObjectType(schema) {
|
|
1451
1480
|
const type = schema["type"];
|
|
1452
1481
|
if (type === void 0 || type === "object") return false;
|
|
@@ -1471,7 +1500,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1471
1500
|
const requiredRaw = schema["required"];
|
|
1472
1501
|
const closed = !Object.hasOwn(schema, "additionalProperties") ? true : schema["additionalProperties"] === false;
|
|
1473
1502
|
if (Array.isArray(requiredRaw) && closed && isObjectSchemaNode(schema)) {
|
|
1474
|
-
const declared =
|
|
1503
|
+
const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
1475
1504
|
const undeclared = requiredRaw.filter(
|
|
1476
1505
|
(name) => typeof name === "string" && !Object.hasOwn(declared, name)
|
|
1477
1506
|
);
|
|
@@ -1484,17 +1513,17 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1484
1513
|
}
|
|
1485
1514
|
}
|
|
1486
1515
|
const properties = schema["properties"];
|
|
1487
|
-
if (
|
|
1516
|
+
if (isRecord(properties)) {
|
|
1488
1517
|
for (const [key, sub] of Object.entries(properties)) {
|
|
1489
|
-
if (
|
|
1518
|
+
if (isRecord(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
|
|
1490
1519
|
}
|
|
1491
1520
|
}
|
|
1492
1521
|
const additional = schema["additionalProperties"];
|
|
1493
|
-
if (
|
|
1522
|
+
if (isRecord(additional)) {
|
|
1494
1523
|
validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
|
|
1495
1524
|
}
|
|
1496
1525
|
const items = schema["items"];
|
|
1497
|
-
if (
|
|
1526
|
+
if (isRecord(items)) {
|
|
1498
1527
|
validateResourceSchemaKeywords(id, `${path2}.items`, items);
|
|
1499
1528
|
} else if (Array.isArray(items)) {
|
|
1500
1529
|
console.warn(
|
|
@@ -1559,7 +1588,7 @@ function validateInputBindings(id, backend, input) {
|
|
|
1559
1588
|
);
|
|
1560
1589
|
}
|
|
1561
1590
|
const seen = /* @__PURE__ */ new Map();
|
|
1562
|
-
const properties =
|
|
1591
|
+
const properties = isRecord(input?.["properties"]) ? input["properties"] : {};
|
|
1563
1592
|
const required = new Set(
|
|
1564
1593
|
Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
|
|
1565
1594
|
);
|
|
@@ -1582,7 +1611,7 @@ function validateInputBindings(id, backend, input) {
|
|
|
1582
1611
|
);
|
|
1583
1612
|
}
|
|
1584
1613
|
const declared = properties[property];
|
|
1585
|
-
if (location !== "body" &&
|
|
1614
|
+
if (location !== "body" && isRecord(declared) && !isObjectSchemaNode(declared)) {
|
|
1586
1615
|
fail(
|
|
1587
1616
|
`binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
|
|
1588
1617
|
{ location, property }
|
|
@@ -1598,7 +1627,7 @@ function validateInputBindings(id, backend, input) {
|
|
|
1598
1627
|
if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
|
|
1599
1628
|
const group = properties[bindings.path];
|
|
1600
1629
|
return {
|
|
1601
|
-
schema:
|
|
1630
|
+
schema: isRecord(group) ? group : void 0,
|
|
1602
1631
|
where: `input.properties.${bindings.path}`
|
|
1603
1632
|
};
|
|
1604
1633
|
}
|
|
@@ -1610,7 +1639,7 @@ function validatePathParametersDeclared(id, url, input, where) {
|
|
|
1610
1639
|
if (stray !== void 0) {
|
|
1611
1640
|
throw new CommerceError(
|
|
1612
1641
|
"CONFIG_INVALID",
|
|
1613
|
-
`Resource "${id}" has backend.url containing "${stray}", which is not a valid path parameter (allowed characters: A-Z a-z 0-9 _ . -). It would
|
|
1642
|
+
`Resource "${id}" has backend.url containing "${stray}", which is not a valid path parameter (allowed characters: A-Z a-z 0-9 _ . -). It would reach the backend as literal text, so a paid resource would take payment for a request the backend cannot serve`,
|
|
1614
1643
|
{ details: { path: `resources.${id}.backend.url`, resourceId: id, token: stray } }
|
|
1615
1644
|
);
|
|
1616
1645
|
}
|
|
@@ -1628,13 +1657,13 @@ function validatePathParametersDeclared(id, url, input, where) {
|
|
|
1628
1657
|
if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
|
|
1629
1658
|
throw new CommerceError(
|
|
1630
1659
|
"CONFIG_INVALID",
|
|
1631
|
-
`Resource "${id}" has a backend.url parameter before the end of the host
|
|
1660
|
+
`Resource "${id}" has a backend.url parameter before the end of the host. A caller-supplied value would choose which host the gateway calls, which is request forgery with the gateway's own network position. Parameters are supported inside the path and query only`,
|
|
1632
1661
|
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
1633
1662
|
);
|
|
1634
1663
|
}
|
|
1635
1664
|
}
|
|
1636
1665
|
const propertiesRaw = input?.["properties"];
|
|
1637
|
-
const properties =
|
|
1666
|
+
const properties = isRecord(propertiesRaw) ? propertiesRaw : {};
|
|
1638
1667
|
const requiredRaw = input?.["required"];
|
|
1639
1668
|
const required = new Set(
|
|
1640
1669
|
Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : []
|
|
@@ -1644,14 +1673,14 @@ function validatePathParametersDeclared(id, url, input, where) {
|
|
|
1644
1673
|
if (!Object.hasOwn(properties, param)) {
|
|
1645
1674
|
throw new CommerceError(
|
|
1646
1675
|
"CONFIG_INVALID",
|
|
1647
|
-
`Resource "${id}" has backend.url path parameter "{${param}}" which is not declared in ${where} - the caller has no way to supply it, so every call would
|
|
1676
|
+
`Resource "${id}" has backend.url path parameter "{${param}}" which is not declared in ${where} - the caller has no way to supply it, so every call would be refused`,
|
|
1648
1677
|
{ details: { path: path2, resourceId: id, param } }
|
|
1649
1678
|
);
|
|
1650
1679
|
}
|
|
1651
1680
|
if (!required.has(param)) {
|
|
1652
1681
|
throw new CommerceError(
|
|
1653
1682
|
"CONFIG_INVALID",
|
|
1654
|
-
`Resource "${id}" has backend.url path parameter "{${param}}" declared in ${where} but not listed in its "required" - a
|
|
1683
|
+
`Resource "${id}" has backend.url path parameter "{${param}}" declared in ${where} but not listed in its "required" - a call that omits it would be refused`,
|
|
1655
1684
|
{ details: { path: path2, resourceId: id, param } }
|
|
1656
1685
|
);
|
|
1657
1686
|
}
|
|
@@ -1664,7 +1693,7 @@ function validateBackendUrl(id, url) {
|
|
|
1664
1693
|
} catch {
|
|
1665
1694
|
throw new CommerceError(
|
|
1666
1695
|
"CONFIG_INVALID",
|
|
1667
|
-
`Resource "${id}" has an invalid backend.url
|
|
1696
|
+
`Resource "${id}" has an invalid backend.url: must be an absolute http:// or https:// URL`,
|
|
1668
1697
|
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
1669
1698
|
);
|
|
1670
1699
|
}
|
|
@@ -1676,6 +1705,19 @@ function validateBackendUrl(id, url) {
|
|
|
1676
1705
|
);
|
|
1677
1706
|
}
|
|
1678
1707
|
}
|
|
1708
|
+
function validateBackendHeaders(id, headers) {
|
|
1709
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
1710
|
+
try {
|
|
1711
|
+
new Headers([[name, value]]);
|
|
1712
|
+
} catch {
|
|
1713
|
+
throw new CommerceError(
|
|
1714
|
+
"CONFIG_INVALID",
|
|
1715
|
+
`Resource "${id}" has backend header "${name}" with an illegal name or value (a name must be an HTTP token; a value must not contain NUL, a character above U+00FF, or a line break except at either end, which fetch trims)`,
|
|
1716
|
+
{ details: { path: `resources.${id}.backend.headers.${name}`, resourceId: id } }
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1679
1721
|
function validateAddress(path2, value) {
|
|
1680
1722
|
if (!ADDRESS_PATTERN.test(value)) {
|
|
1681
1723
|
throw new CommerceError(
|
|
@@ -1694,20 +1736,21 @@ function validateAddress(path2, value) {
|
|
|
1694
1736
|
);
|
|
1695
1737
|
}
|
|
1696
1738
|
}
|
|
1697
|
-
var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN,
|
|
1739
|
+
var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_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, MppSchema, 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;
|
|
1698
1740
|
var init_schema = __esm({
|
|
1699
1741
|
"src/config/schema.ts"() {
|
|
1700
1742
|
init_constants();
|
|
1701
|
-
init_execution();
|
|
1702
1743
|
init_core();
|
|
1744
|
+
init_execution();
|
|
1745
|
+
init_is_record();
|
|
1703
1746
|
init_constants2();
|
|
1704
1747
|
init_guardrails();
|
|
1705
1748
|
init_constants3();
|
|
1749
|
+
init_constants4();
|
|
1706
1750
|
init_env();
|
|
1707
1751
|
SUPPORTED_CONFIG_VERSION = 1;
|
|
1708
1752
|
ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
|
|
1709
1753
|
ZERO_ADDRESS_PATTERN = /^0x0{40}$/i;
|
|
1710
|
-
MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
|
|
1711
1754
|
NumberOrString = z.union([z.number(), z.string()]);
|
|
1712
1755
|
BooleanOrString = z.union([z.boolean(), z.string()]);
|
|
1713
1756
|
MerchantSchema = z.object({
|
|
@@ -1718,14 +1761,14 @@ var init_schema = __esm({
|
|
|
1718
1761
|
ServerSchema = z.object({
|
|
1719
1762
|
port: NumberOrString,
|
|
1720
1763
|
host: z.string().min(1),
|
|
1721
|
-
|
|
1764
|
+
// Shared secret gating /api/receipts and /api/events. Without it both
|
|
1765
|
+
// routes 404.
|
|
1722
1766
|
adminToken: z.string().min(1).optional(),
|
|
1723
|
-
|
|
1724
|
-
//
|
|
1725
|
-
// `Origin` header, so `"*"` and a trailing slash
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
// check. Reject those two shapes with a pointer instead.
|
|
1767
|
+
// Browser origins allowed to call the gateway; none by default. A request
|
|
1768
|
+
// with any other `Origin` gets 403 on every route. Entries match the
|
|
1769
|
+
// `Origin` header exactly (ignoring case), so `"*"` and a trailing slash
|
|
1770
|
+
// would match nothing: a silent lockout that an operator might "fix" by
|
|
1771
|
+
// disabling the check. Both are refused with a pointer.
|
|
1729
1772
|
allowedOrigins: z.array(
|
|
1730
1773
|
z.string().min(1).refine((origin) => origin !== "*", {
|
|
1731
1774
|
message: `wildcard "*" is not supported - allowedOrigins entries are matched literally against the browser's Origin header, so "*" would match nothing. List each scheme://host[:port] explicitly.`
|
|
@@ -1744,16 +1787,14 @@ var init_schema = __esm({
|
|
|
1744
1787
|
"/health",
|
|
1745
1788
|
"/ready",
|
|
1746
1789
|
"/.well-known/agent-commerce",
|
|
1747
|
-
// Fixed by the A2A specification
|
|
1748
|
-
// a configurable mount's to claim.
|
|
1790
|
+
// Fixed by the A2A specification and served by the adapter
|
|
1749
1791
|
"/.well-known/agent-card.json",
|
|
1750
|
-
//
|
|
1792
|
+
// Fixed by the ACP specification
|
|
1751
1793
|
ACP_WELL_KNOWN_PATH,
|
|
1752
1794
|
"/api/resources",
|
|
1753
1795
|
"/api/resources/:id/invoke",
|
|
1754
1796
|
"/api/receipts",
|
|
1755
|
-
"/api/events"
|
|
1756
|
-
"/api/events/stream"
|
|
1797
|
+
"/api/events"
|
|
1757
1798
|
];
|
|
1758
1799
|
MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
|
|
1759
1800
|
message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
|
|
@@ -1785,16 +1826,14 @@ var init_schema = __esm({
|
|
|
1785
1826
|
enabled: BooleanOrString,
|
|
1786
1827
|
mountPath: MountPathSchema
|
|
1787
1828
|
}).strict(),
|
|
1788
|
-
// Optional
|
|
1789
|
-
// existing config predating the adapter stays valid.
|
|
1829
|
+
// Optional; A2A is off unless configured
|
|
1790
1830
|
a2a: z.object({
|
|
1791
1831
|
enabled: BooleanOrString,
|
|
1792
1832
|
mountPath: MountPathSchema.optional()
|
|
1793
1833
|
}).strict().optional(),
|
|
1794
|
-
//
|
|
1795
|
-
//
|
|
1796
|
-
//
|
|
1797
|
-
// message naming the piece it is missing.
|
|
1834
|
+
// Optional and off by default. The business pass requires the sub-blocks
|
|
1835
|
+
// only when ACP is enabled, so a disabled placeholder stays valid and an
|
|
1836
|
+
// enabled block gets a message naming what it lacks.
|
|
1798
1837
|
acp: z.object({
|
|
1799
1838
|
enabled: BooleanOrString,
|
|
1800
1839
|
mountPath: MountPathSchema.optional(),
|
|
@@ -1850,7 +1889,7 @@ var init_schema = __esm({
|
|
|
1850
1889
|
z.object({ type: z.literal("none") }).strict(),
|
|
1851
1890
|
z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
|
|
1852
1891
|
// Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
|
|
1853
|
-
// which signs a fresh JWT per request
|
|
1892
|
+
// which signs a fresh JWT per request.
|
|
1854
1893
|
z.object({
|
|
1855
1894
|
type: z.literal("cdp"),
|
|
1856
1895
|
apiKeyId: z.string().min(1),
|
|
@@ -1865,10 +1904,8 @@ var init_schema = __esm({
|
|
|
1865
1904
|
z.object({
|
|
1866
1905
|
mode: z.literal("remote"),
|
|
1867
1906
|
url: z.string().min(1),
|
|
1868
|
-
// Absent
|
|
1869
|
-
//
|
|
1870
|
-
// combination is refused outright, so the default can never quietly
|
|
1871
|
-
// become an unauthenticated production facilitator.
|
|
1907
|
+
// Absent auth sends no credential and normalizes to `{ type: 'none' }`,
|
|
1908
|
+
// which on mainnet requires `allowUnauthenticatedFacilitator`
|
|
1872
1909
|
auth: FacilitatorAuthSchema.optional()
|
|
1873
1910
|
}).strict()
|
|
1874
1911
|
]);
|
|
@@ -1883,9 +1920,9 @@ var init_schema = __esm({
|
|
|
1883
1920
|
payTo: z.string().min(1),
|
|
1884
1921
|
maxTimeoutSeconds: NumberOrString,
|
|
1885
1922
|
facilitator: FacilitatorSchema,
|
|
1886
|
-
|
|
1923
|
+
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
1887
1924
|
allowMainnet: BooleanOrString.optional(),
|
|
1888
|
-
|
|
1925
|
+
// Accepts credential-free facilitator access on mainnet. Never defaulted
|
|
1889
1926
|
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
1890
1927
|
}).strict();
|
|
1891
1928
|
MppSchema = z.object({
|
|
@@ -1900,9 +1937,9 @@ var init_schema = __esm({
|
|
|
1900
1937
|
challengeSecret: z.string().min(1),
|
|
1901
1938
|
challengeTtlSeconds: NumberOrString.optional(),
|
|
1902
1939
|
facilitator: FacilitatorSchema,
|
|
1903
|
-
// Real funds. Never defaulted
|
|
1940
|
+
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
1904
1941
|
allowMainnet: BooleanOrString.optional(),
|
|
1905
|
-
// Accepts
|
|
1942
|
+
// Accepts credential-free facilitator access on mainnet. Never defaulted
|
|
1906
1943
|
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
1907
1944
|
}).strict();
|
|
1908
1945
|
PaymentsSchema = z.object({
|
|
@@ -1915,11 +1952,8 @@ var init_schema = __esm({
|
|
|
1915
1952
|
}).strict();
|
|
1916
1953
|
Ap2IssuerSchema = z.object({
|
|
1917
1954
|
issuer: z.string().min(1),
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
* merchant would verify here, and there is no value worth guessing for
|
|
1921
|
-
* something that decides that.
|
|
1922
|
-
*/
|
|
1955
|
+
// Required, not defaulted: without it a mandate minted for another
|
|
1956
|
+
// merchant would verify here
|
|
1923
1957
|
audience: z.string().min(1),
|
|
1924
1958
|
keys: z.array(Ap2KeySchema).min(1)
|
|
1925
1959
|
}).strict();
|
|
@@ -2001,9 +2035,8 @@ async function loadConfig(options = {}) {
|
|
|
2001
2035
|
`Configuration file "${configPath}" is not valid YAML: ${reason}`,
|
|
2002
2036
|
{
|
|
2003
2037
|
details: { path: configPath, ...details },
|
|
2004
|
-
//
|
|
2005
|
-
//
|
|
2006
|
-
// `.message` carries the source excerpt (see below).
|
|
2038
|
+
// Kept for programmatic callers. Never print it: its `.message` quotes
|
|
2039
|
+
// the source line (see describeYamlError).
|
|
2007
2040
|
cause: error
|
|
2008
2041
|
}
|
|
2009
2042
|
);
|
|
@@ -2074,9 +2107,9 @@ init_core();
|
|
|
2074
2107
|
function defaultDemoSteps() {
|
|
2075
2108
|
return [
|
|
2076
2109
|
{
|
|
2077
|
-
name: "Start local chain
|
|
2110
|
+
name: "Start the local stack: chain, merchant, gateway and dashboard (docker compose)",
|
|
2078
2111
|
command: "docker",
|
|
2079
|
-
args: ["compose", "up", "--build", "
|
|
2112
|
+
args: ["compose", "up", "--build", "--detach", "--wait"]
|
|
2080
2113
|
},
|
|
2081
2114
|
{ name: "Run the deterministic buyer agent", command: "npm", args: ["run", "demo:agent"] }
|
|
2082
2115
|
];
|
|
@@ -2084,7 +2117,7 @@ function defaultDemoSteps() {
|
|
|
2084
2117
|
async function runDemo(io, deps = {}) {
|
|
2085
2118
|
const steps = deps.steps ?? defaultDemoSteps();
|
|
2086
2119
|
const run = deps.run ?? execCommand;
|
|
2087
|
-
io.stdout("agent-commerce demo
|
|
2120
|
+
io.stdout("agent-commerce demo: starting the local stack, then running the buyer agent");
|
|
2088
2121
|
for (const step of steps) {
|
|
2089
2122
|
io.stdout("");
|
|
2090
2123
|
io.stdout(`==> ${step.name}`);
|
|
@@ -2100,13 +2133,6 @@ async function runDemo(io, deps = {}) {
|
|
|
2100
2133
|
io.stderr(` ${step.command} ${step.args.join(" ")}`);
|
|
2101
2134
|
return 1;
|
|
2102
2135
|
}
|
|
2103
|
-
if (step.healthCheck !== void 0) {
|
|
2104
|
-
const healthy = await step.healthCheck();
|
|
2105
|
-
if (!healthy) {
|
|
2106
|
-
io.stderr(`FAIL "${step.name}" completed but its health check did not pass.`);
|
|
2107
|
-
return 1;
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
2136
|
io.stdout(`OK ${step.name}`);
|
|
2111
2137
|
}
|
|
2112
2138
|
io.stdout("");
|
|
@@ -2137,8 +2163,7 @@ init_constants();
|
|
|
2137
2163
|
// src/authorization/ap2/descriptor.ts
|
|
2138
2164
|
init_constants();
|
|
2139
2165
|
var AP2_UNSUPPORTED = [
|
|
2140
|
-
// Mandate kinds. Open mandates
|
|
2141
|
-
// evaluates, so accepting one would tell a buyer their limits were checked.
|
|
2166
|
+
// Mandate kinds. Open mandates are refused: see AP2_CHECKOUT_MANDATE_VCT
|
|
2142
2167
|
"autonomous mode",
|
|
2143
2168
|
"open checkout mandates (mandate.checkout.open.1)",
|
|
2144
2169
|
"intent mandates",
|
|
@@ -2158,8 +2183,8 @@ var AP2_UNSUPPORTED = [
|
|
|
2158
2183
|
];
|
|
2159
2184
|
|
|
2160
2185
|
// src/cli/commands/doctor.ts
|
|
2161
|
-
init_execution();
|
|
2162
2186
|
init_core();
|
|
2187
|
+
init_execution();
|
|
2163
2188
|
init_constants2();
|
|
2164
2189
|
init_networks();
|
|
2165
2190
|
|
|
@@ -2183,13 +2208,12 @@ var A2A_UNSUPPORTED_METHODS = [
|
|
|
2183
2208
|
|
|
2184
2209
|
// src/protocols/a2a/descriptor.ts
|
|
2185
2210
|
var A2A_UNSUPPORTED = [
|
|
2186
|
-
// Methods,
|
|
2187
|
-
// rejects by name, so the descriptor cannot promise less than it refuses.
|
|
2211
|
+
// Methods, as the protocol names them; the transport refuses the same list
|
|
2188
2212
|
...A2A_UNSUPPORTED_METHODS,
|
|
2189
|
-
// Transports other than the one binding served
|
|
2213
|
+
// Transports other than the one binding served
|
|
2190
2214
|
"HTTP+JSON/REST binding",
|
|
2191
2215
|
"gRPC binding",
|
|
2192
|
-
//
|
|
2216
|
+
// Behaviors
|
|
2193
2217
|
"SSE",
|
|
2194
2218
|
"long-running task persistence",
|
|
2195
2219
|
"task resumption",
|
|
@@ -2221,15 +2245,15 @@ var ACP_UNSUPPORTED = [
|
|
|
2221
2245
|
"standalone orders service",
|
|
2222
2246
|
"delegate_payment",
|
|
2223
2247
|
"delegate_authentication",
|
|
2224
|
-
// Bindings and delivery directions
|
|
2248
|
+
// Bindings and delivery directions
|
|
2225
2249
|
"ACP MCP transport binding",
|
|
2226
2250
|
"webhooks",
|
|
2227
2251
|
"outbound merchant-to-agent delivery",
|
|
2228
2252
|
"ACP client role",
|
|
2229
|
-
// Request authentication beyond the bearer token
|
|
2253
|
+
// Request authentication beyond the bearer token
|
|
2230
2254
|
"Signature header verification",
|
|
2231
2255
|
"Timestamp replay-window verification",
|
|
2232
|
-
// Extensions and
|
|
2256
|
+
// Extensions, versioning and payment handlers
|
|
2233
2257
|
"discount extension",
|
|
2234
2258
|
"general ACP extension framework",
|
|
2235
2259
|
"multiple ACP API versions",
|
|
@@ -2238,7 +2262,7 @@ var ACP_UNSUPPORTED = [
|
|
|
2238
2262
|
|
|
2239
2263
|
// src/storage/receipts/store.ts
|
|
2240
2264
|
init_core();
|
|
2241
|
-
var PACKAGE_VERSION = "1.
|
|
2265
|
+
var PACKAGE_VERSION = "1.6.1" ;
|
|
2242
2266
|
|
|
2243
2267
|
// src/storage/sqlite.ts
|
|
2244
2268
|
init_core();
|
|
@@ -2246,10 +2270,7 @@ function openSqliteDatabase(options) {
|
|
|
2246
2270
|
const { path: path2, label, logger } = options;
|
|
2247
2271
|
const isFileBacked = path2 !== ":memory:";
|
|
2248
2272
|
if (isFileBacked) {
|
|
2249
|
-
|
|
2250
|
-
if (dir !== "" && dir !== "." && !existsSync(dir)) {
|
|
2251
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2252
|
-
}
|
|
2273
|
+
mkdirSync(dirname(path2), { recursive: true, mode: 448 });
|
|
2253
2274
|
}
|
|
2254
2275
|
if (isFileBacked && existsSync(path2)) {
|
|
2255
2276
|
try {
|
|
@@ -2321,8 +2342,8 @@ function receiptToRow(receipt) {
|
|
|
2321
2342
|
payment_json: receipt.payment !== void 0 ? JSON.stringify(redact(receipt.payment)) : null,
|
|
2322
2343
|
delivered_at: receipt.deliveredAt,
|
|
2323
2344
|
backend_status: receipt.backendStatus,
|
|
2324
|
-
duration_ms: receipt.durationMs
|
|
2325
|
-
protocol: receipt.protocol
|
|
2345
|
+
duration_ms: receipt.durationMs ?? null,
|
|
2346
|
+
protocol: receipt.protocol ?? null,
|
|
2326
2347
|
metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
|
|
2327
2348
|
authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
|
|
2328
2349
|
};
|
|
@@ -2349,12 +2370,12 @@ function eventToRow(event) {
|
|
|
2349
2370
|
id: event.id,
|
|
2350
2371
|
type: event.type,
|
|
2351
2372
|
request_id: event.requestId,
|
|
2352
|
-
resource_id: event.resourceId
|
|
2373
|
+
resource_id: event.resourceId ?? null,
|
|
2353
2374
|
at: event.at,
|
|
2354
|
-
adapter: event.adapter
|
|
2355
|
-
payment_provider: event.paymentProvider
|
|
2356
|
-
duration_ms: event.durationMs
|
|
2357
|
-
status: event.status
|
|
2375
|
+
adapter: event.adapter ?? null,
|
|
2376
|
+
payment_provider: event.paymentProvider ?? null,
|
|
2377
|
+
duration_ms: event.durationMs ?? null,
|
|
2378
|
+
status: event.status ?? null,
|
|
2358
2379
|
data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null
|
|
2359
2380
|
};
|
|
2360
2381
|
}
|
|
@@ -2458,7 +2479,7 @@ var MIGRATIONS = [
|
|
|
2458
2479
|
}
|
|
2459
2480
|
}
|
|
2460
2481
|
];
|
|
2461
|
-
var SCHEMA_VERSION = MIGRATIONS
|
|
2482
|
+
var SCHEMA_VERSION = MIGRATIONS.at(-1)?.version ?? 0;
|
|
2462
2483
|
function migrate(db) {
|
|
2463
2484
|
const current = db.pragma("user_version", { simple: true });
|
|
2464
2485
|
for (const migration of MIGRATIONS) {
|
|
@@ -2521,10 +2542,8 @@ function createSqliteReceiptStore(options) {
|
|
|
2521
2542
|
);
|
|
2522
2543
|
const insertPaymentAttemptStmt = db.prepare(
|
|
2523
2544
|
`INSERT INTO payment_attempts (id, request_id, resource_id, provider, replay_key, status, amount, currency, payer, payee, external_reference, rejection_reason, created_at, updated_at)
|
|
2524
|
-
VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)
|
|
2525
|
-
|
|
2526
|
-
const getPaymentAttemptByIdStmt = db.prepare(
|
|
2527
|
-
"SELECT * FROM payment_attempts WHERE id = ?"
|
|
2545
|
+
VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)
|
|
2546
|
+
RETURNING *`
|
|
2528
2547
|
);
|
|
2529
2548
|
const updatePaymentAttemptStmt = db.prepare(
|
|
2530
2549
|
`UPDATE payment_attempts SET status = @status,
|
|
@@ -2575,8 +2594,9 @@ function createSqliteReceiptStore(options) {
|
|
|
2575
2594
|
async reservePaymentAttempt(reservation) {
|
|
2576
2595
|
const now = clock.nowIso();
|
|
2577
2596
|
const id = ids.next("attempt");
|
|
2597
|
+
let row;
|
|
2578
2598
|
try {
|
|
2579
|
-
insertPaymentAttemptStmt.
|
|
2599
|
+
row = insertPaymentAttemptStmt.get({
|
|
2580
2600
|
id,
|
|
2581
2601
|
request_id: reservation.requestId,
|
|
2582
2602
|
resource_id: reservation.resourceId,
|
|
@@ -2596,19 +2616,12 @@ function createSqliteReceiptStore(options) {
|
|
|
2596
2616
|
if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
|
|
2597
2617
|
throw new CommerceError(
|
|
2598
2618
|
"PAYMENT_REPLAYED",
|
|
2599
|
-
`Payment
|
|
2619
|
+
`Payment authorization has already been used (replayKey=${reservation.replayKey})`,
|
|
2600
2620
|
{ requestId: reservation.requestId, resourceId: reservation.resourceId }
|
|
2601
2621
|
);
|
|
2602
2622
|
}
|
|
2603
2623
|
throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");
|
|
2604
2624
|
}
|
|
2605
|
-
const row = getPaymentAttemptByIdStmt.get(id);
|
|
2606
|
-
if (row === void 0) {
|
|
2607
|
-
throw new CommerceError(
|
|
2608
|
-
"STORAGE_ERROR",
|
|
2609
|
-
"Payment attempt vanished immediately after insert"
|
|
2610
|
-
);
|
|
2611
|
-
}
|
|
2612
2625
|
return rowToPaymentAttempt(row);
|
|
2613
2626
|
},
|
|
2614
2627
|
async updatePaymentAttempt(update) {
|
|
@@ -2646,11 +2659,11 @@ function createSqliteReceiptStore(options) {
|
|
|
2646
2659
|
const rows = listOptions.requestId !== void 0 ? listReceiptsByRequestStmt.all(listOptions.requestId, limit) : listReceiptsStmt.all(limit);
|
|
2647
2660
|
return rows.map(rowToReceipt);
|
|
2648
2661
|
},
|
|
2649
|
-
|
|
2662
|
+
// Exact total, not bounded by the listReceipts clamp (see ReceiptStore.countReceipts)
|
|
2650
2663
|
async countReceipts() {
|
|
2651
2664
|
return countReceiptsStmt.get()?.count ?? 0;
|
|
2652
2665
|
},
|
|
2653
|
-
|
|
2666
|
+
// Exact count of non-2xx receipts (see ReceiptStore.countUndeliveredReceipts)
|
|
2654
2667
|
async countUndeliveredReceipts() {
|
|
2655
2668
|
return countUndeliveredReceiptsStmt.get()?.count ?? 0;
|
|
2656
2669
|
},
|
|
@@ -2730,35 +2743,28 @@ function createSqliteReceiptStore(options) {
|
|
|
2730
2743
|
}
|
|
2731
2744
|
|
|
2732
2745
|
// src/cli/lib/config-client.ts
|
|
2733
|
-
|
|
2734
|
-
|
|
2746
|
+
init_core();
|
|
2747
|
+
async function importConfig() {
|
|
2735
2748
|
try {
|
|
2736
|
-
|
|
2749
|
+
return await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2737
2750
|
} catch (err) {
|
|
2738
|
-
throw new
|
|
2751
|
+
throw new CommerceError(
|
|
2752
|
+
"INTERNAL_ERROR",
|
|
2739
2753
|
`the configuration module could not be loaded: ${err instanceof Error ? err.message : String(err)}`
|
|
2740
2754
|
);
|
|
2741
2755
|
}
|
|
2742
|
-
|
|
2756
|
+
}
|
|
2757
|
+
async function loadConfigDynamic(options = {}) {
|
|
2758
|
+
return (await importConfig()).loadConfig(options);
|
|
2743
2759
|
}
|
|
2744
2760
|
async function parseConfigDynamic(raw, env = process.env) {
|
|
2745
|
-
|
|
2746
|
-
try {
|
|
2747
|
-
mod = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2748
|
-
} catch (err) {
|
|
2749
|
-
throw new Error(
|
|
2750
|
-
`the configuration module could not be loaded: ${err instanceof Error ? err.message : String(err)}`
|
|
2751
|
-
);
|
|
2752
|
-
}
|
|
2753
|
-
return mod.parseConfig(raw, env);
|
|
2761
|
+
return (await importConfig()).parseConfig(raw, env);
|
|
2754
2762
|
}
|
|
2755
2763
|
|
|
2756
2764
|
// src/cli/lib/http.ts
|
|
2757
2765
|
async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
|
|
2758
|
-
const controller = new AbortController();
|
|
2759
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2760
2766
|
try {
|
|
2761
|
-
const res = await fetchImpl(url, { signal:
|
|
2767
|
+
const res = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2762
2768
|
let body;
|
|
2763
2769
|
try {
|
|
2764
2770
|
body = await res.json();
|
|
@@ -2772,8 +2778,6 @@ async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
|
|
|
2772
2778
|
status: 0,
|
|
2773
2779
|
error: err instanceof Error ? err.message : "unknown fetch error"
|
|
2774
2780
|
};
|
|
2775
|
-
} finally {
|
|
2776
|
-
clearTimeout(timer);
|
|
2777
2781
|
}
|
|
2778
2782
|
}
|
|
2779
2783
|
|
|
@@ -2818,9 +2822,8 @@ function buildResourcesMap(answers) {
|
|
|
2818
2822
|
resources.report = {
|
|
2819
2823
|
name: "Get market report",
|
|
2820
2824
|
description: "Deterministic demo premium market report.",
|
|
2821
|
-
// No caller-supplied fields
|
|
2822
|
-
// closed schema
|
|
2823
|
-
// never be read as "anything goes".
|
|
2825
|
+
// No caller-supplied fields. Config loads a missing `input` as this same
|
|
2826
|
+
// closed schema; writing it out shows that in the generated file
|
|
2824
2827
|
input: {
|
|
2825
2828
|
type: "object",
|
|
2826
2829
|
properties: {},
|
|
@@ -2871,25 +2874,22 @@ function buildInitConfigObject(answers) {
|
|
|
2871
2874
|
}
|
|
2872
2875
|
var HEADER = `# Generated by \`agent-commerce init\`.
|
|
2873
2876
|
#
|
|
2874
|
-
#
|
|
2875
|
-
#
|
|
2876
|
-
# never use them outside a local chain
|
|
2877
|
-
# not
|
|
2878
|
-
# \`npm run chain:deploy\` and
|
|
2879
|
-
# MockUSDC address
|
|
2880
|
-
#
|
|
2881
|
-
# this at a funded network.
|
|
2877
|
+
# With x402 enabled, the facilitator signerPrivateKey is Anvil's public dev
|
|
2878
|
+
# account #0 and the default payTo is account #1.
|
|
2879
|
+
# LOCAL DEVELOPMENT ONLY - DO NOT FUND: never use them outside a local chain.
|
|
2880
|
+
# payments.x402.asset is a placeholder, not a real contract. Before running
|
|
2881
|
+
# against the local chain, run \`npm run chain:deploy\` and set it to the
|
|
2882
|
+
# MockUSDC address in \`.deploy/local.json\`. Before pointing this at a funded
|
|
2883
|
+
# network, set \`payments.x402.payTo\` to your own settlement address.
|
|
2882
2884
|
#
|
|
2883
|
-
# server.host is loopback (127.0.0.1)
|
|
2884
|
-
#
|
|
2885
|
+
# server.host is loopback (127.0.0.1): binding every interface should be a
|
|
2886
|
+
# deliberate edit.
|
|
2885
2887
|
#
|
|
2886
|
-
# server.adminToken
|
|
2887
|
-
#
|
|
2888
|
-
#
|
|
2889
|
-
#
|
|
2890
|
-
#
|
|
2891
|
-
# hashes \u2014 return 404 rather than serving openly. To enable them, uncomment
|
|
2892
|
-
# and set:
|
|
2888
|
+
# server.adminToken and server.allowedOrigins are left unset, so no secret
|
|
2889
|
+
# lands in a file you might commit. Without a token the operator routes
|
|
2890
|
+
# (/api/receipts, /api/events) return 404. They serve the merchant's commerce
|
|
2891
|
+
# ledger: payer addresses, amounts and settlement hashes. To enable them, add
|
|
2892
|
+
# these keys to the server block below:
|
|
2893
2893
|
# server:
|
|
2894
2894
|
# adminToken: \${ADMIN_TOKEN}
|
|
2895
2895
|
# allowedOrigins:
|
|
@@ -2901,18 +2901,30 @@ function renderInitConfigYaml(answers) {
|
|
|
2901
2901
|
return `${HEADER}
|
|
2902
2902
|
${body}`;
|
|
2903
2903
|
}
|
|
2904
|
+
|
|
2905
|
+
// src/payments/x402/local-chain/manifest.ts
|
|
2906
|
+
init_core();
|
|
2904
2907
|
var LOCAL_CHAIN_MANIFEST_PATH = ".deploy/local.json";
|
|
2905
|
-
var
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2908
|
+
var nonEmpty = z.string().min(1, "must be a non-empty string");
|
|
2909
|
+
var keyedAccount = z.object({ address: z.string(), privateKey: z.string(), note: z.string() });
|
|
2910
|
+
var MANIFEST_SCHEMA = z.object({
|
|
2911
|
+
chainId: z.number(),
|
|
2912
|
+
rpcUrl: nonEmpty,
|
|
2913
|
+
hostRpcUrl: nonEmpty.optional(),
|
|
2914
|
+
asset: nonEmpty,
|
|
2915
|
+
assetName: nonEmpty,
|
|
2916
|
+
assetVersion: nonEmpty,
|
|
2917
|
+
assetDecimals: z.number(),
|
|
2918
|
+
merchant: z.object({ address: z.string(), privateKeyLabel: z.string() }),
|
|
2919
|
+
buyer: keyedAccount,
|
|
2920
|
+
facilitator: keyedAccount,
|
|
2921
|
+
buyerInitialBalance: nonEmpty
|
|
2922
|
+
});
|
|
2912
2923
|
function readLocalChainManifest(cwd = process.cwd()) {
|
|
2913
2924
|
const path2 = join(cwd, LOCAL_CHAIN_MANIFEST_PATH);
|
|
2914
2925
|
if (!existsSync(path2)) {
|
|
2915
|
-
throw new
|
|
2926
|
+
throw new CommerceError(
|
|
2927
|
+
"CONFIG_INVALID",
|
|
2916
2928
|
`Local chain manifest not found at "${path2}". Run "npm run chain:start" then "npm run chain:deploy" first.`
|
|
2917
2929
|
);
|
|
2918
2930
|
}
|
|
@@ -2920,56 +2932,30 @@ function readLocalChainManifest(cwd = process.cwd()) {
|
|
|
2920
2932
|
try {
|
|
2921
2933
|
raw = readFileSync(path2, "utf8");
|
|
2922
2934
|
} catch (cause) {
|
|
2923
|
-
throw new
|
|
2935
|
+
throw new CommerceError("CONFIG_INVALID", `Could not read local chain manifest at "${path2}".`, {
|
|
2936
|
+
cause
|
|
2937
|
+
});
|
|
2924
2938
|
}
|
|
2925
2939
|
let parsed;
|
|
2926
2940
|
try {
|
|
2927
2941
|
parsed = JSON.parse(raw);
|
|
2928
2942
|
} catch (cause) {
|
|
2929
|
-
throw new
|
|
2943
|
+
throw new CommerceError(
|
|
2944
|
+
"CONFIG_INVALID",
|
|
2930
2945
|
`Local chain manifest at "${path2}" is not valid JSON. Re-run "npm run chain:deploy" to regenerate it.`,
|
|
2931
2946
|
{ cause }
|
|
2932
2947
|
);
|
|
2933
2948
|
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
throw new
|
|
2939
|
-
|
|
2949
|
+
const result = MANIFEST_SCHEMA.safeParse(parsed);
|
|
2950
|
+
if (!result.success) {
|
|
2951
|
+
const issue = result.error.issues[0];
|
|
2952
|
+
const where = issue?.path.length ? `${issue.path.join(".")}: ` : "";
|
|
2953
|
+
throw new CommerceError(
|
|
2954
|
+
"CONFIG_INVALID",
|
|
2955
|
+
`Local chain manifest at "${path2}" is invalid: ${where}${issue?.message ?? "unexpected shape"}. Re-run "npm run chain:deploy".`
|
|
2940
2956
|
);
|
|
2941
|
-
};
|
|
2942
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2943
|
-
return fail("expected a JSON object");
|
|
2944
|
-
}
|
|
2945
|
-
const record = value;
|
|
2946
|
-
if (typeof record.chainId !== "number") {
|
|
2947
|
-
return fail('"chainId" must be a number');
|
|
2948
|
-
}
|
|
2949
|
-
if (typeof record.assetDecimals !== "number") {
|
|
2950
|
-
return fail('"assetDecimals" must be a number');
|
|
2951
|
-
}
|
|
2952
|
-
for (const field of REQUIRED_TOP_LEVEL_STRING_FIELDS) {
|
|
2953
|
-
if (typeof record[field] !== "string" || record[field] === "") {
|
|
2954
|
-
return fail(`"${field}" must be a non-empty string`);
|
|
2955
|
-
}
|
|
2956
2957
|
}
|
|
2957
|
-
|
|
2958
|
-
if (typeof record.hostRpcUrl !== "string" || record.hostRpcUrl === "") {
|
|
2959
|
-
return fail('"hostRpcUrl" must be a non-empty string when present');
|
|
2960
|
-
}
|
|
2961
|
-
}
|
|
2962
|
-
const merchant = record.merchant;
|
|
2963
|
-
if (typeof merchant !== "object" || merchant === null || typeof merchant.address !== "string" || typeof merchant.privateKeyLabel !== "string") {
|
|
2964
|
-
return fail('"merchant" must be { address: string, privateKeyLabel: string }');
|
|
2965
|
-
}
|
|
2966
|
-
for (const key of ["buyer", "facilitator"]) {
|
|
2967
|
-
const account = record[key];
|
|
2968
|
-
if (typeof account !== "object" || account === null || typeof account.address !== "string" || typeof account.privateKey !== "string" || typeof account.note !== "string") {
|
|
2969
|
-
return fail(`"${key}" must be { address: string, privateKey: string, note: string }`);
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
return record;
|
|
2958
|
+
return result.data;
|
|
2973
2959
|
}
|
|
2974
2960
|
|
|
2975
2961
|
// src/cli/lib/manifest-env.ts
|
|
@@ -3007,12 +2993,63 @@ function maskMiddle(value, keep = 6) {
|
|
|
3007
2993
|
if (value.length <= keep * 2) return value.length === 0 ? value : "\u2026";
|
|
3008
2994
|
return `${value.slice(0, keep)}\u2026${value.slice(-4)}`;
|
|
3009
2995
|
}
|
|
3010
|
-
|
|
2996
|
+
|
|
2997
|
+
// package.json
|
|
2998
|
+
var package_default = {
|
|
2999
|
+
dependencies: {
|
|
3000
|
+
"@clack/prompts": "1.7.0",
|
|
3001
|
+
"@scalar/openapi-parser": "0.29.0",
|
|
3002
|
+
ajv: "8.20.0",
|
|
3003
|
+
"ajv-formats": "3.0.1",
|
|
3004
|
+
"better-sqlite3": "13.0.3",
|
|
3005
|
+
commander: "15.0.0",
|
|
3006
|
+
fastify: "5.12.5",
|
|
3007
|
+
picocolors: "1.1.1",
|
|
3008
|
+
pino: "10.3.1",
|
|
3009
|
+
yaml: "2.9.0",
|
|
3010
|
+
zod: "3.25.76"
|
|
3011
|
+
},
|
|
3012
|
+
peerDependencies: {
|
|
3013
|
+
"@coinbase/x402": "2.1.0",
|
|
3014
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
3015
|
+
"@sd-jwt/core": "0.20.1",
|
|
3016
|
+
"@x402/core": "2.23.0",
|
|
3017
|
+
"@x402/evm": "2.23.0",
|
|
3018
|
+
canonicalize: "5.0.0",
|
|
3019
|
+
jose: "6.2.12",
|
|
3020
|
+
mppx: "0.10.1",
|
|
3021
|
+
viem: "2.55.18"
|
|
3022
|
+
}};
|
|
3023
|
+
|
|
3024
|
+
// src/cli/lib/versions.ts
|
|
3025
|
+
var PINS = [
|
|
3026
|
+
["@modelcontextprotocol/sdk", "protocols/mcp"],
|
|
3027
|
+
["ajv", "protocols/acp"],
|
|
3028
|
+
["ajv-formats", "protocols/acp"],
|
|
3029
|
+
["@x402/core", "payments/x402"],
|
|
3030
|
+
["@x402/evm", "payments/x402"],
|
|
3031
|
+
["@coinbase/x402", "payments/x402"],
|
|
3032
|
+
["viem", "payments/x402"],
|
|
3033
|
+
["mppx", "payments/mpp"],
|
|
3034
|
+
["jose", "authorization/ap2"],
|
|
3035
|
+
["@sd-jwt/core", "authorization/ap2"],
|
|
3036
|
+
["canonicalize", "authorization/ap2"],
|
|
3037
|
+
["@scalar/openapi-parser", "openapi"],
|
|
3038
|
+
["zod", "config"],
|
|
3039
|
+
["fastify", "gateway"],
|
|
3040
|
+
["pino", "gateway"],
|
|
3041
|
+
["better-sqlite3", "storage"]
|
|
3042
|
+
];
|
|
3011
3043
|
function readVersionReport() {
|
|
3012
|
-
const
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
}
|
|
3044
|
+
const declared = {
|
|
3045
|
+
...package_default.peerDependencies,
|
|
3046
|
+
...package_default.dependencies
|
|
3047
|
+
};
|
|
3048
|
+
const pinned = PINS.flatMap(([name, via]) => {
|
|
3049
|
+
const version = declared[name];
|
|
3050
|
+
return version === void 0 ? [] : [{ name, version, via }];
|
|
3051
|
+
});
|
|
3052
|
+
return { cliVersion: PACKAGE_VERSION, pinned };
|
|
3016
3053
|
}
|
|
3017
3054
|
|
|
3018
3055
|
// src/cli/commands/doctor.ts
|
|
@@ -3030,7 +3067,7 @@ function deriveGatewayUrl(explicit, config) {
|
|
|
3030
3067
|
function substitutePathParams(url) {
|
|
3031
3068
|
let filled = url;
|
|
3032
3069
|
for (const name of extractPathParameterNames(url)) {
|
|
3033
|
-
filled = filled.
|
|
3070
|
+
filled = filled.replaceAll(`{${name}}`, "demo-check");
|
|
3034
3071
|
}
|
|
3035
3072
|
return filled;
|
|
3036
3073
|
}
|
|
@@ -3072,6 +3109,21 @@ function findX402Mismatch(configured, live, payToField = "payTo") {
|
|
|
3072
3109
|
if (diffs.length === 0) return void 0;
|
|
3073
3110
|
return `${diffs.join("; ")}; the gateway may be running against an older deployment - restart it or re-run chain:deploy`;
|
|
3074
3111
|
}
|
|
3112
|
+
function describeSettlement(network, facilitator) {
|
|
3113
|
+
const profile = findNetworkProfile(network);
|
|
3114
|
+
const mode = profile ? resolveDeploymentMode(profile, facilitator.mode) : void 0;
|
|
3115
|
+
const where = profile === void 0 || mode === void 0 ? `unknown network ${network}` : mode === "local" ? `LOCAL dev chain (${network}, chain id shared with ${profile.displayName})` : `${describeDeploymentMode(mode)} on ${profile.displayName} (${network})`;
|
|
3116
|
+
const via = facilitator.mode === "local" ? "local (in-process)" : `remote (auth=${facilitator.auth.type})`;
|
|
3117
|
+
return { mode, where, via };
|
|
3118
|
+
}
|
|
3119
|
+
function mainnetSafetyCheck(name, payToField, facilitator) {
|
|
3120
|
+
const unauthenticated = facilitator.mode === "remote" && facilitator.auth.type === "none";
|
|
3121
|
+
return {
|
|
3122
|
+
name,
|
|
3123
|
+
status: unauthenticated ? "WARN" : "INFO",
|
|
3124
|
+
detail: unauthenticated ? `${describeDeploymentMode("mainnet")}: no credential is sent to the remote facilitator (accepted via allowUnauthenticatedFacilitator); its anonymous-access limits apply. Config load also requires allowMainnet, HTTPS, a non-development ${payToField} and the canonical asset. Payments fail closed.` : `${describeDeploymentMode("mainnet")}. Enforced at config load: explicit allowMainnet opt-in, remote facilitator over HTTPS with a credential, non-development ${payToField}, and the canonical asset for this network. Payments fail closed.`
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3075
3127
|
function describeIssuers(label, issuers) {
|
|
3076
3128
|
const described = issuers.map(
|
|
3077
3129
|
(entry) => `${entry.issuer} (${entry.keys.length} key${entry.keys.length === 1 ? "" : "s"})`
|
|
@@ -3086,72 +3138,26 @@ function isWritableFile(path2) {
|
|
|
3086
3138
|
return false;
|
|
3087
3139
|
}
|
|
3088
3140
|
}
|
|
3089
|
-
function
|
|
3090
|
-
const name = "AP2 replay store";
|
|
3141
|
+
function storeFileCheck(name, path2, risk, suffix = "") {
|
|
3091
3142
|
if (path2 === ":memory:") {
|
|
3092
3143
|
return {
|
|
3093
3144
|
name,
|
|
3094
3145
|
status: "WARN",
|
|
3095
|
-
detail:
|
|
3146
|
+
detail: `in-memory store: ${risk.inMemory}; use a file path in production${suffix}`
|
|
3096
3147
|
};
|
|
3097
3148
|
}
|
|
3098
3149
|
if (existsSync(path2)) {
|
|
3099
|
-
return isWritableFile(path2) ? { name, status: "PASS", detail: `writable at "${path2}"` } : {
|
|
3150
|
+
return isWritableFile(path2) ? { name, status: "PASS", detail: `writable at "${path2}"${suffix}` } : {
|
|
3100
3151
|
name,
|
|
3101
3152
|
status: "FAIL",
|
|
3102
|
-
detail: `"${path2}" exists but is not writable by this user
|
|
3103
|
-
};
|
|
3104
|
-
}
|
|
3105
|
-
const directory = dirname(path2);
|
|
3106
|
-
if (directory !== "" && directory !== "." && !existsSync(directory)) {
|
|
3107
|
-
return {
|
|
3108
|
-
name,
|
|
3109
|
-
status: "WARN",
|
|
3110
|
-
detail: `no store yet at "${path2}" and its directory does not exist - it is created when the gateway starts, provided that path is writable`
|
|
3153
|
+
detail: `"${path2}" exists but is not writable by this user, so ${risk.unwritable}${suffix}`
|
|
3111
3154
|
};
|
|
3112
3155
|
}
|
|
3113
|
-
return {
|
|
3114
|
-
name,
|
|
3115
|
-
status: "WARN",
|
|
3116
|
-
detail: `no store yet at "${path2}" - it is created the first time the gateway starts`
|
|
3117
|
-
};
|
|
3156
|
+
return { name, status: "WARN", detail: `${missingStoreDetail(path2)}${suffix}` };
|
|
3118
3157
|
}
|
|
3119
|
-
function
|
|
3120
|
-
const
|
|
3121
|
-
|
|
3122
|
-
return {
|
|
3123
|
-
name: "ACP idempotency",
|
|
3124
|
-
status: "FAIL",
|
|
3125
|
-
detail: `${retention} is below the 24h ACP minimum \u2014 a replayed key could run a checkout twice`
|
|
3126
|
-
};
|
|
3127
|
-
}
|
|
3128
|
-
if (idempotency.path === ":memory:") {
|
|
3129
|
-
return {
|
|
3130
|
-
name: "ACP idempotency",
|
|
3131
|
-
status: "WARN",
|
|
3132
|
-
detail: `in-memory store, ${retention} \u2014 replay protection is lost on every restart; use a file path in production`
|
|
3133
|
-
};
|
|
3134
|
-
}
|
|
3135
|
-
const directory = dirname(idempotency.path);
|
|
3136
|
-
if (existsSync(idempotency.path)) {
|
|
3137
|
-
return isWritableFile(idempotency.path) ? { name: "ACP idempotency", status: "PASS", detail: `writable, ${retention}` } : {
|
|
3138
|
-
name: "ACP idempotency",
|
|
3139
|
-
status: "FAIL",
|
|
3140
|
-
detail: `"${idempotency.path}" exists but is not writable by this user \u2014 the adapter cannot claim idempotency keys`
|
|
3141
|
-
};
|
|
3142
|
-
}
|
|
3143
|
-
if (directory !== "" && directory !== "." && !existsSync(directory)) {
|
|
3144
|
-
return {
|
|
3145
|
-
name: "ACP idempotency",
|
|
3146
|
-
status: "WARN",
|
|
3147
|
-
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`
|
|
3148
|
-
};
|
|
3149
|
-
}
|
|
3150
|
-
return {
|
|
3151
|
-
name: "ACP idempotency",
|
|
3152
|
-
status: "WARN",
|
|
3153
|
-
detail: `no store yet at "${idempotency.path}" \u2014 it is created the first time the gateway starts (${retention})`
|
|
3154
|
-
};
|
|
3158
|
+
function missingStoreDetail(path2) {
|
|
3159
|
+
const directory = dirname(path2);
|
|
3160
|
+
return directory !== "." && !existsSync(directory) ? `no store at "${path2}" and its directory does not exist. Both are created when the gateway starts, provided the path is writable` : `no store at "${path2}" yet. It is created the first time the gateway starts`;
|
|
3155
3161
|
}
|
|
3156
3162
|
function acpMappingCheck(config, operations) {
|
|
3157
3163
|
const byId = new Map(config.resources.map((resource) => [resource.id, resource]));
|
|
@@ -3195,11 +3201,11 @@ async function runDoctor(options, deps = {}) {
|
|
|
3195
3201
|
checks.push({
|
|
3196
3202
|
name: "Config",
|
|
3197
3203
|
status: "PASS",
|
|
3198
|
-
detail: `valid
|
|
3204
|
+
detail: `valid: ${config.resources.length} resource(s), merchant "${config.merchant.name}"${filledSuffix}`
|
|
3199
3205
|
});
|
|
3200
3206
|
} catch (err) {
|
|
3201
3207
|
const variable = isCommerceError(err) ? err.details?.["variable"] : void 0;
|
|
3202
|
-
const hint = !manifestFound && typeof variable === "string" && MANIFEST_FILLABLE_ENV_VAR_NAMES.has(variable) ?
|
|
3208
|
+
const hint = !manifestFound && typeof variable === "string" && MANIFEST_FILLABLE_ENV_VAR_NAMES.has(variable) ? `. Run "npm run chain:deploy": it writes ${LOCAL_CHAIN_MANIFEST_PATH}, which fills local X402_*/MERCHANT_WALLET placeholders automatically` : "";
|
|
3203
3209
|
checks.push({
|
|
3204
3210
|
name: "Config",
|
|
3205
3211
|
status: "FAIL",
|
|
@@ -3227,7 +3233,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3227
3233
|
);
|
|
3228
3234
|
}
|
|
3229
3235
|
if (config === void 0) {
|
|
3230
|
-
checks.push({ name: "Backend", status: "WARN", detail: "skipped
|
|
3236
|
+
checks.push({ name: "Backend", status: "WARN", detail: "skipped: config invalid" });
|
|
3231
3237
|
} else if (config.resources.length === 0) {
|
|
3232
3238
|
checks.push({ name: "Backend", status: "INFO", detail: "no resources configured" });
|
|
3233
3239
|
} else {
|
|
@@ -3250,7 +3256,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3250
3256
|
CHECK_TIMEOUT_MS
|
|
3251
3257
|
) : void 0;
|
|
3252
3258
|
if (config === void 0) {
|
|
3253
|
-
checks.push({ name: "Protocols", status: "WARN", detail: "skipped
|
|
3259
|
+
checks.push({ name: "Protocols", status: "WARN", detail: "skipped: config invalid" });
|
|
3254
3260
|
} else if (wellKnown === void 0 || !wellKnown.ok) {
|
|
3255
3261
|
checks.push({ name: "Protocols", status: "FAIL", detail: "well-known document unreachable" });
|
|
3256
3262
|
} else {
|
|
@@ -3263,7 +3269,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3263
3269
|
});
|
|
3264
3270
|
}
|
|
3265
3271
|
if (config === void 0) {
|
|
3266
|
-
checks.push({ name: "A2A", status: "WARN", detail: "skipped
|
|
3272
|
+
checks.push({ name: "A2A", status: "WARN", detail: "skipped: config invalid" });
|
|
3267
3273
|
} else if (!config.protocols.a2a.enabled) {
|
|
3268
3274
|
checks.push({ name: "A2A", status: "INFO", detail: "disabled" });
|
|
3269
3275
|
} else {
|
|
@@ -3279,7 +3285,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3279
3285
|
});
|
|
3280
3286
|
}
|
|
3281
3287
|
if (config === void 0) {
|
|
3282
|
-
checks.push({ name: "ACP", status: "WARN", detail: "skipped
|
|
3288
|
+
checks.push({ name: "ACP", status: "WARN", detail: "skipped: config invalid" });
|
|
3283
3289
|
} else if (!config.protocols.acp.enabled) {
|
|
3284
3290
|
checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
|
|
3285
3291
|
} else {
|
|
@@ -3294,7 +3300,17 @@ async function runDoctor(options, deps = {}) {
|
|
|
3294
3300
|
status: "PASS",
|
|
3295
3301
|
detail: `bearer token configured (${acp.auth.token.length} characters, not shown)`
|
|
3296
3302
|
});
|
|
3297
|
-
checks.push(
|
|
3303
|
+
checks.push(
|
|
3304
|
+
storeFileCheck(
|
|
3305
|
+
"ACP idempotency",
|
|
3306
|
+
acp.idempotency.path,
|
|
3307
|
+
{
|
|
3308
|
+
inMemory: "replay protection is lost on every restart",
|
|
3309
|
+
unwritable: "the adapter cannot claim idempotency keys"
|
|
3310
|
+
},
|
|
3311
|
+
` (retention ${acp.idempotency.retentionHours}h)`
|
|
3312
|
+
)
|
|
3313
|
+
);
|
|
3298
3314
|
checks.push(acpMappingCheck(config, acp.checkout.operations));
|
|
3299
3315
|
checks.push({
|
|
3300
3316
|
name: "ACP unsupported",
|
|
@@ -3304,7 +3320,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3304
3320
|
}
|
|
3305
3321
|
const ap2 = config?.authorization?.ap2;
|
|
3306
3322
|
if (config === void 0) {
|
|
3307
|
-
checks.push({ name: "AP2", status: "WARN", detail: "skipped
|
|
3323
|
+
checks.push({ name: "AP2", status: "WARN", detail: "skipped: config invalid" });
|
|
3308
3324
|
} else if (ap2 === void 0 || !ap2.enabled) {
|
|
3309
3325
|
checks.push({ name: "AP2", status: "INFO", detail: "disabled" });
|
|
3310
3326
|
} else {
|
|
@@ -3318,14 +3334,19 @@ async function runDoctor(options, deps = {}) {
|
|
|
3318
3334
|
status: "PASS",
|
|
3319
3335
|
detail: `${describeIssuers("mandate", ap2.trust.mandateIssuers)} \xB7 ${describeIssuers("checkout", ap2.trust.checkoutIssuers)}`
|
|
3320
3336
|
});
|
|
3321
|
-
checks.push(
|
|
3337
|
+
checks.push(
|
|
3338
|
+
storeFileCheck("AP2 replay store", ap2.replay.path, {
|
|
3339
|
+
inMemory: "every spent mandate is forgotten on restart, so one could authorize a second purchase",
|
|
3340
|
+
unwritable: "no mandate could be recorded as spent"
|
|
3341
|
+
})
|
|
3342
|
+
);
|
|
3322
3343
|
const gated = config.resources.filter(
|
|
3323
3344
|
(resource) => resource.authorization?.required.includes("ap2")
|
|
3324
3345
|
);
|
|
3325
3346
|
checks.push({
|
|
3326
3347
|
name: "AP2 resources",
|
|
3327
3348
|
status: gated.length > 0 ? "PASS" : "WARN",
|
|
3328
|
-
detail: gated.length > 0 ? gated.map((resource) => resource.id).join(", ") : "AP2 is enabled but no resource requires it
|
|
3349
|
+
detail: gated.length > 0 ? gated.map((resource) => resource.id).join(", ") : "AP2 is enabled but no resource requires it, so every purchase settles without a mandate"
|
|
3329
3350
|
});
|
|
3330
3351
|
checks.push({
|
|
3331
3352
|
name: "AP2 unsupported",
|
|
@@ -3337,23 +3358,20 @@ async function runDoctor(options, deps = {}) {
|
|
|
3337
3358
|
if (x402 === void 0 || !x402.enabled) {
|
|
3338
3359
|
checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
|
|
3339
3360
|
} else {
|
|
3340
|
-
const
|
|
3341
|
-
const
|
|
3342
|
-
const where = profile === void 0 || mode === void 0 ? `unknown network ${x402.network}` : mode === "local" ? `LOCAL dev chain (${x402.network}, chain id shared with ${profile.displayName})` : `${describeDeploymentMode(mode)} on ${profile.displayName} (${x402.network})`;
|
|
3343
|
-
const facilitatorDetail = x402.facilitator.mode === "local" ? "local" : `remote (auth=${x402.facilitator.auth.type})`;
|
|
3344
|
-
const summary = `x402 v2 (scheme=exact) enabled \u2014 ${where}, destination=${maskMiddle(x402.payTo)}, facilitator=${facilitatorDetail}`;
|
|
3361
|
+
const { mode, where, via } = describeSettlement(x402.network, x402.facilitator);
|
|
3362
|
+
const summary = `x402 v2 (scheme=exact) enabled - ${where}, destination=${maskMiddle(x402.payTo)}, facilitator=${via}`;
|
|
3345
3363
|
const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body) : void 0;
|
|
3346
3364
|
if (sameAddress2(x402.asset, PLACEHOLDER_ASSET_ADDRESS)) {
|
|
3347
3365
|
checks.push({
|
|
3348
3366
|
name: "Payments",
|
|
3349
3367
|
status: "WARN",
|
|
3350
|
-
detail: `${summary}, but the asset is still the init placeholder ${maskMiddle(PLACEHOLDER_ASSET_ADDRESS)}
|
|
3368
|
+
detail: `${summary}, but the asset is still the init placeholder ${maskMiddle(PLACEHOLDER_ASSET_ADDRESS)}. No token contract exists there, so every paid call will fail. Set payments.x402.asset (\${X402_ASSET} is filled from .deploy/local.json, which "npm run chain:deploy" writes)`
|
|
3351
3369
|
});
|
|
3352
3370
|
} else if (live === "disabled") {
|
|
3353
3371
|
checks.push({
|
|
3354
3372
|
name: "Payments",
|
|
3355
3373
|
status: "FAIL",
|
|
3356
|
-
detail: `${summary}, but the gateway at ${gatewayUrl} reports x402 disabled
|
|
3374
|
+
detail: `${summary}, but the gateway at ${gatewayUrl} reports x402 disabled, so it is running a different configuration`
|
|
3357
3375
|
});
|
|
3358
3376
|
} else if (live === void 0) {
|
|
3359
3377
|
checks.push({
|
|
@@ -3368,23 +3386,15 @@ async function runDoctor(options, deps = {}) {
|
|
|
3368
3386
|
);
|
|
3369
3387
|
}
|
|
3370
3388
|
if (mode === "mainnet") {
|
|
3371
|
-
|
|
3372
|
-
checks.push({
|
|
3373
|
-
name: "Mainnet safety",
|
|
3374
|
-
status: unauthenticated ? "WARN" : "INFO",
|
|
3375
|
-
detail: unauthenticated ? `${describeDeploymentMode("mainnet")} \u2014 settling through a facilitator that takes no credential, accepted via allowUnauthenticatedFacilitator. It sees every payment authorisation you handle, with no account or terms behind it. Everything else is enforced at config load: explicit allowMainnet opt-in, HTTPS, non-development payTo, canonical asset. Payments are fail-closed.` : `${describeDeploymentMode("mainnet")} \u2014 enforced at config load: explicit allowMainnet opt-in, remote facilitator over HTTPS with a credential, non-development payTo, and the canonical asset for this network. Payments are fail-closed.`
|
|
3376
|
-
});
|
|
3389
|
+
checks.push(mainnetSafetyCheck("Mainnet safety", "payTo", x402.facilitator));
|
|
3377
3390
|
}
|
|
3378
3391
|
}
|
|
3379
3392
|
const mpp = config?.payments.mpp;
|
|
3380
3393
|
if (mpp === void 0 || !mpp.enabled) {
|
|
3381
3394
|
checks.push({ name: "Payments (MPP)", status: "INFO", detail: "MPP not configured" });
|
|
3382
3395
|
} else {
|
|
3383
|
-
const
|
|
3384
|
-
const
|
|
3385
|
-
const where = profile === void 0 || mode === void 0 ? `unknown network ${mpp.network}` : mode === "local" ? `LOCAL dev chain (${mpp.network}, chain id shared with ${profile.displayName})` : `${describeDeploymentMode(mode)} on ${profile.displayName} (${mpp.network})`;
|
|
3386
|
-
const facilitatorDetail = mpp.facilitator.mode === "local" ? "local" : `remote (auth=${mpp.facilitator.auth.type})`;
|
|
3387
|
-
const summary = `MPP ${MPP_PROFILE.intent}/${MPP_PROFILE.method}/${MPP_PROFILE.credentialType} enabled - ${where}, asset=${MPP_PROFILE.assetSymbol} ${maskMiddle(mpp.asset)}, recipient=${maskMiddle(mpp.recipient)}, facilitator=${facilitatorDetail}, spec ${MPP_SPEC_DRAFTS.core}@${MPP_SPEC_COMMIT.slice(0, 7)}, mppx ${MPPX_VERSION}`;
|
|
3396
|
+
const { mode, where, via } = describeSettlement(mpp.network, mpp.facilitator);
|
|
3397
|
+
const summary = `MPP ${MPP_PROFILE.intent}/${MPP_PROFILE.method}/${MPP_PROFILE.credentialType} enabled - ${where}, asset=${MPP_PROFILE.assetSymbol} ${maskMiddle(mpp.asset)}, recipient=${maskMiddle(mpp.recipient)}, facilitator=${via}, spec ${MPP_SPEC_DRAFTS.core}@${MPP_SPEC_COMMIT.slice(0, 7)}, mppx ${MPPX_VERSION}`;
|
|
3388
3398
|
const configured = { asset: mpp.asset, network: mpp.network, payTo: mpp.recipient };
|
|
3389
3399
|
const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body, "mpp") : void 0;
|
|
3390
3400
|
if (live === "disabled") {
|
|
@@ -3405,23 +3415,24 @@ async function runDoctor(options, deps = {}) {
|
|
|
3405
3415
|
mismatch === void 0 ? { name: "Payments (MPP)", status: "PASS", detail: summary } : { name: "Payments (MPP)", status: "FAIL", detail: mismatch }
|
|
3406
3416
|
);
|
|
3407
3417
|
}
|
|
3418
|
+
if (mode === "mainnet") {
|
|
3419
|
+
checks.push(mainnetSafetyCheck("Mainnet safety (MPP)", "recipient", mpp.facilitator));
|
|
3420
|
+
}
|
|
3408
3421
|
}
|
|
3409
3422
|
if (config === void 0) {
|
|
3410
|
-
checks.push({ name: "Storage", status: "WARN", detail: "skipped
|
|
3423
|
+
checks.push({ name: "Storage", status: "WARN", detail: "skipped: config invalid" });
|
|
3411
3424
|
} else if (
|
|
3412
|
-
// Diagnose without creating: createSqliteReceiptStore
|
|
3413
|
-
//
|
|
3414
|
-
//
|
|
3415
|
-
//
|
|
3416
|
-
//
|
|
3417
|
-
// `:memory:` never exists on disk by definition, so it always falls
|
|
3418
|
-
// through to the real open/health/count branch below, same as today.
|
|
3425
|
+
// Diagnose without creating: createSqliteReceiptStore creates the file,
|
|
3426
|
+
// so opening a wrong path (a typo, or a container path read from the
|
|
3427
|
+
// host) would leave an empty database and report it healthy, hiding the
|
|
3428
|
+
// real one. `:memory:` never exists on disk, so it takes the open branch
|
|
3429
|
+
// below.
|
|
3419
3430
|
config.storage.receipts.path !== ":memory:" && !existsSync(config.storage.receipts.path)
|
|
3420
3431
|
) {
|
|
3421
3432
|
checks.push({
|
|
3422
3433
|
name: "Storage",
|
|
3423
3434
|
status: "WARN",
|
|
3424
|
-
detail:
|
|
3435
|
+
detail: `${missingStoreDetail(config.storage.receipts.path)}. If the gateway is already running, this path does not match the one it uses.`
|
|
3425
3436
|
});
|
|
3426
3437
|
} else {
|
|
3427
3438
|
let store;
|
|
@@ -3458,7 +3469,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3458
3469
|
await store?.close();
|
|
3459
3470
|
}
|
|
3460
3471
|
}
|
|
3461
|
-
if (wellKnown
|
|
3472
|
+
if (wellKnown?.ok) {
|
|
3462
3473
|
checks.push({
|
|
3463
3474
|
name: "Protocol versions",
|
|
3464
3475
|
status: "PASS",
|
|
@@ -3469,7 +3480,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3469
3480
|
checks.push({
|
|
3470
3481
|
name: "Protocol versions",
|
|
3471
3482
|
status: "INFO",
|
|
3472
|
-
detail: `gateway unreachable
|
|
3483
|
+
detail: `gateway unreachable; local pins: ${local.pinned.map((p) => `${p.name}@${p.version}`).join(", ")}`
|
|
3473
3484
|
});
|
|
3474
3485
|
}
|
|
3475
3486
|
const scored = checks.filter((c) => c.status !== "INFO");
|
|
@@ -3508,6 +3519,8 @@ init_core();
|
|
|
3508
3519
|
// src/openapi/discover.ts
|
|
3509
3520
|
init_errors();
|
|
3510
3521
|
init_execution();
|
|
3522
|
+
init_is_record();
|
|
3523
|
+
init_constants4();
|
|
3511
3524
|
|
|
3512
3525
|
// src/openapi/refs.ts
|
|
3513
3526
|
init_errors();
|
|
@@ -3548,7 +3561,7 @@ function dereference(document, node, stack = []) {
|
|
|
3548
3561
|
}
|
|
3549
3562
|
function resolvePointer(document, ref) {
|
|
3550
3563
|
const pointer = ref.slice(1);
|
|
3551
|
-
if (pointer === ""
|
|
3564
|
+
if (pointer === "") return document;
|
|
3552
3565
|
if (!pointer.startsWith("/")) {
|
|
3553
3566
|
throw new CommerceError(
|
|
3554
3567
|
"CONFIG_INVALID",
|
|
@@ -3558,12 +3571,22 @@ function resolvePointer(document, ref) {
|
|
|
3558
3571
|
}
|
|
3559
3572
|
let current = document;
|
|
3560
3573
|
for (const rawSegment of pointer.slice(1).split("/")) {
|
|
3561
|
-
|
|
3574
|
+
let decoded;
|
|
3575
|
+
try {
|
|
3576
|
+
decoded = decodeURIComponent(rawSegment);
|
|
3577
|
+
} catch {
|
|
3578
|
+
throw new CommerceError(
|
|
3579
|
+
"CONFIG_INVALID",
|
|
3580
|
+
`OpenAPI reference "${ref}" contains a malformed percent-escape`,
|
|
3581
|
+
{ details: { ref } }
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3584
|
+
const segment = decoded.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
3562
3585
|
if (Array.isArray(current)) {
|
|
3563
3586
|
const index = Number(segment);
|
|
3564
3587
|
current = Number.isInteger(index) ? current[index] : void 0;
|
|
3565
3588
|
} else if (typeof current === "object" && current !== null) {
|
|
3566
|
-
current = current[segment];
|
|
3589
|
+
current = Object.hasOwn(current, segment) ? current[segment] : void 0;
|
|
3567
3590
|
} else {
|
|
3568
3591
|
current = void 0;
|
|
3569
3592
|
}
|
|
@@ -3587,8 +3610,7 @@ var SUPPORTED_METHODS = {
|
|
|
3587
3610
|
delete: "DELETE"
|
|
3588
3611
|
};
|
|
3589
3612
|
var NON_OPERATION_KEYS = /* @__PURE__ */ new Set(["summary", "description", "servers", "parameters", "$ref"]);
|
|
3590
|
-
var
|
|
3591
|
-
var MAX_ID_LENGTH = 128;
|
|
3613
|
+
var ID_DISALLOWED = new RegExp(`[^${MCP_TOOL_NAME_CHARS}]+`, "g");
|
|
3592
3614
|
function discoverOperations(loaded, options = {}) {
|
|
3593
3615
|
const { document } = loaded;
|
|
3594
3616
|
const diagnostics = [];
|
|
@@ -3681,8 +3703,8 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3681
3703
|
...description !== void 0 ? { description } : {},
|
|
3682
3704
|
parameters: [
|
|
3683
3705
|
...toArray(pathItem["parameters"]),
|
|
3684
|
-
// Operation parameters last:
|
|
3685
|
-
// the OpenAPI override rule (same name
|
|
3706
|
+
// Operation parameters last: `mergeParameters` lets the later one win,
|
|
3707
|
+
// the OpenAPI override rule (same `name` and `in`)
|
|
3686
3708
|
...toArray(operation["parameters"])
|
|
3687
3709
|
],
|
|
3688
3710
|
...operation["requestBody"] !== void 0 ? { requestBody: operation["requestBody"] } : {},
|
|
@@ -3694,12 +3716,12 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3694
3716
|
return { operations, diagnostics };
|
|
3695
3717
|
}
|
|
3696
3718
|
function toResourceId(operationId, method, path2) {
|
|
3697
|
-
const fromOperationId = operationId === void 0 ? "" :
|
|
3719
|
+
const fromOperationId = operationId === void 0 ? "" : normalizeId(operationId);
|
|
3698
3720
|
if (fromOperationId !== "") return fromOperationId;
|
|
3699
|
-
return
|
|
3721
|
+
return normalizeId(`${method.toLowerCase()}_${path2}`);
|
|
3700
3722
|
}
|
|
3701
|
-
function
|
|
3702
|
-
return value.replace(
|
|
3723
|
+
function normalizeId(value) {
|
|
3724
|
+
return value.replace(ID_DISALLOWED, "_").replace(/_+/g, "_").replace(/^[_.-]+|[_.-]+$/g, "").slice(0, MCP_TOOL_NAME_MAX_LENGTH);
|
|
3703
3725
|
}
|
|
3704
3726
|
function selectServer(document, candidates, context, options, diagnostics) {
|
|
3705
3727
|
if (options.baseUrl !== void 0) return options.baseUrl;
|
|
@@ -3797,18 +3819,20 @@ function assertAbsoluteHttpUrl(value, label) {
|
|
|
3797
3819
|
);
|
|
3798
3820
|
}
|
|
3799
3821
|
}
|
|
3800
|
-
function isRecord(value) {
|
|
3801
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3802
|
-
}
|
|
3803
3822
|
function toArray(value) {
|
|
3804
3823
|
return Array.isArray(value) ? value : [];
|
|
3805
3824
|
}
|
|
3806
3825
|
|
|
3826
|
+
// src/openapi/draft.ts
|
|
3827
|
+
init_is_record();
|
|
3828
|
+
|
|
3807
3829
|
// src/openapi/request.ts
|
|
3808
3830
|
init_execution();
|
|
3831
|
+
init_is_record();
|
|
3809
3832
|
|
|
3810
3833
|
// src/openapi/schema.ts
|
|
3811
3834
|
init_errors();
|
|
3835
|
+
init_is_record();
|
|
3812
3836
|
var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
3813
3837
|
"object",
|
|
3814
3838
|
"string",
|
|
@@ -3839,7 +3863,7 @@ function convertSchema(document, node, stack = []) {
|
|
|
3839
3863
|
const dropped = /* @__PURE__ */ new Set();
|
|
3840
3864
|
try {
|
|
3841
3865
|
const schema = convertNode(document, node, stack, dropped);
|
|
3842
|
-
return
|
|
3866
|
+
return { supported: true, schema, dropped: [...dropped] };
|
|
3843
3867
|
} catch (error) {
|
|
3844
3868
|
if (error instanceof UnsupportedSchema) return { supported: false, reason: error.message };
|
|
3845
3869
|
if (isCommerceError(error)) return { supported: false, reason: error.message };
|
|
@@ -3852,11 +3876,8 @@ function convertNode(document, node, stack, dropped) {
|
|
|
3852
3876
|
if (node === true) return {};
|
|
3853
3877
|
if (node === false) throw new UnsupportedSchema("schema is `false`, which accepts no value");
|
|
3854
3878
|
const resolved = dereference(document, node, stack);
|
|
3855
|
-
const
|
|
3856
|
-
if (
|
|
3857
|
-
throw new UnsupportedSchema("schema node is not an object");
|
|
3858
|
-
}
|
|
3859
|
-
const schemaNode = source;
|
|
3879
|
+
const schemaNode = resolved.value;
|
|
3880
|
+
if (!isRecord(schemaNode)) throw new UnsupportedSchema("schema node is not an object");
|
|
3860
3881
|
if (Array.isArray(schemaNode["allOf"])) {
|
|
3861
3882
|
return mergeAllOf(document, schemaNode, resolved.stack, dropped);
|
|
3862
3883
|
}
|
|
@@ -3875,11 +3896,15 @@ function convertNode(document, node, stack, dropped) {
|
|
|
3875
3896
|
}
|
|
3876
3897
|
if (Array.isArray(schemaNode["enum"])) result["enum"] = [...schemaNode["enum"]];
|
|
3877
3898
|
const properties = schemaNode["properties"];
|
|
3878
|
-
if (
|
|
3899
|
+
if (isRecord(properties)) {
|
|
3879
3900
|
const converted = {};
|
|
3880
3901
|
for (const [name, sub] of Object.entries(properties)) {
|
|
3881
|
-
|
|
3882
|
-
|
|
3902
|
+
if (name === "__proto__") {
|
|
3903
|
+
throw new UnsupportedSchema(
|
|
3904
|
+
'schema declares a "__proto__" property, which cannot be imported'
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
converted[name] = convertNode(document, sub, resolved.stack, dropped);
|
|
3883
3908
|
}
|
|
3884
3909
|
result["properties"] = converted;
|
|
3885
3910
|
}
|
|
@@ -3892,17 +3917,15 @@ function convertNode(document, node, stack, dropped) {
|
|
|
3892
3917
|
if (typeof additional === "boolean") {
|
|
3893
3918
|
result["additionalProperties"] = additional;
|
|
3894
3919
|
} else if (additional !== void 0) {
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
} else if (isRecord2(properties)) {
|
|
3920
|
+
result["additionalProperties"] = convertNode(document, additional, resolved.stack, dropped);
|
|
3921
|
+
} else if (isRecord(properties)) {
|
|
3898
3922
|
result["additionalProperties"] = false;
|
|
3899
3923
|
}
|
|
3900
3924
|
const items = schemaNode["items"];
|
|
3901
3925
|
if (Array.isArray(items)) {
|
|
3902
3926
|
dropped.add("items (tuple form)");
|
|
3903
3927
|
} else if (items !== void 0) {
|
|
3904
|
-
|
|
3905
|
-
if (child !== void 0) result["items"] = child;
|
|
3928
|
+
result["items"] = convertNode(document, items, resolved.stack, dropped);
|
|
3906
3929
|
}
|
|
3907
3930
|
for (const keyword of Object.keys(schemaNode)) {
|
|
3908
3931
|
if (isCarriedKeyword(keyword) || ANNOTATION_KEYWORDS.has(keyword) || keyword.startsWith("x-")) {
|
|
@@ -3943,16 +3966,15 @@ function mergeAllOf(document, schema, stack, dropped) {
|
|
|
3943
3966
|
const parts = [...branches, ...Object.keys(siblings).length > 0 ? [siblings] : []];
|
|
3944
3967
|
for (const branch of parts) {
|
|
3945
3968
|
const converted = convertNode(document, branch, stack, dropped);
|
|
3946
|
-
if (converted === void 0) throw new UnsupportedSchema("allOf branch is empty");
|
|
3947
3969
|
const types = typeList2(converted["type"]);
|
|
3948
3970
|
if (types !== void 0 && !types.includes("object")) {
|
|
3949
3971
|
throw new UnsupportedSchema("allOf mixes object and non-object schemas");
|
|
3950
3972
|
}
|
|
3951
3973
|
const branchProperties = converted["properties"];
|
|
3952
|
-
if (
|
|
3974
|
+
if (isRecord(branchProperties)) {
|
|
3953
3975
|
for (const [name, sub] of Object.entries(branchProperties)) {
|
|
3954
3976
|
const existing = properties[name];
|
|
3955
|
-
if (existing !== void 0 &&
|
|
3977
|
+
if (existing !== void 0 && !isDeepStrictEqual(existing, sub)) {
|
|
3956
3978
|
throw new UnsupportedSchema(
|
|
3957
3979
|
`allOf branches declare property "${name}" differently, which cannot be merged`
|
|
3958
3980
|
);
|
|
@@ -3975,7 +3997,7 @@ function mergeAllOf(document, schema, stack, dropped) {
|
|
|
3975
3997
|
}
|
|
3976
3998
|
const additional = converted["additionalProperties"];
|
|
3977
3999
|
if (additional === true) open2 = true;
|
|
3978
|
-
else if (
|
|
4000
|
+
else if (isRecord(additional)) {
|
|
3979
4001
|
throw new UnsupportedSchema(
|
|
3980
4002
|
"allOf branch constrains additionalProperties, which cannot be merged"
|
|
3981
4003
|
);
|
|
@@ -3986,9 +4008,6 @@ function mergeAllOf(document, schema, stack, dropped) {
|
|
|
3986
4008
|
merged["additionalProperties"] = open2;
|
|
3987
4009
|
return merged;
|
|
3988
4010
|
}
|
|
3989
|
-
function isRecord2(value) {
|
|
3990
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3991
|
-
}
|
|
3992
4011
|
|
|
3993
4012
|
// src/openapi/request.ts
|
|
3994
4013
|
var PATH_GROUP = "path";
|
|
@@ -4042,16 +4061,10 @@ function mapRequest(loaded, candidate) {
|
|
|
4042
4061
|
warn("unsupported-optional-parameter", `Omitted optional ${label}: unknown location`);
|
|
4043
4062
|
continue;
|
|
4044
4063
|
}
|
|
4045
|
-
const
|
|
4046
|
-
if (unsupported !== void 0) {
|
|
4047
|
-
if (required2) return skip("unsupported-required-parameter", `${label} ${unsupported}`);
|
|
4048
|
-
warn("unsupported-optional-parameter", `Omitted optional ${label}: it ${unsupported}`);
|
|
4049
|
-
continue;
|
|
4050
|
-
}
|
|
4051
|
-
const converted = convertSchema(document, parameter["schema"]);
|
|
4064
|
+
const converted = convertParameter(document, parameter, location);
|
|
4052
4065
|
if (!converted.supported) {
|
|
4053
4066
|
if (required2) return skip("unsupported-required-parameter", `${label} ${converted.reason}`);
|
|
4054
|
-
warn("unsupported-optional-parameter", `Omitted optional ${label}: ${converted.reason}`);
|
|
4067
|
+
warn("unsupported-optional-parameter", `Omitted optional ${label}: it ${converted.reason}`);
|
|
4055
4068
|
continue;
|
|
4056
4069
|
}
|
|
4057
4070
|
for (const keyword of converted.dropped) dropped.add(keyword);
|
|
@@ -4120,38 +4133,43 @@ function mapRequest(loaded, candidate) {
|
|
|
4120
4133
|
function mergeParameters(document, parameters) {
|
|
4121
4134
|
const byIdentity = /* @__PURE__ */ new Map();
|
|
4122
4135
|
for (const raw of parameters) {
|
|
4123
|
-
const
|
|
4124
|
-
if (
|
|
4125
|
-
const parameter = resolved;
|
|
4136
|
+
const parameter = dereference(document, raw).value;
|
|
4137
|
+
if (!isRecord(parameter)) continue;
|
|
4126
4138
|
byIdentity.set(`${String(parameter["in"])}:${String(parameter["name"])}`, parameter);
|
|
4127
4139
|
}
|
|
4128
4140
|
return [...byIdentity.values()];
|
|
4129
4141
|
}
|
|
4130
|
-
function
|
|
4142
|
+
function convertParameter(document, parameter, location) {
|
|
4143
|
+
const unsupported = (reason) => ({ supported: false, reason });
|
|
4144
|
+
if (parameter["name"] === "__proto__") {
|
|
4145
|
+
return unsupported("has a name that cannot be an input key");
|
|
4146
|
+
}
|
|
4131
4147
|
if (parameter["content"] !== void 0) {
|
|
4132
|
-
return
|
|
4148
|
+
return unsupported(
|
|
4149
|
+
"uses the `content` form, whose media-type serialization the gateway does not perform"
|
|
4150
|
+
);
|
|
4133
4151
|
}
|
|
4134
4152
|
const style = parameter["style"];
|
|
4135
4153
|
if (location === "query" && typeof style === "string" && UNSUPPORTED_QUERY_STYLES.has(style)) {
|
|
4136
|
-
return `uses style "${style}", which the gateway does not serialize
|
|
4154
|
+
return unsupported(`uses style "${style}", which the gateway does not serialize`);
|
|
4137
4155
|
}
|
|
4138
4156
|
if (location === "path" && typeof style === "string" && style !== "simple") {
|
|
4139
|
-
return `uses style "${style}"; the gateway substitutes plain values only
|
|
4157
|
+
return unsupported(`uses style "${style}"; the gateway substitutes plain values only`);
|
|
4140
4158
|
}
|
|
4141
4159
|
const converted = convertSchema(document, parameter["schema"]);
|
|
4142
|
-
if (!converted.supported) return converted.reason;
|
|
4160
|
+
if (!converted.supported) return unsupported(`has an unusable schema: ${converted.reason}`);
|
|
4143
4161
|
if (!isPrimitiveSchema(converted.schema)) {
|
|
4144
|
-
return
|
|
4162
|
+
return unsupported(
|
|
4163
|
+
"is not a primitive; object and array parameters need serialization the gateway does not perform"
|
|
4164
|
+
);
|
|
4145
4165
|
}
|
|
4146
|
-
return
|
|
4166
|
+
return converted;
|
|
4147
4167
|
}
|
|
4148
4168
|
function resolveBody(document, candidate) {
|
|
4149
4169
|
if (candidate.requestBody === void 0) return { kind: "none" };
|
|
4150
4170
|
const resolvedBody = dereference(document, candidate.requestBody);
|
|
4151
|
-
if (typeof resolvedBody.value !== "object" || resolvedBody.value === null || Array.isArray(resolvedBody.value)) {
|
|
4152
|
-
return { kind: "none" };
|
|
4153
|
-
}
|
|
4154
4171
|
const requestBody = resolvedBody.value;
|
|
4172
|
+
if (!isRecord(requestBody)) return { kind: "none" };
|
|
4155
4173
|
const required = requestBody["required"] === true;
|
|
4156
4174
|
if (candidate.method === "GET" || candidate.method === "DELETE") {
|
|
4157
4175
|
return {
|
|
@@ -4161,7 +4179,7 @@ function resolveBody(document, candidate) {
|
|
|
4161
4179
|
};
|
|
4162
4180
|
}
|
|
4163
4181
|
const content = requestBody["content"];
|
|
4164
|
-
if (!
|
|
4182
|
+
if (!isRecord(content)) return { kind: "none" };
|
|
4165
4183
|
const mediaType = pickJsonMediaType(Object.keys(content));
|
|
4166
4184
|
if (mediaType === void 0) {
|
|
4167
4185
|
return {
|
|
@@ -4171,7 +4189,7 @@ function resolveBody(document, candidate) {
|
|
|
4171
4189
|
};
|
|
4172
4190
|
}
|
|
4173
4191
|
const media = dereference(document, content[mediaType], resolvedBody.stack).value;
|
|
4174
|
-
const schemaNode =
|
|
4192
|
+
const schemaNode = isRecord(media) ? media["schema"] : void 0;
|
|
4175
4193
|
if (schemaNode === void 0) {
|
|
4176
4194
|
return {
|
|
4177
4195
|
kind: "schema",
|
|
@@ -4192,10 +4210,10 @@ function resolveBody(document, candidate) {
|
|
|
4192
4210
|
};
|
|
4193
4211
|
}
|
|
4194
4212
|
function pickJsonMediaType(keys) {
|
|
4195
|
-
const
|
|
4196
|
-
const exact =
|
|
4213
|
+
const normalized = keys.map((key) => ({ key, type: key.split(";")[0]?.trim().toLowerCase() }));
|
|
4214
|
+
const exact = normalized.find((entry) => entry.type === "application/json");
|
|
4197
4215
|
if (exact !== void 0) return exact.key;
|
|
4198
|
-
return [...
|
|
4216
|
+
return [...normalized].sort((a, b) => (a.type ?? "").localeCompare(b.type ?? "")).find((entry) => entry.type?.startsWith("application/") && entry.type.endsWith("+json"))?.key;
|
|
4199
4217
|
}
|
|
4200
4218
|
function closedObject(properties, required) {
|
|
4201
4219
|
return {
|
|
@@ -4205,9 +4223,6 @@ function closedObject(properties, required) {
|
|
|
4205
4223
|
additionalProperties: false
|
|
4206
4224
|
};
|
|
4207
4225
|
}
|
|
4208
|
-
function isRecord3(value) {
|
|
4209
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4210
|
-
}
|
|
4211
4226
|
|
|
4212
4227
|
// src/openapi/draft.ts
|
|
4213
4228
|
function buildResourceDrafts(loaded, options = {}) {
|
|
@@ -4280,12 +4295,9 @@ function buildResourceDrafts(loaded, options = {}) {
|
|
|
4280
4295
|
...mapping.contentType !== void 0 ? { headers: { "Content-Type": mapping.contentType } } : {},
|
|
4281
4296
|
...Object.keys(mapping.inputBindings).length > 0 ? { inputBindings: mapping.inputBindings } : {}
|
|
4282
4297
|
},
|
|
4283
|
-
//
|
|
4284
|
-
// `pricing`/`expose` is what makes the draft fail config validation
|
|
4285
|
-
// until a human has decided.
|
|
4298
|
+
// Written only when the operator supplied it (see the file header)
|
|
4286
4299
|
...options.policy?.pricing !== void 0 ? { pricing: options.policy.pricing } : {},
|
|
4287
|
-
...options.policy?.expose !== void 0 ? { expose: [...options.policy.expose] } : {}
|
|
4288
|
-
...options.policy?.payments !== void 0 ? { payments: [...options.policy.payments] } : {}
|
|
4300
|
+
...options.policy?.expose !== void 0 ? { expose: [...options.policy.expose] } : {}
|
|
4289
4301
|
},
|
|
4290
4302
|
review
|
|
4291
4303
|
});
|
|
@@ -4317,18 +4329,18 @@ function declaresSecurity(candidate) {
|
|
|
4317
4329
|
}
|
|
4318
4330
|
function selectOutputSchema(document, candidate, diagnostics, dropped) {
|
|
4319
4331
|
const responses = candidate.responses;
|
|
4320
|
-
if (!
|
|
4332
|
+
if (!isRecord(responses)) return void 0;
|
|
4321
4333
|
const successes = Object.keys(responses).filter((status) => /^2\d\d$/.test(status)).sort((a, b) => rank(a) - rank(b));
|
|
4322
4334
|
if (successes.length === 0) return void 0;
|
|
4323
4335
|
const withBody = successes.flatMap((status) => {
|
|
4324
4336
|
const response = dereference(document, responses[status]);
|
|
4325
|
-
if (!
|
|
4337
|
+
if (!isRecord(response.value)) return [];
|
|
4326
4338
|
const content = response.value["content"];
|
|
4327
|
-
if (!
|
|
4339
|
+
if (!isRecord(content)) return [];
|
|
4328
4340
|
const mediaType = pickJsonMediaType(Object.keys(content));
|
|
4329
4341
|
if (mediaType === void 0) return [];
|
|
4330
4342
|
const media = dereference(document, content[mediaType], response.stack).value;
|
|
4331
|
-
return
|
|
4343
|
+
return isRecord(media) ? [{ status, media }] : [];
|
|
4332
4344
|
});
|
|
4333
4345
|
const chosen = withBody[0];
|
|
4334
4346
|
if (chosen === void 0) return void 0;
|
|
@@ -4373,7 +4385,8 @@ function renderResourcesYaml(result) {
|
|
|
4373
4385
|
const doc = new Document({ resources });
|
|
4374
4386
|
doc.commentBefore = [
|
|
4375
4387
|
" Generated by agent-commerce import openapi. Review before use.",
|
|
4376
|
-
|
|
4388
|
+
// win32.basename splits on both / and \
|
|
4389
|
+
` Source: ${win32.basename(result.sourcePath)} (OpenAPI ${result.version})`,
|
|
4377
4390
|
" Merge the resources below into config.yaml once pricing, exposure and",
|
|
4378
4391
|
" any backend authentication have been decided."
|
|
4379
4392
|
].join("\n");
|
|
@@ -4386,12 +4399,6 @@ function renderResourcesYaml(result) {
|
|
|
4386
4399
|
});
|
|
4387
4400
|
return doc.toString({ lineWidth: 0 });
|
|
4388
4401
|
}
|
|
4389
|
-
function basename(path2) {
|
|
4390
|
-
return path2.split(/[\\/]/).pop() ?? path2;
|
|
4391
|
-
}
|
|
4392
|
-
function isRecord4(value) {
|
|
4393
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4394
|
-
}
|
|
4395
4402
|
|
|
4396
4403
|
// src/openapi/load.ts
|
|
4397
4404
|
init_errors();
|
|
@@ -4472,7 +4479,7 @@ async function loadOpenApiDocument(sourcePath) {
|
|
|
4472
4479
|
}
|
|
4473
4480
|
const document = parsed;
|
|
4474
4481
|
const version = readVersion(document, sourcePath);
|
|
4475
|
-
|
|
4482
|
+
rejectUnsupportedReferences(document, sourcePath);
|
|
4476
4483
|
const result = await validate(document);
|
|
4477
4484
|
if (!result.valid) {
|
|
4478
4485
|
const first = result.errors?.[0];
|
|
@@ -4504,7 +4511,7 @@ function readVersion(document, sourcePath) {
|
|
|
4504
4511
|
}
|
|
4505
4512
|
return featureVersion;
|
|
4506
4513
|
}
|
|
4507
|
-
function
|
|
4514
|
+
function rejectUnsupportedReferences(document, sourcePath) {
|
|
4508
4515
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
4509
4516
|
const queue = [document];
|
|
4510
4517
|
while (queue.length > 0) {
|
|
@@ -4517,31 +4524,45 @@ function rejectExternalReferences(document, sourcePath) {
|
|
|
4517
4524
|
continue;
|
|
4518
4525
|
}
|
|
4519
4526
|
for (const [key, value] of Object.entries(node)) {
|
|
4520
|
-
if (key === "$ref" && typeof value === "string"
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4527
|
+
if (key === "$ref" && typeof value === "string") {
|
|
4528
|
+
if (!value.startsWith("#")) {
|
|
4529
|
+
throw invalid2(
|
|
4530
|
+
`OpenAPI document "${sourcePath}" contains an external reference "${value}". Only internal references (#/...) are supported; the importer performs no network or filesystem lookups`,
|
|
4531
|
+
{ sourcePath, ref: value }
|
|
4532
|
+
);
|
|
4533
|
+
}
|
|
4534
|
+
if (!isPercentDecodable(value)) {
|
|
4535
|
+
throw invalid2(
|
|
4536
|
+
`OpenAPI document "${sourcePath}" contains a reference "${value}" with a malformed percent-escape`,
|
|
4537
|
+
{ sourcePath, ref: value }
|
|
4538
|
+
);
|
|
4539
|
+
}
|
|
4525
4540
|
}
|
|
4526
4541
|
queue.push(value);
|
|
4527
4542
|
}
|
|
4528
4543
|
}
|
|
4529
4544
|
}
|
|
4545
|
+
function isPercentDecodable(value) {
|
|
4546
|
+
try {
|
|
4547
|
+
decodeURIComponent(value);
|
|
4548
|
+
return true;
|
|
4549
|
+
} catch {
|
|
4550
|
+
return false;
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4530
4553
|
|
|
4531
4554
|
// src/cli/commands/import-openapi.ts
|
|
4532
4555
|
function defaultOutputPath(source) {
|
|
4533
|
-
const name = basename
|
|
4556
|
+
const name = basename(source);
|
|
4534
4557
|
const stem = name.slice(0, name.length - extname(name).length) || name;
|
|
4535
4558
|
return `${stem}.agent-commerce.yaml`;
|
|
4536
4559
|
}
|
|
4537
4560
|
async function runImportOpenApi(options, io, deps = {}) {
|
|
4538
4561
|
const fileExists = deps.fileExists ?? existsSync;
|
|
4539
4562
|
const outputPath = options.output ?? defaultOutputPath(options.source);
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
policy
|
|
4543
|
-
} catch (error) {
|
|
4544
|
-
io.stderr(`FAIL ${error instanceof Error ? error.message : String(error)}`);
|
|
4563
|
+
const policy = buildPolicy(options);
|
|
4564
|
+
if (typeof policy === "string") {
|
|
4565
|
+
io.stderr(`FAIL ${policy}`);
|
|
4545
4566
|
return 1;
|
|
4546
4567
|
}
|
|
4547
4568
|
if (fileExists(outputPath) && options.force !== true) {
|
|
@@ -4622,13 +4643,11 @@ function buildPolicy(options) {
|
|
|
4622
4643
|
if (options.expose !== void 0) {
|
|
4623
4644
|
const requested = options.expose.split(",").map((name) => name.trim()).filter((name) => name !== "");
|
|
4624
4645
|
if (requested.length === 0) {
|
|
4625
|
-
|
|
4646
|
+
return `--expose needs at least one protocol (${PROTOCOL_NAMES.join(", ")})`;
|
|
4626
4647
|
}
|
|
4627
4648
|
for (const name of requested) {
|
|
4628
4649
|
if (!PROTOCOL_NAMES.includes(name)) {
|
|
4629
|
-
|
|
4630
|
-
`--expose "${name}" is not a supported protocol. Supported: ${PROTOCOL_NAMES.join(", ")}`
|
|
4631
|
-
);
|
|
4650
|
+
return `--expose "${name}" is not a supported protocol. Supported: ${PROTOCOL_NAMES.join(", ")}`;
|
|
4632
4651
|
}
|
|
4633
4652
|
}
|
|
4634
4653
|
policy.expose = requested;
|
|
@@ -4702,14 +4721,6 @@ function formatConfigError(err, manifestFound = false) {
|
|
|
4702
4721
|
}
|
|
4703
4722
|
return lines.join("\n");
|
|
4704
4723
|
}
|
|
4705
|
-
if (err instanceof ZodError) {
|
|
4706
|
-
const lines = ["FAIL Configuration schema errors:"];
|
|
4707
|
-
for (const issue of err.issues) {
|
|
4708
|
-
const path2 = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
4709
|
-
lines.push(` - ${path2}: ${issue.message}`);
|
|
4710
|
-
}
|
|
4711
|
-
return lines.join("\n");
|
|
4712
|
-
}
|
|
4713
4724
|
if (err instanceof Error) {
|
|
4714
4725
|
return `FAIL ${err.message}`;
|
|
4715
4726
|
}
|
|
@@ -4733,7 +4744,11 @@ async function runValidate(options, io, deps = {}) {
|
|
|
4733
4744
|
io.stdout(
|
|
4734
4745
|
` 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"}`
|
|
4735
4746
|
);
|
|
4736
|
-
|
|
4747
|
+
const onOff = (enabled) => enabled === true ? "on" : "off";
|
|
4748
|
+
io.stdout(
|
|
4749
|
+
` payments: x402=${onOff(config.payments.x402?.enabled)} mpp=${onOff(config.payments.mpp?.enabled)}`
|
|
4750
|
+
);
|
|
4751
|
+
io.stdout(` authorization: ap2=${onOff(config.authorization?.ap2.enabled)}`);
|
|
4737
4752
|
return 0;
|
|
4738
4753
|
} catch (err) {
|
|
4739
4754
|
io.stderr(formatConfigError(err, manifestFound));
|
|
@@ -4747,8 +4762,7 @@ async function collectAnswersInteractive() {
|
|
|
4747
4762
|
const backendBaseUrl = await clack.text({
|
|
4748
4763
|
message: "Backend base URL (your existing merchant API)",
|
|
4749
4764
|
initialValue: "http://localhost:3000",
|
|
4750
|
-
//
|
|
4751
|
-
// rather than surfacing as a config error after the fact.
|
|
4765
|
+
// Checked here, where the user can retype it, rather than as a config error later
|
|
4752
4766
|
validate: (value) => {
|
|
4753
4767
|
const trimmed = (value ?? "").trim();
|
|
4754
4768
|
if (trimmed.length === 0) return "Required";
|
|
@@ -4785,12 +4799,8 @@ async function collectAnswersInteractive() {
|
|
|
4785
4799
|
{ value: "mcp", label: "MCP" }
|
|
4786
4800
|
],
|
|
4787
4801
|
initialValues: ["http", "mcp"],
|
|
4788
|
-
//
|
|
4789
|
-
|
|
4790
|
-
// multiselect has no `validate` hook, so this is caught by the in-memory
|
|
4791
|
-
// validation in `runInit` — which now reports the real schema error and
|
|
4792
|
-
// writes nothing, instead of leaving an invalid file behind.
|
|
4793
|
-
required: false
|
|
4802
|
+
// Every resource needs at least one protocol in `expose`
|
|
4803
|
+
required: true
|
|
4794
4804
|
});
|
|
4795
4805
|
if (clack.isCancel(protocols)) {
|
|
4796
4806
|
clack.cancel("Aborted.");
|
|
@@ -4804,7 +4814,7 @@ async function collectAnswersInteractive() {
|
|
|
4804
4814
|
let merchantPayTo = defaultInitAnswers().merchantPayTo;
|
|
4805
4815
|
if (x402Enabled) {
|
|
4806
4816
|
const answer = await clack.text({
|
|
4807
|
-
message: "Merchant settlement
|
|
4817
|
+
message: "Merchant settlement address (enter an address you control)",
|
|
4808
4818
|
initialValue: merchantPayTo,
|
|
4809
4819
|
validate: (value) => {
|
|
4810
4820
|
const trimmed = (value ?? "").trim();
|
|
@@ -4847,20 +4857,16 @@ async function runInit(options, io, deps = {}) {
|
|
|
4847
4857
|
return 1;
|
|
4848
4858
|
}
|
|
4849
4859
|
const yamlContent = renderInitConfigYaml(answers);
|
|
4850
|
-
|
|
4851
|
-
const dir = dirname(resolvedPath);
|
|
4852
|
-
if (dir !== "" && !existsSync(dir)) {
|
|
4853
|
-
mkdirSync(dir, { recursive: true });
|
|
4854
|
-
}
|
|
4860
|
+
mkdirSync(dirname(resolve(outputPath)), { recursive: true });
|
|
4855
4861
|
await writeFileImpl(outputPath, yamlContent);
|
|
4856
4862
|
io.stdout(`PASS Wrote ${outputPath}`);
|
|
4857
|
-
io.stdout(`PASS ${outputPath} is valid
|
|
4863
|
+
io.stdout(`PASS ${outputPath} is valid: ${config.resources.length} resource(s)`);
|
|
4858
4864
|
try {
|
|
4859
4865
|
await loadConfig2({ path: outputPath });
|
|
4860
4866
|
} catch (err) {
|
|
4861
4867
|
io.stderr(formatConfigError(err));
|
|
4862
4868
|
io.stderr(
|
|
4863
|
-
`FAIL ${outputPath} was written but does not parse, although the same answers validated in memory
|
|
4869
|
+
`FAIL ${outputPath} was written but does not parse, although the same answers validated in memory. This is a bug in the config renderer; please report it.`
|
|
4864
4870
|
);
|
|
4865
4871
|
return 1;
|
|
4866
4872
|
}
|
|
@@ -4879,8 +4885,9 @@ function runVersion(io, deps = {}) {
|
|
|
4879
4885
|
if (report.pinned.length > 0) {
|
|
4880
4886
|
io.stdout("");
|
|
4881
4887
|
io.stdout("Pinned protocol / SDK versions:");
|
|
4888
|
+
const width = Math.max(...report.pinned.map((pin) => pin.name.length));
|
|
4882
4889
|
for (const pin of report.pinned) {
|
|
4883
|
-
io.stdout(` ${pin.name.padEnd(
|
|
4890
|
+
io.stdout(` ${pin.name.padEnd(width)} ${pin.version.padEnd(12)} (via ${pin.via})`);
|
|
4884
4891
|
}
|
|
4885
4892
|
}
|
|
4886
4893
|
return 0;
|
|
@@ -4959,7 +4966,7 @@ function buildProgram(io = processIo) {
|
|
|
4959
4966
|
}
|
|
4960
4967
|
);
|
|
4961
4968
|
program.command("demo").description(
|
|
4962
|
-
"
|
|
4969
|
+
"Start the local stack with docker compose (waiting for healthy services), then run the demo agent. Run it from a repo clone."
|
|
4963
4970
|
).action(async () => {
|
|
4964
4971
|
process.exitCode = await runDemo(io);
|
|
4965
4972
|
});
|