@devlab.group/agent-commerce 0.2.0-beta.0 → 1.0.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-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';
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';
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.
@@ -378,6 +378,7 @@ interface GatewayConfig {
378
378
  readonly maxTimeoutSeconds: number;
379
379
  readonly facilitator: X402FacilitatorConfig;
380
380
  readonly allowMainnet?: boolean;
381
+ readonly allowUnauthenticatedFacilitator?: boolean;
381
382
  };
382
383
  };
383
384
  }
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-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';
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,11 +1121,36 @@ 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
+ "/api/resources",
1129
+ "/api/resources/:id/invoke",
1130
+ "/api/receipts",
1131
+ "/api/events",
1132
+ "/api/events/stream"
1133
+ ];
1134
+ var McpMountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
1135
+ message: 'must start with "/" \u2014 it is an absolute gateway path, e.g. "/mcp".'
1136
+ }).refine((value) => !/[:*?\s]/.test(value), {
1137
+ message: 'must not contain ":", "*", "?" or whitespace \u2014 the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
1138
+ }).refine(
1139
+ (value) => {
1140
+ const base = value.replace(/\/+$/, "");
1141
+ return !RESERVED_GATEWAY_PATHS.some(
1142
+ (reserved) => reserved === base || reserved.startsWith(`${base}/`)
1143
+ );
1144
+ },
1145
+ {
1146
+ message: `must not collide with a route the gateway already serves (${RESERVED_GATEWAY_PATHS.join(", ")}); pick a dedicated prefix such as "/mcp".`
1147
+ }
1148
+ );
1114
1149
  var ProtocolsSchema = z.object({
1115
1150
  http: z.object({ enabled: BooleanOrString }).strict(),
1116
1151
  mcp: z.object({
1117
1152
  enabled: BooleanOrString,
1118
- mountPath: z.string().min(1)
1153
+ mountPath: McpMountPathSchema
1119
1154
  }).strict()
1120
1155
  }).strict();
1121
1156
  var BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
