@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/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-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';
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-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';
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-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';
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
- const templated = applyPathTemplate(handler.url, inputRecord);
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
- requestId: request.requestId,
124
- resourceId: request.resourceId,
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
- if (handler.method === "GET" || handler.method === "DELETE") {
148
- checkQueryCollision(target, remaining, {
149
- requestId: request.requestId,
150
- resourceId: request.resourceId
151
- });
152
- for (const [key, value] of Object.entries(remaining)) {
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
- let templated;
237
+ const parts = buildBackendRequestParts(handler, inputRecord, context);
238
+ let target;
260
239
  try {
261
- templated = applyPathTemplate(handler.url, inputRecord);
262
- } catch (error) {
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
- if (handler.method === "GET" || handler.method === "DELETE") {
282
- let target;
283
- try {
284
- target = new URL(templated.url);
285
- } catch {
286
- return;
287
- }
288
- checkQueryCollision(target, templated.remaining, context);
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 Error(`missing-path-parameter:${missing}`);
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 Error(`invalid-path-parameter:${invalid}`);
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 \u2014 allowedOrigins entries are matched literally against the browser's Origin header, so "*" would match nothing. List each scheme://host[:port] explicitly.`
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 "/" \u2014 a browser Origin header never has a trailing slash, so this entry would match nothing. Use e.g. "http://localhost:5173".'
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 "/" \u2014 it is an absolute gateway path, e.g. "/mcp".'
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 \u2014 the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
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" — an explicit
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 — see src/payments/x402/guardrails.ts. */
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 = /* @__PURE__ */ new Set(["http", "mcp", "a2a"]);
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}") \u2014 each mount registers a wildcard, so overlapping prefixes cannot both be served`,
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
- validatePathParametersDeclared(id, entry.backend.url, entry.input);
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" \u2014 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".`,
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 \u2014 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.`
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 \u2014 e.g. "0.01")`,
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" \u2014 a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
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 validatePathParametersDeclared(id, url, input) {
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 its input schema \u2014 the caller has no way to supply it, so every call would settle payment (if priced) and then fail to reach the backend`,
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 its input schema but not listed in "required" \u2014 a caller that omits it hits the same unservable-request problem as an undeclared parameter`,
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
  }