@devlab.group/agent-commerce 1.0.0 → 1.2.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 +71 -46
- package/dist/{chunk-MEPETSP6.js → chunk-5TKNTWMC.js} +3 -3
- package/dist/{chunk-MEPETSP6.js.map → chunk-5TKNTWMC.js.map} +1 -1
- package/dist/{chunk-BQNE3A5H.js → chunk-EIACECVM.js} +7 -4
- package/dist/chunk-EIACECVM.js.map +1 -0
- package/dist/cli/index.js +1407 -83
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +258 -98
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{protocol-adapter-CZPOb9uG.d.ts → protocol-adapter-BAKGnuZ4.d.ts} +22 -2
- package/dist/{runtime-B-9IjMbO.d.ts → runtime-Bqg9qW3t.d.ts} +34 -3
- package/dist/x402.d.ts +1 -1
- package/dist/x402.js +3 -3
- package/package.json +13 -3
- package/dist/chunk-BQNE3A5H.js.map +0 -1
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
|
|
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-Bqg9qW3t.js';
|
|
2
|
+
export { e as BackendMethod, f as DEFAULT_BACKEND_TIMEOUT_MS, J as JsonSchema, N as NOOP_LOGGER, g as PROTOCOL_NAMES, h as PaymentChallenge, i as PaymentContext, j as PaymentRequirement, k as PaymentResult, l as PaymentSettlementContext, m as PaymentSubmission, n as PaymentVerificationContext, o as Pricing, p as ProtocolName, s as systemClock } from './runtime-Bqg9qW3t.js';
|
|
3
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-
|
|
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-
|
|
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-BAKGnuZ4.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-BAKGnuZ4.js';
|
|
6
6
|
import { FastifyInstance } from 'fastify';
|
|
7
7
|
import 'node:http';
|
|
8
8
|
|
|
@@ -316,7 +316,7 @@ interface ReceiptStore {
|
|
|
316
316
|
* Two-phase validation:
|
|
317
317
|
* 1. Zod validates *shape* (types, required fields, unknown-key rejection).
|
|
318
318
|
* Numeric/boolean leaves accept either their native type or a string
|
|
319
|
-
* (env substitution always produces a string), and are left as-is here
|
|
319
|
+
* (env substitution always produces a string), and are left as-is here -
|
|
320
320
|
* Zod's typed transforms have surprising inference interactions with
|
|
321
321
|
* `.strict()` objects, so numeric/boolean coercion is done explicitly,
|
|
322
322
|
* in plain TypeScript, in the normalisation pass below.
|
|
@@ -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[];
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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-
|
|
1
|
+
import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-5TKNTWMC.js';
|
|
2
|
+
import { PROTOCOL_NAMES, 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-EIACECVM.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, PROTOCOL_NAMES, RETRYABLE_ERROR_CODES, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-EIACECVM.js';
|
|
4
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';
|
|
@@ -96,33 +96,15 @@ var HttpBackendExecutor = class {
|
|
|
96
96
|
async call(handler, request) {
|
|
97
97
|
const timeoutMs = handler.timeoutMs ?? DEFAULT_BACKEND_TIMEOUT_MS;
|
|
98
98
|
const inputRecord = isPlainObject2(request.input) ? request.input : {};
|
|
99
|
+
const context = { requestId: request.requestId, resourceId: request.resourceId };
|
|
100
|
+
const parts = buildBackendRequestParts(handler, inputRecord, context);
|
|
99
101
|
let target;
|
|
100
|
-
let remaining;
|
|
101
102
|
try {
|
|
102
|
-
|
|
103
|
-
target = new URL(templated.url);
|
|
104
|
-
remaining = templated.remaining;
|
|
103
|
+
target = new URL(parts.url);
|
|
105
104
|
} catch (error) {
|
|
106
|
-
if (error instanceof Error && error.message.startsWith("invalid-path-parameter:")) {
|
|
107
|
-
const field = error.message.slice("invalid-path-parameter:".length);
|
|
108
|
-
throw new CommerceError("INPUT_INVALID", `Path parameter "${field}" is not a valid value`, {
|
|
109
|
-
requestId: request.requestId,
|
|
110
|
-
resourceId: request.resourceId,
|
|
111
|
-
details: { field }
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
if (error instanceof Error && error.message.startsWith("missing-path-parameter:")) {
|
|
115
|
-
const field = error.message.slice("missing-path-parameter:".length);
|
|
116
|
-
throw new CommerceError("INPUT_INVALID", `Path parameter "${field}" was not supplied`, {
|
|
117
|
-
requestId: request.requestId,
|
|
118
|
-
resourceId: request.resourceId,
|
|
119
|
-
details: { field }
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
105
|
throw new CommerceError("BACKEND_ERROR", "Backend URL could not be constructed from input", {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
details: { reason: describeConstructionError(error) },
|
|
106
|
+
...context,
|
|
107
|
+
details: { reason: "invalid-url" },
|
|
126
108
|
cause: error
|
|
127
109
|
});
|
|
128
110
|
}
|
|
@@ -144,16 +126,12 @@ var HttpBackendExecutor = class {
|
|
|
144
126
|
}
|
|
145
127
|
const headers = { ...handler.headers ?? {} };
|
|
146
128
|
let body;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
target.searchParams.set(key, stringifyPrimitive(value));
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
body = JSON.stringify(remaining);
|
|
129
|
+
checkQueryCollision(target, parts.query, context);
|
|
130
|
+
for (const [key, value] of Object.entries(parts.query)) {
|
|
131
|
+
target.searchParams.set(key, stringifyPrimitive(value));
|
|
132
|
+
}
|
|
133
|
+
if (parts.body !== void 0) {
|
|
134
|
+
body = JSON.stringify(parts.body.value);
|
|
157
135
|
if (!hasHeader(headers, "content-type")) {
|
|
158
136
|
headers["content-type"] = "application/json";
|
|
159
137
|
}
|
|
@@ -256,37 +234,42 @@ function findUnparsedBraceToken(url) {
|
|
|
256
234
|
}
|
|
257
235
|
function validateBackendRequestShape(handler, input, context) {
|
|
258
236
|
const inputRecord = isPlainObject2(input) ? input : {};
|
|
259
|
-
|
|
237
|
+
const parts = buildBackendRequestParts(handler, inputRecord, context);
|
|
238
|
+
let target;
|
|
260
239
|
try {
|
|
261
|
-
|
|
262
|
-
} catch
|
|
263
|
-
if (error instanceof Error && error.message.startsWith("invalid-path-parameter:")) {
|
|
264
|
-
const field = error.message.slice("invalid-path-parameter:".length);
|
|
265
|
-
throw new CommerceError("INPUT_INVALID", `Path parameter "${field}" is not a valid value`, {
|
|
266
|
-
requestId: context.requestId,
|
|
267
|
-
resourceId: context.resourceId,
|
|
268
|
-
details: { field }
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
if (error instanceof Error && error.message.startsWith("missing-path-parameter:")) {
|
|
272
|
-
const field = error.message.slice("missing-path-parameter:".length);
|
|
273
|
-
throw new CommerceError("INPUT_INVALID", `Path parameter "${field}" was not supplied`, {
|
|
274
|
-
requestId: context.requestId,
|
|
275
|
-
resourceId: context.resourceId,
|
|
276
|
-
details: { field }
|
|
277
|
-
});
|
|
278
|
-
}
|
|
240
|
+
target = new URL(parts.url);
|
|
241
|
+
} catch {
|
|
279
242
|
return;
|
|
280
243
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
244
|
+
checkQueryCollision(target, parts.query, context);
|
|
245
|
+
}
|
|
246
|
+
function acceptsBody(method) {
|
|
247
|
+
return method !== "GET" && method !== "DELETE";
|
|
248
|
+
}
|
|
249
|
+
function buildBackendRequestParts(handler, input, context) {
|
|
250
|
+
const bindings = handler.inputBindings;
|
|
251
|
+
if (bindings === void 0) {
|
|
252
|
+
const { url: url2, remaining } = applyPathTemplate(handler.url, input, context);
|
|
253
|
+
return acceptsBody(handler.method) ? { url: url2, query: {}, body: { value: remaining } } : { url: url2, query: remaining };
|
|
254
|
+
}
|
|
255
|
+
const pathValues = bindings.path === void 0 ? {} : resolveBoundGroup(input, bindings.path, "path", context);
|
|
256
|
+
const { url } = applyPathTemplate(handler.url, pathValues, context);
|
|
257
|
+
const query = bindings.query === void 0 ? {} : resolveBoundGroup(input, bindings.query, "query", context);
|
|
258
|
+
const bodyValue = bindings.body === void 0 ? void 0 : input[bindings.body];
|
|
259
|
+
if (bodyValue === void 0 || !acceptsBody(handler.method)) return { url, query };
|
|
260
|
+
return { url, query, body: { value: bodyValue } };
|
|
261
|
+
}
|
|
262
|
+
function resolveBoundGroup(input, key, kind, context) {
|
|
263
|
+
const value = input[key];
|
|
264
|
+
if (value === void 0) return {};
|
|
265
|
+
if (!isPlainObject2(value)) {
|
|
266
|
+
throw new CommerceError(
|
|
267
|
+
"INPUT_INVALID",
|
|
268
|
+
`Input "${key}" must be an object of ${kind} parameters`,
|
|
269
|
+
{ ...context, details: { field: key } }
|
|
270
|
+
);
|
|
289
271
|
}
|
|
272
|
+
return value;
|
|
290
273
|
}
|
|
291
274
|
function checkQueryCollision(target, remaining, context) {
|
|
292
275
|
const templateKeys = new Set(target.searchParams.keys());
|
|
@@ -304,7 +287,7 @@ function isPlainObject2(value) {
|
|
|
304
287
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
305
288
|
}
|
|
306
289
|
var TRAVERSAL_PATH_VALUES = /* @__PURE__ */ new Set(["", ".", ".."]);
|
|
307
|
-
function applyPathTemplate(template, input) {
|
|
290
|
+
function applyPathTemplate(template, input, context) {
|
|
308
291
|
const remaining = { ...input };
|
|
309
292
|
let missing;
|
|
310
293
|
let invalid;
|
|
@@ -323,19 +306,19 @@ function applyPathTemplate(template, input) {
|
|
|
323
306
|
return encodeURIComponent(raw);
|
|
324
307
|
});
|
|
325
308
|
if (missing !== void 0) {
|
|
326
|
-
throw new
|
|
309
|
+
throw new CommerceError("INPUT_INVALID", `Path parameter "${missing}" was not supplied`, {
|
|
310
|
+
...context,
|
|
311
|
+
details: { field: missing }
|
|
312
|
+
});
|
|
327
313
|
}
|
|
328
314
|
if (invalid !== void 0) {
|
|
329
|
-
throw new
|
|
315
|
+
throw new CommerceError("INPUT_INVALID", `Path parameter "${invalid}" is not a valid value`, {
|
|
316
|
+
...context,
|
|
317
|
+
details: { field: invalid }
|
|
318
|
+
});
|
|
330
319
|
}
|
|
331
320
|
return { url, remaining };
|
|
332
321
|
}
|
|
333
|
-
function describeConstructionError(error) {
|
|
334
|
-
if (error instanceof Error && error.message.startsWith("missing-path-parameter:")) {
|
|
335
|
-
return error.message;
|
|
336
|
-
}
|
|
337
|
-
return "invalid-url";
|
|
338
|
-
}
|
|
339
322
|
function stringifyPrimitive(value) {
|
|
340
323
|
if (value === void 0 || value === null) return "";
|
|
341
324
|
if (typeof value === "string") return value;
|
|
@@ -1103,15 +1086,15 @@ var ServerSchema = z.object({
|
|
|
1103
1086
|
adminToken: z.string().min(1).optional(),
|
|
1104
1087
|
/** Browser origins allowed to read the dashboard-facing routes. Empty by default: closed. */
|
|
1105
1088
|
// Entries are matched literally against the browser's
|
|
1106
|
-
// `Origin` header, so `"*"` and a trailing slash match nothing at all
|
|
1089
|
+
// `Origin` header, so `"*"` and a trailing slash match nothing at all -
|
|
1107
1090
|
// fail-closed (a lockout, not a bypass), but silently, and a lockout with
|
|
1108
1091
|
// no explanation is the kind of thing an operator "fixes" by disabling the
|
|
1109
1092
|
// check. Reject those two shapes with a pointer instead.
|
|
1110
1093
|
allowedOrigins: z.array(
|
|
1111
1094
|
z.string().min(1).refine((origin) => origin !== "*", {
|
|
1112
|
-
message: `wildcard "*" is not supported
|
|
1095
|
+
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.`
|
|
1113
1096
|
}).refine((origin) => !origin.endsWith("/"), {
|
|
1114
|
-
message: 'must not end with "/"
|
|
1097
|
+
message: 'must not end with "/" - a browser Origin header never has a trailing slash, so this entry would match nothing. Use e.g. "http://localhost:5173".'
|
|
1115
1098
|
})
|
|
1116
1099
|
).optional()
|
|
1117
1100
|
}).strict();
|
|
@@ -1125,16 +1108,19 @@ var RESERVED_GATEWAY_PATHS = [
|
|
|
1125
1108
|
"/health",
|
|
1126
1109
|
"/ready",
|
|
1127
1110
|
"/.well-known/agent-commerce",
|
|
1111
|
+
// Fixed by the A2A specification, so it is the adapter's to serve and never
|
|
1112
|
+
// a configurable mount's to claim.
|
|
1113
|
+
"/.well-known/agent-card.json",
|
|
1128
1114
|
"/api/resources",
|
|
1129
1115
|
"/api/resources/:id/invoke",
|
|
1130
1116
|
"/api/receipts",
|
|
1131
1117
|
"/api/events",
|
|
1132
1118
|
"/api/events/stream"
|
|
1133
1119
|
];
|
|
1134
|
-
var
|
|
1135
|
-
message: 'must start with "/"
|
|
1120
|
+
var MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
|
|
1121
|
+
message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
|
|
1136
1122
|
}).refine((value) => !/[:*?\s]/.test(value), {
|
|
1137
|
-
message: 'must not contain ":", "*", "?" or whitespace
|
|
1123
|
+
message: 'must not contain ":", "*", "?" or whitespace - the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
|
|
1138
1124
|
}).refine(
|
|
1139
1125
|
(value) => {
|
|
1140
1126
|
const base = value.replace(/\/+$/, "");
|
|
@@ -1150,16 +1136,29 @@ var ProtocolsSchema = z.object({
|
|
|
1150
1136
|
http: z.object({ enabled: BooleanOrString }).strict(),
|
|
1151
1137
|
mcp: z.object({
|
|
1152
1138
|
enabled: BooleanOrString,
|
|
1153
|
-
mountPath:
|
|
1154
|
-
}).strict()
|
|
1139
|
+
mountPath: MountPathSchema
|
|
1140
|
+
}).strict(),
|
|
1141
|
+
// Optional block: A2A is off unless an operator asks for it, and an
|
|
1142
|
+
// existing config predating the adapter stays valid.
|
|
1143
|
+
a2a: z.object({
|
|
1144
|
+
enabled: BooleanOrString,
|
|
1145
|
+
mountPath: MountPathSchema.optional()
|
|
1146
|
+
}).strict().optional()
|
|
1155
1147
|
}).strict();
|
|
1148
|
+
var DEFAULT_A2A_MOUNT_PATH = "/a2a";
|
|
1156
1149
|
var BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
1150
|
+
var BackendInputBindingsSchema = z.object({
|
|
1151
|
+
path: z.string().min(1).optional(),
|
|
1152
|
+
query: z.string().min(1).optional(),
|
|
1153
|
+
body: z.string().min(1).optional()
|
|
1154
|
+
}).strict();
|
|
1157
1155
|
var BackendHandlerSchema = z.object({
|
|
1158
1156
|
type: z.literal("http"),
|
|
1159
1157
|
method: BackendMethodSchema,
|
|
1160
1158
|
url: z.string().min(1),
|
|
1161
1159
|
headers: z.record(z.string(), z.string()).optional(),
|
|
1162
|
-
timeoutMs: NumberOrString.optional()
|
|
1160
|
+
timeoutMs: NumberOrString.optional(),
|
|
1161
|
+
inputBindings: BackendInputBindingsSchema.optional()
|
|
1163
1162
|
}).strict();
|
|
1164
1163
|
var PricingSchema = z.discriminatedUnion("type", [
|
|
1165
1164
|
z.object({ type: z.literal("free") }).strict(),
|
|
@@ -1204,7 +1203,7 @@ var FacilitatorSchema = z.discriminatedUnion("mode", [
|
|
|
1204
1203
|
z.object({
|
|
1205
1204
|
mode: z.literal("remote"),
|
|
1206
1205
|
url: z.string().min(1),
|
|
1207
|
-
// Absent means "this facilitator takes no credential"
|
|
1206
|
+
// Absent means "this facilitator takes no credential" - an explicit
|
|
1208
1207
|
// statement, normalised to `{ type: 'none' }` below. On a mainnet that
|
|
1209
1208
|
// combination is refused outright, so the default can never quietly
|
|
1210
1209
|
// become an unauthenticated production facilitator.
|
|
@@ -1222,7 +1221,7 @@ var X402Schema = z.object({
|
|
|
1222
1221
|
payTo: z.string().min(1),
|
|
1223
1222
|
maxTimeoutSeconds: NumberOrString,
|
|
1224
1223
|
facilitator: FacilitatorSchema,
|
|
1225
|
-
/** Real funds. Never defaulted
|
|
1224
|
+
/** Real funds. Never defaulted - see src/payments/x402/guardrails.ts. */
|
|
1226
1225
|
allowMainnet: BooleanOrString.optional(),
|
|
1227
1226
|
/** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
|
|
1228
1227
|
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
@@ -1336,7 +1335,7 @@ function toBoolean(value, path2) {
|
|
|
1336
1335
|
{ details: { path: path2 } }
|
|
1337
1336
|
);
|
|
1338
1337
|
}
|
|
1339
|
-
var SUPPORTED_PROTOCOLS =
|
|
1338
|
+
var SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
|
|
1340
1339
|
var SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
|
|
1341
1340
|
function normalise(raw) {
|
|
1342
1341
|
const protocols = {
|
|
@@ -1344,8 +1343,13 @@ function normalise(raw) {
|
|
|
1344
1343
|
mcp: {
|
|
1345
1344
|
enabled: toBoolean(raw.protocols.mcp.enabled, "protocols.mcp.enabled"),
|
|
1346
1345
|
mountPath: raw.protocols.mcp.mountPath
|
|
1346
|
+
},
|
|
1347
|
+
a2a: {
|
|
1348
|
+
enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
|
|
1349
|
+
mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
|
|
1347
1350
|
}
|
|
1348
1351
|
};
|
|
1352
|
+
validateMountPaths(protocols);
|
|
1349
1353
|
const x402Raw = raw.payments.x402;
|
|
1350
1354
|
const facilitator = x402Raw === void 0 ? void 0 : x402Raw.facilitator.mode === "local" ? { mode: "local", signerPrivateKey: x402Raw.facilitator.signerPrivateKey } : {
|
|
1351
1355
|
mode: "remote",
|
|
@@ -1420,6 +1424,25 @@ function normalise(raw) {
|
|
|
1420
1424
|
}
|
|
1421
1425
|
};
|
|
1422
1426
|
}
|
|
1427
|
+
function validateMountPaths(protocols) {
|
|
1428
|
+
const mounts = [
|
|
1429
|
+
["mcp", protocols.mcp],
|
|
1430
|
+
["a2a", protocols.a2a]
|
|
1431
|
+
].filter(([, p]) => p.enabled);
|
|
1432
|
+
for (const [index, [nameA, a]] of mounts.entries()) {
|
|
1433
|
+
for (const [nameB, b] of mounts.slice(index + 1)) {
|
|
1434
|
+
const baseA = a.mountPath.replace(/\/+$/, "");
|
|
1435
|
+
const baseB = b.mountPath.replace(/\/+$/, "");
|
|
1436
|
+
if (baseA === baseB || baseA.startsWith(`${baseB}/`) || baseB.startsWith(`${baseA}/`)) {
|
|
1437
|
+
throw new CommerceError(
|
|
1438
|
+
"CONFIG_INVALID",
|
|
1439
|
+
`protocols.${nameA}.mountPath ("${a.mountPath}") collides with protocols.${nameB}.mountPath ("${b.mountPath}") - each mount registers a wildcard, so overlapping prefixes cannot both be served`,
|
|
1440
|
+
{ details: { path: `protocols.${nameA}.mountPath` } }
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1423
1446
|
function normaliseResource(id, entry, protocols, x402) {
|
|
1424
1447
|
if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
|
|
1425
1448
|
const inputProperties = entry.input?.["properties"];
|
|
@@ -1440,7 +1463,7 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1440
1463
|
const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
|
|
1441
1464
|
throw new CommerceError(
|
|
1442
1465
|
"CONFIG_INVALID",
|
|
1443
|
-
`Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp.`,
|
|
1466
|
+
`Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp, a2a.`,
|
|
1444
1467
|
{ details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
|
|
1445
1468
|
);
|
|
1446
1469
|
}
|
|
@@ -1461,6 +1484,13 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1461
1484
|
);
|
|
1462
1485
|
}
|
|
1463
1486
|
}
|
|
1487
|
+
if (entry.expose.includes("a2a") && !protocols.a2a.enabled) {
|
|
1488
|
+
throw new CommerceError(
|
|
1489
|
+
"CONFIG_INVALID",
|
|
1490
|
+
`Resource "${id}" is exposed via "a2a" but protocols.a2a.enabled is false`,
|
|
1491
|
+
{ details: { path: `resources.${id}.expose`, resourceId: id } }
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1464
1494
|
if (entry.expose.includes("http") && !protocols.http.enabled) {
|
|
1465
1495
|
throw new CommerceError(
|
|
1466
1496
|
"CONFIG_INVALID",
|
|
@@ -1469,7 +1499,8 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1469
1499
|
);
|
|
1470
1500
|
}
|
|
1471
1501
|
validateBackendUrl(id, entry.backend.url);
|
|
1472
|
-
|
|
1502
|
+
const pathScope = validateInputBindings(id, entry.backend, entry.input);
|
|
1503
|
+
validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
|
|
1473
1504
|
if (entry.pricing.type === "dynamic") {
|
|
1474
1505
|
throw new CommerceError(
|
|
1475
1506
|
"CONFIG_INVALID",
|
|
@@ -1522,7 +1553,8 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1522
1553
|
timeoutMs: toNumber(entry.backend.timeoutMs, `resources.${id}.backend.timeoutMs`, {
|
|
1523
1554
|
min: 1
|
|
1524
1555
|
})
|
|
1525
|
-
} : {}
|
|
1556
|
+
} : {},
|
|
1557
|
+
...entry.backend.inputBindings !== void 0 ? { inputBindings: pickDefined(entry.backend.inputBindings) } : {}
|
|
1526
1558
|
},
|
|
1527
1559
|
pricing,
|
|
1528
1560
|
exposedVia: entry.expose,
|
|
@@ -1602,7 +1634,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1602
1634
|
if ((Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required")) && excludesObjectType(schema)) {
|
|
1603
1635
|
throw new CommerceError(
|
|
1604
1636
|
"CONFIG_INVALID",
|
|
1605
|
-
`Resource "${id}" ${path2} declares "properties" and/or "required" but its "type" (${JSON.stringify(schema["type"])}) does not include "object"
|
|
1637
|
+
`Resource "${id}" ${path2} declares "properties" and/or "required" but its "type" (${JSON.stringify(schema["type"])}) does not include "object" - the validator will never route a value through either check under that type, so this schema claims to constrain input it does not actually enforce. Remove "properties"/"required", or include "object" in "type".`,
|
|
1606
1638
|
{ details: { path: `resources.${id}.${path2}`, resourceId: id } }
|
|
1607
1639
|
);
|
|
1608
1640
|
}
|
|
@@ -1636,7 +1668,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1636
1668
|
validateResourceSchemaKeywords(id, `${path2}.items`, items);
|
|
1637
1669
|
} else if (Array.isArray(items)) {
|
|
1638
1670
|
console.warn(
|
|
1639
|
-
`[agent-commerce] resource "${id}" ${path2}.items is a tuple (an array of schemas), which this gateway does not enforce
|
|
1671
|
+
`[agent-commerce] resource "${id}" ${path2}.items is a tuple (an array of schemas), which this gateway does not enforce - only a single schema applied to every array element is supported (see src/core/execution/validation.ts). Each position's schema is unenforced; treat it as documentation only.`
|
|
1640
1672
|
);
|
|
1641
1673
|
}
|
|
1642
1674
|
}
|
|
@@ -1647,14 +1679,14 @@ function validatePricingAmount(id, amount, x402) {
|
|
|
1647
1679
|
if (!PRICING_AMOUNT_PATTERN.test(amount)) {
|
|
1648
1680
|
throw new CommerceError(
|
|
1649
1681
|
"CONFIG_INVALID",
|
|
1650
|
-
`Resource "${id}" has pricing.amount "${amount}", which is not a plain positive decimal (no currency symbol, no thousands separator, no exponent
|
|
1682
|
+
`Resource "${id}" has pricing.amount "${amount}", which is not a plain positive decimal (no currency symbol, no thousands separator, no exponent - e.g. "0.01")`,
|
|
1651
1683
|
{ details: { path: path2, resourceId: id } }
|
|
1652
1684
|
);
|
|
1653
1685
|
}
|
|
1654
1686
|
if (ZERO_AMOUNT_PATTERN.test(amount)) {
|
|
1655
1687
|
throw new CommerceError(
|
|
1656
1688
|
"CONFIG_INVALID",
|
|
1657
|
-
`Resource "${id}" has pricing.amount "0"
|
|
1689
|
+
`Resource "${id}" has pricing.amount "0" - a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
|
|
1658
1690
|
{ details: { path: path2, resourceId: id } }
|
|
1659
1691
|
);
|
|
1660
1692
|
}
|
|
@@ -1669,7 +1701,86 @@ function validatePricingAmount(id, amount, x402) {
|
|
|
1669
1701
|
}
|
|
1670
1702
|
}
|
|
1671
1703
|
}
|
|
1672
|
-
function
|
|
1704
|
+
function validateInputBindings(id, backend, input) {
|
|
1705
|
+
const legacy = { schema: input, where: "its input schema" };
|
|
1706
|
+
const bindings = backend.inputBindings;
|
|
1707
|
+
const templated = extractPathParameterNames(backend.url).length > 0;
|
|
1708
|
+
if (bindings === void 0) return legacy;
|
|
1709
|
+
const path2 = `resources.${id}.backend.inputBindings`;
|
|
1710
|
+
const fail = (message, details = {}) => {
|
|
1711
|
+
throw new CommerceError("CONFIG_INVALID", `Resource "${id}" ${message}`, {
|
|
1712
|
+
details: { path: path2, resourceId: id, ...details }
|
|
1713
|
+
});
|
|
1714
|
+
};
|
|
1715
|
+
const entries = Object.entries(bindings).filter(
|
|
1716
|
+
(entry) => entry[1] !== void 0
|
|
1717
|
+
);
|
|
1718
|
+
if (entries.length === 0) {
|
|
1719
|
+
fail("has an empty backend.inputBindings - remove the block to use the default mapping");
|
|
1720
|
+
}
|
|
1721
|
+
if (backend.method === "GET" || backend.method === "DELETE") {
|
|
1722
|
+
if (bindings.body !== void 0) {
|
|
1723
|
+
fail(
|
|
1724
|
+
`binds a request body on a ${backend.method}, which sends none - the value would be silently dropped`
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (templated && bindings.path === void 0) {
|
|
1729
|
+
fail(
|
|
1730
|
+
'has backend.url path parameters but no "path" binding - in explicit binding mode nothing else supplies them, so every call would fail to reach the backend'
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1734
|
+
const properties = isPlainObject4(input?.["properties"]) ? input["properties"] : {};
|
|
1735
|
+
const required = new Set(
|
|
1736
|
+
Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
|
|
1737
|
+
);
|
|
1738
|
+
for (const [location, property] of entries) {
|
|
1739
|
+
if (property === PAYMENT_INPUT_FIELD) {
|
|
1740
|
+
fail(
|
|
1741
|
+
`binds "${location}" to "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
|
|
1742
|
+
{ location }
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
const other = seen.get(property);
|
|
1746
|
+
if (other !== void 0) {
|
|
1747
|
+
fail(`binds both "${other}" and "${location}" to the input property "${property}"`, {
|
|
1748
|
+
location,
|
|
1749
|
+
property
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
seen.set(property, location);
|
|
1753
|
+
if (!Object.hasOwn(properties, property)) {
|
|
1754
|
+
fail(
|
|
1755
|
+
`binds "${location}" to input property "${property}", which the input schema does not declare - the schema is closed, so a caller could never supply it`,
|
|
1756
|
+
{ location, property }
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
const declared = properties[property];
|
|
1760
|
+
if (location !== "body" && isPlainObject4(declared) && !isObjectSchemaNode(declared)) {
|
|
1761
|
+
fail(
|
|
1762
|
+
`binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
|
|
1763
|
+
{ location, property }
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
if (templated && bindings.path !== void 0 && !required.has(bindings.path)) {
|
|
1768
|
+
fail(
|
|
1769
|
+
`binds path parameters to input property "${bindings.path}" without listing it in the input schema's "required" - a caller that omits it cannot supply any path parameter, so the request could never be built`,
|
|
1770
|
+
{ property: bindings.path }
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
|
|
1774
|
+
const group = properties[bindings.path];
|
|
1775
|
+
return {
|
|
1776
|
+
schema: isPlainObject4(group) ? group : void 0,
|
|
1777
|
+
where: `input.properties.${bindings.path}`
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
function pickDefined(value) {
|
|
1781
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
1782
|
+
}
|
|
1783
|
+
function validatePathParametersDeclared(id, url, input, where) {
|
|
1673
1784
|
const stray = findUnparsedBraceToken(url);
|
|
1674
1785
|
if (stray !== void 0) {
|
|
1675
1786
|
throw new CommerceError(
|
|
@@ -1708,14 +1819,14 @@ function validatePathParametersDeclared(id, url, input) {
|
|
|
1708
1819
|
if (!Object.hasOwn(properties, param)) {
|
|
1709
1820
|
throw new CommerceError(
|
|
1710
1821
|
"CONFIG_INVALID",
|
|
1711
|
-
`Resource "${id}" has backend.url path parameter "{${param}}" which is not declared in
|
|
1822
|
+
`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 settle payment (if priced) and then fail to reach the backend`,
|
|
1712
1823
|
{ details: { path: path2, resourceId: id, param } }
|
|
1713
1824
|
);
|
|
1714
1825
|
}
|
|
1715
1826
|
if (!required.has(param)) {
|
|
1716
1827
|
throw new CommerceError(
|
|
1717
1828
|
"CONFIG_INVALID",
|
|
1718
|
-
`Resource "${id}" has backend.url path parameter "{${param}}" declared in
|
|
1829
|
+
`Resource "${id}" has backend.url path parameter "{${param}}" declared in ${where} but not listed in its "required" - a caller that omits it hits the same unservable-request problem as an undeclared parameter`,
|
|
1719
1830
|
{ details: { path: path2, resourceId: id, param } }
|
|
1720
1831
|
);
|
|
1721
1832
|
}
|
|
@@ -1977,6 +2088,7 @@ function hashToken(value) {
|
|
|
1977
2088
|
// src/gateway/adapters.ts
|
|
1978
2089
|
var MOUNT_BODY_LIMIT_BYTES = 256 * 1024;
|
|
1979
2090
|
async function startAndMountAdapters(options) {
|
|
2091
|
+
assertNoRouteConflicts(options.adapters);
|
|
1980
2092
|
const runtimes = [];
|
|
1981
2093
|
for (const adapter of options.adapters) {
|
|
1982
2094
|
const adapterLogger = options.logger.child({ adapter: adapter.name });
|
|
@@ -2002,11 +2114,51 @@ async function startAndMountAdapters(options) {
|
|
|
2002
2114
|
}
|
|
2003
2115
|
return runtimes;
|
|
2004
2116
|
}
|
|
2117
|
+
function routeClaims(adapters) {
|
|
2118
|
+
const claims = [];
|
|
2119
|
+
for (const adapter of adapters) {
|
|
2120
|
+
if (!isHttpProtocolAdapter(adapter)) continue;
|
|
2121
|
+
claims.push({
|
|
2122
|
+
adapter: adapter.name,
|
|
2123
|
+
path: adapter.mountPath.replace(/\/+$/, ""),
|
|
2124
|
+
wildcard: true,
|
|
2125
|
+
method: "ALL"
|
|
2126
|
+
});
|
|
2127
|
+
for (const route of adapter.additionalHttpRoutes ?? []) {
|
|
2128
|
+
claims.push({
|
|
2129
|
+
adapter: adapter.name,
|
|
2130
|
+
path: route.path.replace(/\/+$/, ""),
|
|
2131
|
+
wildcard: false,
|
|
2132
|
+
method: route.method
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return claims;
|
|
2137
|
+
}
|
|
2138
|
+
function claimsCollide(a, b) {
|
|
2139
|
+
if (a.wildcard && (b.path === a.path || b.path.startsWith(`${a.path}/`))) return true;
|
|
2140
|
+
if (b.wildcard && a.path.startsWith(`${b.path}/`)) return true;
|
|
2141
|
+
return a.path === b.path && (a.method === b.method || a.method === "ALL" || b.method === "ALL");
|
|
2142
|
+
}
|
|
2143
|
+
function assertNoRouteConflicts(adapters) {
|
|
2144
|
+
const claims = routeClaims(adapters);
|
|
2145
|
+
for (const [index, a] of claims.entries()) {
|
|
2146
|
+
for (const b of claims.slice(index + 1)) {
|
|
2147
|
+
if (a.adapter === b.adapter || !claimsCollide(a, b)) continue;
|
|
2148
|
+
throw new CommerceError(
|
|
2149
|
+
"CONFIG_INVALID",
|
|
2150
|
+
`Protocol adapters "${a.adapter}" and "${b.adapter}" both claim the HTTP path "${b.path}"; each adapter path must be served by exactly one adapter`,
|
|
2151
|
+
{ details: { adapters: [a.adapter, b.adapter], path: b.path } }
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2005
2156
|
var MOUNT_MAX_CONCURRENT_REQUESTS = 20;
|
|
2006
2157
|
var MOUNT_BUSY_RETRY_AFTER_SECONDS = 1;
|
|
2007
2158
|
function mountHttpAdapter(server, adapter, logger) {
|
|
2008
2159
|
const mountPath = adapter.mountPath;
|
|
2009
2160
|
const wildcard = mountPath.endsWith("/") ? `${mountPath}*` : `${mountPath}/*`;
|
|
2161
|
+
const additionalRoutes = adapter.additionalHttpRoutes ?? [];
|
|
2010
2162
|
let inFlight = 0;
|
|
2011
2163
|
void server.register(async (instance) => {
|
|
2012
2164
|
instance.addContentTypeParser(
|
|
@@ -2018,7 +2170,7 @@ function mountHttpAdapter(server, adapter, logger) {
|
|
|
2018
2170
|
instance.addContentTypeParser("*", (_request, _payload, done) => {
|
|
2019
2171
|
done(null);
|
|
2020
2172
|
});
|
|
2021
|
-
const
|
|
2173
|
+
const makeHandler = (handleHttp) => async (request, reply) => {
|
|
2022
2174
|
if (inFlight >= MOUNT_MAX_CONCURRENT_REQUESTS) {
|
|
2023
2175
|
const error = new CommerceError(
|
|
2024
2176
|
"GATEWAY_BUSY",
|
|
@@ -2036,7 +2188,7 @@ function mountHttpAdapter(server, adapter, logger) {
|
|
|
2036
2188
|
logger
|
|
2037
2189
|
);
|
|
2038
2190
|
try {
|
|
2039
|
-
await
|
|
2191
|
+
await handleHttp(request.raw, reply.raw);
|
|
2040
2192
|
} catch (error) {
|
|
2041
2193
|
logger.error({ err: describeError3(error) }, "Protocol adapter request handler threw");
|
|
2042
2194
|
if (!reply.raw.headersSent) {
|
|
@@ -2050,8 +2202,16 @@ function mountHttpAdapter(server, adapter, logger) {
|
|
|
2050
2202
|
inFlight -= 1;
|
|
2051
2203
|
}
|
|
2052
2204
|
};
|
|
2053
|
-
|
|
2054
|
-
instance.all(
|
|
2205
|
+
const mountHandler = makeHandler((req, res) => adapter.handleHttp(req, res));
|
|
2206
|
+
instance.all(mountPath, mountHandler);
|
|
2207
|
+
instance.all(wildcard, mountHandler);
|
|
2208
|
+
for (const route of additionalRoutes) {
|
|
2209
|
+
instance.route({
|
|
2210
|
+
method: route.method,
|
|
2211
|
+
url: route.path,
|
|
2212
|
+
handler: makeHandler((req, res) => route.handleHttp(req, res))
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2055
2215
|
});
|
|
2056
2216
|
}
|
|
2057
2217
|
function enforceMountBodyLimit(req, res, maxBytes, logger) {
|