@devlab.group/agent-commerce 0.2.0-beta.0 → 1.1.0

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