@devlab.group/agent-commerce 0.1.0-alpha.0 → 1.0.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 +144 -46
- package/dist/{chunk-CZ6ZDAYD.js → chunk-BQNE3A5H.js} +9 -7
- package/dist/chunk-BQNE3A5H.js.map +1 -0
- package/dist/chunk-MEPETSP6.js +255 -0
- package/dist/chunk-MEPETSP6.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +401 -61
- package/dist/cli/index.js.map +1 -1
- package/dist/guardrails-Dmwi63KB.d.ts +38 -0
- package/dist/index.d.ts +38 -19
- package/dist/index.js +177 -48
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/mcp.js.map +1 -1
- package/dist/{protocol-adapter-C0goMgLK.d.ts → protocol-adapter-CZPOb9uG.d.ts} +2 -3
- package/dist/{runtime-LCk2TzGN.d.ts → runtime-B-9IjMbO.d.ts} +17 -5
- package/dist/x402.d.ts +83 -17
- package/dist/x402.js +321 -216
- package/dist/x402.js.map +1 -1
- package/package.json +19 -7
- package/dist/chunk-CZ6ZDAYD.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { ZodError, z } from 'zod';
|
|
3
|
-
import { existsSync, mkdirSync, accessSync, constants, readFileSync, chmodSync, promises } from 'fs';
|
|
3
|
+
import { existsSync, mkdirSync, accessSync, constants, closeSync, openSync, readFileSync, chmodSync, promises } from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { resolve, dirname, join } from 'path';
|
|
6
6
|
import { stringify, parse, YAMLError } from 'yaml';
|
|
@@ -267,6 +267,64 @@ var init_core = __esm({
|
|
|
267
267
|
}
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
+
// src/payments/x402/networks.ts
|
|
271
|
+
function findNetworkProfile(id) {
|
|
272
|
+
return BY_ID.get(id);
|
|
273
|
+
}
|
|
274
|
+
function requireNetworkProfile(id, path2) {
|
|
275
|
+
const profile = BY_ID.get(id);
|
|
276
|
+
if (!profile) {
|
|
277
|
+
throw new CommerceError(
|
|
278
|
+
"CONFIG_INVALID",
|
|
279
|
+
`${path2}: "${id}" is not a supported network. Supported CAIP-2 identifiers: ${SUPPORTED_NETWORK_IDS.join(", ")}`,
|
|
280
|
+
{ details: { path: path2, network: id } }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return profile;
|
|
284
|
+
}
|
|
285
|
+
function resolveDeploymentMode(profile, facilitatorMode) {
|
|
286
|
+
if (facilitatorMode === "local") return "local";
|
|
287
|
+
return profile.kind;
|
|
288
|
+
}
|
|
289
|
+
function describeDeploymentMode(mode) {
|
|
290
|
+
return mode === "mainnet" ? LIVE_MAINNET_BANNER : mode.toUpperCase();
|
|
291
|
+
}
|
|
292
|
+
var PROFILES, BY_ID, SUPPORTED_NETWORK_IDS, LIVE_MAINNET_BANNER;
|
|
293
|
+
var init_networks = __esm({
|
|
294
|
+
"src/payments/x402/networks.ts"() {
|
|
295
|
+
init_core();
|
|
296
|
+
PROFILES = [
|
|
297
|
+
{
|
|
298
|
+
id: "eip155:84532",
|
|
299
|
+
chainId: 84532,
|
|
300
|
+
displayName: "Base Sepolia",
|
|
301
|
+
kind: "testnet",
|
|
302
|
+
canonicalAsset: {
|
|
303
|
+
symbol: "USDC",
|
|
304
|
+
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
305
|
+
name: "USDC",
|
|
306
|
+
version: "2"
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: "eip155:8453",
|
|
311
|
+
chainId: 8453,
|
|
312
|
+
displayName: "Base",
|
|
313
|
+
kind: "mainnet",
|
|
314
|
+
canonicalAsset: {
|
|
315
|
+
symbol: "USDC",
|
|
316
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
317
|
+
name: "USD Coin",
|
|
318
|
+
version: "2"
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
BY_ID = new Map(PROFILES.map((profile) => [profile.id, profile]));
|
|
323
|
+
SUPPORTED_NETWORK_IDS = PROFILES.map((p) => p.id);
|
|
324
|
+
LIVE_MAINNET_BANNER = "LIVE MAINNET MODE \u2014 REAL FUNDS";
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
270
328
|
// src/config/env.ts
|
|
271
329
|
function substituteEnv(value, env, path2 = "$") {
|
|
272
330
|
if (typeof value === "string") {
|
|
@@ -285,19 +343,26 @@ function substituteEnv(value, env, path2 = "$") {
|
|
|
285
343
|
return value;
|
|
286
344
|
}
|
|
287
345
|
function substituteString(value, env, path2) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
PLACEHOLDER_PATTERN.lastIndex = 0;
|
|
346
|
+
assertTemplateIsSupported(value, path2);
|
|
347
|
+
return substituteOnce(value, env, path2);
|
|
348
|
+
}
|
|
349
|
+
function assertTemplateIsSupported(value, path2) {
|
|
350
|
+
if (NESTED_PLACEHOLDER.test(value)) {
|
|
294
351
|
throw new CommerceError(
|
|
295
352
|
"CONFIG_INVALID",
|
|
296
|
-
`Configuration value at "${path2}"
|
|
297
|
-
{ details: { path: path2
|
|
353
|
+
`Configuration value at "${path2}" nests placeholders (e.g. "\${A:-\${B}}"), which is not supported \u2014 the inner one is not resolved and its closing brace ends up in the value. Use a plain default, or set the variable.`,
|
|
354
|
+
{ details: { path: path2 } }
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
BRACE_TOKEN.lastIndex = 0;
|
|
358
|
+
for (const [token] of value.matchAll(BRACE_TOKEN)) {
|
|
359
|
+
if (SUPPORTED_TOKEN.test(token)) continue;
|
|
360
|
+
throw new CommerceError(
|
|
361
|
+
"CONFIG_INVALID",
|
|
362
|
+
`Configuration value at "${path2}" contains "${token}", which is not a supported placeholder. Only "\${VAR}" and "\${VAR:-default}" are recognised; shell forms such as "\${VAR-default}", "\${VAR:=default}" and "\${VAR:?message}" would load as literal text.`,
|
|
363
|
+
{ details: { path: path2, token } }
|
|
298
364
|
);
|
|
299
365
|
}
|
|
300
|
-
return substituted;
|
|
301
366
|
}
|
|
302
367
|
function substituteOnce(value, env, path2) {
|
|
303
368
|
return value.replace(
|
|
@@ -322,11 +387,191 @@ function substituteOnce(value, env, path2) {
|
|
|
322
387
|
function isPlainObject(value) {
|
|
323
388
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
324
389
|
}
|
|
325
|
-
var PLACEHOLDER_PATTERN;
|
|
390
|
+
var PLACEHOLDER_PATTERN, BRACE_TOKEN, SUPPORTED_TOKEN, NESTED_PLACEHOLDER;
|
|
326
391
|
var init_env = __esm({
|
|
327
392
|
"src/config/env.ts"() {
|
|
328
393
|
init_core();
|
|
329
394
|
PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
|
|
395
|
+
BRACE_TOKEN = /\$\{[^{}]*\}/g;
|
|
396
|
+
SUPPORTED_TOKEN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}$/;
|
|
397
|
+
NESTED_PLACEHOLDER = /\$\{[^{}]*\$\{/;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// src/payments/x402/local-chain/accounts.ts
|
|
402
|
+
var ANVIL_WELL_KNOWN_ACCOUNTS;
|
|
403
|
+
var init_accounts = __esm({
|
|
404
|
+
"src/payments/x402/local-chain/accounts.ts"() {
|
|
405
|
+
ANVIL_WELL_KNOWN_ACCOUNTS = [
|
|
406
|
+
{
|
|
407
|
+
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
|
408
|
+
privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
|
|
412
|
+
privateKey: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
|
|
416
|
+
privateKey: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
|
|
417
|
+
}
|
|
418
|
+
];
|
|
419
|
+
ANVIL_WELL_KNOWN_ACCOUNTS[0];
|
|
420
|
+
ANVIL_WELL_KNOWN_ACCOUNTS[1];
|
|
421
|
+
ANVIL_WELL_KNOWN_ACCOUNTS[2];
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// src/payments/x402/dev-key-guard.ts
|
|
426
|
+
function isWellKnownDevAddress(address) {
|
|
427
|
+
return WELL_KNOWN_DEV_ADDRESSES.has(address.toLowerCase());
|
|
428
|
+
}
|
|
429
|
+
function isLikelyLocalOrPrivateHost(hostname) {
|
|
430
|
+
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
431
|
+
if (h === "localhost" || h === "127.0.0.1" || h === "::1") return true;
|
|
432
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
433
|
+
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
434
|
+
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
435
|
+
if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
436
|
+
if (!h.includes(".") && !h.includes(":")) return true;
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
var WELL_KNOWN_DEV_ADDRESSES;
|
|
440
|
+
var init_dev_key_guard = __esm({
|
|
441
|
+
"src/payments/x402/dev-key-guard.ts"() {
|
|
442
|
+
init_core();
|
|
443
|
+
init_accounts();
|
|
444
|
+
new Set(
|
|
445
|
+
ANVIL_WELL_KNOWN_ACCOUNTS.map((a) => a.privateKey.toLowerCase())
|
|
446
|
+
);
|
|
447
|
+
WELL_KNOWN_DEV_ADDRESSES = new Set(
|
|
448
|
+
ANVIL_WELL_KNOWN_ACCOUNTS.map((a) => a.address.toLowerCase())
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// src/payments/x402/guardrails.ts
|
|
454
|
+
function invalid(message, path2) {
|
|
455
|
+
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2 } });
|
|
456
|
+
}
|
|
457
|
+
function resolveX402Deployment(input) {
|
|
458
|
+
const profile = requireNetworkProfile(input.network, "payments.x402.network");
|
|
459
|
+
const mode = resolveDeploymentMode(profile, input.facilitator.mode);
|
|
460
|
+
if (profile.kind === "mainnet" && input.facilitator.mode === "local") {
|
|
461
|
+
throw invalid(
|
|
462
|
+
`payments.x402: 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.`,
|
|
463
|
+
"payments.x402.facilitator.mode"
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (mode === "mainnet" && input.allowMainnet !== true) {
|
|
467
|
+
throw invalid(
|
|
468
|
+
`payments.x402: network "${profile.id}" (${profile.displayName}) settles real funds. Set payments.x402.allowMainnet: true to acknowledge this explicitly \u2014 it is never the default.`,
|
|
469
|
+
"payments.x402.allowMainnet"
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
if (input.facilitator.mode === "remote") {
|
|
473
|
+
assertFacilitatorUrlIsSafe(input.facilitator.url, mode);
|
|
474
|
+
if (mode === "mainnet" && input.facilitator.auth.type === "none" && input.allowUnauthenticatedFacilitator !== true) {
|
|
475
|
+
throw invalid(
|
|
476
|
+
`payments.x402: facilitator ${describeOrigin(input.facilitator.url)} takes no credential, and this is a mainnet deployment. It will see every payment authorisation you handle, with no account, terms or support behind it. Set payments.x402.allowUnauthenticatedFacilitator: true to accept that, or configure facilitator.auth.`,
|
|
477
|
+
"payments.x402.allowUnauthenticatedFacilitator"
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
for (const [field, value] of credentialFields(input.facilitator.auth)) {
|
|
481
|
+
if (value.trim() === "") {
|
|
482
|
+
throw invalid(
|
|
483
|
+
`payments.x402: facilitator.auth.type is "${input.facilitator.auth.type}" but ${field} is empty. An empty credential is refused rather than sent.`,
|
|
484
|
+
`payments.x402.facilitator.auth.${field}`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (/^0x0{40}$/i.test(input.payTo)) {
|
|
490
|
+
throw invalid(
|
|
491
|
+
'payments.x402: "payTo" is the zero address. Every payment settled there is destroyed.',
|
|
492
|
+
"payments.x402.payTo"
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (mode !== "local" && isWellKnownDevAddress(input.payTo)) {
|
|
496
|
+
throw invalid(
|
|
497
|
+
`payments.x402: "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.`,
|
|
498
|
+
"payments.x402.payTo"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const canonical = profile.canonicalAsset;
|
|
502
|
+
if (mode === "mainnet" && canonical) {
|
|
503
|
+
if (!sameAddress(input.asset, canonical.address)) {
|
|
504
|
+
throw invalid(
|
|
505
|
+
`payments.x402: asset ${input.asset} is not ${canonical.symbol} on ${profile.displayName} (expected ${canonical.address}). Settling a mainnet payment in an unintended token is refused.`,
|
|
506
|
+
"payments.x402.asset"
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
if (input.assetName !== void 0 && input.assetName !== canonical.name) {
|
|
510
|
+
throw invalid(
|
|
511
|
+
`payments.x402: 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.`,
|
|
512
|
+
"payments.x402.assetName"
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
if (input.assetVersion !== void 0 && input.assetVersion !== canonical.version) {
|
|
516
|
+
throw invalid(
|
|
517
|
+
`payments.x402: assetVersion "${input.assetVersion}" is not the EIP-712 domain version ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.version}").`,
|
|
518
|
+
"payments.x402.assetVersion"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return { profile, mode };
|
|
523
|
+
}
|
|
524
|
+
function assertFacilitatorUrlIsSafe(url, mode) {
|
|
525
|
+
let parsed;
|
|
526
|
+
try {
|
|
527
|
+
parsed = new URL(url);
|
|
528
|
+
} catch (cause) {
|
|
529
|
+
throw new CommerceError("CONFIG_INVALID", `payments.x402: facilitator.url is not a valid URL`, {
|
|
530
|
+
cause,
|
|
531
|
+
details: { path: "payments.x402.facilitator.url" }
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
if (parsed.protocol === "https:") return;
|
|
535
|
+
if (parsed.protocol !== "http:") {
|
|
536
|
+
throw invalid(
|
|
537
|
+
`payments.x402: facilitator ${describeOrigin(url)} must be reached over https (or http on a local/private host); got "${parsed.protocol}//"`,
|
|
538
|
+
"payments.x402.facilitator.url"
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
if (mode !== "mainnet" && isLikelyLocalOrPrivateHost(parsed.hostname)) return;
|
|
542
|
+
throw invalid(
|
|
543
|
+
`payments.x402: facilitator ${describeOrigin(url)} is reached over plain HTTP. Payment authorisations and settlement results would travel unencrypted. Use https, or point at a local/private host on a non-mainnet deployment.`,
|
|
544
|
+
"payments.x402.facilitator.url"
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
function describeOrigin(url) {
|
|
548
|
+
try {
|
|
549
|
+
return new URL(url).origin;
|
|
550
|
+
} catch {
|
|
551
|
+
return "[unparseable facilitator.url]";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function credentialFields(auth) {
|
|
555
|
+
switch (auth.type) {
|
|
556
|
+
case "bearer":
|
|
557
|
+
return [["token", auth.token]];
|
|
558
|
+
case "cdp":
|
|
559
|
+
return [
|
|
560
|
+
["apiKeyId", auth.apiKeyId],
|
|
561
|
+
["apiKeySecret", auth.apiKeySecret]
|
|
562
|
+
];
|
|
563
|
+
default:
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function sameAddress(a, b) {
|
|
568
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
569
|
+
}
|
|
570
|
+
var init_guardrails = __esm({
|
|
571
|
+
"src/payments/x402/guardrails.ts"() {
|
|
572
|
+
init_core();
|
|
573
|
+
init_dev_key_guard();
|
|
574
|
+
init_networks();
|
|
330
575
|
}
|
|
331
576
|
});
|
|
332
577
|
function parseConfig(raw, env) {
|
|
@@ -435,14 +680,12 @@ function normalise(raw) {
|
|
|
435
680
|
}
|
|
436
681
|
};
|
|
437
682
|
const x402Raw = raw.payments.x402;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
const x402 = x402Raw ? {
|
|
683
|
+
const facilitator = x402Raw === void 0 ? void 0 : x402Raw.facilitator.mode === "local" ? { mode: "local", signerPrivateKey: x402Raw.facilitator.signerPrivateKey } : {
|
|
684
|
+
mode: "remote",
|
|
685
|
+
url: x402Raw.facilitator.url,
|
|
686
|
+
auth: x402Raw.facilitator.auth ?? { type: "none" }
|
|
687
|
+
};
|
|
688
|
+
const x402 = x402Raw && facilitator ? {
|
|
446
689
|
enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
|
|
447
690
|
network: x402Raw.network,
|
|
448
691
|
rpcUrl: x402Raw.rpcUrl,
|
|
@@ -454,18 +697,35 @@ function normalise(raw) {
|
|
|
454
697
|
max: 36
|
|
455
698
|
}),
|
|
456
699
|
payTo: x402Raw.payTo,
|
|
457
|
-
maxTimeoutSeconds: toNumber(
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
700
|
+
maxTimeoutSeconds: toNumber(
|
|
701
|
+
x402Raw.maxTimeoutSeconds,
|
|
702
|
+
"payments.x402.maxTimeoutSeconds",
|
|
703
|
+
{
|
|
704
|
+
min: 1
|
|
705
|
+
}
|
|
706
|
+
),
|
|
707
|
+
facilitator,
|
|
708
|
+
...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
|
|
709
|
+
...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
710
|
+
allowUnauthenticatedFacilitator: toBoolean(
|
|
711
|
+
x402Raw.allowUnauthenticatedFacilitator,
|
|
712
|
+
"payments.x402.allowUnauthenticatedFacilitator"
|
|
713
|
+
)
|
|
714
|
+
} : {}
|
|
465
715
|
} : void 0;
|
|
466
716
|
if (x402) {
|
|
467
717
|
validateAddress("payments.x402.payTo", x402.payTo);
|
|
468
718
|
validateAddress("payments.x402.asset", x402.asset);
|
|
719
|
+
resolveX402Deployment({
|
|
720
|
+
network: x402.network,
|
|
721
|
+
payTo: x402.payTo,
|
|
722
|
+
asset: x402.asset,
|
|
723
|
+
assetName: x402.assetName,
|
|
724
|
+
assetVersion: x402.assetVersion,
|
|
725
|
+
facilitator: x402.facilitator,
|
|
726
|
+
...x402.allowMainnet !== void 0 ? { allowMainnet: x402.allowMainnet } : {},
|
|
727
|
+
...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
|
|
728
|
+
});
|
|
469
729
|
}
|
|
470
730
|
const resources = Object.entries(raw.resources).map(
|
|
471
731
|
([id, entry]) => normaliseResource(id, entry, protocols, x402)
|
|
@@ -510,7 +770,7 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
510
770
|
}
|
|
511
771
|
for (const protocol of entry.expose) {
|
|
512
772
|
if (!SUPPORTED_PROTOCOLS.has(protocol)) {
|
|
513
|
-
const hint = protocol === "ucp" ? " (UCP is planned, not supported in
|
|
773
|
+
const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
|
|
514
774
|
throw new CommerceError(
|
|
515
775
|
"CONFIG_INVALID",
|
|
516
776
|
`Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp.`,
|
|
@@ -546,7 +806,7 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
546
806
|
if (entry.pricing.type === "dynamic") {
|
|
547
807
|
throw new CommerceError(
|
|
548
808
|
"CONFIG_INVALID",
|
|
549
|
-
`Resource "${id}" uses pricing.type "dynamic", which is not supported in
|
|
809
|
+
`Resource "${id}" uses pricing.type "dynamic", which is not supported in this release`,
|
|
550
810
|
{ details: { path: `resources.${id}.pricing.type`, resourceId: id } }
|
|
551
811
|
);
|
|
552
812
|
}
|
|
@@ -623,6 +883,10 @@ function defaultClosedObjectSchema(schema) {
|
|
|
623
883
|
if (isPlainObject2(items)) {
|
|
624
884
|
result["items"] = defaultClosedObjectSchema(items);
|
|
625
885
|
}
|
|
886
|
+
const additional = schema["additionalProperties"];
|
|
887
|
+
if (isPlainObject2(additional)) {
|
|
888
|
+
result["additionalProperties"] = defaultClosedObjectSchema(additional);
|
|
889
|
+
}
|
|
626
890
|
return result;
|
|
627
891
|
}
|
|
628
892
|
function isPlainObject2(value) {
|
|
@@ -670,6 +934,10 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
670
934
|
if (isPlainObject2(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
|
|
671
935
|
}
|
|
672
936
|
}
|
|
937
|
+
const additional = schema["additionalProperties"];
|
|
938
|
+
if (isPlainObject2(additional)) {
|
|
939
|
+
validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
|
|
940
|
+
}
|
|
673
941
|
const items = schema["items"];
|
|
674
942
|
if (isPlainObject2(items)) {
|
|
675
943
|
validateResourceSchemaKeywords(id, `${path2}.items`, items);
|
|
@@ -717,6 +985,23 @@ function validatePathParametersDeclared(id, url, input) {
|
|
|
717
985
|
}
|
|
718
986
|
const params = extractPathParameterNames(url);
|
|
719
987
|
if (params.length === 0) return;
|
|
988
|
+
const firstBrace = url.indexOf("{");
|
|
989
|
+
if (firstBrace !== -1) {
|
|
990
|
+
const prefix = url.slice(0, firstBrace);
|
|
991
|
+
let origin;
|
|
992
|
+
try {
|
|
993
|
+
const parsedPrefix = new URL(prefix);
|
|
994
|
+
origin = parsedPrefix.hostname === "" ? void 0 : parsedPrefix.origin;
|
|
995
|
+
} catch {
|
|
996
|
+
}
|
|
997
|
+
if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
|
|
998
|
+
throw new CommerceError(
|
|
999
|
+
"CONFIG_INVALID",
|
|
1000
|
+
`Resource "${id}" has a backend.url parameter before the end of the host ("${url}"). 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`,
|
|
1001
|
+
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
720
1005
|
const propertiesRaw = input?.["properties"];
|
|
721
1006
|
const properties = isPlainObject2(propertiesRaw) ? propertiesRaw : {};
|
|
722
1007
|
const requiredRaw = input?.["required"];
|
|
@@ -778,11 +1063,12 @@ function validateAddress(path2, value) {
|
|
|
778
1063
|
);
|
|
779
1064
|
}
|
|
780
1065
|
}
|
|
781
|
-
var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, MCP_TOOL_NAME_PATTERN, NumberOrString, BooleanOrString, MerchantSchema, ServerSchema, StorageSchema, ProtocolsSchema, BackendMethodSchema, BackendHandlerSchema, PricingSchema, JsonSchemaValueSchema, ResourceEntrySchema, ResourcesMapSchema, FacilitatorSchema, X402Schema, PaymentsSchema, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
|
|
1066
|
+
var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, MCP_TOOL_NAME_PATTERN, NumberOrString, BooleanOrString, MerchantSchema, ServerSchema, StorageSchema, RESERVED_GATEWAY_PATHS, McpMountPathSchema, ProtocolsSchema, BackendMethodSchema, BackendHandlerSchema, PricingSchema, JsonSchemaValueSchema, ResourceEntrySchema, ResourcesMapSchema, FacilitatorAuthSchema, FacilitatorSchema, X402Schema, PaymentsSchema, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
|
|
782
1067
|
var init_schema = __esm({
|
|
783
1068
|
"src/config/schema.ts"() {
|
|
784
1069
|
init_execution();
|
|
785
1070
|
init_core();
|
|
1071
|
+
init_guardrails();
|
|
786
1072
|
init_env();
|
|
787
1073
|
SUPPORTED_CONFIG_VERSION = 1;
|
|
788
1074
|
ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
|
|
@@ -820,11 +1106,36 @@ var init_schema = __esm({
|
|
|
820
1106
|
path: z.string().min(1)
|
|
821
1107
|
}).strict()
|
|
822
1108
|
}).strict();
|
|
1109
|
+
RESERVED_GATEWAY_PATHS = [
|
|
1110
|
+
"/health",
|
|
1111
|
+
"/ready",
|
|
1112
|
+
"/.well-known/agent-commerce",
|
|
1113
|
+
"/api/resources",
|
|
1114
|
+
"/api/resources/:id/invoke",
|
|
1115
|
+
"/api/receipts",
|
|
1116
|
+
"/api/events",
|
|
1117
|
+
"/api/events/stream"
|
|
1118
|
+
];
|
|
1119
|
+
McpMountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
|
|
1120
|
+
message: 'must start with "/" \u2014 it is an absolute gateway path, e.g. "/mcp".'
|
|
1121
|
+
}).refine((value) => !/[:*?\s]/.test(value), {
|
|
1122
|
+
message: 'must not contain ":", "*", "?" or whitespace \u2014 the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
|
|
1123
|
+
}).refine(
|
|
1124
|
+
(value) => {
|
|
1125
|
+
const base = value.replace(/\/+$/, "");
|
|
1126
|
+
return !RESERVED_GATEWAY_PATHS.some(
|
|
1127
|
+
(reserved) => reserved === base || reserved.startsWith(`${base}/`)
|
|
1128
|
+
);
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
message: `must not collide with a route the gateway already serves (${RESERVED_GATEWAY_PATHS.join(", ")}); pick a dedicated prefix such as "/mcp".`
|
|
1132
|
+
}
|
|
1133
|
+
);
|
|
823
1134
|
ProtocolsSchema = z.object({
|
|
824
1135
|
http: z.object({ enabled: BooleanOrString }).strict(),
|
|
825
1136
|
mcp: z.object({
|
|
826
1137
|
enabled: BooleanOrString,
|
|
827
|
-
mountPath:
|
|
1138
|
+
mountPath: McpMountPathSchema
|
|
828
1139
|
}).strict()
|
|
829
1140
|
}).strict();
|
|
830
1141
|
BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
@@ -859,6 +1170,17 @@ var init_schema = __esm({
|
|
|
859
1170
|
payments: z.array(z.string().min(1)).optional()
|
|
860
1171
|
}).strict();
|
|
861
1172
|
ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
|
|
1173
|
+
FacilitatorAuthSchema = z.discriminatedUnion("type", [
|
|
1174
|
+
z.object({ type: z.literal("none") }).strict(),
|
|
1175
|
+
z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
|
|
1176
|
+
// Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
|
|
1177
|
+
// which signs a fresh JWT per request; a static header cannot express it.
|
|
1178
|
+
z.object({
|
|
1179
|
+
type: z.literal("cdp"),
|
|
1180
|
+
apiKeyId: z.string().min(1),
|
|
1181
|
+
apiKeySecret: z.string().min(1)
|
|
1182
|
+
}).strict()
|
|
1183
|
+
]);
|
|
862
1184
|
FacilitatorSchema = z.discriminatedUnion("mode", [
|
|
863
1185
|
z.object({
|
|
864
1186
|
mode: z.literal("local"),
|
|
@@ -866,7 +1188,12 @@ var init_schema = __esm({
|
|
|
866
1188
|
}).strict(),
|
|
867
1189
|
z.object({
|
|
868
1190
|
mode: z.literal("remote"),
|
|
869
|
-
url: z.string().min(1)
|
|
1191
|
+
url: z.string().min(1),
|
|
1192
|
+
// Absent means "this facilitator takes no credential" — an explicit
|
|
1193
|
+
// statement, normalised to `{ type: 'none' }` below. On a mainnet that
|
|
1194
|
+
// combination is refused outright, so the default can never quietly
|
|
1195
|
+
// become an unauthenticated production facilitator.
|
|
1196
|
+
auth: FacilitatorAuthSchema.optional()
|
|
870
1197
|
}).strict()
|
|
871
1198
|
]);
|
|
872
1199
|
X402Schema = z.object({
|
|
@@ -879,7 +1206,11 @@ var init_schema = __esm({
|
|
|
879
1206
|
assetDecimals: NumberOrString,
|
|
880
1207
|
payTo: z.string().min(1),
|
|
881
1208
|
maxTimeoutSeconds: NumberOrString,
|
|
882
|
-
facilitator: FacilitatorSchema
|
|
1209
|
+
facilitator: FacilitatorSchema,
|
|
1210
|
+
/** Real funds. Never defaulted — see src/payments/x402/guardrails.ts. */
|
|
1211
|
+
allowMainnet: BooleanOrString.optional(),
|
|
1212
|
+
/** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
|
|
1213
|
+
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
883
1214
|
}).strict();
|
|
884
1215
|
PaymentsSchema = z.object({
|
|
885
1216
|
x402: X402Schema.optional()
|
|
@@ -1080,10 +1411,11 @@ async function execCommand(command, args) {
|
|
|
1080
1411
|
// src/cli/commands/doctor.ts
|
|
1081
1412
|
init_execution();
|
|
1082
1413
|
init_core();
|
|
1414
|
+
init_networks();
|
|
1083
1415
|
|
|
1084
1416
|
// src/storage/receipts/store.ts
|
|
1085
1417
|
init_core();
|
|
1086
|
-
var PACKAGE_VERSION = "
|
|
1418
|
+
var PACKAGE_VERSION = "1.0.0" ;
|
|
1087
1419
|
|
|
1088
1420
|
// src/storage/receipts/redact.ts
|
|
1089
1421
|
var SECRET_KEY_PATTERN = /private[-_ ]?key|secret|password|mnemonic|seed[-_ ]?phrase|api[-_ ]?key|authoriz(e|ation)|^auth$|auth[-_]?header|payment[-_ ]?proof|^proof$|x-payment|signature|signed[-_ ]?message|token|bearer|credential|cookie|session|jwt/i;
|
|
@@ -1308,6 +1640,12 @@ function createSqliteReceiptStore(options) {
|
|
|
1308
1640
|
);
|
|
1309
1641
|
}
|
|
1310
1642
|
}
|
|
1643
|
+
if (isFileBacked) {
|
|
1644
|
+
try {
|
|
1645
|
+
closeSync(openSync(path2, "a", 384));
|
|
1646
|
+
} catch {
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1311
1649
|
const db = new Database(path2);
|
|
1312
1650
|
db.pragma("journal_mode = WAL");
|
|
1313
1651
|
db.pragma("foreign_keys = ON");
|
|
@@ -1598,23 +1936,8 @@ async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
|
|
|
1598
1936
|
}
|
|
1599
1937
|
}
|
|
1600
1938
|
|
|
1601
|
-
// src/payments/x402/local-chain/accounts.ts
|
|
1602
|
-
var ANVIL_WELL_KNOWN_ACCOUNTS = [
|
|
1603
|
-
{
|
|
1604
|
-
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
|
1605
|
-
privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
1606
|
-
},
|
|
1607
|
-
{
|
|
1608
|
-
address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
|
|
1609
|
-
privateKey: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
|
|
1610
|
-
},
|
|
1611
|
-
{
|
|
1612
|
-
address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
|
|
1613
|
-
privateKey: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
|
|
1614
|
-
}
|
|
1615
|
-
];
|
|
1616
|
-
|
|
1617
1939
|
// src/cli/lib/init-config.ts
|
|
1940
|
+
init_accounts();
|
|
1618
1941
|
var LOCAL_DEV_MERCHANT_ADDRESS = ANVIL_WELL_KNOWN_ACCOUNTS[1].address;
|
|
1619
1942
|
var LOCAL_DEV_FACILITATOR_PRIVATE_KEY = ANVIL_WELL_KNOWN_ACCOUNTS[0].privateKey;
|
|
1620
1943
|
var PLACEHOLDER_ASSET_ADDRESS = "0x000000000000000000000000000000000000dEaD";
|
|
@@ -1692,7 +2015,7 @@ function buildInitConfigObject(answers) {
|
|
|
1692
2015
|
payments: answers.x402Enabled ? {
|
|
1693
2016
|
x402: {
|
|
1694
2017
|
enabled: true,
|
|
1695
|
-
network: "
|
|
2018
|
+
network: "eip155:84532",
|
|
1696
2019
|
rpcUrl: "http://127.0.0.1:8545",
|
|
1697
2020
|
asset: PLACEHOLDER_ASSET_ADDRESS,
|
|
1698
2021
|
assetName: "MockUSDC",
|
|
@@ -1845,7 +2168,7 @@ function maskMiddle(value, keep = 6) {
|
|
|
1845
2168
|
}
|
|
1846
2169
|
createRequire(import.meta.url);
|
|
1847
2170
|
function readVersionReport() {
|
|
1848
|
-
const injected = '{"cliVersion":"
|
|
2171
|
+
const injected = '{"cliVersion":"1.0.0","pinned":[{"name":"@coinbase/x402","version":"2.1.0","via":"@devlab.group/agent-commerce"},{"name":"@modelcontextprotocol/sdk","version":"1.30.0","via":"@devlab.group/agent-commerce"},{"name":"@x402/core","version":"2.23.0","via":"@devlab.group/agent-commerce"},{"name":"@x402/evm","version":"2.23.0","via":"@devlab.group/agent-commerce"},{"name":"viem","version":"2.55.18","via":"@devlab.group/agent-commerce"},{"name":"better-sqlite3","version":"13.0.3","via":"@devlab.group/agent-commerce"},{"name":"fastify","version":"5.12.0","via":"@devlab.group/agent-commerce"},{"name":"pino","version":"10.3.1","via":"@devlab.group/agent-commerce"},{"name":"zod","version":"3.25.76","via":"@devlab.group/agent-commerce"}]}' ;
|
|
1849
2172
|
{
|
|
1850
2173
|
return JSON.parse(injected);
|
|
1851
2174
|
}
|
|
@@ -1884,12 +2207,12 @@ function extractWellKnownX402(body) {
|
|
|
1884
2207
|
}
|
|
1885
2208
|
return { asset, network, payTo };
|
|
1886
2209
|
}
|
|
1887
|
-
function
|
|
2210
|
+
function sameAddress2(a, b) {
|
|
1888
2211
|
return a.toLowerCase() === b.toLowerCase();
|
|
1889
2212
|
}
|
|
1890
2213
|
function findX402Mismatch(configured, live) {
|
|
1891
2214
|
const diffs = [];
|
|
1892
|
-
if (!
|
|
2215
|
+
if (!sameAddress2(configured.asset, live.asset)) {
|
|
1893
2216
|
diffs.push(
|
|
1894
2217
|
`asset: gateway is using ${live.asset} but local config resolves to ${configured.asset}`
|
|
1895
2218
|
);
|
|
@@ -1899,7 +2222,7 @@ function findX402Mismatch(configured, live) {
|
|
|
1899
2222
|
`network: gateway is using "${live.network}" but local config resolves to "${configured.network}"`
|
|
1900
2223
|
);
|
|
1901
2224
|
}
|
|
1902
|
-
if (!
|
|
2225
|
+
if (!sameAddress2(configured.payTo, live.payTo)) {
|
|
1903
2226
|
diffs.push(
|
|
1904
2227
|
`payTo: gateway is using ${live.payTo} but local config resolves to ${configured.payTo}`
|
|
1905
2228
|
);
|
|
@@ -1994,9 +2317,13 @@ async function runDoctor(options, deps = {}) {
|
|
|
1994
2317
|
if (x402 === void 0 || !x402.enabled) {
|
|
1995
2318
|
checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
|
|
1996
2319
|
} else {
|
|
1997
|
-
const
|
|
2320
|
+
const profile = findNetworkProfile(x402.network);
|
|
2321
|
+
const mode = profile ? resolveDeploymentMode(profile, x402.facilitator.mode) : void 0;
|
|
2322
|
+
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})`;
|
|
2323
|
+
const facilitatorDetail = x402.facilitator.mode === "local" ? "local" : `remote (auth=${x402.facilitator.auth.type})`;
|
|
2324
|
+
const summary = `x402 v2 (scheme=exact) enabled \u2014 ${where}, destination=${maskMiddle(x402.payTo)}, facilitator=${facilitatorDetail}`;
|
|
1998
2325
|
const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body) : void 0;
|
|
1999
|
-
if (
|
|
2326
|
+
if (sameAddress2(x402.asset, PLACEHOLDER_ASSET_ADDRESS)) {
|
|
2000
2327
|
checks.push({
|
|
2001
2328
|
name: "Payments",
|
|
2002
2329
|
status: "WARN",
|
|
@@ -2020,11 +2347,19 @@ async function runDoctor(options, deps = {}) {
|
|
|
2020
2347
|
mismatch === void 0 ? { name: "Payments", status: "PASS", detail: summary } : { name: "Payments", status: "FAIL", detail: mismatch }
|
|
2021
2348
|
);
|
|
2022
2349
|
}
|
|
2350
|
+
if (mode === "mainnet") {
|
|
2351
|
+
const unauthenticated = x402.facilitator.mode === "remote" && x402.facilitator.auth.type === "none";
|
|
2352
|
+
checks.push({
|
|
2353
|
+
name: "Mainnet safety",
|
|
2354
|
+
status: unauthenticated ? "WARN" : "INFO",
|
|
2355
|
+
detail: unauthenticated ? `${describeDeploymentMode("mainnet")} \u2014 settling through a facilitator that takes no credential, accepted via allowUnauthenticatedFacilitator. It sees every payment authorisation you handle, with no account or terms behind it. Everything else is enforced at config load: explicit allowMainnet opt-in, HTTPS, non-development payTo, canonical asset. Payments are fail-closed.` : `${describeDeploymentMode("mainnet")} \u2014 enforced at config load: explicit allowMainnet opt-in, remote facilitator over HTTPS with a credential, non-development payTo, and the canonical asset for this network. Payments are fail-closed.`
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2023
2358
|
}
|
|
2024
2359
|
checks.push({
|
|
2025
2360
|
name: "Payments (MPP)",
|
|
2026
2361
|
status: "INFO",
|
|
2027
|
-
detail: "planned \u2014 not implemented in
|
|
2362
|
+
detail: "planned \u2014 not implemented in this release"
|
|
2028
2363
|
});
|
|
2029
2364
|
if (config === void 0) {
|
|
2030
2365
|
checks.push({ name: "Storage", status: "WARN", detail: "skipped \u2014 config invalid" });
|
|
@@ -2034,9 +2369,8 @@ async function runDoctor(options, deps = {}) {
|
|
|
2034
2369
|
// from the host) silently produces a fresh empty database and a false
|
|
2035
2370
|
// "healthy, receipts=0" PASS — a read-only command must not have this
|
|
2036
2371
|
// side effect, and it shadows the real store's real receipts entirely.
|
|
2037
|
-
|
|
2038
|
-
//
|
|
2039
|
-
// branch below, same as today.
|
|
2372
|
+
// `:memory:` never exists on disk by definition, so it always falls
|
|
2373
|
+
// through to the real open/health/count branch below, same as today.
|
|
2040
2374
|
config.storage.receipts.path !== ":memory:" && !existsSync(config.storage.receipts.path)
|
|
2041
2375
|
) {
|
|
2042
2376
|
checks.push({
|
|
@@ -2379,6 +2713,12 @@ function buildProgram(io = processIo) {
|
|
|
2379
2713
|
}
|
|
2380
2714
|
|
|
2381
2715
|
// src/cli/index.ts
|
|
2716
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
2717
|
+
stream.on("error", (err) => {
|
|
2718
|
+
if (err.code === "EPIPE") process.exit(0);
|
|
2719
|
+
throw err;
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2382
2722
|
async function main() {
|
|
2383
2723
|
const program = buildProgram();
|
|
2384
2724
|
try {
|