@devlab.group/agent-commerce 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -183
- package/dist/ap2.d.ts +21 -24
- package/dist/ap2.js +68 -82
- package/dist/ap2.js.map +1 -1
- package/dist/authorization-BpbWGGrf.d.ts +115 -0
- package/dist/{chunk-NWIQLJNU.js → chunk-74BSEXB5.js} +37 -34
- package/dist/chunk-74BSEXB5.js.map +1 -0
- package/dist/chunk-AFI2CEZS.js +10 -0
- package/dist/chunk-AFI2CEZS.js.map +1 -0
- package/dist/chunk-ATTWSFQP.js +680 -0
- package/dist/chunk-ATTWSFQP.js.map +1 -0
- package/dist/{chunk-ZEGFLRGV.js → chunk-IVJC5YLE.js} +63 -26
- package/dist/chunk-IVJC5YLE.js.map +1 -0
- package/dist/{chunk-GF6KHHKL.js → chunk-RAJGPGJG.js} +5 -8
- package/dist/chunk-RAJGPGJG.js.map +1 -0
- package/dist/chunk-V2V2E7HX.js +27 -0
- package/dist/chunk-V2V2E7HX.js.map +1 -0
- package/dist/cli/index.js +812 -573
- package/dist/cli/index.js.map +1 -1
- package/dist/constants-Bkq04FRZ.d.ts +48 -0
- package/dist/guardrails-71XwtXlT.d.ts +31 -0
- package/dist/index.d.ts +245 -265
- package/dist/index.js +1222 -626
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +6 -59
- package/dist/mcp.js +64 -53
- package/dist/mcp.js.map +1 -1
- package/dist/mpp.d.ts +50 -0
- package/dist/mpp.js +353 -0
- package/dist/mpp.js.map +1 -0
- 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 +13 -693
- package/dist/x402.js.map +1 -1
- package/package.json +13 -2
- package/dist/authorization-CPDcpJVa.d.ts +0 -123
- package/dist/chunk-GF6KHHKL.js.map +0 -1
- package/dist/chunk-NWIQLJNU.js.map +0 -1
- package/dist/chunk-ZEGFLRGV.js.map +0 -1
- package/dist/guardrails-Dmwi63KB.d.ts +0 -38
- package/dist/protocol-adapter-Bebmggf_.d.ts +0 -273
- package/dist/runtime-CKq3d_3K.d.ts +0 -340
package/dist/cli/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
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
|
-
import { writeFile, stat,
|
|
13
|
+
import { writeFile, stat, rename, rm, open } from 'fs/promises';
|
|
13
14
|
import { validate } from '@scalar/openapi-parser';
|
|
14
15
|
import * as clack from '@clack/prompts';
|
|
15
16
|
|
|
@@ -32,10 +33,22 @@ var init_filename = __esm({
|
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
// src/core/domain/common.ts
|
|
35
|
-
var PROTOCOL_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
|
+
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
|
+
);
|
|
39
52
|
}
|
|
40
53
|
});
|
|
41
54
|
|
|
@@ -64,10 +77,9 @@ var init_codes = __esm({
|
|
|
64
77
|
PAYMENT_REPLAYED: 409,
|
|
65
78
|
PAYMENT_PROVIDER_UNAVAILABLE: 503,
|
|
66
79
|
PAYMENT_SETTLEMENT_FAILED: 502,
|
|
67
|
-
// 403, not 402:
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// 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
|
|
71
83
|
AUTHORIZATION_REQUIRED: 403,
|
|
72
84
|
AUTHORIZATION_INVALID: 403,
|
|
73
85
|
AUTHORIZATION_REPLAYED: 409,
|
|
@@ -81,17 +93,13 @@ var init_codes = __esm({
|
|
|
81
93
|
};
|
|
82
94
|
RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
83
95
|
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// 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.
|
|
87
98
|
"AUTHORIZATION_PROVIDER_UNAVAILABLE",
|
|
88
99
|
"BACKEND_TIMEOUT",
|
|
89
|
-
// Load shedding is transient
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// keyed on the machine-readable flag (which exists precisely so clients need
|
|
93
|
-
// not parse prose) would read a momentary throttle as a permanent capability
|
|
94
|
-
// 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.
|
|
95
103
|
"GATEWAY_BUSY"
|
|
96
104
|
]);
|
|
97
105
|
}
|
|
@@ -127,7 +135,7 @@ var init_commerce_error = __esm({
|
|
|
127
135
|
if (options.resourceId !== void 0) this.resourceId = options.resourceId;
|
|
128
136
|
Error.captureStackTrace?.(this, _CommerceError);
|
|
129
137
|
}
|
|
130
|
-
/** Non-secret, client-safe representation. */
|
|
138
|
+
/** Non-secret, client-safe representation. `cause` is never included */
|
|
131
139
|
toInfo() {
|
|
132
140
|
return {
|
|
133
141
|
code: this.code,
|
|
@@ -151,11 +159,21 @@ var init_errors = __esm({
|
|
|
151
159
|
}
|
|
152
160
|
});
|
|
153
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
|
+
|
|
154
171
|
// src/core/domain/wire.ts
|
|
155
172
|
var PAYMENT_INPUT_FIELD, AUTHORIZATION_INPUT_FIELD, RESERVED_INPUT_FIELDS;
|
|
156
173
|
var init_wire = __esm({
|
|
157
174
|
"src/core/domain/wire.ts"() {
|
|
158
175
|
init_errors();
|
|
176
|
+
init_is_record();
|
|
159
177
|
PAYMENT_INPUT_FIELD = "_payment";
|
|
160
178
|
AUTHORIZATION_INPUT_FIELD = "_authorization";
|
|
161
179
|
RESERVED_INPUT_FIELDS = [
|
|
@@ -257,17 +275,10 @@ var init_backend_http = __esm({
|
|
|
257
275
|
init_resource();
|
|
258
276
|
init_errors();
|
|
259
277
|
init_logger();
|
|
278
|
+
init_is_record();
|
|
260
279
|
PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
|
|
261
280
|
}
|
|
262
281
|
});
|
|
263
|
-
|
|
264
|
-
// src/core/execution/event-bus.ts
|
|
265
|
-
var init_event_bus = __esm({
|
|
266
|
-
"src/core/execution/event-bus.ts"() {
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// src/core/execution/validation.ts
|
|
271
282
|
function typeList(raw) {
|
|
272
283
|
if (typeof raw === "string") return [raw];
|
|
273
284
|
if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
|
|
@@ -282,15 +293,17 @@ function isObjectSchemaNode(schema) {
|
|
|
282
293
|
}
|
|
283
294
|
var init_validation = __esm({
|
|
284
295
|
"src/core/execution/validation.ts"() {
|
|
296
|
+
init_is_record();
|
|
285
297
|
}
|
|
286
298
|
});
|
|
287
299
|
var init_pipeline = __esm({
|
|
288
300
|
"src/core/execution/pipeline.ts"() {
|
|
289
301
|
init_wire();
|
|
290
302
|
init_errors();
|
|
303
|
+
init_is_record();
|
|
291
304
|
init_backend_http();
|
|
292
305
|
init_validation();
|
|
293
|
-
|
|
306
|
+
[...RESERVED_INPUT_FIELDS, "__proto__"];
|
|
294
307
|
}
|
|
295
308
|
});
|
|
296
309
|
|
|
@@ -301,17 +314,52 @@ var init_registry = __esm({
|
|
|
301
314
|
}
|
|
302
315
|
});
|
|
303
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
|
+
|
|
304
324
|
// src/core/execution/index.ts
|
|
305
325
|
var init_execution = __esm({
|
|
306
326
|
"src/core/execution/index.ts"() {
|
|
307
327
|
init_backend_http();
|
|
308
|
-
init_event_bus();
|
|
309
328
|
init_pipeline();
|
|
310
329
|
init_registry();
|
|
330
|
+
init_store_event_sink();
|
|
311
331
|
init_validation();
|
|
312
332
|
}
|
|
313
333
|
});
|
|
314
334
|
|
|
335
|
+
// src/payments/mpp/constants.ts
|
|
336
|
+
function isMppNetwork(value) {
|
|
337
|
+
return MPP_NETWORKS.includes(value);
|
|
338
|
+
}
|
|
339
|
+
var MPP_SPEC_COMMIT, MPP_SPEC_DRAFTS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPPX_VERSION, MPP_PROFILE, MPP_NETWORKS, MPP_DEFAULT_NETWORK;
|
|
340
|
+
var init_constants2 = __esm({
|
|
341
|
+
"src/payments/mpp/constants.ts"() {
|
|
342
|
+
MPP_SPEC_COMMIT = "806fdb8b8c92cda5c84b8660558921b6bbeef7e0";
|
|
343
|
+
MPP_SPEC_DRAFTS = {
|
|
344
|
+
core: "draft-httpauth-payment-00",
|
|
345
|
+
intent: "draft-payment-intent-charge-00",
|
|
346
|
+
method: "draft-evm-charge-00"
|
|
347
|
+
};
|
|
348
|
+
MPP_MIN_CHALLENGE_SECRET_LENGTH = 32;
|
|
349
|
+
MPPX_VERSION = "0.10.1";
|
|
350
|
+
MPP_PROFILE = {
|
|
351
|
+
intent: "charge",
|
|
352
|
+
method: "evm",
|
|
353
|
+
credentialType: "authorization",
|
|
354
|
+
assetSymbol: "USDC",
|
|
355
|
+
assetDecimals: 6
|
|
356
|
+
};
|
|
357
|
+
MPP_NETWORKS = ["eip155:84532", "eip155:8453"];
|
|
358
|
+
MPP_DEFAULT_NETWORK = "eip155:84532";
|
|
359
|
+
`mpp/${MPP_SPEC_DRAFTS.core} intent=${MPP_PROFILE.intent} method=${MPP_PROFILE.method}`;
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
315
363
|
// src/payments/x402/networks.ts
|
|
316
364
|
function findNetworkProfile(id) {
|
|
317
365
|
return BY_ID.get(id);
|
|
@@ -366,13 +414,13 @@ var init_networks = __esm({
|
|
|
366
414
|
];
|
|
367
415
|
BY_ID = new Map(PROFILES.map((profile) => [profile.id, profile]));
|
|
368
416
|
SUPPORTED_NETWORK_IDS = PROFILES.map((p) => p.id);
|
|
369
|
-
LIVE_MAINNET_BANNER = "LIVE MAINNET MODE
|
|
417
|
+
LIVE_MAINNET_BANNER = "LIVE MAINNET MODE - REAL FUNDS";
|
|
370
418
|
}
|
|
371
419
|
});
|
|
372
420
|
|
|
373
421
|
// src/protocols/acp/constants.ts
|
|
374
|
-
var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS;
|
|
375
|
-
var
|
|
422
|
+
var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS, ACP_OPERATION_OPTIONAL_INPUT_KEYS;
|
|
423
|
+
var init_constants3 = __esm({
|
|
376
424
|
"src/protocols/acp/constants.ts"() {
|
|
377
425
|
ACP_SPEC_VERSION = "2026-04-17";
|
|
378
426
|
ACP_API_VERSION = "2026-04-17";
|
|
@@ -391,6 +439,9 @@ var init_constants2 = __esm({
|
|
|
391
439
|
completeCheckoutSession: ["path", "body"],
|
|
392
440
|
cancelCheckoutSession: ["path"]
|
|
393
441
|
};
|
|
442
|
+
ACP_OPERATION_OPTIONAL_INPUT_KEYS = {
|
|
443
|
+
cancelCheckoutSession: ["body"]
|
|
444
|
+
};
|
|
394
445
|
}
|
|
395
446
|
});
|
|
396
447
|
|
|
@@ -402,7 +453,7 @@ function substituteEnv(value, env, path2 = "$") {
|
|
|
402
453
|
if (Array.isArray(value)) {
|
|
403
454
|
return value.map((entry, index) => substituteEnv(entry, env, `${path2}[${index}]`));
|
|
404
455
|
}
|
|
405
|
-
if (
|
|
456
|
+
if (isRecord(value)) {
|
|
406
457
|
const result = {};
|
|
407
458
|
for (const [key, entry] of Object.entries(value)) {
|
|
408
459
|
result[key] = substituteEnv(entry, env, `${path2}.${key}`);
|
|
@@ -419,16 +470,15 @@ function assertTemplateIsSupported(value, path2) {
|
|
|
419
470
|
if (NESTED_PLACEHOLDER.test(value)) {
|
|
420
471
|
throw new CommerceError(
|
|
421
472
|
"CONFIG_INVALID",
|
|
422
|
-
`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.`,
|
|
423
474
|
{ details: { path: path2 } }
|
|
424
475
|
);
|
|
425
476
|
}
|
|
426
|
-
BRACE_TOKEN.lastIndex = 0;
|
|
427
477
|
for (const [token] of value.matchAll(BRACE_TOKEN)) {
|
|
428
478
|
if (SUPPORTED_TOKEN.test(token)) continue;
|
|
429
479
|
throw new CommerceError(
|
|
430
480
|
"CONFIG_INVALID",
|
|
431
|
-
`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.`,
|
|
432
482
|
{ details: { path: path2, token } }
|
|
433
483
|
);
|
|
434
484
|
}
|
|
@@ -453,13 +503,11 @@ function substituteOnce(value, env, path2) {
|
|
|
453
503
|
}
|
|
454
504
|
);
|
|
455
505
|
}
|
|
456
|
-
function isPlainObject(value) {
|
|
457
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
458
|
-
}
|
|
459
506
|
var PLACEHOLDER_PATTERN, BRACE_TOKEN, SUPPORTED_TOKEN, NESTED_PLACEHOLDER;
|
|
460
507
|
var init_env = __esm({
|
|
461
508
|
"src/config/env.ts"() {
|
|
462
509
|
init_core();
|
|
510
|
+
init_is_record();
|
|
463
511
|
PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
|
|
464
512
|
BRACE_TOKEN = /\$\{[^{}]*\}/g;
|
|
465
513
|
SUPPORTED_TOKEN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}$/;
|
|
@@ -497,7 +545,7 @@ function isWellKnownDevAddress(address) {
|
|
|
497
545
|
}
|
|
498
546
|
function isLikelyLocalOrPrivateHost(hostname) {
|
|
499
547
|
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
500
|
-
if (h === "localhost" || h === "
|
|
548
|
+
if (h === "localhost" || h === "::1") return true;
|
|
501
549
|
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
502
550
|
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
503
551
|
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
@@ -524,93 +572,95 @@ function invalid(message, path2) {
|
|
|
524
572
|
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2 } });
|
|
525
573
|
}
|
|
526
574
|
function resolveX402Deployment(input) {
|
|
527
|
-
const
|
|
575
|
+
const at = input.configPath ?? "payments.x402";
|
|
576
|
+
const payTo = input.payToField ?? "payTo";
|
|
577
|
+
const profile = requireNetworkProfile(input.network, `${at}.network`);
|
|
528
578
|
const mode = resolveDeploymentMode(profile, input.facilitator.mode);
|
|
529
579
|
if (profile.kind === "mainnet" && input.facilitator.mode === "local") {
|
|
530
580
|
throw invalid(
|
|
531
|
-
|
|
532
|
-
|
|
581
|
+
`${at}: network "${profile.id}" (${profile.displayName}) is a mainnet and cannot be served by facilitator.mode "local". A mainnet deployment must settle through a remote facilitator.`,
|
|
582
|
+
`${at}.facilitator.mode`
|
|
533
583
|
);
|
|
534
584
|
}
|
|
535
585
|
if (mode === "mainnet" && input.allowMainnet !== true) {
|
|
536
586
|
throw invalid(
|
|
537
|
-
|
|
538
|
-
|
|
587
|
+
`${at}: network "${profile.id}" (${profile.displayName}) settles real funds. Set ${at}.allowMainnet: true to acknowledge this explicitly - it is never the default.`,
|
|
588
|
+
`${at}.allowMainnet`
|
|
539
589
|
);
|
|
540
590
|
}
|
|
541
591
|
if (input.facilitator.mode === "remote") {
|
|
542
|
-
assertFacilitatorUrlIsSafe(input.facilitator.url, mode);
|
|
592
|
+
assertFacilitatorUrlIsSafe(input.facilitator.url, mode, at);
|
|
543
593
|
if (mode === "mainnet" && input.facilitator.auth.type === "none" && input.allowUnauthenticatedFacilitator !== true) {
|
|
544
594
|
throw invalid(
|
|
545
|
-
|
|
546
|
-
|
|
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.`,
|
|
596
|
+
`${at}.allowUnauthenticatedFacilitator`
|
|
547
597
|
);
|
|
548
598
|
}
|
|
549
599
|
for (const [field, value] of credentialFields(input.facilitator.auth)) {
|
|
550
600
|
if (value.trim() === "") {
|
|
551
601
|
throw invalid(
|
|
552
|
-
|
|
553
|
-
|
|
602
|
+
`${at}: facilitator.auth.type is "${input.facilitator.auth.type}" but ${field} is empty. An empty credential is refused rather than sent.`,
|
|
603
|
+
`${at}.facilitator.auth.${field}`
|
|
554
604
|
);
|
|
555
605
|
}
|
|
556
606
|
}
|
|
557
607
|
}
|
|
558
608
|
if (/^0x0{40}$/i.test(input.payTo)) {
|
|
559
609
|
throw invalid(
|
|
560
|
-
|
|
561
|
-
|
|
610
|
+
`${at}: "${payTo}" is the zero address. Every payment settled there is destroyed.`,
|
|
611
|
+
`${at}.${payTo}`
|
|
562
612
|
);
|
|
563
613
|
}
|
|
564
614
|
if (mode !== "local" && isWellKnownDevAddress(input.payTo)) {
|
|
565
615
|
throw invalid(
|
|
566
|
-
|
|
567
|
-
|
|
616
|
+
`${at}: "${payTo}" (${input.payTo}) is a well-known Anvil development address and this is a ${mode} deployment. Anyone can spend what settles there. Set ${payTo} to your own merchant wallet.`,
|
|
617
|
+
`${at}.${payTo}`
|
|
568
618
|
);
|
|
569
619
|
}
|
|
570
620
|
const canonical = profile.canonicalAsset;
|
|
571
621
|
if (mode === "mainnet" && canonical) {
|
|
572
622
|
if (!sameAddress(input.asset, canonical.address)) {
|
|
573
623
|
throw invalid(
|
|
574
|
-
|
|
575
|
-
|
|
624
|
+
`${at}: asset ${input.asset} is not ${canonical.symbol} on ${profile.displayName} (expected ${canonical.address}). Settling a mainnet payment in an unintended token is refused.`,
|
|
625
|
+
`${at}.asset`
|
|
576
626
|
);
|
|
577
627
|
}
|
|
578
628
|
if (input.assetName !== void 0 && input.assetName !== canonical.name) {
|
|
579
629
|
throw invalid(
|
|
580
|
-
|
|
581
|
-
|
|
630
|
+
`${at}: assetName "${input.assetName}" is not the EIP-712 domain name ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.name}"). Every payment would be refused after the buyer signed.`,
|
|
631
|
+
`${at}.assetName`
|
|
582
632
|
);
|
|
583
633
|
}
|
|
584
634
|
if (input.assetVersion !== void 0 && input.assetVersion !== canonical.version) {
|
|
585
635
|
throw invalid(
|
|
586
|
-
|
|
587
|
-
|
|
636
|
+
`${at}: assetVersion "${input.assetVersion}" is not the EIP-712 domain version ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.version}").`,
|
|
637
|
+
`${at}.assetVersion`
|
|
588
638
|
);
|
|
589
639
|
}
|
|
590
640
|
}
|
|
591
641
|
return { profile, mode };
|
|
592
642
|
}
|
|
593
|
-
function assertFacilitatorUrlIsSafe(url, mode) {
|
|
643
|
+
function assertFacilitatorUrlIsSafe(url, mode, at) {
|
|
594
644
|
let parsed;
|
|
595
645
|
try {
|
|
596
646
|
parsed = new URL(url);
|
|
597
647
|
} catch (cause) {
|
|
598
|
-
throw new CommerceError("CONFIG_INVALID",
|
|
648
|
+
throw new CommerceError("CONFIG_INVALID", `${at}: facilitator.url is not a valid URL`, {
|
|
599
649
|
cause,
|
|
600
|
-
details: { path:
|
|
650
|
+
details: { path: `${at}.facilitator.url` }
|
|
601
651
|
});
|
|
602
652
|
}
|
|
603
653
|
if (parsed.protocol === "https:") return;
|
|
604
654
|
if (parsed.protocol !== "http:") {
|
|
605
655
|
throw invalid(
|
|
606
|
-
|
|
607
|
-
|
|
656
|
+
`${at}: facilitator ${describeOrigin(url)} must be reached over https (or http on a local/private host); got "${parsed.protocol}//"`,
|
|
657
|
+
`${at}.facilitator.url`
|
|
608
658
|
);
|
|
609
659
|
}
|
|
610
660
|
if (mode !== "mainnet" && isLikelyLocalOrPrivateHost(parsed.hostname)) return;
|
|
611
661
|
throw invalid(
|
|
612
|
-
|
|
613
|
-
|
|
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.`,
|
|
663
|
+
`${at}.facilitator.url`
|
|
614
664
|
);
|
|
615
665
|
}
|
|
616
666
|
function describeOrigin(url) {
|
|
@@ -643,8 +693,20 @@ var init_guardrails = __esm({
|
|
|
643
693
|
init_networks();
|
|
644
694
|
}
|
|
645
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
|
+
});
|
|
646
708
|
function parseConfig(raw, env) {
|
|
647
|
-
if (
|
|
709
|
+
if (!isRecord(raw)) {
|
|
648
710
|
throw new CommerceError(
|
|
649
711
|
"CONFIG_INVALID",
|
|
650
712
|
'Configuration root must be a mapping (object) at "$"',
|
|
@@ -656,7 +718,7 @@ function parseConfig(raw, env) {
|
|
|
656
718
|
checkVersion(raw);
|
|
657
719
|
const substituted = substituteEnv(raw, env);
|
|
658
720
|
const parsed = parseWithZod(RawConfigSchema, substituted);
|
|
659
|
-
return
|
|
721
|
+
return normalize(parsed);
|
|
660
722
|
}
|
|
661
723
|
function checkVersion(raw) {
|
|
662
724
|
if (!("version" in raw)) {
|
|
@@ -664,12 +726,12 @@ function checkVersion(raw) {
|
|
|
664
726
|
details: { path: "version" }
|
|
665
727
|
});
|
|
666
728
|
}
|
|
667
|
-
const
|
|
668
|
-
const version = typeof rawVersion === "string" ? Number(rawVersion) : rawVersion;
|
|
729
|
+
const version = raw["version"];
|
|
669
730
|
if (version !== SUPPORTED_CONFIG_VERSION) {
|
|
731
|
+
const hint = typeof version === "string" ? ", written as a number without quotes" : "";
|
|
670
732
|
throw new CommerceError(
|
|
671
733
|
"CONFIG_INVALID",
|
|
672
|
-
`Unsupported config version "${String(
|
|
734
|
+
`Unsupported config version "${String(version)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}${hint}`,
|
|
673
735
|
{ details: { path: "version", supported: SUPPORTED_CONFIG_VERSION } }
|
|
674
736
|
);
|
|
675
737
|
}
|
|
@@ -740,7 +802,7 @@ function toBoolean(value, path2) {
|
|
|
740
802
|
{ details: { path: path2 } }
|
|
741
803
|
);
|
|
742
804
|
}
|
|
743
|
-
function
|
|
805
|
+
function normalize(raw) {
|
|
744
806
|
const protocols = {
|
|
745
807
|
http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
|
|
746
808
|
mcp: {
|
|
@@ -751,16 +813,11 @@ function normalise(raw) {
|
|
|
751
813
|
enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
|
|
752
814
|
mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
|
|
753
815
|
},
|
|
754
|
-
acp:
|
|
816
|
+
acp: normalizeAcp(raw.protocols.acp)
|
|
755
817
|
};
|
|
756
818
|
validateMountPaths(protocols);
|
|
757
819
|
const x402Raw = raw.payments.x402;
|
|
758
|
-
const
|
|
759
|
-
mode: "remote",
|
|
760
|
-
url: x402Raw.facilitator.url,
|
|
761
|
-
auth: x402Raw.facilitator.auth ?? { type: "none" }
|
|
762
|
-
};
|
|
763
|
-
const x402 = x402Raw && facilitator ? {
|
|
820
|
+
const x402 = x402Raw !== void 0 ? {
|
|
764
821
|
enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
|
|
765
822
|
network: x402Raw.network,
|
|
766
823
|
rpcUrl: x402Raw.rpcUrl,
|
|
@@ -779,7 +836,7 @@ function normalise(raw) {
|
|
|
779
836
|
min: 1
|
|
780
837
|
}
|
|
781
838
|
),
|
|
782
|
-
facilitator,
|
|
839
|
+
facilitator: normalizeFacilitator(x402Raw.facilitator),
|
|
783
840
|
...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
|
|
784
841
|
...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
785
842
|
allowUnauthenticatedFacilitator: toBoolean(
|
|
@@ -802,12 +859,13 @@ function normalise(raw) {
|
|
|
802
859
|
...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
|
|
803
860
|
});
|
|
804
861
|
}
|
|
805
|
-
const
|
|
862
|
+
const mpp = normalizeMpp(raw.payments.mpp);
|
|
863
|
+
const ap2 = normalizeAp2(raw.authorization?.ap2);
|
|
806
864
|
if (ap2?.enabled) {
|
|
807
865
|
validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
|
|
808
866
|
}
|
|
809
867
|
const resources = Object.entries(raw.resources).map(
|
|
810
|
-
([id, entry]) =>
|
|
868
|
+
([id, entry]) => normalizeResource(id, entry, protocols, x402, mpp, ap2)
|
|
811
869
|
);
|
|
812
870
|
if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
|
|
813
871
|
return {
|
|
@@ -817,8 +875,7 @@ function normalise(raw) {
|
|
|
817
875
|
name: raw.merchant.name,
|
|
818
876
|
publicBaseUrl: raw.merchant.publicBaseUrl
|
|
819
877
|
},
|
|
820
|
-
//
|
|
821
|
-
// convention, useful for tests and ephemeral dev/demo instances.
|
|
878
|
+
// Port 0 lets the OS pick a free port, for tests and throwaway instances
|
|
822
879
|
server: {
|
|
823
880
|
port: toNumber(raw.server.port, "server.port", { min: 0, max: 65535 }),
|
|
824
881
|
host: raw.server.host,
|
|
@@ -829,7 +886,8 @@ function normalise(raw) {
|
|
|
829
886
|
protocols,
|
|
830
887
|
resources,
|
|
831
888
|
payments: {
|
|
832
|
-
...x402 !== void 0 ? { x402 } : {}
|
|
889
|
+
...x402 !== void 0 ? { x402 } : {},
|
|
890
|
+
...mpp !== void 0 ? { mpp } : {}
|
|
833
891
|
},
|
|
834
892
|
...ap2 !== void 0 ? { authorization: { ap2 } } : {}
|
|
835
893
|
};
|
|
@@ -857,7 +915,7 @@ function validateMountPaths(protocols) {
|
|
|
857
915
|
function ap2Invalid(path2, message, extra = {}) {
|
|
858
916
|
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
|
|
859
917
|
}
|
|
860
|
-
function
|
|
918
|
+
function normalizeAp2(raw) {
|
|
861
919
|
if (raw === void 0) return void 0;
|
|
862
920
|
if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
|
|
863
921
|
const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
|
|
@@ -877,14 +935,14 @@ function normaliseAp2(raw) {
|
|
|
877
935
|
if (raw.replay === void 0) {
|
|
878
936
|
throw ap2Invalid(
|
|
879
937
|
"authorization.ap2.replay",
|
|
880
|
-
"authorization.ap2.replay is required when AP2 is enabled - it names the SQLite file recording which mandates have been spent, and without it a verified mandate could
|
|
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"
|
|
881
939
|
);
|
|
882
940
|
}
|
|
883
|
-
const mandateIssuers =
|
|
941
|
+
const mandateIssuers = normalizeIssuers(
|
|
884
942
|
raw.trust?.mandateIssuers,
|
|
885
943
|
"authorization.ap2.trust.mandateIssuers"
|
|
886
944
|
);
|
|
887
|
-
const checkoutIssuers =
|
|
945
|
+
const checkoutIssuers = normalizeIssuers(
|
|
888
946
|
raw.trust?.checkoutIssuers,
|
|
889
947
|
"authorization.ap2.trust.checkoutIssuers"
|
|
890
948
|
);
|
|
@@ -901,7 +959,7 @@ function normaliseAp2(raw) {
|
|
|
901
959
|
replay: { path: raw.replay.path }
|
|
902
960
|
};
|
|
903
961
|
}
|
|
904
|
-
function
|
|
962
|
+
function normalizeIssuers(raw, path2) {
|
|
905
963
|
if (raw === void 0 || raw.length === 0) {
|
|
906
964
|
throw ap2Invalid(
|
|
907
965
|
path2,
|
|
@@ -987,11 +1045,11 @@ function validateJwk(jwk, kid, path2) {
|
|
|
987
1045
|
if (jwk["kid"] !== void 0 && value("kid") !== kid) {
|
|
988
1046
|
fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
|
|
989
1047
|
}
|
|
990
|
-
const
|
|
1048
|
+
const normalized = {};
|
|
991
1049
|
for (const member of AP2_JWK_MEMBERS) {
|
|
992
|
-
if (jwk[member] !== void 0)
|
|
1050
|
+
if (jwk[member] !== void 0) normalized[member] = value(member);
|
|
993
1051
|
}
|
|
994
|
-
return
|
|
1052
|
+
return normalized;
|
|
995
1053
|
}
|
|
996
1054
|
function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
|
|
997
1055
|
if (replayPath === ":memory:") return;
|
|
@@ -1005,7 +1063,7 @@ function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
|
|
|
1005
1063
|
);
|
|
1006
1064
|
}
|
|
1007
1065
|
}
|
|
1008
|
-
function
|
|
1066
|
+
function normalizeResourceAuthorization(id, entry, pricing, ap2) {
|
|
1009
1067
|
const required = entry.authorization?.required;
|
|
1010
1068
|
if (required === void 0) return void 0;
|
|
1011
1069
|
const path2 = `resources.${id}.authorization.required`;
|
|
@@ -1045,7 +1103,7 @@ function normaliseResourceAuthorization(id, entry, pricing, ap2) {
|
|
|
1045
1103
|
function acpInvalid(path2, message, extra = {}) {
|
|
1046
1104
|
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
|
|
1047
1105
|
}
|
|
1048
|
-
function
|
|
1106
|
+
function normalizeAcp(raw) {
|
|
1049
1107
|
const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
|
|
1050
1108
|
if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
|
|
1051
1109
|
return { enabled: false, mountPath };
|
|
@@ -1075,9 +1133,8 @@ function normaliseAcp(raw) {
|
|
|
1075
1133
|
{ min: ACP_RETENTION_HOURS }
|
|
1076
1134
|
)
|
|
1077
1135
|
},
|
|
1078
|
-
checkout: { operations:
|
|
1079
|
-
// Built key by key
|
|
1080
|
-
// 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`
|
|
1081
1138
|
...discovery !== void 0 ? {
|
|
1082
1139
|
discovery: {
|
|
1083
1140
|
...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
|
|
@@ -1088,7 +1145,7 @@ function normaliseAcp(raw) {
|
|
|
1088
1145
|
} : {}
|
|
1089
1146
|
};
|
|
1090
1147
|
}
|
|
1091
|
-
function
|
|
1148
|
+
function normalizeAcpOperations(checkout) {
|
|
1092
1149
|
const configured = checkout?.operations ?? {};
|
|
1093
1150
|
for (const key of Object.keys(configured)) {
|
|
1094
1151
|
if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
|
|
@@ -1156,13 +1213,15 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
1156
1213
|
const schema = resource.inputSchema;
|
|
1157
1214
|
if (schema === void 0) return;
|
|
1158
1215
|
const keys = ACP_OPERATION_INPUT_KEYS[operation];
|
|
1216
|
+
const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
|
|
1159
1217
|
const properties = schema["properties"];
|
|
1160
|
-
if (schema["additionalProperties"] === false &&
|
|
1161
|
-
for (const key of keys) {
|
|
1218
|
+
if (schema["additionalProperties"] === false && isRecord(properties)) {
|
|
1219
|
+
for (const key of [...keys, ...optionalKeys]) {
|
|
1162
1220
|
if (!Object.hasOwn(properties, key)) {
|
|
1221
|
+
const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
|
|
1163
1222
|
throw acpInvalid(
|
|
1164
1223
|
path2,
|
|
1165
|
-
`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`,
|
|
1166
1225
|
{ resourceId: resource.id }
|
|
1167
1226
|
);
|
|
1168
1227
|
}
|
|
@@ -1174,14 +1233,81 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
1174
1233
|
if (typeof entry === "string" && !keys.includes(entry)) {
|
|
1175
1234
|
throw acpInvalid(
|
|
1176
1235
|
path2,
|
|
1177
|
-
`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(", ")})`,
|
|
1178
1237
|
{ resourceId: resource.id }
|
|
1179
1238
|
);
|
|
1180
1239
|
}
|
|
1181
1240
|
}
|
|
1182
1241
|
}
|
|
1183
1242
|
}
|
|
1184
|
-
function
|
|
1243
|
+
function normalizeFacilitator(raw) {
|
|
1244
|
+
return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
|
|
1245
|
+
}
|
|
1246
|
+
function normalizeMpp(raw) {
|
|
1247
|
+
if (raw === void 0) return void 0;
|
|
1248
|
+
const network = raw.network ?? MPP_DEFAULT_NETWORK;
|
|
1249
|
+
if (!isMppNetwork(network)) {
|
|
1250
|
+
throw new CommerceError(
|
|
1251
|
+
"CONFIG_INVALID",
|
|
1252
|
+
`payments.mpp.network must be one of ${MPP_NETWORKS.join(", ")}`,
|
|
1253
|
+
{ details: { path: "payments.mpp.network" } }
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
const mpp = {
|
|
1257
|
+
enabled: toBoolean(raw.enabled, "payments.mpp.enabled"),
|
|
1258
|
+
network,
|
|
1259
|
+
rpcUrl: raw.rpcUrl,
|
|
1260
|
+
asset: raw.asset,
|
|
1261
|
+
assetName: raw.assetName,
|
|
1262
|
+
assetVersion: raw.assetVersion,
|
|
1263
|
+
recipient: raw.recipient,
|
|
1264
|
+
realm: raw.realm,
|
|
1265
|
+
challengeSecret: raw.challengeSecret,
|
|
1266
|
+
...raw.challengeTtlSeconds !== void 0 ? {
|
|
1267
|
+
challengeTtlSeconds: toNumber(
|
|
1268
|
+
raw.challengeTtlSeconds,
|
|
1269
|
+
"payments.mpp.challengeTtlSeconds",
|
|
1270
|
+
{ min: 1 }
|
|
1271
|
+
)
|
|
1272
|
+
} : {},
|
|
1273
|
+
facilitator: normalizeFacilitator(raw.facilitator),
|
|
1274
|
+
...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
|
|
1275
|
+
...raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
1276
|
+
allowUnauthenticatedFacilitator: toBoolean(
|
|
1277
|
+
raw.allowUnauthenticatedFacilitator,
|
|
1278
|
+
"payments.mpp.allowUnauthenticatedFacilitator"
|
|
1279
|
+
)
|
|
1280
|
+
} : {}
|
|
1281
|
+
};
|
|
1282
|
+
validateAddress("payments.mpp.recipient", mpp.recipient);
|
|
1283
|
+
validateAddress("payments.mpp.asset", mpp.asset);
|
|
1284
|
+
if (/[\r\n]/.test(mpp.realm)) {
|
|
1285
|
+
throw new CommerceError("CONFIG_INVALID", "payments.mpp.realm must be a single line", {
|
|
1286
|
+
details: { path: "payments.mpp.realm" }
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
if (mpp.challengeSecret.length < MPP_MIN_CHALLENGE_SECRET_LENGTH) {
|
|
1290
|
+
throw new CommerceError(
|
|
1291
|
+
"CONFIG_INVALID",
|
|
1292
|
+
`payments.mpp.challengeSecret must have length at least ${MPP_MIN_CHALLENGE_SECRET_LENGTH}`,
|
|
1293
|
+
{ details: { path: "payments.mpp.challengeSecret" } }
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
resolveX402Deployment({
|
|
1297
|
+
network: mpp.network,
|
|
1298
|
+
payTo: mpp.recipient,
|
|
1299
|
+
asset: mpp.asset,
|
|
1300
|
+
assetName: mpp.assetName,
|
|
1301
|
+
assetVersion: mpp.assetVersion,
|
|
1302
|
+
facilitator: mpp.facilitator,
|
|
1303
|
+
...mpp.allowMainnet !== void 0 ? { allowMainnet: mpp.allowMainnet } : {},
|
|
1304
|
+
...mpp.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: mpp.allowUnauthenticatedFacilitator } : {},
|
|
1305
|
+
configPath: "payments.mpp",
|
|
1306
|
+
payToField: "recipient"
|
|
1307
|
+
});
|
|
1308
|
+
return mpp;
|
|
1309
|
+
}
|
|
1310
|
+
function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
|
|
1185
1311
|
if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
|
|
1186
1312
|
const inputProperties = entry.input?.["properties"];
|
|
1187
1313
|
if (inputProperties && typeof inputProperties === "object") {
|
|
@@ -1247,6 +1373,7 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
|
|
|
1247
1373
|
);
|
|
1248
1374
|
}
|
|
1249
1375
|
validateBackendUrl(id, entry.backend.url);
|
|
1376
|
+
validateBackendHeaders(id, entry.backend.headers);
|
|
1250
1377
|
const pathScope = validateInputBindings(id, entry.backend, entry.input);
|
|
1251
1378
|
validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
|
|
1252
1379
|
if (entry.pricing.type === "dynamic") {
|
|
@@ -1257,7 +1384,7 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
|
|
|
1257
1384
|
);
|
|
1258
1385
|
}
|
|
1259
1386
|
if (entry.pricing.type === "fixed") {
|
|
1260
|
-
validatePricingAmount(id, entry.pricing.amount, x402);
|
|
1387
|
+
validatePricingAmount(id, entry.pricing.amount, x402?.assetDecimals);
|
|
1261
1388
|
}
|
|
1262
1389
|
const paymentMethods = entry.payments ?? [];
|
|
1263
1390
|
if (entry.pricing.type === "fixed") {
|
|
@@ -1272,21 +1399,32 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
|
|
|
1272
1399
|
if (!SUPPORTED_PAYMENT_METHODS.has(method)) {
|
|
1273
1400
|
throw new CommerceError(
|
|
1274
1401
|
"CONFIG_INVALID",
|
|
1275
|
-
`Resource "${id}" names unsupported payment method "${method}". Supported:
|
|
1402
|
+
`Resource "${id}" names unsupported payment method "${method}". Supported: ${PAYMENT_METHOD_NAMES.join(", ")}.`,
|
|
1276
1403
|
{ details: { path: `resources.${id}.payments`, resourceId: id, method } }
|
|
1277
1404
|
);
|
|
1278
1405
|
}
|
|
1279
|
-
|
|
1406
|
+
}
|
|
1407
|
+
const enabledRails = [...x402?.enabled ? ["x402"] : [], ...mpp?.enabled ? ["mpp"] : []];
|
|
1408
|
+
if (!paymentMethods.some((method) => enabledRails.includes(method))) {
|
|
1409
|
+
throw new CommerceError(
|
|
1410
|
+
"CONFIG_INVALID",
|
|
1411
|
+
`Resource "${id}" names payment method(s) "${paymentMethods.join(", ")}", none of which is configured and enabled under "payments"`,
|
|
1412
|
+
{ details: { path: `resources.${id}.payments`, resourceId: id, paymentMethods } }
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
if (mpp?.enabled && paymentMethods.includes("mpp")) {
|
|
1416
|
+
if (entry.pricing.currency !== MPP_PROFILE.assetSymbol) {
|
|
1280
1417
|
throw new CommerceError(
|
|
1281
1418
|
"CONFIG_INVALID",
|
|
1282
|
-
`Resource "${id}"
|
|
1283
|
-
{ details: { path: `resources.${id}.
|
|
1419
|
+
`Resource "${id}" is priced in ${entry.pricing.currency}, but MPP charges ${MPP_PROFILE.assetSymbol}`,
|
|
1420
|
+
{ details: { path: `resources.${id}.pricing.currency`, resourceId: id } }
|
|
1284
1421
|
);
|
|
1285
1422
|
}
|
|
1423
|
+
validatePricingAmount(id, entry.pricing.amount, MPP_PROFILE.assetDecimals);
|
|
1286
1424
|
}
|
|
1287
1425
|
}
|
|
1288
1426
|
const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
|
|
1289
|
-
const authorization =
|
|
1427
|
+
const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
|
|
1290
1428
|
return {
|
|
1291
1429
|
id,
|
|
1292
1430
|
name: entry.name,
|
|
@@ -1316,11 +1454,11 @@ function defaultClosedObjectSchema(schema) {
|
|
|
1316
1454
|
const result = { ...schema };
|
|
1317
1455
|
if (isObjectSchema) {
|
|
1318
1456
|
const properties = schema["properties"];
|
|
1319
|
-
if (
|
|
1457
|
+
if (isRecord(properties)) {
|
|
1320
1458
|
result["properties"] = Object.fromEntries(
|
|
1321
1459
|
Object.entries(properties).map(([key, sub]) => [
|
|
1322
1460
|
key,
|
|
1323
|
-
|
|
1461
|
+
isRecord(sub) ? defaultClosedObjectSchema(sub) : sub
|
|
1324
1462
|
])
|
|
1325
1463
|
);
|
|
1326
1464
|
}
|
|
@@ -1329,18 +1467,15 @@ function defaultClosedObjectSchema(schema) {
|
|
|
1329
1467
|
}
|
|
1330
1468
|
}
|
|
1331
1469
|
const items = schema["items"];
|
|
1332
|
-
if (
|
|
1470
|
+
if (isRecord(items)) {
|
|
1333
1471
|
result["items"] = defaultClosedObjectSchema(items);
|
|
1334
1472
|
}
|
|
1335
1473
|
const additional = schema["additionalProperties"];
|
|
1336
|
-
if (
|
|
1474
|
+
if (isRecord(additional)) {
|
|
1337
1475
|
result["additionalProperties"] = defaultClosedObjectSchema(additional);
|
|
1338
1476
|
}
|
|
1339
1477
|
return result;
|
|
1340
1478
|
}
|
|
1341
|
-
function isPlainObject2(value) {
|
|
1342
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1343
|
-
}
|
|
1344
1479
|
function excludesObjectType(schema) {
|
|
1345
1480
|
const type = schema["type"];
|
|
1346
1481
|
if (type === void 0 || type === "object") return false;
|
|
@@ -1365,7 +1500,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1365
1500
|
const requiredRaw = schema["required"];
|
|
1366
1501
|
const closed = !Object.hasOwn(schema, "additionalProperties") ? true : schema["additionalProperties"] === false;
|
|
1367
1502
|
if (Array.isArray(requiredRaw) && closed && isObjectSchemaNode(schema)) {
|
|
1368
|
-
const declared =
|
|
1503
|
+
const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
1369
1504
|
const undeclared = requiredRaw.filter(
|
|
1370
1505
|
(name) => typeof name === "string" && !Object.hasOwn(declared, name)
|
|
1371
1506
|
);
|
|
@@ -1378,17 +1513,17 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1378
1513
|
}
|
|
1379
1514
|
}
|
|
1380
1515
|
const properties = schema["properties"];
|
|
1381
|
-
if (
|
|
1516
|
+
if (isRecord(properties)) {
|
|
1382
1517
|
for (const [key, sub] of Object.entries(properties)) {
|
|
1383
|
-
if (
|
|
1518
|
+
if (isRecord(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
|
|
1384
1519
|
}
|
|
1385
1520
|
}
|
|
1386
1521
|
const additional = schema["additionalProperties"];
|
|
1387
|
-
if (
|
|
1522
|
+
if (isRecord(additional)) {
|
|
1388
1523
|
validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
|
|
1389
1524
|
}
|
|
1390
1525
|
const items = schema["items"];
|
|
1391
|
-
if (
|
|
1526
|
+
if (isRecord(items)) {
|
|
1392
1527
|
validateResourceSchemaKeywords(id, `${path2}.items`, items);
|
|
1393
1528
|
} else if (Array.isArray(items)) {
|
|
1394
1529
|
console.warn(
|
|
@@ -1396,7 +1531,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1396
1531
|
);
|
|
1397
1532
|
}
|
|
1398
1533
|
}
|
|
1399
|
-
function validatePricingAmount(id, amount,
|
|
1534
|
+
function validatePricingAmount(id, amount, decimals) {
|
|
1400
1535
|
const path2 = `resources.${id}.pricing.amount`;
|
|
1401
1536
|
if (!PRICING_AMOUNT_PATTERN.test(amount)) {
|
|
1402
1537
|
throw new CommerceError(
|
|
@@ -1412,12 +1547,12 @@ function validatePricingAmount(id, amount, x402) {
|
|
|
1412
1547
|
{ details: { path: path2, resourceId: id } }
|
|
1413
1548
|
);
|
|
1414
1549
|
}
|
|
1415
|
-
if (
|
|
1550
|
+
if (decimals !== void 0) {
|
|
1416
1551
|
const fractionalDigits = amount.includes(".") ? amount.split(".")[1]?.length ?? 0 : 0;
|
|
1417
|
-
if (fractionalDigits >
|
|
1552
|
+
if (fractionalDigits > decimals) {
|
|
1418
1553
|
throw new CommerceError(
|
|
1419
1554
|
"CONFIG_INVALID",
|
|
1420
|
-
`Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${
|
|
1555
|
+
`Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${decimals} decimals)`,
|
|
1421
1556
|
{ details: { path: path2, resourceId: id } }
|
|
1422
1557
|
);
|
|
1423
1558
|
}
|
|
@@ -1453,7 +1588,7 @@ function validateInputBindings(id, backend, input) {
|
|
|
1453
1588
|
);
|
|
1454
1589
|
}
|
|
1455
1590
|
const seen = /* @__PURE__ */ new Map();
|
|
1456
|
-
const properties =
|
|
1591
|
+
const properties = isRecord(input?.["properties"]) ? input["properties"] : {};
|
|
1457
1592
|
const required = new Set(
|
|
1458
1593
|
Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
|
|
1459
1594
|
);
|
|
@@ -1476,7 +1611,7 @@ function validateInputBindings(id, backend, input) {
|
|
|
1476
1611
|
);
|
|
1477
1612
|
}
|
|
1478
1613
|
const declared = properties[property];
|
|
1479
|
-
if (location !== "body" &&
|
|
1614
|
+
if (location !== "body" && isRecord(declared) && !isObjectSchemaNode(declared)) {
|
|
1480
1615
|
fail(
|
|
1481
1616
|
`binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
|
|
1482
1617
|
{ location, property }
|
|
@@ -1492,7 +1627,7 @@ function validateInputBindings(id, backend, input) {
|
|
|
1492
1627
|
if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
|
|
1493
1628
|
const group = properties[bindings.path];
|
|
1494
1629
|
return {
|
|
1495
|
-
schema:
|
|
1630
|
+
schema: isRecord(group) ? group : void 0,
|
|
1496
1631
|
where: `input.properties.${bindings.path}`
|
|
1497
1632
|
};
|
|
1498
1633
|
}
|
|
@@ -1504,7 +1639,7 @@ function validatePathParametersDeclared(id, url, input, where) {
|
|
|
1504
1639
|
if (stray !== void 0) {
|
|
1505
1640
|
throw new CommerceError(
|
|
1506
1641
|
"CONFIG_INVALID",
|
|
1507
|
-
`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`,
|
|
1508
1643
|
{ details: { path: `resources.${id}.backend.url`, resourceId: id, token: stray } }
|
|
1509
1644
|
);
|
|
1510
1645
|
}
|
|
@@ -1522,13 +1657,13 @@ function validatePathParametersDeclared(id, url, input, where) {
|
|
|
1522
1657
|
if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
|
|
1523
1658
|
throw new CommerceError(
|
|
1524
1659
|
"CONFIG_INVALID",
|
|
1525
|
-
`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`,
|
|
1526
1661
|
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
1527
1662
|
);
|
|
1528
1663
|
}
|
|
1529
1664
|
}
|
|
1530
1665
|
const propertiesRaw = input?.["properties"];
|
|
1531
|
-
const properties =
|
|
1666
|
+
const properties = isRecord(propertiesRaw) ? propertiesRaw : {};
|
|
1532
1667
|
const requiredRaw = input?.["required"];
|
|
1533
1668
|
const required = new Set(
|
|
1534
1669
|
Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : []
|
|
@@ -1538,14 +1673,14 @@ function validatePathParametersDeclared(id, url, input, where) {
|
|
|
1538
1673
|
if (!Object.hasOwn(properties, param)) {
|
|
1539
1674
|
throw new CommerceError(
|
|
1540
1675
|
"CONFIG_INVALID",
|
|
1541
|
-
`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`,
|
|
1542
1677
|
{ details: { path: path2, resourceId: id, param } }
|
|
1543
1678
|
);
|
|
1544
1679
|
}
|
|
1545
1680
|
if (!required.has(param)) {
|
|
1546
1681
|
throw new CommerceError(
|
|
1547
1682
|
"CONFIG_INVALID",
|
|
1548
|
-
`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`,
|
|
1549
1684
|
{ details: { path: path2, resourceId: id, param } }
|
|
1550
1685
|
);
|
|
1551
1686
|
}
|
|
@@ -1558,7 +1693,7 @@ function validateBackendUrl(id, url) {
|
|
|
1558
1693
|
} catch {
|
|
1559
1694
|
throw new CommerceError(
|
|
1560
1695
|
"CONFIG_INVALID",
|
|
1561
|
-
`Resource "${id}" has an invalid backend.url
|
|
1696
|
+
`Resource "${id}" has an invalid backend.url: must be an absolute http:// or https:// URL`,
|
|
1562
1697
|
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
1563
1698
|
);
|
|
1564
1699
|
}
|
|
@@ -1570,6 +1705,19 @@ function validateBackendUrl(id, url) {
|
|
|
1570
1705
|
);
|
|
1571
1706
|
}
|
|
1572
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
|
+
}
|
|
1573
1721
|
function validateAddress(path2, value) {
|
|
1574
1722
|
if (!ADDRESS_PATTERN.test(value)) {
|
|
1575
1723
|
throw new CommerceError(
|
|
@@ -1588,19 +1736,21 @@ function validateAddress(path2, value) {
|
|
|
1588
1736
|
);
|
|
1589
1737
|
}
|
|
1590
1738
|
}
|
|
1591
|
-
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;
|
|
1592
1740
|
var init_schema = __esm({
|
|
1593
1741
|
"src/config/schema.ts"() {
|
|
1594
1742
|
init_constants();
|
|
1595
|
-
init_execution();
|
|
1596
1743
|
init_core();
|
|
1597
|
-
|
|
1744
|
+
init_execution();
|
|
1745
|
+
init_is_record();
|
|
1598
1746
|
init_constants2();
|
|
1747
|
+
init_guardrails();
|
|
1748
|
+
init_constants3();
|
|
1749
|
+
init_constants4();
|
|
1599
1750
|
init_env();
|
|
1600
1751
|
SUPPORTED_CONFIG_VERSION = 1;
|
|
1601
1752
|
ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
|
|
1602
1753
|
ZERO_ADDRESS_PATTERN = /^0x0{40}$/i;
|
|
1603
|
-
MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
|
|
1604
1754
|
NumberOrString = z.union([z.number(), z.string()]);
|
|
1605
1755
|
BooleanOrString = z.union([z.boolean(), z.string()]);
|
|
1606
1756
|
MerchantSchema = z.object({
|
|
@@ -1611,14 +1761,14 @@ var init_schema = __esm({
|
|
|
1611
1761
|
ServerSchema = z.object({
|
|
1612
1762
|
port: NumberOrString,
|
|
1613
1763
|
host: z.string().min(1),
|
|
1614
|
-
|
|
1764
|
+
// Shared secret gating /api/receipts and /api/events. Without it both
|
|
1765
|
+
// routes 404.
|
|
1615
1766
|
adminToken: z.string().min(1).optional(),
|
|
1616
|
-
|
|
1617
|
-
//
|
|
1618
|
-
// `Origin` header, so `"*"` and a trailing slash
|
|
1619
|
-
//
|
|
1620
|
-
//
|
|
1621
|
-
// 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.
|
|
1622
1772
|
allowedOrigins: z.array(
|
|
1623
1773
|
z.string().min(1).refine((origin) => origin !== "*", {
|
|
1624
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.`
|
|
@@ -1637,16 +1787,14 @@ var init_schema = __esm({
|
|
|
1637
1787
|
"/health",
|
|
1638
1788
|
"/ready",
|
|
1639
1789
|
"/.well-known/agent-commerce",
|
|
1640
|
-
// Fixed by the A2A specification
|
|
1641
|
-
// a configurable mount's to claim.
|
|
1790
|
+
// Fixed by the A2A specification and served by the adapter
|
|
1642
1791
|
"/.well-known/agent-card.json",
|
|
1643
|
-
//
|
|
1792
|
+
// Fixed by the ACP specification
|
|
1644
1793
|
ACP_WELL_KNOWN_PATH,
|
|
1645
1794
|
"/api/resources",
|
|
1646
1795
|
"/api/resources/:id/invoke",
|
|
1647
1796
|
"/api/receipts",
|
|
1648
|
-
"/api/events"
|
|
1649
|
-
"/api/events/stream"
|
|
1797
|
+
"/api/events"
|
|
1650
1798
|
];
|
|
1651
1799
|
MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
|
|
1652
1800
|
message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
|
|
@@ -1678,16 +1826,14 @@ var init_schema = __esm({
|
|
|
1678
1826
|
enabled: BooleanOrString,
|
|
1679
1827
|
mountPath: MountPathSchema
|
|
1680
1828
|
}).strict(),
|
|
1681
|
-
// Optional
|
|
1682
|
-
// existing config predating the adapter stays valid.
|
|
1829
|
+
// Optional; A2A is off unless configured
|
|
1683
1830
|
a2a: z.object({
|
|
1684
1831
|
enabled: BooleanOrString,
|
|
1685
1832
|
mountPath: MountPathSchema.optional()
|
|
1686
1833
|
}).strict().optional(),
|
|
1687
|
-
//
|
|
1688
|
-
//
|
|
1689
|
-
//
|
|
1690
|
-
// 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.
|
|
1691
1837
|
acp: z.object({
|
|
1692
1838
|
enabled: BooleanOrString,
|
|
1693
1839
|
mountPath: MountPathSchema.optional(),
|
|
@@ -1743,7 +1889,7 @@ var init_schema = __esm({
|
|
|
1743
1889
|
z.object({ type: z.literal("none") }).strict(),
|
|
1744
1890
|
z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
|
|
1745
1891
|
// Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
|
|
1746
|
-
// which signs a fresh JWT per request
|
|
1892
|
+
// which signs a fresh JWT per request.
|
|
1747
1893
|
z.object({
|
|
1748
1894
|
type: z.literal("cdp"),
|
|
1749
1895
|
apiKeyId: z.string().min(1),
|
|
@@ -1758,10 +1904,8 @@ var init_schema = __esm({
|
|
|
1758
1904
|
z.object({
|
|
1759
1905
|
mode: z.literal("remote"),
|
|
1760
1906
|
url: z.string().min(1),
|
|
1761
|
-
// Absent
|
|
1762
|
-
//
|
|
1763
|
-
// combination is refused outright, so the default can never quietly
|
|
1764
|
-
// become an unauthenticated production facilitator.
|
|
1907
|
+
// Absent auth sends no credential and normalizes to `{ type: 'none' }`,
|
|
1908
|
+
// which on mainnet requires `allowUnauthenticatedFacilitator`
|
|
1765
1909
|
auth: FacilitatorAuthSchema.optional()
|
|
1766
1910
|
}).strict()
|
|
1767
1911
|
]);
|
|
@@ -1776,13 +1920,31 @@ var init_schema = __esm({
|
|
|
1776
1920
|
payTo: z.string().min(1),
|
|
1777
1921
|
maxTimeoutSeconds: NumberOrString,
|
|
1778
1922
|
facilitator: FacilitatorSchema,
|
|
1779
|
-
|
|
1923
|
+
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
1924
|
+
allowMainnet: BooleanOrString.optional(),
|
|
1925
|
+
// Accepts credential-free facilitator access on mainnet. Never defaulted
|
|
1926
|
+
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
1927
|
+
}).strict();
|
|
1928
|
+
MppSchema = z.object({
|
|
1929
|
+
enabled: BooleanOrString,
|
|
1930
|
+
network: z.string().min(1).optional(),
|
|
1931
|
+
rpcUrl: z.string().min(1),
|
|
1932
|
+
asset: z.string().min(1),
|
|
1933
|
+
assetName: z.string().min(1),
|
|
1934
|
+
assetVersion: z.string().min(1),
|
|
1935
|
+
recipient: z.string().min(1),
|
|
1936
|
+
realm: z.string().min(1),
|
|
1937
|
+
challengeSecret: z.string().min(1),
|
|
1938
|
+
challengeTtlSeconds: NumberOrString.optional(),
|
|
1939
|
+
facilitator: FacilitatorSchema,
|
|
1940
|
+
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
1780
1941
|
allowMainnet: BooleanOrString.optional(),
|
|
1781
|
-
|
|
1942
|
+
// Accepts credential-free facilitator access on mainnet. Never defaulted
|
|
1782
1943
|
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
1783
1944
|
}).strict();
|
|
1784
1945
|
PaymentsSchema = z.object({
|
|
1785
|
-
x402: X402Schema.optional()
|
|
1946
|
+
x402: X402Schema.optional(),
|
|
1947
|
+
mpp: MppSchema.optional()
|
|
1786
1948
|
}).strict();
|
|
1787
1949
|
Ap2KeySchema = z.object({
|
|
1788
1950
|
kid: z.string().min(1),
|
|
@@ -1790,11 +1952,8 @@ var init_schema = __esm({
|
|
|
1790
1952
|
}).strict();
|
|
1791
1953
|
Ap2IssuerSchema = z.object({
|
|
1792
1954
|
issuer: z.string().min(1),
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
* merchant would verify here, and there is no value worth guessing for
|
|
1796
|
-
* something that decides that.
|
|
1797
|
-
*/
|
|
1955
|
+
// Required, not defaulted: without it a mandate minted for another
|
|
1956
|
+
// merchant would verify here
|
|
1798
1957
|
audience: z.string().min(1),
|
|
1799
1958
|
keys: z.array(Ap2KeySchema).min(1)
|
|
1800
1959
|
}).strict();
|
|
@@ -1821,7 +1980,7 @@ var init_schema = __esm({
|
|
|
1821
1980
|
authorization: AuthorizationSchema.optional()
|
|
1822
1981
|
}).strict();
|
|
1823
1982
|
SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
|
|
1824
|
-
SUPPORTED_PAYMENT_METHODS =
|
|
1983
|
+
SUPPORTED_PAYMENT_METHODS = new Set(PAYMENT_METHOD_NAMES);
|
|
1825
1984
|
SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
|
|
1826
1985
|
EMPTY_CLOSED_OBJECT_SCHEMA = {
|
|
1827
1986
|
type: "object",
|
|
@@ -1876,9 +2035,8 @@ async function loadConfig(options = {}) {
|
|
|
1876
2035
|
`Configuration file "${configPath}" is not valid YAML: ${reason}`,
|
|
1877
2036
|
{
|
|
1878
2037
|
details: { path: configPath, ...details },
|
|
1879
|
-
//
|
|
1880
|
-
//
|
|
1881
|
-
// `.message` carries the source excerpt (see below).
|
|
2038
|
+
// Kept for programmatic callers. Never print it: its `.message` quotes
|
|
2039
|
+
// the source line (see describeYamlError).
|
|
1882
2040
|
cause: error
|
|
1883
2041
|
}
|
|
1884
2042
|
);
|
|
@@ -1949,9 +2107,9 @@ init_core();
|
|
|
1949
2107
|
function defaultDemoSteps() {
|
|
1950
2108
|
return [
|
|
1951
2109
|
{
|
|
1952
|
-
name: "Start local chain
|
|
2110
|
+
name: "Start the local stack: chain, merchant, gateway and dashboard (docker compose)",
|
|
1953
2111
|
command: "docker",
|
|
1954
|
-
args: ["compose", "up", "--build", "
|
|
2112
|
+
args: ["compose", "up", "--build", "--detach", "--wait"]
|
|
1955
2113
|
},
|
|
1956
2114
|
{ name: "Run the deterministic buyer agent", command: "npm", args: ["run", "demo:agent"] }
|
|
1957
2115
|
];
|
|
@@ -1959,7 +2117,7 @@ function defaultDemoSteps() {
|
|
|
1959
2117
|
async function runDemo(io, deps = {}) {
|
|
1960
2118
|
const steps = deps.steps ?? defaultDemoSteps();
|
|
1961
2119
|
const run = deps.run ?? execCommand;
|
|
1962
|
-
io.stdout("agent-commerce demo
|
|
2120
|
+
io.stdout("agent-commerce demo: starting the local stack, then running the buyer agent");
|
|
1963
2121
|
for (const step of steps) {
|
|
1964
2122
|
io.stdout("");
|
|
1965
2123
|
io.stdout(`==> ${step.name}`);
|
|
@@ -1975,13 +2133,6 @@ async function runDemo(io, deps = {}) {
|
|
|
1975
2133
|
io.stderr(` ${step.command} ${step.args.join(" ")}`);
|
|
1976
2134
|
return 1;
|
|
1977
2135
|
}
|
|
1978
|
-
if (step.healthCheck !== void 0) {
|
|
1979
|
-
const healthy = await step.healthCheck();
|
|
1980
|
-
if (!healthy) {
|
|
1981
|
-
io.stderr(`FAIL "${step.name}" completed but its health check did not pass.`);
|
|
1982
|
-
return 1;
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
2136
|
io.stdout(`OK ${step.name}`);
|
|
1986
2137
|
}
|
|
1987
2138
|
io.stdout("");
|
|
@@ -2012,8 +2163,7 @@ init_constants();
|
|
|
2012
2163
|
// src/authorization/ap2/descriptor.ts
|
|
2013
2164
|
init_constants();
|
|
2014
2165
|
var AP2_UNSUPPORTED = [
|
|
2015
|
-
// Mandate kinds. Open mandates
|
|
2016
|
-
// evaluates, so accepting one would tell a buyer their limits were checked.
|
|
2166
|
+
// Mandate kinds. Open mandates are refused: see AP2_CHECKOUT_MANDATE_VCT
|
|
2017
2167
|
"autonomous mode",
|
|
2018
2168
|
"open checkout mandates (mandate.checkout.open.1)",
|
|
2019
2169
|
"intent mandates",
|
|
@@ -2029,13 +2179,13 @@ var AP2_UNSUPPORTED = [
|
|
|
2029
2179
|
"digest algorithms other than sha-256",
|
|
2030
2180
|
// Roles this gateway does not play
|
|
2031
2181
|
"mandate issuance",
|
|
2032
|
-
"merchant checkout JWT issuance",
|
|
2033
2182
|
"AP2 over the ACP checkout adapter"
|
|
2034
2183
|
];
|
|
2035
2184
|
|
|
2036
2185
|
// src/cli/commands/doctor.ts
|
|
2037
|
-
init_execution();
|
|
2038
2186
|
init_core();
|
|
2187
|
+
init_execution();
|
|
2188
|
+
init_constants2();
|
|
2039
2189
|
init_networks();
|
|
2040
2190
|
|
|
2041
2191
|
// src/protocols/a2a/constants.ts
|
|
@@ -2058,13 +2208,12 @@ var A2A_UNSUPPORTED_METHODS = [
|
|
|
2058
2208
|
|
|
2059
2209
|
// src/protocols/a2a/descriptor.ts
|
|
2060
2210
|
var A2A_UNSUPPORTED = [
|
|
2061
|
-
// Methods,
|
|
2062
|
-
// 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
|
|
2063
2212
|
...A2A_UNSUPPORTED_METHODS,
|
|
2064
|
-
// Transports other than the one binding served
|
|
2213
|
+
// Transports other than the one binding served
|
|
2065
2214
|
"HTTP+JSON/REST binding",
|
|
2066
2215
|
"gRPC binding",
|
|
2067
|
-
//
|
|
2216
|
+
// Behaviors
|
|
2068
2217
|
"SSE",
|
|
2069
2218
|
"long-running task persistence",
|
|
2070
2219
|
"task resumption",
|
|
@@ -2076,10 +2225,10 @@ var A2A_UNSUPPORTED = [
|
|
|
2076
2225
|
];
|
|
2077
2226
|
|
|
2078
2227
|
// src/cli/commands/doctor.ts
|
|
2079
|
-
|
|
2228
|
+
init_constants3();
|
|
2080
2229
|
|
|
2081
2230
|
// src/protocols/acp/descriptor.ts
|
|
2082
|
-
|
|
2231
|
+
init_constants3();
|
|
2083
2232
|
[
|
|
2084
2233
|
"rest transport",
|
|
2085
2234
|
"well-known discovery",
|
|
@@ -2096,15 +2245,15 @@ var ACP_UNSUPPORTED = [
|
|
|
2096
2245
|
"standalone orders service",
|
|
2097
2246
|
"delegate_payment",
|
|
2098
2247
|
"delegate_authentication",
|
|
2099
|
-
// Bindings and delivery directions
|
|
2248
|
+
// Bindings and delivery directions
|
|
2100
2249
|
"ACP MCP transport binding",
|
|
2101
2250
|
"webhooks",
|
|
2102
2251
|
"outbound merchant-to-agent delivery",
|
|
2103
2252
|
"ACP client role",
|
|
2104
|
-
// Request authentication beyond the bearer token
|
|
2253
|
+
// Request authentication beyond the bearer token
|
|
2105
2254
|
"Signature header verification",
|
|
2106
2255
|
"Timestamp replay-window verification",
|
|
2107
|
-
// Extensions and
|
|
2256
|
+
// Extensions, versioning and payment handlers
|
|
2108
2257
|
"discount extension",
|
|
2109
2258
|
"general ACP extension framework",
|
|
2110
2259
|
"multiple ACP API versions",
|
|
@@ -2113,7 +2262,7 @@ var ACP_UNSUPPORTED = [
|
|
|
2113
2262
|
|
|
2114
2263
|
// src/storage/receipts/store.ts
|
|
2115
2264
|
init_core();
|
|
2116
|
-
var PACKAGE_VERSION = "1.
|
|
2265
|
+
var PACKAGE_VERSION = "1.6.0" ;
|
|
2117
2266
|
|
|
2118
2267
|
// src/storage/sqlite.ts
|
|
2119
2268
|
init_core();
|
|
@@ -2121,10 +2270,7 @@ function openSqliteDatabase(options) {
|
|
|
2121
2270
|
const { path: path2, label, logger } = options;
|
|
2122
2271
|
const isFileBacked = path2 !== ":memory:";
|
|
2123
2272
|
if (isFileBacked) {
|
|
2124
|
-
|
|
2125
|
-
if (dir !== "" && dir !== "." && !existsSync(dir)) {
|
|
2126
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2127
|
-
}
|
|
2273
|
+
mkdirSync(dirname(path2), { recursive: true, mode: 448 });
|
|
2128
2274
|
}
|
|
2129
2275
|
if (isFileBacked && existsSync(path2)) {
|
|
2130
2276
|
try {
|
|
@@ -2196,8 +2342,8 @@ function receiptToRow(receipt) {
|
|
|
2196
2342
|
payment_json: receipt.payment !== void 0 ? JSON.stringify(redact(receipt.payment)) : null,
|
|
2197
2343
|
delivered_at: receipt.deliveredAt,
|
|
2198
2344
|
backend_status: receipt.backendStatus,
|
|
2199
|
-
duration_ms: receipt.durationMs
|
|
2200
|
-
protocol: receipt.protocol
|
|
2345
|
+
duration_ms: receipt.durationMs ?? null,
|
|
2346
|
+
protocol: receipt.protocol ?? null,
|
|
2201
2347
|
metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
|
|
2202
2348
|
authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
|
|
2203
2349
|
};
|
|
@@ -2224,12 +2370,12 @@ function eventToRow(event) {
|
|
|
2224
2370
|
id: event.id,
|
|
2225
2371
|
type: event.type,
|
|
2226
2372
|
request_id: event.requestId,
|
|
2227
|
-
resource_id: event.resourceId
|
|
2373
|
+
resource_id: event.resourceId ?? null,
|
|
2228
2374
|
at: event.at,
|
|
2229
|
-
adapter: event.adapter
|
|
2230
|
-
payment_provider: event.paymentProvider
|
|
2231
|
-
duration_ms: event.durationMs
|
|
2232
|
-
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,
|
|
2233
2379
|
data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null
|
|
2234
2380
|
};
|
|
2235
2381
|
}
|
|
@@ -2333,7 +2479,7 @@ var MIGRATIONS = [
|
|
|
2333
2479
|
}
|
|
2334
2480
|
}
|
|
2335
2481
|
];
|
|
2336
|
-
var SCHEMA_VERSION = MIGRATIONS
|
|
2482
|
+
var SCHEMA_VERSION = MIGRATIONS.at(-1)?.version ?? 0;
|
|
2337
2483
|
function migrate(db) {
|
|
2338
2484
|
const current = db.pragma("user_version", { simple: true });
|
|
2339
2485
|
for (const migration of MIGRATIONS) {
|
|
@@ -2396,10 +2542,8 @@ function createSqliteReceiptStore(options) {
|
|
|
2396
2542
|
);
|
|
2397
2543
|
const insertPaymentAttemptStmt = db.prepare(
|
|
2398
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)
|
|
2399
|
-
VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)
|
|
2400
|
-
|
|
2401
|
-
const getPaymentAttemptByIdStmt = db.prepare(
|
|
2402
|
-
"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 *`
|
|
2403
2547
|
);
|
|
2404
2548
|
const updatePaymentAttemptStmt = db.prepare(
|
|
2405
2549
|
`UPDATE payment_attempts SET status = @status,
|
|
@@ -2450,8 +2594,9 @@ function createSqliteReceiptStore(options) {
|
|
|
2450
2594
|
async reservePaymentAttempt(reservation) {
|
|
2451
2595
|
const now = clock.nowIso();
|
|
2452
2596
|
const id = ids.next("attempt");
|
|
2597
|
+
let row;
|
|
2453
2598
|
try {
|
|
2454
|
-
insertPaymentAttemptStmt.
|
|
2599
|
+
row = insertPaymentAttemptStmt.get({
|
|
2455
2600
|
id,
|
|
2456
2601
|
request_id: reservation.requestId,
|
|
2457
2602
|
resource_id: reservation.resourceId,
|
|
@@ -2471,19 +2616,12 @@ function createSqliteReceiptStore(options) {
|
|
|
2471
2616
|
if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
|
|
2472
2617
|
throw new CommerceError(
|
|
2473
2618
|
"PAYMENT_REPLAYED",
|
|
2474
|
-
`Payment
|
|
2619
|
+
`Payment authorization has already been used (replayKey=${reservation.replayKey})`,
|
|
2475
2620
|
{ requestId: reservation.requestId, resourceId: reservation.resourceId }
|
|
2476
2621
|
);
|
|
2477
2622
|
}
|
|
2478
2623
|
throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");
|
|
2479
2624
|
}
|
|
2480
|
-
const row = getPaymentAttemptByIdStmt.get(id);
|
|
2481
|
-
if (row === void 0) {
|
|
2482
|
-
throw new CommerceError(
|
|
2483
|
-
"STORAGE_ERROR",
|
|
2484
|
-
"Payment attempt vanished immediately after insert"
|
|
2485
|
-
);
|
|
2486
|
-
}
|
|
2487
2625
|
return rowToPaymentAttempt(row);
|
|
2488
2626
|
},
|
|
2489
2627
|
async updatePaymentAttempt(update) {
|
|
@@ -2521,11 +2659,11 @@ function createSqliteReceiptStore(options) {
|
|
|
2521
2659
|
const rows = listOptions.requestId !== void 0 ? listReceiptsByRequestStmt.all(listOptions.requestId, limit) : listReceiptsStmt.all(limit);
|
|
2522
2660
|
return rows.map(rowToReceipt);
|
|
2523
2661
|
},
|
|
2524
|
-
|
|
2662
|
+
// Exact total, not bounded by the listReceipts clamp (see ReceiptStore.countReceipts)
|
|
2525
2663
|
async countReceipts() {
|
|
2526
2664
|
return countReceiptsStmt.get()?.count ?? 0;
|
|
2527
2665
|
},
|
|
2528
|
-
|
|
2666
|
+
// Exact count of non-2xx receipts (see ReceiptStore.countUndeliveredReceipts)
|
|
2529
2667
|
async countUndeliveredReceipts() {
|
|
2530
2668
|
return countUndeliveredReceiptsStmt.get()?.count ?? 0;
|
|
2531
2669
|
},
|
|
@@ -2605,35 +2743,28 @@ function createSqliteReceiptStore(options) {
|
|
|
2605
2743
|
}
|
|
2606
2744
|
|
|
2607
2745
|
// src/cli/lib/config-client.ts
|
|
2608
|
-
|
|
2609
|
-
|
|
2746
|
+
init_core();
|
|
2747
|
+
async function importConfig() {
|
|
2610
2748
|
try {
|
|
2611
|
-
|
|
2749
|
+
return await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2612
2750
|
} catch (err) {
|
|
2613
|
-
throw new
|
|
2751
|
+
throw new CommerceError(
|
|
2752
|
+
"INTERNAL_ERROR",
|
|
2614
2753
|
`the configuration module could not be loaded: ${err instanceof Error ? err.message : String(err)}`
|
|
2615
2754
|
);
|
|
2616
2755
|
}
|
|
2617
|
-
|
|
2756
|
+
}
|
|
2757
|
+
async function loadConfigDynamic(options = {}) {
|
|
2758
|
+
return (await importConfig()).loadConfig(options);
|
|
2618
2759
|
}
|
|
2619
2760
|
async function parseConfigDynamic(raw, env = process.env) {
|
|
2620
|
-
|
|
2621
|
-
try {
|
|
2622
|
-
mod = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2623
|
-
} catch (err) {
|
|
2624
|
-
throw new Error(
|
|
2625
|
-
`the configuration module could not be loaded: ${err instanceof Error ? err.message : String(err)}`
|
|
2626
|
-
);
|
|
2627
|
-
}
|
|
2628
|
-
return mod.parseConfig(raw, env);
|
|
2761
|
+
return (await importConfig()).parseConfig(raw, env);
|
|
2629
2762
|
}
|
|
2630
2763
|
|
|
2631
2764
|
// src/cli/lib/http.ts
|
|
2632
2765
|
async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
|
|
2633
|
-
const controller = new AbortController();
|
|
2634
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2635
2766
|
try {
|
|
2636
|
-
const res = await fetchImpl(url, { signal:
|
|
2767
|
+
const res = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2637
2768
|
let body;
|
|
2638
2769
|
try {
|
|
2639
2770
|
body = await res.json();
|
|
@@ -2647,8 +2778,6 @@ async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
|
|
|
2647
2778
|
status: 0,
|
|
2648
2779
|
error: err instanceof Error ? err.message : "unknown fetch error"
|
|
2649
2780
|
};
|
|
2650
|
-
} finally {
|
|
2651
|
-
clearTimeout(timer);
|
|
2652
2781
|
}
|
|
2653
2782
|
}
|
|
2654
2783
|
|
|
@@ -2693,9 +2822,8 @@ function buildResourcesMap(answers) {
|
|
|
2693
2822
|
resources.report = {
|
|
2694
2823
|
name: "Get market report",
|
|
2695
2824
|
description: "Deterministic demo premium market report.",
|
|
2696
|
-
// No caller-supplied fields
|
|
2697
|
-
// closed schema
|
|
2698
|
-
// 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
|
|
2699
2827
|
input: {
|
|
2700
2828
|
type: "object",
|
|
2701
2829
|
properties: {},
|
|
@@ -2746,25 +2874,22 @@ function buildInitConfigObject(answers) {
|
|
|
2746
2874
|
}
|
|
2747
2875
|
var HEADER = `# Generated by \`agent-commerce init\`.
|
|
2748
2876
|
#
|
|
2749
|
-
#
|
|
2750
|
-
#
|
|
2751
|
-
# never use them outside a local chain
|
|
2752
|
-
# not
|
|
2753
|
-
# \`npm run chain:deploy\` and
|
|
2754
|
-
# MockUSDC address
|
|
2755
|
-
#
|
|
2756
|
-
# 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.
|
|
2757
2884
|
#
|
|
2758
|
-
# server.host is loopback (127.0.0.1)
|
|
2759
|
-
#
|
|
2885
|
+
# server.host is loopback (127.0.0.1): binding every interface should be a
|
|
2886
|
+
# deliberate edit.
|
|
2760
2887
|
#
|
|
2761
|
-
# server.adminToken
|
|
2762
|
-
#
|
|
2763
|
-
#
|
|
2764
|
-
#
|
|
2765
|
-
#
|
|
2766
|
-
# hashes \u2014 return 404 rather than serving openly. To enable them, uncomment
|
|
2767
|
-
# 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:
|
|
2768
2893
|
# server:
|
|
2769
2894
|
# adminToken: \${ADMIN_TOKEN}
|
|
2770
2895
|
# allowedOrigins:
|
|
@@ -2776,18 +2901,30 @@ function renderInitConfigYaml(answers) {
|
|
|
2776
2901
|
return `${HEADER}
|
|
2777
2902
|
${body}`;
|
|
2778
2903
|
}
|
|
2904
|
+
|
|
2905
|
+
// src/payments/x402/local-chain/manifest.ts
|
|
2906
|
+
init_core();
|
|
2779
2907
|
var LOCAL_CHAIN_MANIFEST_PATH = ".deploy/local.json";
|
|
2780
|
-
var
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
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
|
+
});
|
|
2787
2923
|
function readLocalChainManifest(cwd = process.cwd()) {
|
|
2788
2924
|
const path2 = join(cwd, LOCAL_CHAIN_MANIFEST_PATH);
|
|
2789
2925
|
if (!existsSync(path2)) {
|
|
2790
|
-
throw new
|
|
2926
|
+
throw new CommerceError(
|
|
2927
|
+
"CONFIG_INVALID",
|
|
2791
2928
|
`Local chain manifest not found at "${path2}". Run "npm run chain:start" then "npm run chain:deploy" first.`
|
|
2792
2929
|
);
|
|
2793
2930
|
}
|
|
@@ -2795,56 +2932,30 @@ function readLocalChainManifest(cwd = process.cwd()) {
|
|
|
2795
2932
|
try {
|
|
2796
2933
|
raw = readFileSync(path2, "utf8");
|
|
2797
2934
|
} catch (cause) {
|
|
2798
|
-
throw new
|
|
2935
|
+
throw new CommerceError("CONFIG_INVALID", `Could not read local chain manifest at "${path2}".`, {
|
|
2936
|
+
cause
|
|
2937
|
+
});
|
|
2799
2938
|
}
|
|
2800
2939
|
let parsed;
|
|
2801
2940
|
try {
|
|
2802
2941
|
parsed = JSON.parse(raw);
|
|
2803
2942
|
} catch (cause) {
|
|
2804
|
-
throw new
|
|
2943
|
+
throw new CommerceError(
|
|
2944
|
+
"CONFIG_INVALID",
|
|
2805
2945
|
`Local chain manifest at "${path2}" is not valid JSON. Re-run "npm run chain:deploy" to regenerate it.`,
|
|
2806
2946
|
{ cause }
|
|
2807
2947
|
);
|
|
2808
2948
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
throw new
|
|
2814
|
-
|
|
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".`
|
|
2815
2956
|
);
|
|
2816
|
-
};
|
|
2817
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2818
|
-
return fail("expected a JSON object");
|
|
2819
|
-
}
|
|
2820
|
-
const record = value;
|
|
2821
|
-
if (typeof record.chainId !== "number") {
|
|
2822
|
-
return fail('"chainId" must be a number');
|
|
2823
2957
|
}
|
|
2824
|
-
|
|
2825
|
-
return fail('"assetDecimals" must be a number');
|
|
2826
|
-
}
|
|
2827
|
-
for (const field of REQUIRED_TOP_LEVEL_STRING_FIELDS) {
|
|
2828
|
-
if (typeof record[field] !== "string" || record[field] === "") {
|
|
2829
|
-
return fail(`"${field}" must be a non-empty string`);
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
if ("hostRpcUrl" in record && record.hostRpcUrl !== void 0) {
|
|
2833
|
-
if (typeof record.hostRpcUrl !== "string" || record.hostRpcUrl === "") {
|
|
2834
|
-
return fail('"hostRpcUrl" must be a non-empty string when present');
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
const merchant = record.merchant;
|
|
2838
|
-
if (typeof merchant !== "object" || merchant === null || typeof merchant.address !== "string" || typeof merchant.privateKeyLabel !== "string") {
|
|
2839
|
-
return fail('"merchant" must be { address: string, privateKeyLabel: string }');
|
|
2840
|
-
}
|
|
2841
|
-
for (const key of ["buyer", "facilitator"]) {
|
|
2842
|
-
const account = record[key];
|
|
2843
|
-
if (typeof account !== "object" || account === null || typeof account.address !== "string" || typeof account.privateKey !== "string" || typeof account.note !== "string") {
|
|
2844
|
-
return fail(`"${key}" must be { address: string, privateKey: string, note: string }`);
|
|
2845
|
-
}
|
|
2846
|
-
}
|
|
2847
|
-
return record;
|
|
2958
|
+
return result.data;
|
|
2848
2959
|
}
|
|
2849
2960
|
|
|
2850
2961
|
// src/cli/lib/manifest-env.ts
|
|
@@ -2882,12 +2993,63 @@ function maskMiddle(value, keep = 6) {
|
|
|
2882
2993
|
if (value.length <= keep * 2) return value.length === 0 ? value : "\u2026";
|
|
2883
2994
|
return `${value.slice(0, keep)}\u2026${value.slice(-4)}`;
|
|
2884
2995
|
}
|
|
2885
|
-
|
|
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.0",
|
|
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
|
+
];
|
|
2886
3043
|
function readVersionReport() {
|
|
2887
|
-
const
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
}
|
|
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 };
|
|
2891
3053
|
}
|
|
2892
3054
|
|
|
2893
3055
|
// src/cli/commands/doctor.ts
|
|
@@ -2905,19 +3067,20 @@ function deriveGatewayUrl(explicit, config) {
|
|
|
2905
3067
|
function substitutePathParams(url) {
|
|
2906
3068
|
let filled = url;
|
|
2907
3069
|
for (const name of extractPathParameterNames(url)) {
|
|
2908
|
-
filled = filled.
|
|
3070
|
+
filled = filled.replaceAll(`{${name}}`, "demo-check");
|
|
2909
3071
|
}
|
|
2910
3072
|
return filled;
|
|
2911
3073
|
}
|
|
2912
|
-
function extractWellKnownX402(body) {
|
|
3074
|
+
function extractWellKnownX402(body, rail = "x402") {
|
|
2913
3075
|
const payments = body?.["payments"];
|
|
2914
3076
|
if (typeof payments !== "object" || payments === null) return void 0;
|
|
2915
|
-
const
|
|
2916
|
-
if (typeof
|
|
2917
|
-
const rec =
|
|
3077
|
+
const block = payments[rail];
|
|
3078
|
+
if (typeof block !== "object" || block === null) return void 0;
|
|
3079
|
+
const rec = block;
|
|
2918
3080
|
if (rec["enabled"] === false) return "disabled";
|
|
2919
3081
|
if (rec["enabled"] !== true) return void 0;
|
|
2920
|
-
const { asset, network
|
|
3082
|
+
const { asset, network } = rec;
|
|
3083
|
+
const payTo = rec[rail === "mpp" ? "recipient" : "payTo"];
|
|
2921
3084
|
if (typeof asset !== "string" || typeof network !== "string" || typeof payTo !== "string") {
|
|
2922
3085
|
return void 0;
|
|
2923
3086
|
}
|
|
@@ -2926,7 +3089,7 @@ function extractWellKnownX402(body) {
|
|
|
2926
3089
|
function sameAddress2(a, b) {
|
|
2927
3090
|
return a.toLowerCase() === b.toLowerCase();
|
|
2928
3091
|
}
|
|
2929
|
-
function findX402Mismatch(configured, live) {
|
|
3092
|
+
function findX402Mismatch(configured, live, payToField = "payTo") {
|
|
2930
3093
|
const diffs = [];
|
|
2931
3094
|
if (!sameAddress2(configured.asset, live.asset)) {
|
|
2932
3095
|
diffs.push(
|
|
@@ -2940,11 +3103,26 @@ function findX402Mismatch(configured, live) {
|
|
|
2940
3103
|
}
|
|
2941
3104
|
if (!sameAddress2(configured.payTo, live.payTo)) {
|
|
2942
3105
|
diffs.push(
|
|
2943
|
-
|
|
3106
|
+
`${payToField}: gateway is using ${live.payTo} but local config resolves to ${configured.payTo}`
|
|
2944
3107
|
);
|
|
2945
3108
|
}
|
|
2946
3109
|
if (diffs.length === 0) return void 0;
|
|
2947
|
-
return `${diffs.join("; ")}
|
|
3110
|
+
return `${diffs.join("; ")}; the gateway may be running against an older deployment - restart it or re-run chain:deploy`;
|
|
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
|
+
};
|
|
2948
3126
|
}
|
|
2949
3127
|
function describeIssuers(label, issuers) {
|
|
2950
3128
|
const described = issuers.map(
|
|
@@ -2960,72 +3138,26 @@ function isWritableFile(path2) {
|
|
|
2960
3138
|
return false;
|
|
2961
3139
|
}
|
|
2962
3140
|
}
|
|
2963
|
-
function
|
|
2964
|
-
const name = "AP2 replay store";
|
|
3141
|
+
function storeFileCheck(name, path2, risk, suffix = "") {
|
|
2965
3142
|
if (path2 === ":memory:") {
|
|
2966
3143
|
return {
|
|
2967
3144
|
name,
|
|
2968
3145
|
status: "WARN",
|
|
2969
|
-
detail:
|
|
3146
|
+
detail: `in-memory store: ${risk.inMemory}; use a file path in production${suffix}`
|
|
2970
3147
|
};
|
|
2971
3148
|
}
|
|
2972
3149
|
if (existsSync(path2)) {
|
|
2973
|
-
return isWritableFile(path2) ? { name, status: "PASS", detail: `writable at "${path2}"` } : {
|
|
3150
|
+
return isWritableFile(path2) ? { name, status: "PASS", detail: `writable at "${path2}"${suffix}` } : {
|
|
2974
3151
|
name,
|
|
2975
3152
|
status: "FAIL",
|
|
2976
|
-
detail: `"${path2}" exists but is not writable by this user
|
|
3153
|
+
detail: `"${path2}" exists but is not writable by this user, so ${risk.unwritable}${suffix}`
|
|
2977
3154
|
};
|
|
2978
3155
|
}
|
|
2979
|
-
|
|
2980
|
-
if (directory !== "" && directory !== "." && !existsSync(directory)) {
|
|
2981
|
-
return {
|
|
2982
|
-
name,
|
|
2983
|
-
status: "WARN",
|
|
2984
|
-
detail: `no store yet at "${path2}" and its directory does not exist - it is created when the gateway starts, provided that path is writable`
|
|
2985
|
-
};
|
|
2986
|
-
}
|
|
2987
|
-
return {
|
|
2988
|
-
name,
|
|
2989
|
-
status: "WARN",
|
|
2990
|
-
detail: `no store yet at "${path2}" - it is created the first time the gateway starts`
|
|
2991
|
-
};
|
|
3156
|
+
return { name, status: "WARN", detail: `${missingStoreDetail(path2)}${suffix}` };
|
|
2992
3157
|
}
|
|
2993
|
-
function
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
return {
|
|
2997
|
-
name: "ACP idempotency",
|
|
2998
|
-
status: "FAIL",
|
|
2999
|
-
detail: `${retention} is below the 24h ACP minimum \u2014 a replayed key could run a checkout twice`
|
|
3000
|
-
};
|
|
3001
|
-
}
|
|
3002
|
-
if (idempotency.path === ":memory:") {
|
|
3003
|
-
return {
|
|
3004
|
-
name: "ACP idempotency",
|
|
3005
|
-
status: "WARN",
|
|
3006
|
-
detail: `in-memory store, ${retention} \u2014 replay protection is lost on every restart; use a file path in production`
|
|
3007
|
-
};
|
|
3008
|
-
}
|
|
3009
|
-
const directory = dirname(idempotency.path);
|
|
3010
|
-
if (existsSync(idempotency.path)) {
|
|
3011
|
-
return isWritableFile(idempotency.path) ? { name: "ACP idempotency", status: "PASS", detail: `writable, ${retention}` } : {
|
|
3012
|
-
name: "ACP idempotency",
|
|
3013
|
-
status: "FAIL",
|
|
3014
|
-
detail: `"${idempotency.path}" exists but is not writable by this user \u2014 the adapter cannot claim idempotency keys`
|
|
3015
|
-
};
|
|
3016
|
-
}
|
|
3017
|
-
if (directory !== "" && directory !== "." && !existsSync(directory)) {
|
|
3018
|
-
return {
|
|
3019
|
-
name: "ACP idempotency",
|
|
3020
|
-
status: "WARN",
|
|
3021
|
-
detail: `no store yet at "${idempotency.path}" and its directory does not exist \u2014 it is created when the gateway starts, provided that path is writable`
|
|
3022
|
-
};
|
|
3023
|
-
}
|
|
3024
|
-
return {
|
|
3025
|
-
name: "ACP idempotency",
|
|
3026
|
-
status: "WARN",
|
|
3027
|
-
detail: `no store yet at "${idempotency.path}" \u2014 it is created the first time the gateway starts (${retention})`
|
|
3028
|
-
};
|
|
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`;
|
|
3029
3161
|
}
|
|
3030
3162
|
function acpMappingCheck(config, operations) {
|
|
3031
3163
|
const byId = new Map(config.resources.map((resource) => [resource.id, resource]));
|
|
@@ -3069,11 +3201,11 @@ async function runDoctor(options, deps = {}) {
|
|
|
3069
3201
|
checks.push({
|
|
3070
3202
|
name: "Config",
|
|
3071
3203
|
status: "PASS",
|
|
3072
|
-
detail: `valid
|
|
3204
|
+
detail: `valid: ${config.resources.length} resource(s), merchant "${config.merchant.name}"${filledSuffix}`
|
|
3073
3205
|
});
|
|
3074
3206
|
} catch (err) {
|
|
3075
3207
|
const variable = isCommerceError(err) ? err.details?.["variable"] : void 0;
|
|
3076
|
-
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` : "";
|
|
3077
3209
|
checks.push({
|
|
3078
3210
|
name: "Config",
|
|
3079
3211
|
status: "FAIL",
|
|
@@ -3101,7 +3233,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3101
3233
|
);
|
|
3102
3234
|
}
|
|
3103
3235
|
if (config === void 0) {
|
|
3104
|
-
checks.push({ name: "Backend", status: "WARN", detail: "skipped
|
|
3236
|
+
checks.push({ name: "Backend", status: "WARN", detail: "skipped: config invalid" });
|
|
3105
3237
|
} else if (config.resources.length === 0) {
|
|
3106
3238
|
checks.push({ name: "Backend", status: "INFO", detail: "no resources configured" });
|
|
3107
3239
|
} else {
|
|
@@ -3124,7 +3256,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3124
3256
|
CHECK_TIMEOUT_MS
|
|
3125
3257
|
) : void 0;
|
|
3126
3258
|
if (config === void 0) {
|
|
3127
|
-
checks.push({ name: "Protocols", status: "WARN", detail: "skipped
|
|
3259
|
+
checks.push({ name: "Protocols", status: "WARN", detail: "skipped: config invalid" });
|
|
3128
3260
|
} else if (wellKnown === void 0 || !wellKnown.ok) {
|
|
3129
3261
|
checks.push({ name: "Protocols", status: "FAIL", detail: "well-known document unreachable" });
|
|
3130
3262
|
} else {
|
|
@@ -3137,7 +3269,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3137
3269
|
});
|
|
3138
3270
|
}
|
|
3139
3271
|
if (config === void 0) {
|
|
3140
|
-
checks.push({ name: "A2A", status: "WARN", detail: "skipped
|
|
3272
|
+
checks.push({ name: "A2A", status: "WARN", detail: "skipped: config invalid" });
|
|
3141
3273
|
} else if (!config.protocols.a2a.enabled) {
|
|
3142
3274
|
checks.push({ name: "A2A", status: "INFO", detail: "disabled" });
|
|
3143
3275
|
} else {
|
|
@@ -3153,7 +3285,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3153
3285
|
});
|
|
3154
3286
|
}
|
|
3155
3287
|
if (config === void 0) {
|
|
3156
|
-
checks.push({ name: "ACP", status: "WARN", detail: "skipped
|
|
3288
|
+
checks.push({ name: "ACP", status: "WARN", detail: "skipped: config invalid" });
|
|
3157
3289
|
} else if (!config.protocols.acp.enabled) {
|
|
3158
3290
|
checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
|
|
3159
3291
|
} else {
|
|
@@ -3168,7 +3300,17 @@ async function runDoctor(options, deps = {}) {
|
|
|
3168
3300
|
status: "PASS",
|
|
3169
3301
|
detail: `bearer token configured (${acp.auth.token.length} characters, not shown)`
|
|
3170
3302
|
});
|
|
3171
|
-
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
|
+
);
|
|
3172
3314
|
checks.push(acpMappingCheck(config, acp.checkout.operations));
|
|
3173
3315
|
checks.push({
|
|
3174
3316
|
name: "ACP unsupported",
|
|
@@ -3178,7 +3320,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3178
3320
|
}
|
|
3179
3321
|
const ap2 = config?.authorization?.ap2;
|
|
3180
3322
|
if (config === void 0) {
|
|
3181
|
-
checks.push({ name: "AP2", status: "WARN", detail: "skipped
|
|
3323
|
+
checks.push({ name: "AP2", status: "WARN", detail: "skipped: config invalid" });
|
|
3182
3324
|
} else if (ap2 === void 0 || !ap2.enabled) {
|
|
3183
3325
|
checks.push({ name: "AP2", status: "INFO", detail: "disabled" });
|
|
3184
3326
|
} else {
|
|
@@ -3192,14 +3334,19 @@ async function runDoctor(options, deps = {}) {
|
|
|
3192
3334
|
status: "PASS",
|
|
3193
3335
|
detail: `${describeIssuers("mandate", ap2.trust.mandateIssuers)} \xB7 ${describeIssuers("checkout", ap2.trust.checkoutIssuers)}`
|
|
3194
3336
|
});
|
|
3195
|
-
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
|
+
);
|
|
3196
3343
|
const gated = config.resources.filter(
|
|
3197
3344
|
(resource) => resource.authorization?.required.includes("ap2")
|
|
3198
3345
|
);
|
|
3199
3346
|
checks.push({
|
|
3200
3347
|
name: "AP2 resources",
|
|
3201
3348
|
status: gated.length > 0 ? "PASS" : "WARN",
|
|
3202
|
-
detail: gated.length > 0 ? gated.map((resource) => resource.id).join(", ") : "AP2 is enabled but no resource requires it
|
|
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"
|
|
3203
3350
|
});
|
|
3204
3351
|
checks.push({
|
|
3205
3352
|
name: "AP2 unsupported",
|
|
@@ -3211,23 +3358,20 @@ async function runDoctor(options, deps = {}) {
|
|
|
3211
3358
|
if (x402 === void 0 || !x402.enabled) {
|
|
3212
3359
|
checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
|
|
3213
3360
|
} else {
|
|
3214
|
-
const
|
|
3215
|
-
const
|
|
3216
|
-
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})`;
|
|
3217
|
-
const facilitatorDetail = x402.facilitator.mode === "local" ? "local" : `remote (auth=${x402.facilitator.auth.type})`;
|
|
3218
|
-
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}`;
|
|
3219
3363
|
const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body) : void 0;
|
|
3220
3364
|
if (sameAddress2(x402.asset, PLACEHOLDER_ASSET_ADDRESS)) {
|
|
3221
3365
|
checks.push({
|
|
3222
3366
|
name: "Payments",
|
|
3223
3367
|
status: "WARN",
|
|
3224
|
-
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)`
|
|
3225
3369
|
});
|
|
3226
3370
|
} else if (live === "disabled") {
|
|
3227
3371
|
checks.push({
|
|
3228
3372
|
name: "Payments",
|
|
3229
3373
|
status: "FAIL",
|
|
3230
|
-
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`
|
|
3231
3375
|
});
|
|
3232
3376
|
} else if (live === void 0) {
|
|
3233
3377
|
checks.push({
|
|
@@ -3242,35 +3386,53 @@ async function runDoctor(options, deps = {}) {
|
|
|
3242
3386
|
);
|
|
3243
3387
|
}
|
|
3244
3388
|
if (mode === "mainnet") {
|
|
3245
|
-
|
|
3389
|
+
checks.push(mainnetSafetyCheck("Mainnet safety", "payTo", x402.facilitator));
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
const mpp = config?.payments.mpp;
|
|
3393
|
+
if (mpp === void 0 || !mpp.enabled) {
|
|
3394
|
+
checks.push({ name: "Payments (MPP)", status: "INFO", detail: "MPP not configured" });
|
|
3395
|
+
} else {
|
|
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}`;
|
|
3398
|
+
const configured = { asset: mpp.asset, network: mpp.network, payTo: mpp.recipient };
|
|
3399
|
+
const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body, "mpp") : void 0;
|
|
3400
|
+
if (live === "disabled") {
|
|
3246
3401
|
checks.push({
|
|
3247
|
-
name: "
|
|
3248
|
-
status:
|
|
3249
|
-
detail:
|
|
3402
|
+
name: "Payments (MPP)",
|
|
3403
|
+
status: "FAIL",
|
|
3404
|
+
detail: `Local config enables MPP, but ${gatewayUrl} reports it disabled`
|
|
3250
3405
|
});
|
|
3406
|
+
} else if (live === void 0) {
|
|
3407
|
+
checks.push({
|
|
3408
|
+
name: "Payments (MPP)",
|
|
3409
|
+
status: "INFO",
|
|
3410
|
+
detail: `${summary} (live MPP settlement config unavailable for comparison)`
|
|
3411
|
+
});
|
|
3412
|
+
} else {
|
|
3413
|
+
const mismatch = findX402Mismatch(configured, live, "recipient");
|
|
3414
|
+
checks.push(
|
|
3415
|
+
mismatch === void 0 ? { name: "Payments (MPP)", status: "PASS", detail: summary } : { name: "Payments (MPP)", status: "FAIL", detail: mismatch }
|
|
3416
|
+
);
|
|
3417
|
+
}
|
|
3418
|
+
if (mode === "mainnet") {
|
|
3419
|
+
checks.push(mainnetSafetyCheck("Mainnet safety (MPP)", "recipient", mpp.facilitator));
|
|
3251
3420
|
}
|
|
3252
3421
|
}
|
|
3253
|
-
checks.push({
|
|
3254
|
-
name: "Payments (MPP)",
|
|
3255
|
-
status: "INFO",
|
|
3256
|
-
detail: "planned \u2014 not implemented in this release"
|
|
3257
|
-
});
|
|
3258
3422
|
if (config === void 0) {
|
|
3259
|
-
checks.push({ name: "Storage", status: "WARN", detail: "skipped
|
|
3423
|
+
checks.push({ name: "Storage", status: "WARN", detail: "skipped: config invalid" });
|
|
3260
3424
|
} else if (
|
|
3261
|
-
// Diagnose without creating: createSqliteReceiptStore
|
|
3262
|
-
//
|
|
3263
|
-
//
|
|
3264
|
-
//
|
|
3265
|
-
//
|
|
3266
|
-
// `:memory:` never exists on disk by definition, so it always falls
|
|
3267
|
-
// 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.
|
|
3268
3430
|
config.storage.receipts.path !== ":memory:" && !existsSync(config.storage.receipts.path)
|
|
3269
3431
|
) {
|
|
3270
3432
|
checks.push({
|
|
3271
3433
|
name: "Storage",
|
|
3272
3434
|
status: "WARN",
|
|
3273
|
-
detail:
|
|
3435
|
+
detail: `${missingStoreDetail(config.storage.receipts.path)}. If the gateway is already running, this path does not match the one it uses.`
|
|
3274
3436
|
});
|
|
3275
3437
|
} else {
|
|
3276
3438
|
let store;
|
|
@@ -3307,7 +3469,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3307
3469
|
await store?.close();
|
|
3308
3470
|
}
|
|
3309
3471
|
}
|
|
3310
|
-
if (wellKnown
|
|
3472
|
+
if (wellKnown?.ok) {
|
|
3311
3473
|
checks.push({
|
|
3312
3474
|
name: "Protocol versions",
|
|
3313
3475
|
status: "PASS",
|
|
@@ -3318,7 +3480,7 @@ async function runDoctor(options, deps = {}) {
|
|
|
3318
3480
|
checks.push({
|
|
3319
3481
|
name: "Protocol versions",
|
|
3320
3482
|
status: "INFO",
|
|
3321
|
-
detail: `gateway unreachable
|
|
3483
|
+
detail: `gateway unreachable; local pins: ${local.pinned.map((p) => `${p.name}@${p.version}`).join(", ")}`
|
|
3322
3484
|
});
|
|
3323
3485
|
}
|
|
3324
3486
|
const scored = checks.filter((c) => c.status !== "INFO");
|
|
@@ -3357,6 +3519,8 @@ init_core();
|
|
|
3357
3519
|
// src/openapi/discover.ts
|
|
3358
3520
|
init_errors();
|
|
3359
3521
|
init_execution();
|
|
3522
|
+
init_is_record();
|
|
3523
|
+
init_constants4();
|
|
3360
3524
|
|
|
3361
3525
|
// src/openapi/refs.ts
|
|
3362
3526
|
init_errors();
|
|
@@ -3397,7 +3561,7 @@ function dereference(document, node, stack = []) {
|
|
|
3397
3561
|
}
|
|
3398
3562
|
function resolvePointer(document, ref) {
|
|
3399
3563
|
const pointer = ref.slice(1);
|
|
3400
|
-
if (pointer === ""
|
|
3564
|
+
if (pointer === "") return document;
|
|
3401
3565
|
if (!pointer.startsWith("/")) {
|
|
3402
3566
|
throw new CommerceError(
|
|
3403
3567
|
"CONFIG_INVALID",
|
|
@@ -3407,12 +3571,22 @@ function resolvePointer(document, ref) {
|
|
|
3407
3571
|
}
|
|
3408
3572
|
let current = document;
|
|
3409
3573
|
for (const rawSegment of pointer.slice(1).split("/")) {
|
|
3410
|
-
|
|
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", "~");
|
|
3411
3585
|
if (Array.isArray(current)) {
|
|
3412
3586
|
const index = Number(segment);
|
|
3413
3587
|
current = Number.isInteger(index) ? current[index] : void 0;
|
|
3414
3588
|
} else if (typeof current === "object" && current !== null) {
|
|
3415
|
-
current = current[segment];
|
|
3589
|
+
current = Object.hasOwn(current, segment) ? current[segment] : void 0;
|
|
3416
3590
|
} else {
|
|
3417
3591
|
current = void 0;
|
|
3418
3592
|
}
|
|
@@ -3436,8 +3610,7 @@ var SUPPORTED_METHODS = {
|
|
|
3436
3610
|
delete: "DELETE"
|
|
3437
3611
|
};
|
|
3438
3612
|
var NON_OPERATION_KEYS = /* @__PURE__ */ new Set(["summary", "description", "servers", "parameters", "$ref"]);
|
|
3439
|
-
var
|
|
3440
|
-
var MAX_ID_LENGTH = 128;
|
|
3613
|
+
var ID_DISALLOWED = new RegExp(`[^${MCP_TOOL_NAME_CHARS}]+`, "g");
|
|
3441
3614
|
function discoverOperations(loaded, options = {}) {
|
|
3442
3615
|
const { document } = loaded;
|
|
3443
3616
|
const diagnostics = [];
|
|
@@ -3463,16 +3636,24 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3463
3636
|
}
|
|
3464
3637
|
const pathItem = dereference(document, rawPathItem).value;
|
|
3465
3638
|
if (!isRecord(pathItem)) continue;
|
|
3639
|
+
const unsupportedMethod = (name) => {
|
|
3640
|
+
diagnostics.push({
|
|
3641
|
+
severity: "warning",
|
|
3642
|
+
code: "unsupported-method",
|
|
3643
|
+
operation: `${name.toUpperCase()} ${path2}`,
|
|
3644
|
+
message: `Skipped ${name.toUpperCase()} ${path2}: only GET, POST, PUT, PATCH and DELETE are supported`
|
|
3645
|
+
});
|
|
3646
|
+
};
|
|
3466
3647
|
for (const [key, rawOperation] of Object.entries(pathItem)) {
|
|
3467
3648
|
if (NON_OPERATION_KEYS.has(key) || key.startsWith("x-")) continue;
|
|
3649
|
+
if (key === "additionalOperations") {
|
|
3650
|
+
if (isRecord(rawOperation))
|
|
3651
|
+
for (const name of Object.keys(rawOperation)) unsupportedMethod(name);
|
|
3652
|
+
continue;
|
|
3653
|
+
}
|
|
3468
3654
|
const method = SUPPORTED_METHODS[key.toLowerCase()];
|
|
3469
3655
|
if (method === void 0) {
|
|
3470
|
-
|
|
3471
|
-
severity: "warning",
|
|
3472
|
-
code: "unsupported-method",
|
|
3473
|
-
operation: `${key.toUpperCase()} ${path2}`,
|
|
3474
|
-
message: `Skipped ${key.toUpperCase()} ${path2}: only GET, POST, PUT, PATCH and DELETE are supported`
|
|
3475
|
-
});
|
|
3656
|
+
unsupportedMethod(key);
|
|
3476
3657
|
continue;
|
|
3477
3658
|
}
|
|
3478
3659
|
const operation = dereference(document, rawOperation).value;
|
|
@@ -3522,8 +3703,8 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3522
3703
|
...description !== void 0 ? { description } : {},
|
|
3523
3704
|
parameters: [
|
|
3524
3705
|
...toArray(pathItem["parameters"]),
|
|
3525
|
-
// Operation parameters last:
|
|
3526
|
-
// 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`)
|
|
3527
3708
|
...toArray(operation["parameters"])
|
|
3528
3709
|
],
|
|
3529
3710
|
...operation["requestBody"] !== void 0 ? { requestBody: operation["requestBody"] } : {},
|
|
@@ -3535,12 +3716,12 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3535
3716
|
return { operations, diagnostics };
|
|
3536
3717
|
}
|
|
3537
3718
|
function toResourceId(operationId, method, path2) {
|
|
3538
|
-
const fromOperationId = operationId === void 0 ? "" :
|
|
3719
|
+
const fromOperationId = operationId === void 0 ? "" : normalizeId(operationId);
|
|
3539
3720
|
if (fromOperationId !== "") return fromOperationId;
|
|
3540
|
-
return
|
|
3721
|
+
return normalizeId(`${method.toLowerCase()}_${path2}`);
|
|
3541
3722
|
}
|
|
3542
|
-
function
|
|
3543
|
-
return value.replace(
|
|
3723
|
+
function normalizeId(value) {
|
|
3724
|
+
return value.replace(ID_DISALLOWED, "_").replace(/_+/g, "_").replace(/^[_.-]+|[_.-]+$/g, "").slice(0, MCP_TOOL_NAME_MAX_LENGTH);
|
|
3544
3725
|
}
|
|
3545
3726
|
function selectServer(document, candidates, context, options, diagnostics) {
|
|
3546
3727
|
if (options.baseUrl !== void 0) return options.baseUrl;
|
|
@@ -3552,12 +3733,13 @@ function selectServer(document, candidates, context, options, diagnostics) {
|
|
|
3552
3733
|
if (typeof url !== "string" || url === "") continue;
|
|
3553
3734
|
const substituted = substituteServerVariables(url, first["variables"], context, diagnostics);
|
|
3554
3735
|
if (substituted === void 0) return void 0;
|
|
3555
|
-
|
|
3736
|
+
const issue = describeServerUrlIssue(substituted);
|
|
3737
|
+
if (issue !== void 0) {
|
|
3556
3738
|
diagnostics.push({
|
|
3557
3739
|
severity: "error",
|
|
3558
|
-
code:
|
|
3740
|
+
code: issue.code,
|
|
3559
3741
|
operation: context.resourceId,
|
|
3560
|
-
message: `Skipped ${context.where}: server URL "${substituted}"
|
|
3742
|
+
message: `Skipped ${context.where}: server URL "${substituted}" ${issue.reason}. Pass --base-url`
|
|
3561
3743
|
});
|
|
3562
3744
|
return void 0;
|
|
3563
3745
|
}
|
|
@@ -3606,12 +3788,26 @@ function substituteServerVariables(url, variables, context, diagnostics) {
|
|
|
3606
3788
|
return substituted;
|
|
3607
3789
|
}
|
|
3608
3790
|
function isAbsoluteHttpUrl(value) {
|
|
3791
|
+
return describeServerUrlIssue(value) === void 0;
|
|
3792
|
+
}
|
|
3793
|
+
function describeServerUrlIssue(value) {
|
|
3609
3794
|
try {
|
|
3610
3795
|
const parsed = new URL(value);
|
|
3611
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
3612
|
-
|
|
3796
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
3797
|
+
return { code: "invalid-server-url", reason: "must use http:// or https://" };
|
|
3798
|
+
}
|
|
3799
|
+
if (parsed.search !== "" || parsed.hash !== "") {
|
|
3800
|
+
return {
|
|
3801
|
+
code: "invalid-server-url",
|
|
3802
|
+
reason: "must not contain a query string or fragment"
|
|
3803
|
+
};
|
|
3804
|
+
}
|
|
3805
|
+
return void 0;
|
|
3613
3806
|
} catch {
|
|
3614
|
-
return
|
|
3807
|
+
return {
|
|
3808
|
+
code: "relative-server-url",
|
|
3809
|
+
reason: "is relative or malformed, so no backend host is known"
|
|
3810
|
+
};
|
|
3615
3811
|
}
|
|
3616
3812
|
}
|
|
3617
3813
|
function assertAbsoluteHttpUrl(value, label) {
|
|
@@ -3623,18 +3819,20 @@ function assertAbsoluteHttpUrl(value, label) {
|
|
|
3623
3819
|
);
|
|
3624
3820
|
}
|
|
3625
3821
|
}
|
|
3626
|
-
function isRecord(value) {
|
|
3627
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3628
|
-
}
|
|
3629
3822
|
function toArray(value) {
|
|
3630
3823
|
return Array.isArray(value) ? value : [];
|
|
3631
3824
|
}
|
|
3632
3825
|
|
|
3826
|
+
// src/openapi/draft.ts
|
|
3827
|
+
init_is_record();
|
|
3828
|
+
|
|
3633
3829
|
// src/openapi/request.ts
|
|
3634
3830
|
init_execution();
|
|
3831
|
+
init_is_record();
|
|
3635
3832
|
|
|
3636
3833
|
// src/openapi/schema.ts
|
|
3637
3834
|
init_errors();
|
|
3835
|
+
init_is_record();
|
|
3638
3836
|
var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
3639
3837
|
"object",
|
|
3640
3838
|
"string",
|
|
@@ -3665,7 +3863,7 @@ function convertSchema(document, node, stack = []) {
|
|
|
3665
3863
|
const dropped = /* @__PURE__ */ new Set();
|
|
3666
3864
|
try {
|
|
3667
3865
|
const schema = convertNode(document, node, stack, dropped);
|
|
3668
|
-
return
|
|
3866
|
+
return { supported: true, schema, dropped: [...dropped] };
|
|
3669
3867
|
} catch (error) {
|
|
3670
3868
|
if (error instanceof UnsupportedSchema) return { supported: false, reason: error.message };
|
|
3671
3869
|
if (isCommerceError(error)) return { supported: false, reason: error.message };
|
|
@@ -3678,11 +3876,8 @@ function convertNode(document, node, stack, dropped) {
|
|
|
3678
3876
|
if (node === true) return {};
|
|
3679
3877
|
if (node === false) throw new UnsupportedSchema("schema is `false`, which accepts no value");
|
|
3680
3878
|
const resolved = dereference(document, node, stack);
|
|
3681
|
-
const
|
|
3682
|
-
if (
|
|
3683
|
-
throw new UnsupportedSchema("schema node is not an object");
|
|
3684
|
-
}
|
|
3685
|
-
const schemaNode = source;
|
|
3879
|
+
const schemaNode = resolved.value;
|
|
3880
|
+
if (!isRecord(schemaNode)) throw new UnsupportedSchema("schema node is not an object");
|
|
3686
3881
|
if (Array.isArray(schemaNode["allOf"])) {
|
|
3687
3882
|
return mergeAllOf(document, schemaNode, resolved.stack, dropped);
|
|
3688
3883
|
}
|
|
@@ -3701,11 +3896,15 @@ function convertNode(document, node, stack, dropped) {
|
|
|
3701
3896
|
}
|
|
3702
3897
|
if (Array.isArray(schemaNode["enum"])) result["enum"] = [...schemaNode["enum"]];
|
|
3703
3898
|
const properties = schemaNode["properties"];
|
|
3704
|
-
if (
|
|
3899
|
+
if (isRecord(properties)) {
|
|
3705
3900
|
const converted = {};
|
|
3706
3901
|
for (const [name, sub] of Object.entries(properties)) {
|
|
3707
|
-
|
|
3708
|
-
|
|
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);
|
|
3709
3908
|
}
|
|
3710
3909
|
result["properties"] = converted;
|
|
3711
3910
|
}
|
|
@@ -3718,17 +3917,15 @@ function convertNode(document, node, stack, dropped) {
|
|
|
3718
3917
|
if (typeof additional === "boolean") {
|
|
3719
3918
|
result["additionalProperties"] = additional;
|
|
3720
3919
|
} else if (additional !== void 0) {
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
} else if (isRecord2(properties)) {
|
|
3920
|
+
result["additionalProperties"] = convertNode(document, additional, resolved.stack, dropped);
|
|
3921
|
+
} else if (isRecord(properties)) {
|
|
3724
3922
|
result["additionalProperties"] = false;
|
|
3725
3923
|
}
|
|
3726
3924
|
const items = schemaNode["items"];
|
|
3727
3925
|
if (Array.isArray(items)) {
|
|
3728
3926
|
dropped.add("items (tuple form)");
|
|
3729
3927
|
} else if (items !== void 0) {
|
|
3730
|
-
|
|
3731
|
-
if (child !== void 0) result["items"] = child;
|
|
3928
|
+
result["items"] = convertNode(document, items, resolved.stack, dropped);
|
|
3732
3929
|
}
|
|
3733
3930
|
for (const keyword of Object.keys(schemaNode)) {
|
|
3734
3931
|
if (isCarriedKeyword(keyword) || ANNOTATION_KEYWORDS.has(keyword) || keyword.startsWith("x-")) {
|
|
@@ -3765,19 +3962,19 @@ function mergeAllOf(document, schema, stack, dropped) {
|
|
|
3765
3962
|
const merged = { type: "object", properties: {}, required: [] };
|
|
3766
3963
|
const properties = merged["properties"];
|
|
3767
3964
|
const required = /* @__PURE__ */ new Set();
|
|
3965
|
+
let open2 = false;
|
|
3768
3966
|
const parts = [...branches, ...Object.keys(siblings).length > 0 ? [siblings] : []];
|
|
3769
3967
|
for (const branch of parts) {
|
|
3770
3968
|
const converted = convertNode(document, branch, stack, dropped);
|
|
3771
|
-
if (converted === void 0) throw new UnsupportedSchema("allOf branch is empty");
|
|
3772
3969
|
const types = typeList2(converted["type"]);
|
|
3773
3970
|
if (types !== void 0 && !types.includes("object")) {
|
|
3774
3971
|
throw new UnsupportedSchema("allOf mixes object and non-object schemas");
|
|
3775
3972
|
}
|
|
3776
3973
|
const branchProperties = converted["properties"];
|
|
3777
|
-
if (
|
|
3974
|
+
if (isRecord(branchProperties)) {
|
|
3778
3975
|
for (const [name, sub] of Object.entries(branchProperties)) {
|
|
3779
3976
|
const existing = properties[name];
|
|
3780
|
-
if (existing !== void 0 &&
|
|
3977
|
+
if (existing !== void 0 && !isDeepStrictEqual(existing, sub)) {
|
|
3781
3978
|
throw new UnsupportedSchema(
|
|
3782
3979
|
`allOf branches declare property "${name}" differently, which cannot be merged`
|
|
3783
3980
|
);
|
|
@@ -3793,18 +3990,24 @@ function mergeAllOf(document, schema, stack, dropped) {
|
|
|
3793
3990
|
throw new UnsupportedSchema(`allOf branch uses "${keyword}", which cannot be merged`);
|
|
3794
3991
|
}
|
|
3795
3992
|
}
|
|
3796
|
-
|
|
3797
|
-
merged
|
|
3993
|
+
for (const keyword of METADATA_KEYWORDS) {
|
|
3994
|
+
if (Object.hasOwn(converted, keyword) && !Object.hasOwn(merged, keyword)) {
|
|
3995
|
+
merged[keyword] = converted[keyword];
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
const additional = converted["additionalProperties"];
|
|
3999
|
+
if (additional === true) open2 = true;
|
|
4000
|
+
else if (isRecord(additional)) {
|
|
4001
|
+
throw new UnsupportedSchema(
|
|
4002
|
+
"allOf branch constrains additionalProperties, which cannot be merged"
|
|
4003
|
+
);
|
|
3798
4004
|
}
|
|
3799
4005
|
}
|
|
3800
4006
|
if (required.size > 0) merged["required"] = [...required];
|
|
3801
4007
|
else delete merged["required"];
|
|
3802
|
-
merged["additionalProperties"] =
|
|
4008
|
+
merged["additionalProperties"] = open2;
|
|
3803
4009
|
return merged;
|
|
3804
4010
|
}
|
|
3805
|
-
function isRecord2(value) {
|
|
3806
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3807
|
-
}
|
|
3808
4011
|
|
|
3809
4012
|
// src/openapi/request.ts
|
|
3810
4013
|
var PATH_GROUP = "path";
|
|
@@ -3858,16 +4061,10 @@ function mapRequest(loaded, candidate) {
|
|
|
3858
4061
|
warn("unsupported-optional-parameter", `Omitted optional ${label}: unknown location`);
|
|
3859
4062
|
continue;
|
|
3860
4063
|
}
|
|
3861
|
-
const
|
|
3862
|
-
if (unsupported !== void 0) {
|
|
3863
|
-
if (required2) return skip("unsupported-required-parameter", `${label} ${unsupported}`);
|
|
3864
|
-
warn("unsupported-optional-parameter", `Omitted optional ${label}: it ${unsupported}`);
|
|
3865
|
-
continue;
|
|
3866
|
-
}
|
|
3867
|
-
const converted = convertSchema(document, parameter["schema"]);
|
|
4064
|
+
const converted = convertParameter(document, parameter, location);
|
|
3868
4065
|
if (!converted.supported) {
|
|
3869
4066
|
if (required2) return skip("unsupported-required-parameter", `${label} ${converted.reason}`);
|
|
3870
|
-
warn("unsupported-optional-parameter", `Omitted optional ${label}: ${converted.reason}`);
|
|
4067
|
+
warn("unsupported-optional-parameter", `Omitted optional ${label}: it ${converted.reason}`);
|
|
3871
4068
|
continue;
|
|
3872
4069
|
}
|
|
3873
4070
|
for (const keyword of converted.dropped) dropped.add(keyword);
|
|
@@ -3936,38 +4133,43 @@ function mapRequest(loaded, candidate) {
|
|
|
3936
4133
|
function mergeParameters(document, parameters) {
|
|
3937
4134
|
const byIdentity = /* @__PURE__ */ new Map();
|
|
3938
4135
|
for (const raw of parameters) {
|
|
3939
|
-
const
|
|
3940
|
-
if (
|
|
3941
|
-
const parameter = resolved;
|
|
4136
|
+
const parameter = dereference(document, raw).value;
|
|
4137
|
+
if (!isRecord(parameter)) continue;
|
|
3942
4138
|
byIdentity.set(`${String(parameter["in"])}:${String(parameter["name"])}`, parameter);
|
|
3943
4139
|
}
|
|
3944
4140
|
return [...byIdentity.values()];
|
|
3945
4141
|
}
|
|
3946
|
-
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
|
+
}
|
|
3947
4147
|
if (parameter["content"] !== void 0) {
|
|
3948
|
-
return
|
|
4148
|
+
return unsupported(
|
|
4149
|
+
"uses the `content` form, whose media-type serialization the gateway does not perform"
|
|
4150
|
+
);
|
|
3949
4151
|
}
|
|
3950
4152
|
const style = parameter["style"];
|
|
3951
4153
|
if (location === "query" && typeof style === "string" && UNSUPPORTED_QUERY_STYLES.has(style)) {
|
|
3952
|
-
return `uses style "${style}", which the gateway does not serialize
|
|
4154
|
+
return unsupported(`uses style "${style}", which the gateway does not serialize`);
|
|
3953
4155
|
}
|
|
3954
4156
|
if (location === "path" && typeof style === "string" && style !== "simple") {
|
|
3955
|
-
return `uses style "${style}"; the gateway substitutes plain values only
|
|
4157
|
+
return unsupported(`uses style "${style}"; the gateway substitutes plain values only`);
|
|
3956
4158
|
}
|
|
3957
4159
|
const converted = convertSchema(document, parameter["schema"]);
|
|
3958
|
-
if (!converted.supported) return converted.reason;
|
|
4160
|
+
if (!converted.supported) return unsupported(`has an unusable schema: ${converted.reason}`);
|
|
3959
4161
|
if (!isPrimitiveSchema(converted.schema)) {
|
|
3960
|
-
return
|
|
4162
|
+
return unsupported(
|
|
4163
|
+
"is not a primitive; object and array parameters need serialization the gateway does not perform"
|
|
4164
|
+
);
|
|
3961
4165
|
}
|
|
3962
|
-
return
|
|
4166
|
+
return converted;
|
|
3963
4167
|
}
|
|
3964
4168
|
function resolveBody(document, candidate) {
|
|
3965
4169
|
if (candidate.requestBody === void 0) return { kind: "none" };
|
|
3966
|
-
const
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
}
|
|
3970
|
-
const requestBody = resolved;
|
|
4170
|
+
const resolvedBody = dereference(document, candidate.requestBody);
|
|
4171
|
+
const requestBody = resolvedBody.value;
|
|
4172
|
+
if (!isRecord(requestBody)) return { kind: "none" };
|
|
3971
4173
|
const required = requestBody["required"] === true;
|
|
3972
4174
|
if (candidate.method === "GET" || candidate.method === "DELETE") {
|
|
3973
4175
|
return {
|
|
@@ -3977,7 +4179,7 @@ function resolveBody(document, candidate) {
|
|
|
3977
4179
|
};
|
|
3978
4180
|
}
|
|
3979
4181
|
const content = requestBody["content"];
|
|
3980
|
-
if (!
|
|
4182
|
+
if (!isRecord(content)) return { kind: "none" };
|
|
3981
4183
|
const mediaType = pickJsonMediaType(Object.keys(content));
|
|
3982
4184
|
if (mediaType === void 0) {
|
|
3983
4185
|
return {
|
|
@@ -3986,8 +4188,8 @@ function resolveBody(document, candidate) {
|
|
|
3986
4188
|
reason: `no JSON request body content type (found: ${Object.keys(content).join(", ") || "none"}). Only application/json and application/*+json are supported - multipart and form data are never serialized as JSON`
|
|
3987
4189
|
};
|
|
3988
4190
|
}
|
|
3989
|
-
const media = content[mediaType];
|
|
3990
|
-
const schemaNode =
|
|
4191
|
+
const media = dereference(document, content[mediaType], resolvedBody.stack).value;
|
|
4192
|
+
const schemaNode = isRecord(media) ? media["schema"] : void 0;
|
|
3991
4193
|
if (schemaNode === void 0) {
|
|
3992
4194
|
return {
|
|
3993
4195
|
kind: "schema",
|
|
@@ -4008,10 +4210,10 @@ function resolveBody(document, candidate) {
|
|
|
4008
4210
|
};
|
|
4009
4211
|
}
|
|
4010
4212
|
function pickJsonMediaType(keys) {
|
|
4011
|
-
const
|
|
4012
|
-
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");
|
|
4013
4215
|
if (exact !== void 0) return exact.key;
|
|
4014
|
-
return [...
|
|
4216
|
+
return [...normalized].sort((a, b) => (a.type ?? "").localeCompare(b.type ?? "")).find((entry) => entry.type?.startsWith("application/") && entry.type.endsWith("+json"))?.key;
|
|
4015
4217
|
}
|
|
4016
4218
|
function closedObject(properties, required) {
|
|
4017
4219
|
return {
|
|
@@ -4021,9 +4223,6 @@ function closedObject(properties, required) {
|
|
|
4021
4223
|
additionalProperties: false
|
|
4022
4224
|
};
|
|
4023
4225
|
}
|
|
4024
|
-
function isRecord3(value) {
|
|
4025
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4026
|
-
}
|
|
4027
4226
|
|
|
4028
4227
|
// src/openapi/draft.ts
|
|
4029
4228
|
function buildResourceDrafts(loaded, options = {}) {
|
|
@@ -4031,11 +4230,12 @@ function buildResourceDrafts(loaded, options = {}) {
|
|
|
4031
4230
|
loaded,
|
|
4032
4231
|
options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {}
|
|
4033
4232
|
);
|
|
4034
|
-
const diagnostics = [
|
|
4233
|
+
const diagnostics = [];
|
|
4035
4234
|
const drafts = [];
|
|
4036
4235
|
const wanted = options.include?.operationIds;
|
|
4037
4236
|
const wantedTags = options.include?.tags;
|
|
4038
4237
|
const matched = /* @__PURE__ */ new Set();
|
|
4238
|
+
const selected = /* @__PURE__ */ new Set();
|
|
4039
4239
|
for (const candidate of discovery.operations) {
|
|
4040
4240
|
if (wanted !== void 0 && !selects(wanted, candidate)) continue;
|
|
4041
4241
|
if (wantedTags !== void 0 && !candidate.tags.some((tag) => wantedTags.includes(tag))) {
|
|
@@ -4044,6 +4244,7 @@ function buildResourceDrafts(loaded, options = {}) {
|
|
|
4044
4244
|
if (wanted !== void 0) {
|
|
4045
4245
|
for (const id of wanted) if (selects([id], candidate)) matched.add(id);
|
|
4046
4246
|
}
|
|
4247
|
+
selected.add(candidate.resourceId);
|
|
4047
4248
|
const mapping = mapRequest(loaded, candidate);
|
|
4048
4249
|
diagnostics.push(...mapping.diagnostics);
|
|
4049
4250
|
if (!mapping.supported) continue;
|
|
@@ -4073,8 +4274,8 @@ function buildResourceDrafts(loaded, options = {}) {
|
|
|
4073
4274
|
if (options.policy?.pricing === void 0 || options.policy.expose === void 0) {
|
|
4074
4275
|
review.push(
|
|
4075
4276
|
"REVIEW: pricing and exposure are not inferred from OpenAPI. Add e.g.",
|
|
4076
|
-
' pricing: { type: free } # or { type: fixed, amount: "0.01", currency: USDC }',
|
|
4077
|
-
" expose: [http] # http | mcp | a2a"
|
|
4277
|
+
' pricing: { type: free } # or { type: fixed, amount: "0.01", currency: USDC } with payments: [x402]',
|
|
4278
|
+
" expose: [http] # http | mcp | a2a | acp"
|
|
4078
4279
|
);
|
|
4079
4280
|
}
|
|
4080
4281
|
drafts.push({
|
|
@@ -4094,12 +4295,9 @@ function buildResourceDrafts(loaded, options = {}) {
|
|
|
4094
4295
|
...mapping.contentType !== void 0 ? { headers: { "Content-Type": mapping.contentType } } : {},
|
|
4095
4296
|
...Object.keys(mapping.inputBindings).length > 0 ? { inputBindings: mapping.inputBindings } : {}
|
|
4096
4297
|
},
|
|
4097
|
-
//
|
|
4098
|
-
// `pricing`/`expose` is what makes the draft fail config validation
|
|
4099
|
-
// until a human has decided.
|
|
4298
|
+
// Written only when the operator supplied it (see the file header)
|
|
4100
4299
|
...options.policy?.pricing !== void 0 ? { pricing: options.policy.pricing } : {},
|
|
4101
|
-
...options.policy?.expose !== void 0 ? { expose: [...options.policy.expose] } : {}
|
|
4102
|
-
...options.policy?.payments !== void 0 ? { payments: [...options.policy.payments] } : {}
|
|
4300
|
+
...options.policy?.expose !== void 0 ? { expose: [...options.policy.expose] } : {}
|
|
4103
4301
|
},
|
|
4104
4302
|
review
|
|
4105
4303
|
});
|
|
@@ -4108,10 +4306,19 @@ function buildResourceDrafts(loaded, options = {}) {
|
|
|
4108
4306
|
version: loaded.version,
|
|
4109
4307
|
sourcePath: loaded.sourcePath,
|
|
4110
4308
|
drafts,
|
|
4111
|
-
diagnostics
|
|
4309
|
+
diagnostics: [
|
|
4310
|
+
...selectDiscoveryDiagnostics(discovery.diagnostics, options.include, selected),
|
|
4311
|
+
...diagnostics
|
|
4312
|
+
],
|
|
4112
4313
|
unmatchedOperationIds: (wanted ?? []).filter((id) => !matched.has(id))
|
|
4113
4314
|
};
|
|
4114
4315
|
}
|
|
4316
|
+
function selectDiscoveryDiagnostics(found, include, selected) {
|
|
4317
|
+
if (include?.operationIds === void 0 && include?.tags === void 0) return found;
|
|
4318
|
+
return found.filter(
|
|
4319
|
+
({ operation }) => operation === void 0 || selected.has(operation) || include.operationIds?.includes(operation) === true
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4115
4322
|
function selects(ids, candidate) {
|
|
4116
4323
|
return ids.includes(candidate.resourceId) || candidate.operationId !== void 0 && ids.includes(candidate.operationId);
|
|
4117
4324
|
}
|
|
@@ -4122,22 +4329,31 @@ function declaresSecurity(candidate) {
|
|
|
4122
4329
|
}
|
|
4123
4330
|
function selectOutputSchema(document, candidate, diagnostics, dropped) {
|
|
4124
4331
|
const responses = candidate.responses;
|
|
4125
|
-
if (!
|
|
4332
|
+
if (!isRecord(responses)) return void 0;
|
|
4126
4333
|
const successes = Object.keys(responses).filter((status) => /^2\d\d$/.test(status)).sort((a, b) => rank(a) - rank(b));
|
|
4127
4334
|
if (successes.length === 0) return void 0;
|
|
4128
|
-
const withBody = successes.
|
|
4129
|
-
const
|
|
4130
|
-
|
|
4131
|
-
|
|
4335
|
+
const withBody = successes.flatMap((status) => {
|
|
4336
|
+
const response = dereference(document, responses[status]);
|
|
4337
|
+
if (!isRecord(response.value)) return [];
|
|
4338
|
+
const content = response.value["content"];
|
|
4339
|
+
if (!isRecord(content)) return [];
|
|
4340
|
+
const mediaType = pickJsonMediaType(Object.keys(content));
|
|
4341
|
+
if (mediaType === void 0) return [];
|
|
4342
|
+
const media = dereference(document, content[mediaType], response.stack).value;
|
|
4343
|
+
return isRecord(media) ? [{ status, media }] : [];
|
|
4132
4344
|
});
|
|
4133
4345
|
const chosen = withBody[0];
|
|
4134
4346
|
if (chosen === void 0) return void 0;
|
|
4135
|
-
const
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4347
|
+
const schemaNode = chosen.media["schema"];
|
|
4348
|
+
if (schemaNode === void 0) {
|
|
4349
|
+
diagnostics.push({
|
|
4350
|
+
severity: "warning",
|
|
4351
|
+
code: "missing-output-schema",
|
|
4352
|
+
operation: candidate.resourceId,
|
|
4353
|
+
message: `${candidate.resourceId}: response ${chosen.status} has JSON content without a schema; omitted the output schema`
|
|
4354
|
+
});
|
|
4355
|
+
return void 0;
|
|
4356
|
+
}
|
|
4141
4357
|
const converted = convertSchema(document, schemaNode);
|
|
4142
4358
|
if (!converted.supported) {
|
|
4143
4359
|
diagnostics.push({
|
|
@@ -4154,7 +4370,7 @@ function selectOutputSchema(document, candidate, diagnostics, dropped) {
|
|
|
4154
4370
|
severity: "warning",
|
|
4155
4371
|
code: "multiple-success-responses",
|
|
4156
4372
|
operation: candidate.resourceId,
|
|
4157
|
-
message: `${candidate.resourceId}: responses ${withBody.join(", ")} all carry a JSON body; used ${chosen}`
|
|
4373
|
+
message: `${candidate.resourceId}: responses ${withBody.map(({ status }) => status).join(", ")} all carry a JSON body; used ${chosen.status}`
|
|
4158
4374
|
});
|
|
4159
4375
|
}
|
|
4160
4376
|
return converted.schema;
|
|
@@ -4169,7 +4385,8 @@ function renderResourcesYaml(result) {
|
|
|
4169
4385
|
const doc = new Document({ resources });
|
|
4170
4386
|
doc.commentBefore = [
|
|
4171
4387
|
" Generated by agent-commerce import openapi. Review before use.",
|
|
4172
|
-
|
|
4388
|
+
// win32.basename splits on both / and \
|
|
4389
|
+
` Source: ${win32.basename(result.sourcePath)} (OpenAPI ${result.version})`,
|
|
4173
4390
|
" Merge the resources below into config.yaml once pricing, exposure and",
|
|
4174
4391
|
" any backend authentication have been decided."
|
|
4175
4392
|
].join("\n");
|
|
@@ -4182,12 +4399,6 @@ function renderResourcesYaml(result) {
|
|
|
4182
4399
|
});
|
|
4183
4400
|
return doc.toString({ lineWidth: 0 });
|
|
4184
4401
|
}
|
|
4185
|
-
function basename(path2) {
|
|
4186
|
-
return path2.split(/[\\/]/).pop() ?? path2;
|
|
4187
|
-
}
|
|
4188
|
-
function isRecord4(value) {
|
|
4189
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4190
|
-
}
|
|
4191
4402
|
|
|
4192
4403
|
// src/openapi/load.ts
|
|
4193
4404
|
init_errors();
|
|
@@ -4200,6 +4411,31 @@ function invalid2(message, details, cause) {
|
|
|
4200
4411
|
...cause !== void 0 ? { cause } : {}
|
|
4201
4412
|
});
|
|
4202
4413
|
}
|
|
4414
|
+
async function readCapped(sourcePath, details) {
|
|
4415
|
+
const buffer = Buffer.alloc(MAX_SOURCE_BYTES + 1);
|
|
4416
|
+
let length = 0;
|
|
4417
|
+
try {
|
|
4418
|
+
const handle = await open(sourcePath, "r");
|
|
4419
|
+
try {
|
|
4420
|
+
while (length < buffer.length) {
|
|
4421
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
|
|
4422
|
+
if (bytesRead === 0) break;
|
|
4423
|
+
length += bytesRead;
|
|
4424
|
+
}
|
|
4425
|
+
} finally {
|
|
4426
|
+
await handle.close();
|
|
4427
|
+
}
|
|
4428
|
+
} catch (error) {
|
|
4429
|
+
throw invalid2(`OpenAPI document "${sourcePath}" could not be read`, details, error);
|
|
4430
|
+
}
|
|
4431
|
+
if (length > MAX_SOURCE_BYTES) {
|
|
4432
|
+
throw invalid2(`OpenAPI document "${sourcePath}" is over the ${MAX_SOURCE_BYTES}-byte limit`, {
|
|
4433
|
+
...details,
|
|
4434
|
+
maxBytes: MAX_SOURCE_BYTES
|
|
4435
|
+
});
|
|
4436
|
+
}
|
|
4437
|
+
return buffer.toString("utf8", 0, length);
|
|
4438
|
+
}
|
|
4203
4439
|
async function loadOpenApiDocument(sourcePath) {
|
|
4204
4440
|
const details = { sourcePath };
|
|
4205
4441
|
let stats;
|
|
@@ -4224,7 +4460,7 @@ async function loadOpenApiDocument(sourcePath) {
|
|
|
4224
4460
|
details
|
|
4225
4461
|
);
|
|
4226
4462
|
}
|
|
4227
|
-
const source = await
|
|
4463
|
+
const source = await readCapped(sourcePath, details);
|
|
4228
4464
|
if (source.trim() === "") {
|
|
4229
4465
|
throw invalid2(`OpenAPI document "${sourcePath}" is empty`, details);
|
|
4230
4466
|
}
|
|
@@ -4243,7 +4479,7 @@ async function loadOpenApiDocument(sourcePath) {
|
|
|
4243
4479
|
}
|
|
4244
4480
|
const document = parsed;
|
|
4245
4481
|
const version = readVersion(document, sourcePath);
|
|
4246
|
-
|
|
4482
|
+
rejectUnsupportedReferences(document, sourcePath);
|
|
4247
4483
|
const result = await validate(document);
|
|
4248
4484
|
if (!result.valid) {
|
|
4249
4485
|
const first = result.errors?.[0];
|
|
@@ -4275,7 +4511,7 @@ function readVersion(document, sourcePath) {
|
|
|
4275
4511
|
}
|
|
4276
4512
|
return featureVersion;
|
|
4277
4513
|
}
|
|
4278
|
-
function
|
|
4514
|
+
function rejectUnsupportedReferences(document, sourcePath) {
|
|
4279
4515
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
4280
4516
|
const queue = [document];
|
|
4281
4517
|
while (queue.length > 0) {
|
|
@@ -4288,31 +4524,45 @@ function rejectExternalReferences(document, sourcePath) {
|
|
|
4288
4524
|
continue;
|
|
4289
4525
|
}
|
|
4290
4526
|
for (const [key, value] of Object.entries(node)) {
|
|
4291
|
-
if (key === "$ref" && typeof value === "string"
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
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
|
+
}
|
|
4296
4540
|
}
|
|
4297
4541
|
queue.push(value);
|
|
4298
4542
|
}
|
|
4299
4543
|
}
|
|
4300
4544
|
}
|
|
4545
|
+
function isPercentDecodable(value) {
|
|
4546
|
+
try {
|
|
4547
|
+
decodeURIComponent(value);
|
|
4548
|
+
return true;
|
|
4549
|
+
} catch {
|
|
4550
|
+
return false;
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4301
4553
|
|
|
4302
4554
|
// src/cli/commands/import-openapi.ts
|
|
4303
4555
|
function defaultOutputPath(source) {
|
|
4304
|
-
const name = basename
|
|
4556
|
+
const name = basename(source);
|
|
4305
4557
|
const stem = name.slice(0, name.length - extname(name).length) || name;
|
|
4306
4558
|
return `${stem}.agent-commerce.yaml`;
|
|
4307
4559
|
}
|
|
4308
4560
|
async function runImportOpenApi(options, io, deps = {}) {
|
|
4309
4561
|
const fileExists = deps.fileExists ?? existsSync;
|
|
4310
4562
|
const outputPath = options.output ?? defaultOutputPath(options.source);
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
policy
|
|
4314
|
-
} catch (error) {
|
|
4315
|
-
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}`);
|
|
4316
4566
|
return 1;
|
|
4317
4567
|
}
|
|
4318
4568
|
if (fileExists(outputPath) && options.force !== true) {
|
|
@@ -4393,13 +4643,11 @@ function buildPolicy(options) {
|
|
|
4393
4643
|
if (options.expose !== void 0) {
|
|
4394
4644
|
const requested = options.expose.split(",").map((name) => name.trim()).filter((name) => name !== "");
|
|
4395
4645
|
if (requested.length === 0) {
|
|
4396
|
-
|
|
4646
|
+
return `--expose needs at least one protocol (${PROTOCOL_NAMES.join(", ")})`;
|
|
4397
4647
|
}
|
|
4398
4648
|
for (const name of requested) {
|
|
4399
4649
|
if (!PROTOCOL_NAMES.includes(name)) {
|
|
4400
|
-
|
|
4401
|
-
`--expose "${name}" is not a supported protocol. Supported: ${PROTOCOL_NAMES.join(", ")}`
|
|
4402
|
-
);
|
|
4650
|
+
return `--expose "${name}" is not a supported protocol. Supported: ${PROTOCOL_NAMES.join(", ")}`;
|
|
4403
4651
|
}
|
|
4404
4652
|
}
|
|
4405
4653
|
policy.expose = requested;
|
|
@@ -4473,14 +4721,6 @@ function formatConfigError(err, manifestFound = false) {
|
|
|
4473
4721
|
}
|
|
4474
4722
|
return lines.join("\n");
|
|
4475
4723
|
}
|
|
4476
|
-
if (err instanceof ZodError) {
|
|
4477
|
-
const lines = ["FAIL Configuration schema errors:"];
|
|
4478
|
-
for (const issue of err.issues) {
|
|
4479
|
-
const path2 = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
4480
|
-
lines.push(` - ${path2}: ${issue.message}`);
|
|
4481
|
-
}
|
|
4482
|
-
return lines.join("\n");
|
|
4483
|
-
}
|
|
4484
4724
|
if (err instanceof Error) {
|
|
4485
4725
|
return `FAIL ${err.message}`;
|
|
4486
4726
|
}
|
|
@@ -4504,7 +4744,11 @@ async function runValidate(options, io, deps = {}) {
|
|
|
4504
4744
|
io.stdout(
|
|
4505
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"}`
|
|
4506
4746
|
);
|
|
4507
|
-
|
|
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)}`);
|
|
4508
4752
|
return 0;
|
|
4509
4753
|
} catch (err) {
|
|
4510
4754
|
io.stderr(formatConfigError(err, manifestFound));
|
|
@@ -4518,8 +4762,7 @@ async function collectAnswersInteractive() {
|
|
|
4518
4762
|
const backendBaseUrl = await clack.text({
|
|
4519
4763
|
message: "Backend base URL (your existing merchant API)",
|
|
4520
4764
|
initialValue: "http://localhost:3000",
|
|
4521
|
-
//
|
|
4522
|
-
// 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
|
|
4523
4766
|
validate: (value) => {
|
|
4524
4767
|
const trimmed = (value ?? "").trim();
|
|
4525
4768
|
if (trimmed.length === 0) return "Required";
|
|
@@ -4556,12 +4799,8 @@ async function collectAnswersInteractive() {
|
|
|
4556
4799
|
{ value: "mcp", label: "MCP" }
|
|
4557
4800
|
],
|
|
4558
4801
|
initialValues: ["http", "mcp"],
|
|
4559
|
-
//
|
|
4560
|
-
|
|
4561
|
-
// multiselect has no `validate` hook, so this is caught by the in-memory
|
|
4562
|
-
// validation in `runInit` — which now reports the real schema error and
|
|
4563
|
-
// writes nothing, instead of leaving an invalid file behind.
|
|
4564
|
-
required: false
|
|
4802
|
+
// Every resource needs at least one protocol in `expose`
|
|
4803
|
+
required: true
|
|
4565
4804
|
});
|
|
4566
4805
|
if (clack.isCancel(protocols)) {
|
|
4567
4806
|
clack.cancel("Aborted.");
|
|
@@ -4575,7 +4814,7 @@ async function collectAnswersInteractive() {
|
|
|
4575
4814
|
let merchantPayTo = defaultInitAnswers().merchantPayTo;
|
|
4576
4815
|
if (x402Enabled) {
|
|
4577
4816
|
const answer = await clack.text({
|
|
4578
|
-
message: "Merchant settlement
|
|
4817
|
+
message: "Merchant settlement address (enter an address you control)",
|
|
4579
4818
|
initialValue: merchantPayTo,
|
|
4580
4819
|
validate: (value) => {
|
|
4581
4820
|
const trimmed = (value ?? "").trim();
|
|
@@ -4618,20 +4857,16 @@ async function runInit(options, io, deps = {}) {
|
|
|
4618
4857
|
return 1;
|
|
4619
4858
|
}
|
|
4620
4859
|
const yamlContent = renderInitConfigYaml(answers);
|
|
4621
|
-
|
|
4622
|
-
const dir = dirname(resolvedPath);
|
|
4623
|
-
if (dir !== "" && !existsSync(dir)) {
|
|
4624
|
-
mkdirSync(dir, { recursive: true });
|
|
4625
|
-
}
|
|
4860
|
+
mkdirSync(dirname(resolve(outputPath)), { recursive: true });
|
|
4626
4861
|
await writeFileImpl(outputPath, yamlContent);
|
|
4627
4862
|
io.stdout(`PASS Wrote ${outputPath}`);
|
|
4628
|
-
io.stdout(`PASS ${outputPath} is valid
|
|
4863
|
+
io.stdout(`PASS ${outputPath} is valid: ${config.resources.length} resource(s)`);
|
|
4629
4864
|
try {
|
|
4630
4865
|
await loadConfig2({ path: outputPath });
|
|
4631
4866
|
} catch (err) {
|
|
4632
4867
|
io.stderr(formatConfigError(err));
|
|
4633
4868
|
io.stderr(
|
|
4634
|
-
`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.`
|
|
4635
4870
|
);
|
|
4636
4871
|
return 1;
|
|
4637
4872
|
}
|
|
@@ -4650,8 +4885,9 @@ function runVersion(io, deps = {}) {
|
|
|
4650
4885
|
if (report.pinned.length > 0) {
|
|
4651
4886
|
io.stdout("");
|
|
4652
4887
|
io.stdout("Pinned protocol / SDK versions:");
|
|
4888
|
+
const width = Math.max(...report.pinned.map((pin) => pin.name.length));
|
|
4653
4889
|
for (const pin of report.pinned) {
|
|
4654
|
-
io.stdout(` ${pin.name.padEnd(
|
|
4890
|
+
io.stdout(` ${pin.name.padEnd(width)} ${pin.version.padEnd(12)} (via ${pin.via})`);
|
|
4655
4891
|
}
|
|
4656
4892
|
}
|
|
4657
4893
|
return 0;
|
|
@@ -4702,7 +4938,10 @@ function buildProgram(io = processIo) {
|
|
|
4702
4938
|
process.exitCode = code;
|
|
4703
4939
|
});
|
|
4704
4940
|
const importCommand = program.command("import").description("Generate Agent Commerce resource drafts from an API description.");
|
|
4705
|
-
importCommand.command("openapi").description("Convert a local OpenAPI 3.0/3.1/3.2 document into resource drafts.").argument("<source>", "path to a local .yaml, .yml or .json OpenAPI document").option(
|
|
4941
|
+
importCommand.command("openapi").description("Convert a local OpenAPI 3.0/3.1/3.2 document into resource drafts.").argument("<source>", "path to a local .yaml, .yml or .json OpenAPI document").option(
|
|
4942
|
+
"--output <path>",
|
|
4943
|
+
"output path (default: <source name without extension>.agent-commerce.yaml in the current directory)"
|
|
4944
|
+
).option("--force", "overwrite an existing output file", false).option("--base-url <url>", "backend base URL, overriding the document servers").option(
|
|
4706
4945
|
"--operation <operationId>",
|
|
4707
4946
|
"import only this operation (repeatable)",
|
|
4708
4947
|
collect,
|
|
@@ -4727,7 +4966,7 @@ function buildProgram(io = processIo) {
|
|
|
4727
4966
|
}
|
|
4728
4967
|
);
|
|
4729
4968
|
program.command("demo").description(
|
|
4730
|
-
"
|
|
4969
|
+
"Start the local stack with docker compose (waiting for healthy services), then run the demo agent. Run it from a repo clone."
|
|
4731
4970
|
).action(async () => {
|
|
4732
4971
|
process.exitCode = await runDemo(io);
|
|
4733
4972
|
});
|