@devlab.group/agent-commerce 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-B-9IjMbO.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-B-9IjMbO.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
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-CZPOb9uG.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-CZPOb9uG.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
 
@@ -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-MEPETSP6.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-BQNE3A5H.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-BQNE3A5H.js';
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
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';
@@ -1125,13 +1125,16 @@ var RESERVED_GATEWAY_PATHS = [
1125
1125
  "/health",
1126
1126
  "/ready",
1127
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",
1128
1131
  "/api/resources",
1129
1132
  "/api/resources/:id/invoke",
1130
1133
  "/api/receipts",
1131
1134
  "/api/events",
1132
1135
  "/api/events/stream"
1133
1136
  ];
1134
- var McpMountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
1137
+ var MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
1135
1138
  message: 'must start with "/" \u2014 it is an absolute gateway path, e.g. "/mcp".'
1136
1139
  }).refine((value) => !/[:*?\s]/.test(value), {
1137
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.'
@@ -1150,9 +1153,16 @@ var ProtocolsSchema = z.object({
1150
1153
  http: z.object({ enabled: BooleanOrString }).strict(),
1151
1154
  mcp: z.object({
1152
1155
  enabled: BooleanOrString,
1153
- mountPath: McpMountPathSchema
1154
- }).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()
1155
1164
  }).strict();
1165
+ var DEFAULT_A2A_MOUNT_PATH = "/a2a";
1156
1166
  var BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
1157
1167
  var BackendHandlerSchema = z.object({
1158
1168
  type: z.literal("http"),
@@ -1336,7 +1346,7 @@ function toBoolean(value, path2) {
1336
1346
  { details: { path: path2 } }
1337
1347
  );
1338
1348
  }
1339
- var SUPPORTED_PROTOCOLS = /* @__PURE__ */ new Set(["http", "mcp"]);
1349
+ var SUPPORTED_PROTOCOLS = /* @__PURE__ */ new Set(["http", "mcp", "a2a"]);
1340
1350
  var SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
1341
1351
  function normalise(raw) {
1342
1352
  const protocols = {
@@ -1344,8 +1354,13 @@ function normalise(raw) {
1344
1354
  mcp: {
1345
1355
  enabled: toBoolean(raw.protocols.mcp.enabled, "protocols.mcp.enabled"),
1346
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
1347
1361
  }
1348
1362
  };
1363
+ validateMountPaths(protocols);
1349
1364
  const x402Raw = raw.payments.x402;
1350
1365
  const facilitator = x402Raw === void 0 ? void 0 : x402Raw.facilitator.mode === "local" ? { mode: "local", signerPrivateKey: x402Raw.facilitator.signerPrivateKey } : {
1351
1366
  mode: "remote",
@@ -1420,6 +1435,25 @@ function normalise(raw) {
1420
1435
  }
1421
1436
  };
1422
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
+ }
1423
1457
  function normaliseResource(id, entry, protocols, x402) {
1424
1458
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
1425
1459
  const inputProperties = entry.input?.["properties"];
@@ -1440,7 +1474,7 @@ function normaliseResource(id, entry, protocols, x402) {
1440
1474
  const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
1441
1475
  throw new CommerceError(
1442
1476
  "CONFIG_INVALID",
1443
- `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp.`,
1477
+ `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp, a2a.`,
1444
1478
  { details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
1445
1479
  );
1446
1480
  }
@@ -1461,6 +1495,13 @@ function normaliseResource(id, entry, protocols, x402) {
1461
1495
  );
1462
1496
  }
1463
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
+ }
1464
1505
  if (entry.expose.includes("http") && !protocols.http.enabled) {
1465
1506
  throw new CommerceError(
1466
1507
  "CONFIG_INVALID",
@@ -1977,6 +2018,7 @@ function hashToken(value) {
1977
2018
  // src/gateway/adapters.ts
1978
2019
  var MOUNT_BODY_LIMIT_BYTES = 256 * 1024;
1979
2020
  async function startAndMountAdapters(options) {
2021
+ assertNoRouteConflicts(options.adapters);
1980
2022
  const runtimes = [];
1981
2023
  for (const adapter of options.adapters) {
1982
2024
  const adapterLogger = options.logger.child({ adapter: adapter.name });
@@ -2002,11 +2044,51 @@ async function startAndMountAdapters(options) {
2002
2044
  }
2003
2045
  return runtimes;
2004
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
+ }
2005
2086
  var MOUNT_MAX_CONCURRENT_REQUESTS = 20;
2006
2087
  var MOUNT_BUSY_RETRY_AFTER_SECONDS = 1;
2007
2088
  function mountHttpAdapter(server, adapter, logger) {
2008
2089
  const mountPath = adapter.mountPath;
2009
2090
  const wildcard = mountPath.endsWith("/") ? `${mountPath}*` : `${mountPath}/*`;
2091
+ const additionalRoutes = adapter.additionalHttpRoutes ?? [];
2010
2092
  let inFlight = 0;
2011
2093
  void server.register(async (instance) => {
2012
2094
  instance.addContentTypeParser(
@@ -2018,7 +2100,7 @@ function mountHttpAdapter(server, adapter, logger) {
2018
2100
  instance.addContentTypeParser("*", (_request, _payload, done) => {
2019
2101
  done(null);
2020
2102
  });
2021
- const handler = async (request, reply) => {
2103
+ const makeHandler = (handleHttp) => async (request, reply) => {
2022
2104
  if (inFlight >= MOUNT_MAX_CONCURRENT_REQUESTS) {
2023
2105
  const error = new CommerceError(
2024
2106
  "GATEWAY_BUSY",
@@ -2036,7 +2118,7 @@ function mountHttpAdapter(server, adapter, logger) {
2036
2118
  logger
2037
2119
  );
2038
2120
  try {
2039
- await adapter.handleHttp(request.raw, reply.raw);
2121
+ await handleHttp(request.raw, reply.raw);
2040
2122
  } catch (error) {
2041
2123
  logger.error({ err: describeError3(error) }, "Protocol adapter request handler threw");
2042
2124
  if (!reply.raw.headersSent) {
@@ -2050,8 +2132,16 @@ function mountHttpAdapter(server, adapter, logger) {
2050
2132
  inFlight -= 1;
2051
2133
  }
2052
2134
  };
2053
- instance.all(mountPath, handler);
2054
- 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
+ }
2055
2145
  });
2056
2146
  }
2057
2147
  function enforceMountBodyLimit(req, res, maxBytes, logger) {