@devlab.group/agent-commerce 0.2.0-beta.0 → 1.1.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 +145 -59
- package/dist/{chunk-ASK67X56.js → chunk-64UZVJEO.js} +3 -3
- package/dist/{chunk-ASK67X56.js.map → chunk-64UZVJEO.js.map} +1 -1
- package/dist/{chunk-MQMKQGAB.js → chunk-M4I7I2UY.js} +88 -32
- package/dist/chunk-M4I7I2UY.js.map +1 -0
- package/dist/cli/index.js +279 -48
- package/dist/cli/index.js.map +1 -1
- package/dist/guardrails-Dmwi63KB.d.ts +38 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +208 -30
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{protocol-adapter-CtB0sOnV.d.ts → protocol-adapter-DZ8qkkAa.d.ts} +22 -2
- package/dist/{runtime-Bie-9FAM.d.ts → runtime-CC6EFmLP.d.ts} +3 -3
- package/dist/x402.d.ts +20 -6
- package/dist/x402.js +79 -23
- package/dist/x402.js.map +1 -1
- package/package.json +8 -1
- package/dist/chunk-MQMKQGAB.js.map +0 -1
- package/dist/guardrails-Di_yI_DW.d.ts +0 -25
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the gateway authenticates to a remote facilitator.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately a list, not a vendor: x402 facilitators are not a Coinbase-only
|
|
5
|
+
* category, and a scheme that only fitted one vendor's credentials would make
|
|
6
|
+
* the abstraction a fiction.
|
|
7
|
+
*
|
|
8
|
+
* - `none` — the facilitator takes no credential (the public testnet one).
|
|
9
|
+
* - `bearer` — a static token. Covers most self-hosted and third-party
|
|
10
|
+
* facilitators, and needs nothing installed.
|
|
11
|
+
* - `cdp` — Coinbase Developer Platform, which signs a fresh JWT per request
|
|
12
|
+
* over method + host + path, so a static header cannot express it. Handled by
|
|
13
|
+
* the optional peer `@coinbase/x402`, imported only when this type is
|
|
14
|
+
* configured. Anyone not using CDP never installs it.
|
|
15
|
+
*
|
|
16
|
+
* A facilitator whose scheme is none of these is refused at config load rather
|
|
17
|
+
* than sent nothing.
|
|
18
|
+
*/
|
|
19
|
+
type FacilitatorAuth = {
|
|
20
|
+
readonly type: 'none';
|
|
21
|
+
} | {
|
|
22
|
+
readonly type: 'bearer';
|
|
23
|
+
readonly token: string;
|
|
24
|
+
} | {
|
|
25
|
+
readonly type: 'cdp';
|
|
26
|
+
readonly apiKeyId: string;
|
|
27
|
+
readonly apiKeySecret: string;
|
|
28
|
+
};
|
|
29
|
+
type X402FacilitatorConfig = {
|
|
30
|
+
readonly mode: 'local';
|
|
31
|
+
readonly signerPrivateKey: string;
|
|
32
|
+
} | {
|
|
33
|
+
readonly mode: 'remote';
|
|
34
|
+
readonly url: string;
|
|
35
|
+
readonly auth: FacilitatorAuth;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type { FacilitatorAuth as F, X402FacilitatorConfig as X };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { I as IsoTimestamp, D as DecimalAmount, P as PaymentMethodName, B as BackendHandler, A as AdapterDescriptor, a as AdapterHealth, C as CommerceResource, R as ResourceRegistry, b as PaymentProvider, L as Logger, c as Clock, d as IdGenerator } from './runtime-
|
|
2
|
-
export { e as BackendMethod, f as DEFAULT_BACKEND_TIMEOUT_MS, J as JsonSchema, N as NOOP_LOGGER, g as PaymentChallenge, h as PaymentContext, i as PaymentRequirement, j as PaymentResult, k as PaymentSettlementContext, l as PaymentSubmission, m as PaymentVerificationContext, n as Pricing, o as ProtocolName, s as systemClock } from './runtime-
|
|
3
|
-
import { X as X402FacilitatorConfig } from './guardrails-
|
|
4
|
-
import { D as DeliveredOutcome, P as PaymentRequiredOutcome, C as CommerceEvent, a as PaymentAttempt, b as CommerceReceipt, E as ExecutionPipeline, c as ProtocolAdapter } from './protocol-adapter-
|
|
5
|
-
export { d as COMMERCE_EVENT_TYPES, e as CanonicalRequest, f as CommerceEventType, g as EventSink, h as ExecutionOutcome, H as HttpProtocolAdapter, i as ProtocolAdapterContext, j as isHttpProtocolAdapter } from './protocol-adapter-
|
|
1
|
+
import { I as IsoTimestamp, D as DecimalAmount, P as PaymentMethodName, B as BackendHandler, A as AdapterDescriptor, a as AdapterHealth, C as CommerceResource, R as ResourceRegistry, b as PaymentProvider, L as Logger, c as Clock, d as IdGenerator } from './runtime-CC6EFmLP.js';
|
|
2
|
+
export { e as BackendMethod, f as DEFAULT_BACKEND_TIMEOUT_MS, J as JsonSchema, N as NOOP_LOGGER, g as PaymentChallenge, h as PaymentContext, i as PaymentRequirement, j as PaymentResult, k as PaymentSettlementContext, l as PaymentSubmission, m as PaymentVerificationContext, n as Pricing, o as ProtocolName, s as systemClock } from './runtime-CC6EFmLP.js';
|
|
3
|
+
import { X as X402FacilitatorConfig } from './guardrails-Dmwi63KB.js';
|
|
4
|
+
import { D as DeliveredOutcome, P as PaymentRequiredOutcome, C as CommerceEvent, a as PaymentAttempt, b as CommerceReceipt, E as ExecutionPipeline, c as ProtocolAdapter } from './protocol-adapter-DZ8qkkAa.js';
|
|
5
|
+
export { A as AdapterHttpRoute, d as COMMERCE_EVENT_TYPES, e as CanonicalRequest, f as CommerceEventType, g as EventSink, h as ExecutionOutcome, H as HttpProtocolAdapter, i as ProtocolAdapterContext, j as isHttpProtocolAdapter } from './protocol-adapter-DZ8qkkAa.js';
|
|
6
6
|
import { FastifyInstance } from 'fastify';
|
|
7
7
|
import 'node:http';
|
|
8
8
|
|
|
@@ -286,7 +286,7 @@ interface ReceiptStore {
|
|
|
286
286
|
* Receipts whose delivery did not succeed — `backendStatus` outside 2xx.
|
|
287
287
|
*
|
|
288
288
|
* A paid-but-undelivered purchase is the one row a merchant must notice:
|
|
289
|
-
* settlement is final and
|
|
289
|
+
* settlement is final and this release has no refunds, so *seeing* it is the only
|
|
290
290
|
* remedy available. Counted rather than listed for the same reason as
|
|
291
291
|
* {@link countReceipts} — `listReceipts` is clamped, so counting by list
|
|
292
292
|
* length silently saturates.
|
|
@@ -362,6 +362,10 @@ interface GatewayConfig {
|
|
|
362
362
|
readonly enabled: boolean;
|
|
363
363
|
readonly mountPath: string;
|
|
364
364
|
};
|
|
365
|
+
readonly a2a: {
|
|
366
|
+
readonly enabled: boolean;
|
|
367
|
+
readonly mountPath: string;
|
|
368
|
+
};
|
|
365
369
|
};
|
|
366
370
|
/** Canonical resources, already normalised. */
|
|
367
371
|
readonly resources: readonly CommerceResource[];
|
|
@@ -378,6 +382,7 @@ interface GatewayConfig {
|
|
|
378
382
|
readonly maxTimeoutSeconds: number;
|
|
379
383
|
readonly facilitator: X402FacilitatorConfig;
|
|
380
384
|
readonly allowMainnet?: boolean;
|
|
385
|
+
readonly allowUnauthenticatedFacilitator?: boolean;
|
|
381
386
|
};
|
|
382
387
|
};
|
|
383
388
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-
|
|
2
|
-
import { CommerceError, PAYMENT_INPUT_FIELD, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, PAYMENT_HEADER, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER } from './chunk-
|
|
3
|
-
export { COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, RETRYABLE_ERROR_CODES, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-
|
|
4
|
-
import { promises, existsSync, mkdirSync, accessSync, constants, chmodSync } from 'fs';
|
|
1
|
+
import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-M4I7I2UY.js';
|
|
2
|
+
import { CommerceError, PAYMENT_INPUT_FIELD, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, PAYMENT_HEADER, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER } from './chunk-64UZVJEO.js';
|
|
3
|
+
export { COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, RETRYABLE_ERROR_CODES, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-64UZVJEO.js';
|
|
4
|
+
import { promises, existsSync, mkdirSync, accessSync, constants, closeSync, openSync, chmodSync } from 'fs';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { dirname } from 'path';
|
|
7
7
|
import { parse, YAMLError } from 'yaml';
|
|
@@ -14,6 +14,9 @@ import Database from 'better-sqlite3';
|
|
|
14
14
|
|
|
15
15
|
// src/config/env.ts
|
|
16
16
|
var PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
|
|
17
|
+
var BRACE_TOKEN = /\$\{[^{}]*\}/g;
|
|
18
|
+
var SUPPORTED_TOKEN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}$/;
|
|
19
|
+
var NESTED_PLACEHOLDER = /\$\{[^{}]*\$\{/;
|
|
17
20
|
function substituteEnv(value, env, path2 = "$") {
|
|
18
21
|
if (typeof value === "string") {
|
|
19
22
|
return substituteString(value, env, path2);
|
|
@@ -31,19 +34,26 @@ function substituteEnv(value, env, path2 = "$") {
|
|
|
31
34
|
return value;
|
|
32
35
|
}
|
|
33
36
|
function substituteString(value, env, path2) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
assertTemplateIsSupported(value, path2);
|
|
38
|
+
return substituteOnce(value, env, path2);
|
|
39
|
+
}
|
|
40
|
+
function assertTemplateIsSupported(value, path2) {
|
|
41
|
+
if (NESTED_PLACEHOLDER.test(value)) {
|
|
42
|
+
throw new CommerceError(
|
|
43
|
+
"CONFIG_INVALID",
|
|
44
|
+
`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.`,
|
|
45
|
+
{ details: { path: path2 } }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
BRACE_TOKEN.lastIndex = 0;
|
|
49
|
+
for (const [token] of value.matchAll(BRACE_TOKEN)) {
|
|
50
|
+
if (SUPPORTED_TOKEN.test(token)) continue;
|
|
40
51
|
throw new CommerceError(
|
|
41
52
|
"CONFIG_INVALID",
|
|
42
|
-
`Configuration value at "${path2}"
|
|
43
|
-
{ details: { path: path2,
|
|
53
|
+
`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.`,
|
|
54
|
+
{ details: { path: path2, token } }
|
|
44
55
|
);
|
|
45
56
|
}
|
|
46
|
-
return substituted;
|
|
47
57
|
}
|
|
48
58
|
function substituteOnce(value, env, path2) {
|
|
49
59
|
return value.replace(
|
|
@@ -1111,13 +1121,48 @@ var StorageSchema = z.object({
|
|
|
1111
1121
|
path: z.string().min(1)
|
|
1112
1122
|
}).strict()
|
|
1113
1123
|
}).strict();
|
|
1124
|
+
var RESERVED_GATEWAY_PATHS = [
|
|
1125
|
+
"/health",
|
|
1126
|
+
"/ready",
|
|
1127
|
+
"/.well-known/agent-commerce",
|
|
1128
|
+
// Fixed by the A2A specification, so it is the adapter's to serve and never
|
|
1129
|
+
// a configurable mount's to claim.
|
|
1130
|
+
"/.well-known/agent-card.json",
|
|
1131
|
+
"/api/resources",
|
|
1132
|
+
"/api/resources/:id/invoke",
|
|
1133
|
+
"/api/receipts",
|
|
1134
|
+
"/api/events",
|
|
1135
|
+
"/api/events/stream"
|
|
1136
|
+
];
|
|
1137
|
+
var MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
|
|
1138
|
+
message: 'must start with "/" \u2014 it is an absolute gateway path, e.g. "/mcp".'
|
|
1139
|
+
}).refine((value) => !/[:*?\s]/.test(value), {
|
|
1140
|
+
message: 'must not contain ":", "*", "?" or whitespace \u2014 the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
|
|
1141
|
+
}).refine(
|
|
1142
|
+
(value) => {
|
|
1143
|
+
const base = value.replace(/\/+$/, "");
|
|
1144
|
+
return !RESERVED_GATEWAY_PATHS.some(
|
|
1145
|
+
(reserved) => reserved === base || reserved.startsWith(`${base}/`)
|
|
1146
|
+
);
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
message: `must not collide with a route the gateway already serves (${RESERVED_GATEWAY_PATHS.join(", ")}); pick a dedicated prefix such as "/mcp".`
|
|
1150
|
+
}
|
|
1151
|
+
);
|
|
1114
1152
|
var ProtocolsSchema = z.object({
|
|
1115
1153
|
http: z.object({ enabled: BooleanOrString }).strict(),
|
|
1116
1154
|
mcp: z.object({
|
|
1117
1155
|
enabled: BooleanOrString,
|
|
1118
|
-
mountPath:
|
|
1119
|
-
}).strict()
|
|
1156
|
+
mountPath: MountPathSchema
|
|
1157
|
+
}).strict(),
|
|
1158
|
+
// Optional block: A2A is off unless an operator asks for it, and an
|
|
1159
|
+
// existing config predating the adapter stays valid.
|
|
1160
|
+
a2a: z.object({
|
|
1161
|
+
enabled: BooleanOrString,
|
|
1162
|
+
mountPath: MountPathSchema.optional()
|
|
1163
|
+
}).strict().optional()
|
|
1120
1164
|
}).strict();
|
|
1165
|
+
var DEFAULT_A2A_MOUNT_PATH = "/a2a";
|
|
1121
1166
|
var BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
1122
1167
|
var BackendHandlerSchema = z.object({
|
|
1123
1168
|
type: z.literal("http"),
|
|
@@ -1152,7 +1197,14 @@ var ResourceEntrySchema = z.object({
|
|
|
1152
1197
|
var ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
|
|
1153
1198
|
var FacilitatorAuthSchema = z.discriminatedUnion("type", [
|
|
1154
1199
|
z.object({ type: z.literal("none") }).strict(),
|
|
1155
|
-
z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict()
|
|
1200
|
+
z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
|
|
1201
|
+
// Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
|
|
1202
|
+
// which signs a fresh JWT per request; a static header cannot express it.
|
|
1203
|
+
z.object({
|
|
1204
|
+
type: z.literal("cdp"),
|
|
1205
|
+
apiKeyId: z.string().min(1),
|
|
1206
|
+
apiKeySecret: z.string().min(1)
|
|
1207
|
+
}).strict()
|
|
1156
1208
|
]);
|
|
1157
1209
|
var FacilitatorSchema = z.discriminatedUnion("mode", [
|
|
1158
1210
|
z.object({
|
|
@@ -1181,7 +1233,9 @@ var X402Schema = z.object({
|
|
|
1181
1233
|
maxTimeoutSeconds: NumberOrString,
|
|
1182
1234
|
facilitator: FacilitatorSchema,
|
|
1183
1235
|
/** Real funds. Never defaulted — see src/payments/x402/guardrails.ts. */
|
|
1184
|
-
allowMainnet: BooleanOrString.optional()
|
|
1236
|
+
allowMainnet: BooleanOrString.optional(),
|
|
1237
|
+
/** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
|
|
1238
|
+
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
1185
1239
|
}).strict();
|
|
1186
1240
|
var PaymentsSchema = z.object({
|
|
1187
1241
|
x402: X402Schema.optional()
|
|
@@ -1292,7 +1346,7 @@ function toBoolean(value, path2) {
|
|
|
1292
1346
|
{ details: { path: path2 } }
|
|
1293
1347
|
);
|
|
1294
1348
|
}
|
|
1295
|
-
var SUPPORTED_PROTOCOLS = /* @__PURE__ */ new Set(["http", "mcp"]);
|
|
1349
|
+
var SUPPORTED_PROTOCOLS = /* @__PURE__ */ new Set(["http", "mcp", "a2a"]);
|
|
1296
1350
|
var SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
|
|
1297
1351
|
function normalise(raw) {
|
|
1298
1352
|
const protocols = {
|
|
@@ -1300,8 +1354,13 @@ function normalise(raw) {
|
|
|
1300
1354
|
mcp: {
|
|
1301
1355
|
enabled: toBoolean(raw.protocols.mcp.enabled, "protocols.mcp.enabled"),
|
|
1302
1356
|
mountPath: raw.protocols.mcp.mountPath
|
|
1357
|
+
},
|
|
1358
|
+
a2a: {
|
|
1359
|
+
enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
|
|
1360
|
+
mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
|
|
1303
1361
|
}
|
|
1304
1362
|
};
|
|
1363
|
+
validateMountPaths(protocols);
|
|
1305
1364
|
const x402Raw = raw.payments.x402;
|
|
1306
1365
|
const facilitator = x402Raw === void 0 ? void 0 : x402Raw.facilitator.mode === "local" ? { mode: "local", signerPrivateKey: x402Raw.facilitator.signerPrivateKey } : {
|
|
1307
1366
|
mode: "remote",
|
|
@@ -1328,7 +1387,13 @@ function normalise(raw) {
|
|
|
1328
1387
|
}
|
|
1329
1388
|
),
|
|
1330
1389
|
facilitator,
|
|
1331
|
-
...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {}
|
|
1390
|
+
...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
|
|
1391
|
+
...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
1392
|
+
allowUnauthenticatedFacilitator: toBoolean(
|
|
1393
|
+
x402Raw.allowUnauthenticatedFacilitator,
|
|
1394
|
+
"payments.x402.allowUnauthenticatedFacilitator"
|
|
1395
|
+
)
|
|
1396
|
+
} : {}
|
|
1332
1397
|
} : void 0;
|
|
1333
1398
|
if (x402) {
|
|
1334
1399
|
validateAddress("payments.x402.payTo", x402.payTo);
|
|
@@ -1337,8 +1402,11 @@ function normalise(raw) {
|
|
|
1337
1402
|
network: x402.network,
|
|
1338
1403
|
payTo: x402.payTo,
|
|
1339
1404
|
asset: x402.asset,
|
|
1405
|
+
assetName: x402.assetName,
|
|
1406
|
+
assetVersion: x402.assetVersion,
|
|
1340
1407
|
facilitator: x402.facilitator,
|
|
1341
|
-
...x402.allowMainnet !== void 0 ? { allowMainnet: x402.allowMainnet } : {}
|
|
1408
|
+
...x402.allowMainnet !== void 0 ? { allowMainnet: x402.allowMainnet } : {},
|
|
1409
|
+
...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
|
|
1342
1410
|
});
|
|
1343
1411
|
}
|
|
1344
1412
|
const resources = Object.entries(raw.resources).map(
|
|
@@ -1367,6 +1435,25 @@ function normalise(raw) {
|
|
|
1367
1435
|
}
|
|
1368
1436
|
};
|
|
1369
1437
|
}
|
|
1438
|
+
function validateMountPaths(protocols) {
|
|
1439
|
+
const mounts = [
|
|
1440
|
+
["mcp", protocols.mcp],
|
|
1441
|
+
["a2a", protocols.a2a]
|
|
1442
|
+
].filter(([, p]) => p.enabled);
|
|
1443
|
+
for (const [index, [nameA, a]] of mounts.entries()) {
|
|
1444
|
+
for (const [nameB, b] of mounts.slice(index + 1)) {
|
|
1445
|
+
const baseA = a.mountPath.replace(/\/+$/, "");
|
|
1446
|
+
const baseB = b.mountPath.replace(/\/+$/, "");
|
|
1447
|
+
if (baseA === baseB || baseA.startsWith(`${baseB}/`) || baseB.startsWith(`${baseA}/`)) {
|
|
1448
|
+
throw new CommerceError(
|
|
1449
|
+
"CONFIG_INVALID",
|
|
1450
|
+
`protocols.${nameA}.mountPath ("${a.mountPath}") collides with protocols.${nameB}.mountPath ("${b.mountPath}") \u2014 each mount registers a wildcard, so overlapping prefixes cannot both be served`,
|
|
1451
|
+
{ details: { path: `protocols.${nameA}.mountPath` } }
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1370
1457
|
function normaliseResource(id, entry, protocols, x402) {
|
|
1371
1458
|
if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
|
|
1372
1459
|
const inputProperties = entry.input?.["properties"];
|
|
@@ -1387,7 +1474,7 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1387
1474
|
const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
|
|
1388
1475
|
throw new CommerceError(
|
|
1389
1476
|
"CONFIG_INVALID",
|
|
1390
|
-
`Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp.`,
|
|
1477
|
+
`Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp, a2a.`,
|
|
1391
1478
|
{ details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
|
|
1392
1479
|
);
|
|
1393
1480
|
}
|
|
@@ -1408,6 +1495,13 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1408
1495
|
);
|
|
1409
1496
|
}
|
|
1410
1497
|
}
|
|
1498
|
+
if (entry.expose.includes("a2a") && !protocols.a2a.enabled) {
|
|
1499
|
+
throw new CommerceError(
|
|
1500
|
+
"CONFIG_INVALID",
|
|
1501
|
+
`Resource "${id}" is exposed via "a2a" but protocols.a2a.enabled is false`,
|
|
1502
|
+
{ details: { path: `resources.${id}.expose`, resourceId: id } }
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1411
1505
|
if (entry.expose.includes("http") && !protocols.http.enabled) {
|
|
1412
1506
|
throw new CommerceError(
|
|
1413
1507
|
"CONFIG_INVALID",
|
|
@@ -1497,6 +1591,10 @@ function defaultClosedObjectSchema(schema) {
|
|
|
1497
1591
|
if (isPlainObject4(items)) {
|
|
1498
1592
|
result["items"] = defaultClosedObjectSchema(items);
|
|
1499
1593
|
}
|
|
1594
|
+
const additional = schema["additionalProperties"];
|
|
1595
|
+
if (isPlainObject4(additional)) {
|
|
1596
|
+
result["additionalProperties"] = defaultClosedObjectSchema(additional);
|
|
1597
|
+
}
|
|
1500
1598
|
return result;
|
|
1501
1599
|
}
|
|
1502
1600
|
var EMPTY_CLOSED_OBJECT_SCHEMA = {
|
|
@@ -1570,6 +1668,10 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1570
1668
|
if (isPlainObject4(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
|
|
1571
1669
|
}
|
|
1572
1670
|
}
|
|
1671
|
+
const additional = schema["additionalProperties"];
|
|
1672
|
+
if (isPlainObject4(additional)) {
|
|
1673
|
+
validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
|
|
1674
|
+
}
|
|
1573
1675
|
const items = schema["items"];
|
|
1574
1676
|
if (isPlainObject4(items)) {
|
|
1575
1677
|
validateResourceSchemaKeywords(id, `${path2}.items`, items);
|
|
@@ -1619,6 +1721,23 @@ function validatePathParametersDeclared(id, url, input) {
|
|
|
1619
1721
|
}
|
|
1620
1722
|
const params = extractPathParameterNames(url);
|
|
1621
1723
|
if (params.length === 0) return;
|
|
1724
|
+
const firstBrace = url.indexOf("{");
|
|
1725
|
+
if (firstBrace !== -1) {
|
|
1726
|
+
const prefix = url.slice(0, firstBrace);
|
|
1727
|
+
let origin;
|
|
1728
|
+
try {
|
|
1729
|
+
const parsedPrefix = new URL(prefix);
|
|
1730
|
+
origin = parsedPrefix.hostname === "" ? void 0 : parsedPrefix.origin;
|
|
1731
|
+
} catch {
|
|
1732
|
+
}
|
|
1733
|
+
if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
|
|
1734
|
+
throw new CommerceError(
|
|
1735
|
+
"CONFIG_INVALID",
|
|
1736
|
+
`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`,
|
|
1737
|
+
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1622
1741
|
const propertiesRaw = input?.["properties"];
|
|
1623
1742
|
const properties = isPlainObject4(propertiesRaw) ? propertiesRaw : {};
|
|
1624
1743
|
const requiredRaw = input?.["required"];
|
|
@@ -1899,6 +2018,7 @@ function hashToken(value) {
|
|
|
1899
2018
|
// src/gateway/adapters.ts
|
|
1900
2019
|
var MOUNT_BODY_LIMIT_BYTES = 256 * 1024;
|
|
1901
2020
|
async function startAndMountAdapters(options) {
|
|
2021
|
+
assertNoRouteConflicts(options.adapters);
|
|
1902
2022
|
const runtimes = [];
|
|
1903
2023
|
for (const adapter of options.adapters) {
|
|
1904
2024
|
const adapterLogger = options.logger.child({ adapter: adapter.name });
|
|
@@ -1924,11 +2044,51 @@ async function startAndMountAdapters(options) {
|
|
|
1924
2044
|
}
|
|
1925
2045
|
return runtimes;
|
|
1926
2046
|
}
|
|
2047
|
+
function routeClaims(adapters) {
|
|
2048
|
+
const claims = [];
|
|
2049
|
+
for (const adapter of adapters) {
|
|
2050
|
+
if (!isHttpProtocolAdapter(adapter)) continue;
|
|
2051
|
+
claims.push({
|
|
2052
|
+
adapter: adapter.name,
|
|
2053
|
+
path: adapter.mountPath.replace(/\/+$/, ""),
|
|
2054
|
+
wildcard: true,
|
|
2055
|
+
method: "ALL"
|
|
2056
|
+
});
|
|
2057
|
+
for (const route of adapter.additionalHttpRoutes ?? []) {
|
|
2058
|
+
claims.push({
|
|
2059
|
+
adapter: adapter.name,
|
|
2060
|
+
path: route.path.replace(/\/+$/, ""),
|
|
2061
|
+
wildcard: false,
|
|
2062
|
+
method: route.method
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
return claims;
|
|
2067
|
+
}
|
|
2068
|
+
function claimsCollide(a, b) {
|
|
2069
|
+
if (a.wildcard && (b.path === a.path || b.path.startsWith(`${a.path}/`))) return true;
|
|
2070
|
+
if (b.wildcard && a.path.startsWith(`${b.path}/`)) return true;
|
|
2071
|
+
return a.path === b.path && (a.method === b.method || a.method === "ALL" || b.method === "ALL");
|
|
2072
|
+
}
|
|
2073
|
+
function assertNoRouteConflicts(adapters) {
|
|
2074
|
+
const claims = routeClaims(adapters);
|
|
2075
|
+
for (const [index, a] of claims.entries()) {
|
|
2076
|
+
for (const b of claims.slice(index + 1)) {
|
|
2077
|
+
if (a.adapter === b.adapter || !claimsCollide(a, b)) continue;
|
|
2078
|
+
throw new CommerceError(
|
|
2079
|
+
"CONFIG_INVALID",
|
|
2080
|
+
`Protocol adapters "${a.adapter}" and "${b.adapter}" both claim the HTTP path "${b.path}"; each adapter path must be served by exactly one adapter`,
|
|
2081
|
+
{ details: { adapters: [a.adapter, b.adapter], path: b.path } }
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
1927
2086
|
var MOUNT_MAX_CONCURRENT_REQUESTS = 20;
|
|
1928
2087
|
var MOUNT_BUSY_RETRY_AFTER_SECONDS = 1;
|
|
1929
2088
|
function mountHttpAdapter(server, adapter, logger) {
|
|
1930
2089
|
const mountPath = adapter.mountPath;
|
|
1931
2090
|
const wildcard = mountPath.endsWith("/") ? `${mountPath}*` : `${mountPath}/*`;
|
|
2091
|
+
const additionalRoutes = adapter.additionalHttpRoutes ?? [];
|
|
1932
2092
|
let inFlight = 0;
|
|
1933
2093
|
void server.register(async (instance) => {
|
|
1934
2094
|
instance.addContentTypeParser(
|
|
@@ -1940,7 +2100,7 @@ function mountHttpAdapter(server, adapter, logger) {
|
|
|
1940
2100
|
instance.addContentTypeParser("*", (_request, _payload, done) => {
|
|
1941
2101
|
done(null);
|
|
1942
2102
|
});
|
|
1943
|
-
const
|
|
2103
|
+
const makeHandler = (handleHttp) => async (request, reply) => {
|
|
1944
2104
|
if (inFlight >= MOUNT_MAX_CONCURRENT_REQUESTS) {
|
|
1945
2105
|
const error = new CommerceError(
|
|
1946
2106
|
"GATEWAY_BUSY",
|
|
@@ -1958,7 +2118,7 @@ function mountHttpAdapter(server, adapter, logger) {
|
|
|
1958
2118
|
logger
|
|
1959
2119
|
);
|
|
1960
2120
|
try {
|
|
1961
|
-
await
|
|
2121
|
+
await handleHttp(request.raw, reply.raw);
|
|
1962
2122
|
} catch (error) {
|
|
1963
2123
|
logger.error({ err: describeError3(error) }, "Protocol adapter request handler threw");
|
|
1964
2124
|
if (!reply.raw.headersSent) {
|
|
@@ -1972,8 +2132,16 @@ function mountHttpAdapter(server, adapter, logger) {
|
|
|
1972
2132
|
inFlight -= 1;
|
|
1973
2133
|
}
|
|
1974
2134
|
};
|
|
1975
|
-
|
|
1976
|
-
instance.all(
|
|
2135
|
+
const mountHandler = makeHandler((req, res) => adapter.handleHttp(req, res));
|
|
2136
|
+
instance.all(mountPath, mountHandler);
|
|
2137
|
+
instance.all(wildcard, mountHandler);
|
|
2138
|
+
for (const route of additionalRoutes) {
|
|
2139
|
+
instance.route({
|
|
2140
|
+
method: route.method,
|
|
2141
|
+
url: route.path,
|
|
2142
|
+
handler: makeHandler((req, res) => route.handleHttp(req, res))
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
1977
2145
|
});
|
|
1978
2146
|
}
|
|
1979
2147
|
function enforceMountBodyLimit(req, res, maxBytes, logger) {
|
|
@@ -2163,13 +2331,17 @@ function createReadinessProbe(options, ttlMs = READINESS_TTL_MS) {
|
|
|
2163
2331
|
}
|
|
2164
2332
|
|
|
2165
2333
|
// src/gateway/well-known.ts
|
|
2166
|
-
var GATEWAY_SUPPORTED_SPEC = "agent-commerce/
|
|
2334
|
+
var GATEWAY_SUPPORTED_SPEC = "agent-commerce/v1.0.0";
|
|
2335
|
+
function publicHealth(health) {
|
|
2336
|
+
const { detail: _detail, ...rest } = health;
|
|
2337
|
+
return rest;
|
|
2338
|
+
}
|
|
2167
2339
|
async function buildWellKnownDocument(options) {
|
|
2168
2340
|
const adapters = await Promise.all(
|
|
2169
|
-
options.adapterRuntimes.map(async (runtime) =>
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
})
|
|
2341
|
+
options.adapterRuntimes.map(async (runtime) => {
|
|
2342
|
+
const health = await getAdapterHealth(runtime, options.clock);
|
|
2343
|
+
return { ...runtime.adapter.descriptor, health: publicHealth(health) };
|
|
2344
|
+
})
|
|
2173
2345
|
);
|
|
2174
2346
|
const x402 = options.config.payments.x402;
|
|
2175
2347
|
return {
|
|
@@ -2724,6 +2896,12 @@ function createSqliteReceiptStore(options) {
|
|
|
2724
2896
|
);
|
|
2725
2897
|
}
|
|
2726
2898
|
}
|
|
2899
|
+
if (isFileBacked) {
|
|
2900
|
+
try {
|
|
2901
|
+
closeSync(openSync(path2, "a", 384));
|
|
2902
|
+
} catch {
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2727
2905
|
const db = new Database(path2);
|
|
2728
2906
|
db.pragma("journal_mode = WAL");
|
|
2729
2907
|
db.pragma("foreign_keys = ON");
|