@devlab.group/agent-commerce 1.1.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 +58 -40
- package/dist/{chunk-M4I7I2UY.js → chunk-5TKNTWMC.js} +3 -3
- package/dist/{chunk-M4I7I2UY.js.map → chunk-5TKNTWMC.js.map} +1 -1
- package/dist/{chunk-64UZVJEO.js → chunk-EIACECVM.js} +7 -4
- package/dist/chunk-EIACECVM.js.map +1 -0
- package/dist/cli/index.js +1307 -78
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +161 -91
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{protocol-adapter-DZ8qkkAa.d.ts → protocol-adapter-BAKGnuZ4.d.ts} +1 -1
- package/dist/{runtime-CC6EFmLP.d.ts → runtime-Bqg9qW3t.d.ts} +33 -2
- package/dist/x402.d.ts +1 -1
- package/dist/x402.js +3 -3
- package/package.json +12 -3
- package/dist/chunk-64UZVJEO.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 { 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-
|
|
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.
|
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();
|
|
@@ -1135,9 +1118,9 @@ var RESERVED_GATEWAY_PATHS = [
|
|
|
1135
1118
|
"/api/events/stream"
|
|
1136
1119
|
];
|
|
1137
1120
|
var MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
|
|
1138
|
-
message: 'must start with "/"
|
|
1121
|
+
message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
|
|
1139
1122
|
}).refine((value) => !/[:*?\s]/.test(value), {
|
|
1140
|
-
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.'
|
|
1141
1124
|
}).refine(
|
|
1142
1125
|
(value) => {
|
|
1143
1126
|
const base = value.replace(/\/+$/, "");
|
|
@@ -1164,12 +1147,18 @@ var ProtocolsSchema = z.object({
|
|
|
1164
1147
|
}).strict();
|
|
1165
1148
|
var DEFAULT_A2A_MOUNT_PATH = "/a2a";
|
|
1166
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();
|
|
1167
1155
|
var BackendHandlerSchema = z.object({
|
|
1168
1156
|
type: z.literal("http"),
|
|
1169
1157
|
method: BackendMethodSchema,
|
|
1170
1158
|
url: z.string().min(1),
|
|
1171
1159
|
headers: z.record(z.string(), z.string()).optional(),
|
|
1172
|
-
timeoutMs: NumberOrString.optional()
|
|
1160
|
+
timeoutMs: NumberOrString.optional(),
|
|
1161
|
+
inputBindings: BackendInputBindingsSchema.optional()
|
|
1173
1162
|
}).strict();
|
|
1174
1163
|
var PricingSchema = z.discriminatedUnion("type", [
|
|
1175
1164
|
z.object({ type: z.literal("free") }).strict(),
|
|
@@ -1214,7 +1203,7 @@ var FacilitatorSchema = z.discriminatedUnion("mode", [
|
|
|
1214
1203
|
z.object({
|
|
1215
1204
|
mode: z.literal("remote"),
|
|
1216
1205
|
url: z.string().min(1),
|
|
1217
|
-
// Absent means "this facilitator takes no credential"
|
|
1206
|
+
// Absent means "this facilitator takes no credential" - an explicit
|
|
1218
1207
|
// statement, normalised to `{ type: 'none' }` below. On a mainnet that
|
|
1219
1208
|
// combination is refused outright, so the default can never quietly
|
|
1220
1209
|
// become an unauthenticated production facilitator.
|
|
@@ -1232,7 +1221,7 @@ var X402Schema = z.object({
|
|
|
1232
1221
|
payTo: z.string().min(1),
|
|
1233
1222
|
maxTimeoutSeconds: NumberOrString,
|
|
1234
1223
|
facilitator: FacilitatorSchema,
|
|
1235
|
-
/** Real funds. Never defaulted
|
|
1224
|
+
/** Real funds. Never defaulted - see src/payments/x402/guardrails.ts. */
|
|
1236
1225
|
allowMainnet: BooleanOrString.optional(),
|
|
1237
1226
|
/** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
|
|
1238
1227
|
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
@@ -1346,7 +1335,7 @@ function toBoolean(value, path2) {
|
|
|
1346
1335
|
{ details: { path: path2 } }
|
|
1347
1336
|
);
|
|
1348
1337
|
}
|
|
1349
|
-
var SUPPORTED_PROTOCOLS =
|
|
1338
|
+
var SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
|
|
1350
1339
|
var SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
|
|
1351
1340
|
function normalise(raw) {
|
|
1352
1341
|
const protocols = {
|
|
@@ -1447,7 +1436,7 @@ function validateMountPaths(protocols) {
|
|
|
1447
1436
|
if (baseA === baseB || baseA.startsWith(`${baseB}/`) || baseB.startsWith(`${baseA}/`)) {
|
|
1448
1437
|
throw new CommerceError(
|
|
1449
1438
|
"CONFIG_INVALID",
|
|
1450
|
-
`protocols.${nameA}.mountPath ("${a.mountPath}") collides with protocols.${nameB}.mountPath ("${b.mountPath}")
|
|
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`,
|
|
1451
1440
|
{ details: { path: `protocols.${nameA}.mountPath` } }
|
|
1452
1441
|
);
|
|
1453
1442
|
}
|
|
@@ -1510,7 +1499,8 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1510
1499
|
);
|
|
1511
1500
|
}
|
|
1512
1501
|
validateBackendUrl(id, entry.backend.url);
|
|
1513
|
-
|
|
1502
|
+
const pathScope = validateInputBindings(id, entry.backend, entry.input);
|
|
1503
|
+
validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
|
|
1514
1504
|
if (entry.pricing.type === "dynamic") {
|
|
1515
1505
|
throw new CommerceError(
|
|
1516
1506
|
"CONFIG_INVALID",
|
|
@@ -1563,7 +1553,8 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1563
1553
|
timeoutMs: toNumber(entry.backend.timeoutMs, `resources.${id}.backend.timeoutMs`, {
|
|
1564
1554
|
min: 1
|
|
1565
1555
|
})
|
|
1566
|
-
} : {}
|
|
1556
|
+
} : {},
|
|
1557
|
+
...entry.backend.inputBindings !== void 0 ? { inputBindings: pickDefined(entry.backend.inputBindings) } : {}
|
|
1567
1558
|
},
|
|
1568
1559
|
pricing,
|
|
1569
1560
|
exposedVia: entry.expose,
|
|
@@ -1643,7 +1634,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1643
1634
|
if ((Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required")) && excludesObjectType(schema)) {
|
|
1644
1635
|
throw new CommerceError(
|
|
1645
1636
|
"CONFIG_INVALID",
|
|
1646
|
-
`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".`,
|
|
1647
1638
|
{ details: { path: `resources.${id}.${path2}`, resourceId: id } }
|
|
1648
1639
|
);
|
|
1649
1640
|
}
|
|
@@ -1677,7 +1668,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1677
1668
|
validateResourceSchemaKeywords(id, `${path2}.items`, items);
|
|
1678
1669
|
} else if (Array.isArray(items)) {
|
|
1679
1670
|
console.warn(
|
|
1680
|
-
`[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.`
|
|
1681
1672
|
);
|
|
1682
1673
|
}
|
|
1683
1674
|
}
|
|
@@ -1688,14 +1679,14 @@ function validatePricingAmount(id, amount, x402) {
|
|
|
1688
1679
|
if (!PRICING_AMOUNT_PATTERN.test(amount)) {
|
|
1689
1680
|
throw new CommerceError(
|
|
1690
1681
|
"CONFIG_INVALID",
|
|
1691
|
-
`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")`,
|
|
1692
1683
|
{ details: { path: path2, resourceId: id } }
|
|
1693
1684
|
);
|
|
1694
1685
|
}
|
|
1695
1686
|
if (ZERO_AMOUNT_PATTERN.test(amount)) {
|
|
1696
1687
|
throw new CommerceError(
|
|
1697
1688
|
"CONFIG_INVALID",
|
|
1698
|
-
`Resource "${id}" has pricing.amount "0"
|
|
1689
|
+
`Resource "${id}" has pricing.amount "0" - a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
|
|
1699
1690
|
{ details: { path: path2, resourceId: id } }
|
|
1700
1691
|
);
|
|
1701
1692
|
}
|
|
@@ -1710,7 +1701,86 @@ function validatePricingAmount(id, amount, x402) {
|
|
|
1710
1701
|
}
|
|
1711
1702
|
}
|
|
1712
1703
|
}
|
|
1713
|
-
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) {
|
|
1714
1784
|
const stray = findUnparsedBraceToken(url);
|
|
1715
1785
|
if (stray !== void 0) {
|
|
1716
1786
|
throw new CommerceError(
|
|
@@ -1749,14 +1819,14 @@ function validatePathParametersDeclared(id, url, input) {
|
|
|
1749
1819
|
if (!Object.hasOwn(properties, param)) {
|
|
1750
1820
|
throw new CommerceError(
|
|
1751
1821
|
"CONFIG_INVALID",
|
|
1752
|
-
`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`,
|
|
1753
1823
|
{ details: { path: path2, resourceId: id, param } }
|
|
1754
1824
|
);
|
|
1755
1825
|
}
|
|
1756
1826
|
if (!required.has(param)) {
|
|
1757
1827
|
throw new CommerceError(
|
|
1758
1828
|
"CONFIG_INVALID",
|
|
1759
|
-
`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`,
|
|
1760
1830
|
{ details: { path: path2, resourceId: id, param } }
|
|
1761
1831
|
);
|
|
1762
1832
|
}
|