@@ -1152,7 +1187,14 @@ var ResourceEntrySchema = z.object({
1152
1187
  var ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
1153
1188
  var FacilitatorAuthSchema = z.discriminatedUnion("type", [
1154
1189
  z.object({ type: z.literal("none") }).strict(),
1155
- z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict()
1190
+ z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
1191
+ // Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
1192
+ // which signs a fresh JWT per request; a static header cannot express it.
1193
+ z.object({
1194
+ type: z.literal("cdp"),
1195
+ apiKeyId: z.string().min(1),
1196
+ apiKeySecret: z.string().min(1)
1197
+ }).strict()
1156
1198
  ]);
1157
1199
  var FacilitatorSchema = z.discriminatedUnion("mode", [
1158
1200
  z.object({
@@ -1181,7 +1223,9 @@ var X402Schema = z.object({
1181
1223
  maxTimeoutSeconds: NumberOrString,
1182
1224
  facilitator: FacilitatorSchema,
1183
1225
  /** Real funds. Never defaulted — see src/payments/x402/guardrails.ts. */
1184
- allowMainnet: BooleanOrString.optional()
1226
+ allowMainnet: BooleanOrString.optional(),
1227
+ /** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
1228
+ allowUnauthenticatedFacilitator: BooleanOrString.optional()
1185
1229
  }).strict();
1186
1230
  var PaymentsSchema = z.object({
1187
1231
  x402: X402Schema.optional()
@@ -1328,7 +1372,13 @@ function normalise(raw) {
1328
1372
  }
1329
1373
  ),
1330
1374
  facilitator,
1331
- ...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {}
1375
+ ...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
1376
+ ...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
1377
+ allowUnauthenticatedFacilitator: toBoolean(
1378
+ x402Raw.allowUnauthenticatedFacilitator,
1379
+ "payments.x402.allowUnauthenticatedFacilitator"
1380
+ )
1381
+ } : {}
1332
1382
  } : void 0;
1333
1383
  if (x402) {
1334
1384
  validateAddress("payments.x402.payTo", x402.payTo);
@@ -1337,8 +1387,11 @@ function normalise(raw) {
1337
1387
  network: x402.network,
1338
1388
  payTo: x402.payTo,
1339
1389
  asset: x402.asset,
1390
+ assetName: x402.assetName,
1391
+ assetVersion: x402.assetVersion,
1340
1392
  facilitator: x402.facilitator,
1341
- ...x402.allowMainnet !== void 0 ? { allowMainnet: x402.allowMainnet } : {}
1393
+ ...x402.allowMainnet !== void 0 ? { allowMainnet: x402.allowMainnet } : {},
1394
+ ...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
1342
1395
  });
1343
1396
  }
1344
1397
  const resources = Object.entries(raw.resources).map(
@@ -1497,6 +1550,10 @@ function defaultClosedObjectSchema(schema) {
1497
1550
  if (isPlainObject4(items)) {
1498
1551
  result["items"] = defaultClosedObjectSchema(items);
1499
1552
  }
1553
+ const additional = schema["additionalProperties"];
1554
+ if (isPlainObject4(additional)) {
1555
+ result["additionalProperties"] = defaultClosedObjectSchema(additional);
1556
+ }
1500
1557
  return result;
1501
1558
  }
1502
1559
  var EMPTY_CLOSED_OBJECT_SCHEMA = {
@@ -1570,6 +1627,10 @@ function validateResourceSchemaKeywords(id, path2, schema) {
1570
1627
  if (isPlainObject4(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
1571
1628
  }
1572
1629
  }
1630
+ const additional = schema["additionalProperties"];
1631
+ if (isPlainObject4(additional)) {
1632
+ validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
1633
+ }
1573
1634
  const items = schema["items"];
1574
1635
  if (isPlainObject4(items)) {
1575
1636
  validateResourceSchemaKeywords(id, `${path2}.items`, items);
@@ -1619,6 +1680,23 @@ function validatePathParametersDeclared(id, url, input) {
1619
1680
  }
1620
1681
  const params = extractPathParameterNames(url);
1621
1682
  if (params.length === 0) return;
1683
+ const firstBrace = url.indexOf("{");
1684
+ if (firstBrace !== -1) {
1685
+ const prefix = url.slice(0, firstBrace);
1686
+ let origin;
1687
+ try {
1688
+ const parsedPrefix = new URL(prefix);
1689
+ origin = parsedPrefix.hostname === "" ? void 0 : parsedPrefix.origin;
1690
+ } catch {
1691
+ }
1692
+ if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
1693
+ throw new CommerceError(
1694
+ "CONFIG_INVALID",
1695
+ `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`,
1696
+ { details: { path: `resources.${id}.backend.url`, resourceId: id } }
1697
+ );
1698
+ }
1699
+ }
1622
1700
  const propertiesRaw = input?.["properties"];
1623
1701
  const properties = isPlainObject4(propertiesRaw) ? propertiesRaw : {};
1624
1702
  const requiredRaw = input?.["required"];
@@ -2163,13 +2241,17 @@ function createReadinessProbe(options, ttlMs = READINESS_TTL_MS) {
2163
2241
  }
2164
2242
 
2165
2243
  // src/gateway/well-known.ts
2166
- var GATEWAY_SUPPORTED_SPEC = "agent-commerce/v0.2.0-beta";
2244
+ var GATEWAY_SUPPORTED_SPEC = "agent-commerce/v1.0.0";
2245
+ function publicHealth(health) {
2246
+ const { detail: _detail, ...rest } = health;
2247
+ return rest;
2248
+ }
2167
2249
  async function buildWellKnownDocument(options) {
2168
2250
  const adapters = await Promise.all(
2169
- options.adapterRuntimes.map(async (runtime) => ({
2170
- ...runtime.adapter.descriptor,
2171
- health: await getAdapterHealth(runtime, options.clock)
2172
- }))
2251
+ options.adapterRuntimes.map(async (runtime) => {
2252
+ const health = await getAdapterHealth(runtime, options.clock);
2253
+ return { ...runtime.adapter.descriptor, health: publicHealth(health) };
2254
+ })
2173
2255
  );
2174
2256
  const x402 = options.config.payments.x402;
2175
2257
  return {
@@ -2724,6 +2806,12 @@ function createSqliteReceiptStore(options) {
2724
2806
  );
2725
2807
  }
2726
2808
  }
2809
+ if (isFileBacked) {
2810
+ try {
2811
+ closeSync(openSync(path2, "a", 384));
2812
+ } catch {
2813
+ }
2814
+ }
2727
2815
  const db = new Database(path2);
2728
2816
  db.pragma("journal_mode = WAL");
2729
2817
  db.pragma("foreign_keys = ON");