@devlab.group/agent-commerce 0.1.0-alpha.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,7 +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-LCk2TzGN.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-LCk2TzGN.js';
3
- 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-C0goMgLK.js';
4
- 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-C0goMgLK.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';
5
6
  import { FastifyInstance } from 'fastify';
6
7
  import 'node:http';
7
8
 
@@ -93,13 +94,29 @@ declare function toCommerceError(value: unknown, fallbackCode?: CommerceErrorCod
93
94
 
94
95
  /**
95
96
  * Reserved input property carrying a payment proof on protocols that have no
96
- * header channel (MCP tool calls). Over HTTP the `X-PAYMENT` header is used.
97
+ * header channel (MCP tool calls). Over HTTP the `PAYMENT-SIGNATURE` header is
98
+ * used.
97
99
  */
98
100
  declare const PAYMENT_INPUT_FIELD = "_payment";
99
- /** HTTP header carrying the payment proof. */
100
- declare const PAYMENT_HEADER = "x-payment";
101
- /** HTTP response header carrying the settlement reference. */
102
- declare const PAYMENT_RESPONSE_HEADER = "x-payment-response";
101
+ /**
102
+ * HTTP request header carrying the payment proof.
103
+ *
104
+ * x402 v2 header names, lowercased because that is how Node presents incoming
105
+ * headers. v1's `X-PAYMENT` / `X-PAYMENT-RESPONSE` pair is not accepted: this
106
+ * gateway speaks one protocol version, and quietly honouring both would mean
107
+ * two verification paths for the same money.
108
+ */
109
+ declare const PAYMENT_HEADER = "payment-signature";
110
+ /** HTTP response header carrying the settlement result. */
111
+ declare const PAYMENT_RESPONSE_HEADER = "payment-response";
112
+ /**
113
+ * HTTP response header carrying the base64 payment challenge on a 402.
114
+ *
115
+ * The 402 body still carries {@link PaymentRequiredEnvelope} — richer, and the
116
+ * only channel MCP has — but an x402 v2 client reads the challenge from this
117
+ * header and ignores the body, so both are sent.
118
+ */
119
+ declare const PAYMENT_REQUIRED_HEADER = "payment-required";
103
120
  /**
104
121
  * Key under which a protocol adapter attaches a {@link DeliverySummary} to its
105
122
  * result metadata — MCP's `CallToolResult._meta`, and anywhere else a protocol
@@ -130,6 +147,12 @@ interface PaymentRequiredEnvelope {
130
147
  readonly expiresAt?: IsoTimestamp;
131
148
  /** Provider-native requirement objects, passed through verbatim. */
132
149
  readonly accepts: readonly Readonly<Record<string, unknown>>[];
150
+ /**
151
+ * The provider's own challenge document, verbatim — see
152
+ * {@link PaymentChallenge.envelope}. Present for providers that have one;
153
+ * a buyer's protocol client can hand this straight to its SDK.
154
+ */
155
+ readonly envelope?: Readonly<Record<string, unknown>>;
133
156
  };
134
157
  }
135
158
  interface ErrorEnvelope {
@@ -150,7 +173,7 @@ interface ErrorEnvelope {
150
173
  * `/api/receipts` is an operator route behind `server.adminToken`.
151
174
  *
152
175
  * Before this existed the two surfaces disagreed: HTTP callers received a
153
- * settlement summary in `X-PAYMENT-RESPONSE` while MCP callers received
176
+ * settlement summary in the payment-response header while MCP callers received
154
177
  * nothing, so an MCP buyer's only route to their own receipt was the merchant's
155
178
  * ledger. Both surfaces now emit this same shape.
156
179
  */
@@ -263,7 +286,7 @@ interface ReceiptStore {
263
286
  * Receipts whose delivery did not succeed — `backendStatus` outside 2xx.
264
287
  *
265
288
  * A paid-but-undelivered purchase is the one row a merchant must notice:
266
- * 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
267
290
  * remedy available. Counted rather than listed for the same reason as
268
291
  * {@link countReceipts} — `listReceipts` is clamped, so counting by list
269
292
  * length silently saturates.
@@ -353,13 +376,9 @@ interface GatewayConfig {
353
376
  readonly assetDecimals: number;
354
377
  readonly payTo: string;
355
378
  readonly maxTimeoutSeconds: number;
356
- readonly facilitator: {
357
- readonly mode: 'local';
358
- readonly signerPrivateKey: string;
359
- } | {
360
- readonly mode: 'remote';
361
- readonly url: string;
362
- };
379
+ readonly facilitator: X402FacilitatorConfig;
380
+ readonly allowMainnet?: boolean;
381
+ readonly allowUnauthenticatedFacilitator?: boolean;
363
382
  };
364
383
  };
365
384
  }
@@ -409,4 +428,4 @@ interface SqliteReceiptStoreOptions {
409
428
  }
410
429
  declare function createSqliteReceiptStore(options: SqliteReceiptStoreOptions): ReceiptStore;
411
430
 
412
- export { AdapterDescriptor, AdapterHealth, type BackendExecutor, BackendHandler, type BackendRequest, type BackendResponse, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, Clock, CommerceError, type CommerceErrorCode, type CommerceErrorInfo, type CommerceErrorOptions, CommerceEvent, CommerceReceipt, CommerceResource, DELIVERY_SUMMARY_META_KEY, DecimalAmount, DeliveredOutcome, type DeliverySummary, type ErrorEnvelope, ExecutionPipeline, type GatewayConfig, type GatewayInstance, type GatewayOptions, IdGenerator, IsoTimestamp, type ListOptions, Logger, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_RESPONSE_HEADER, PaymentAttempt, type PaymentAttemptReservation, type PaymentAttemptUpdate, PaymentMethodName, PaymentProvider, type PaymentRequiredEnvelope, PaymentRequiredOutcome, ProtocolAdapter, RETRYABLE_ERROR_CODES, type ReceiptStore, ResourceRegistry, createGateway, createSqliteReceiptStore, isCommerceError, isPaymentRequiredEnvelope, loadConfig, parseConfig, createSqliteReceiptStore as receipts, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope };
431
+ export { AdapterDescriptor, AdapterHealth, type BackendExecutor, BackendHandler, type BackendRequest, type BackendResponse, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, Clock, CommerceError, type CommerceErrorCode, type CommerceErrorInfo, type CommerceErrorOptions, CommerceEvent, CommerceReceipt, CommerceResource, DELIVERY_SUMMARY_META_KEY, DecimalAmount, DeliveredOutcome, type DeliverySummary, type ErrorEnvelope, ExecutionPipeline, type GatewayConfig, type GatewayInstance, type GatewayOptions, IdGenerator, IsoTimestamp, type ListOptions, Logger, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PaymentAttempt, type PaymentAttemptReservation, type PaymentAttemptUpdate, PaymentMethodName, PaymentProvider, type PaymentRequiredEnvelope, PaymentRequiredOutcome, ProtocolAdapter, RETRYABLE_ERROR_CODES, type ReceiptStore, ResourceRegistry, createGateway, createSqliteReceiptStore, isCommerceError, isPaymentRequiredEnvelope, loadConfig, parseConfig, createSqliteReceiptStore as receipts, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { CommerceError, PAYMENT_INPUT_FIELD, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, PAYMENT_HEADER, toPaymentRequiredEnvelope, PAYMENT_RESPONSE_HEADER } from './chunk-CZ6ZDAYD.js';
2
- 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_RESPONSE_HEADER, RETRYABLE_ERROR_CODES, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-CZ6ZDAYD.js';
3
- 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';
4
5
  import * as path from 'path';
5
6
  import { dirname } from 'path';
6
7
  import { parse, YAMLError } from 'yaml';
@@ -13,6 +14,9 @@ import Database from 'better-sqlite3';
13
14
 
14
15
  // src/config/env.ts
15
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 = /\$\{[^{}]*\$\{/;
16
20
  function substituteEnv(value, env, path2 = "$") {
17
21
  if (typeof value === "string") {
18
22
  return substituteString(value, env, path2);
@@ -30,19 +34,26 @@ function substituteEnv(value, env, path2 = "$") {
30
34
  return value;
31
35
  }
32
36
  function substituteString(value, env, path2) {
33
- const substituted = substituteOnce(value, env, path2);
34
- PLACEHOLDER_PATTERN.lastIndex = 0;
35
- if (PLACEHOLDER_PATTERN.test(substituted)) {
36
- PLACEHOLDER_PATTERN.lastIndex = 0;
37
- const residual = PLACEHOLDER_PATTERN.exec(substituted)?.[0] ?? "${\u2026}";
38
- 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;
39
51
  throw new CommerceError(
40
52
  "CONFIG_INVALID",
41
- `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.`,
42
- { 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 } }
43
55
  );
44
56
  }
45
- return substituted;
46
57
  }
47
58
  function substituteOnce(value, env, path2) {
48
59
  return value.replace(
@@ -952,6 +963,7 @@ function createExecutionPipeline(options) {
952
963
  provider: paymentResult.provider,
953
964
  amount: paymentResult.amount,
954
965
  currency: paymentResult.currency,
966
+ ...paymentResult.network !== void 0 ? { network: paymentResult.network } : {},
955
967
  ...paymentResult.externalReference !== void 0 ? { externalReference: paymentResult.externalReference } : {}
956
968
  }
957
969
  }
@@ -1109,11 +1121,36 @@ var StorageSchema = z.object({
1109
1121
  path: z.string().min(1)
1110
1122
  }).strict()
1111
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
+ );
1112
1149
  var ProtocolsSchema = z.object({
1113
1150
  http: z.object({ enabled: BooleanOrString }).strict(),
1114
1151
  mcp: z.object({
1115
1152
  enabled: BooleanOrString,
1116
- mountPath: z.string().min(1)
1153
+ mountPath: McpMountPathSchema
1117
1154
  }).strict()
1118
1155
  }).strict();
1119
1156
  var BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
@@ -1148,6 +1185,17 @@ var ResourceEntrySchema = z.object({
1148
1185
  payments: z.array(z.string().min(1)).optional()
1149
1186
  }).strict();
1150
1187
  var ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
1188
+ var FacilitatorAuthSchema = z.discriminatedUnion("type", [
1189
+ z.object({ type: z.literal("none") }).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()
1198
+ ]);
1151
1199
  var FacilitatorSchema = z.discriminatedUnion("mode", [
1152
1200
  z.object({
1153
1201
  mode: z.literal("local"),
@@ -1155,7 +1203,12 @@ var FacilitatorSchema = z.discriminatedUnion("mode", [
1155
1203
  }).strict(),
1156
1204
  z.object({
1157
1205
  mode: z.literal("remote"),
1158
- url: z.string().min(1)
1206
+ url: z.string().min(1),
1207
+ // Absent means "this facilitator takes no credential" — an explicit
1208
+ // statement, normalised to `{ type: 'none' }` below. On a mainnet that
1209
+ // combination is refused outright, so the default can never quietly
1210
+ // become an unauthenticated production facilitator.
1211
+ auth: FacilitatorAuthSchema.optional()
1159
1212
  }).strict()
1160
1213
  ]);
1161
1214
  var X402Schema = z.object({
@@ -1168,7 +1221,11 @@ var X402Schema = z.object({
1168
1221
  assetDecimals: NumberOrString,
1169
1222
  payTo: z.string().min(1),
1170
1223
  maxTimeoutSeconds: NumberOrString,
1171
- facilitator: FacilitatorSchema
1224
+ facilitator: FacilitatorSchema,
1225
+ /** Real funds. Never defaulted — see src/payments/x402/guardrails.ts. */
1226
+ allowMainnet: BooleanOrString.optional(),
1227
+ /** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
1228
+ allowUnauthenticatedFacilitator: BooleanOrString.optional()
1172
1229
  }).strict();
1173
1230
  var PaymentsSchema = z.object({
1174
1231
  x402: X402Schema.optional()
@@ -1290,14 +1347,12 @@ function normalise(raw) {
1290
1347
  }
1291
1348
  };
1292
1349
  const x402Raw = raw.payments.x402;
1293
- if (x402Raw?.facilitator.mode === "remote") {
1294
- throw new CommerceError(
1295
- "CONFIG_INVALID",
1296
- 'payments.x402.facilitator.mode "remote" is not supported in v0.1.0-alpha; use "local" (the deterministic dev-chain facilitator)',
1297
- { details: { path: "payments.x402.facilitator.mode" } }
1298
- );
1299
- }
1300
- const x402 = x402Raw ? {
1350
+ const facilitator = x402Raw === void 0 ? void 0 : x402Raw.facilitator.mode === "local" ? { mode: "local", signerPrivateKey: x402Raw.facilitator.signerPrivateKey } : {
1351
+ mode: "remote",
1352
+ url: x402Raw.facilitator.url,
1353
+ auth: x402Raw.facilitator.auth ?? { type: "none" }
1354
+ };
1355
+ const x402 = x402Raw && facilitator ? {
1301
1356
  enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
1302
1357
  network: x402Raw.network,
1303
1358
  rpcUrl: x402Raw.rpcUrl,
@@ -1309,18 +1364,35 @@ function normalise(raw) {
1309
1364
  max: 36
1310
1365
  }),
1311
1366
  payTo: x402Raw.payTo,
1312
- maxTimeoutSeconds: toNumber(x402Raw.maxTimeoutSeconds, "payments.x402.maxTimeoutSeconds", {
1313
- min: 1
1314
- }),
1315
- // Only "local" can reach here — "remote" was rejected above.
1316
- facilitator: {
1317
- mode: "local",
1318
- signerPrivateKey: x402Raw.facilitator.signerPrivateKey
1319
- }
1367
+ maxTimeoutSeconds: toNumber(
1368
+ x402Raw.maxTimeoutSeconds,
1369
+ "payments.x402.maxTimeoutSeconds",
1370
+ {
1371
+ min: 1
1372
+ }
1373
+ ),
1374
+ facilitator,
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
+ } : {}
1320
1382
  } : void 0;
1321
1383
  if (x402) {
1322
1384
  validateAddress("payments.x402.payTo", x402.payTo);
1323
1385
  validateAddress("payments.x402.asset", x402.asset);
1386
+ resolveX402Deployment({
1387
+ network: x402.network,
1388
+ payTo: x402.payTo,
1389
+ asset: x402.asset,
1390
+ assetName: x402.assetName,
1391
+ assetVersion: x402.assetVersion,
1392
+ facilitator: x402.facilitator,
1393
+ ...x402.allowMainnet !== void 0 ? { allowMainnet: x402.allowMainnet } : {},
1394
+ ...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
1395
+ });
1324
1396
  }
1325
1397
  const resources = Object.entries(raw.resources).map(
1326
1398
  ([id, entry]) => normaliseResource(id, entry, protocols, x402)
@@ -1365,7 +1437,7 @@ function normaliseResource(id, entry, protocols, x402) {
1365
1437
  }
1366
1438
  for (const protocol of entry.expose) {
1367
1439
  if (!SUPPORTED_PROTOCOLS.has(protocol)) {
1368
- const hint = protocol === "ucp" ? " (UCP is planned, not supported in v0.1.0-alpha)" : "";
1440
+ const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
1369
1441
  throw new CommerceError(
1370
1442
  "CONFIG_INVALID",
1371
1443
  `Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp.`,
@@ -1401,7 +1473,7 @@ function normaliseResource(id, entry, protocols, x402) {
1401
1473
  if (entry.pricing.type === "dynamic") {
1402
1474
  throw new CommerceError(
1403
1475
  "CONFIG_INVALID",
1404
- `Resource "${id}" uses pricing.type "dynamic", which is not supported in v0.1.0-alpha`,
1476
+ `Resource "${id}" uses pricing.type "dynamic", which is not supported in this release`,
1405
1477
  { details: { path: `resources.${id}.pricing.type`, resourceId: id } }
1406
1478
  );
1407
1479
  }
@@ -1478,6 +1550,10 @@ function defaultClosedObjectSchema(schema) {
1478
1550
  if (isPlainObject4(items)) {
1479
1551
  result["items"] = defaultClosedObjectSchema(items);
1480
1552
  }
1553
+ const additional = schema["additionalProperties"];
1554
+ if (isPlainObject4(additional)) {
1555
+ result["additionalProperties"] = defaultClosedObjectSchema(additional);
1556
+ }
1481
1557
  return result;
1482
1558
  }
1483
1559
  var EMPTY_CLOSED_OBJECT_SCHEMA = {
@@ -1551,6 +1627,10 @@ function validateResourceSchemaKeywords(id, path2, schema) {
1551
1627
  if (isPlainObject4(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
1552
1628
  }
1553
1629
  }
1630
+ const additional = schema["additionalProperties"];
1631
+ if (isPlainObject4(additional)) {
1632
+ validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
1633
+ }
1554
1634
  const items = schema["items"];
1555
1635
  if (isPlainObject4(items)) {
1556
1636
  validateResourceSchemaKeywords(id, `${path2}.items`, items);
@@ -1600,6 +1680,23 @@ function validatePathParametersDeclared(id, url, input) {
1600
1680
  }
1601
1681
  const params = extractPathParameterNames(url);
1602
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
+ }
1603
1700
  const propertiesRaw = input?.["properties"];
1604
1701
  const properties = isPlainObject4(propertiesRaw) ? propertiesRaw : {};
1605
1702
  const requiredRaw = input?.["required"];
@@ -1755,7 +1852,7 @@ var SECRET_FIELD_NAMES = [
1755
1852
  ];
1756
1853
  var REDACT_PATHS = [
1757
1854
  "req.headers.authorization",
1758
- 'req.headers["x-payment"]',
1855
+ 'req.headers["payment-signature"]',
1759
1856
  ...SECRET_FIELD_NAMES,
1760
1857
  ...SECRET_FIELD_NAMES.map((name) => `*.${name}`)
1761
1858
  ];
@@ -1813,7 +1910,8 @@ function wrapPino(instance) {
1813
1910
  return logger;
1814
1911
  }
1815
1912
  var CORS_METHODS = "GET,POST,DELETE,OPTIONS";
1816
- var CORS_HEADERS = "content-type,x-payment,x-request-id,authorization";
1913
+ var CORS_HEADERS = "content-type,payment-signature,x-request-id,authorization";
1914
+ var CORS_EXPOSED_HEADERS = "payment-required,payment-response,x-request-id";
1817
1915
  function buildAccessControlHook(options) {
1818
1916
  const allowedOrigins = new Set(options.allowedOrigins.map((origin) => origin.toLowerCase()));
1819
1917
  const publicHostname = hostnameOf(options.publicBaseUrl);
@@ -1832,6 +1930,7 @@ function buildAccessControlHook(options) {
1832
1930
  reply.header("access-control-allow-origin", origin);
1833
1931
  reply.header("access-control-allow-methods", CORS_METHODS);
1834
1932
  reply.header("access-control-allow-headers", CORS_HEADERS);
1933
+ reply.header("access-control-expose-headers", CORS_EXPOSED_HEADERS);
1835
1934
  reply.header("vary", "Origin");
1836
1935
  if (request.method === "OPTIONS") {
1837
1936
  reply.status(204).send();
@@ -2142,13 +2241,17 @@ function createReadinessProbe(options, ttlMs = READINESS_TTL_MS) {
2142
2241
  }
2143
2242
 
2144
2243
  // src/gateway/well-known.ts
2145
- var GATEWAY_SUPPORTED_SPEC = "agent-commerce/v0.1.0-alpha";
2244
+ var GATEWAY_SUPPORTED_SPEC = "agent-commerce/v1.0.0";
2245
+ function publicHealth(health) {
2246
+ const { detail: _detail, ...rest } = health;
2247
+ return rest;
2248
+ }
2146
2249
  async function buildWellKnownDocument(options) {
2147
2250
  const adapters = await Promise.all(
2148
- options.adapterRuntimes.map(async (runtime) => ({
2149
- ...runtime.adapter.descriptor,
2150
- health: await getAdapterHealth(runtime, options.clock)
2151
- }))
2251
+ options.adapterRuntimes.map(async (runtime) => {
2252
+ const health = await getAdapterHealth(runtime, options.clock);
2253
+ return { ...runtime.adapter.descriptor, health: publicHealth(health) };
2254
+ })
2152
2255
  );
2153
2256
  const x402 = options.config.payments.x402;
2154
2257
  return {
@@ -2172,9 +2275,18 @@ async function buildWellKnownDocument(options) {
2172
2275
  assetDecimals: x402.assetDecimals,
2173
2276
  payTo: x402.payTo,
2174
2277
  maxTimeoutSeconds: x402.maxTimeoutSeconds,
2175
- // Never the signerPrivateKey — only whether the facilitator is
2176
- // local (dev-chain only) or remote, plus its (non-secret) URL.
2177
- facilitator: x402.facilitator.mode === "local" ? { mode: "local" } : { mode: "remote", url: x402.facilitator.url }
2278
+ // Never the signerPrivateKey, and never the facilitator URL:
2279
+ // a facilitator endpoint is a per-deployment operational detail
2280
+ // that can carry a tenant path or an API key, exactly like
2281
+ // `rpcUrl` above. Only whether it is local or remote is public.
2282
+ facilitator: { mode: x402.facilitator.mode },
2283
+ // Chain id 84532 is shared between the local dev chain and
2284
+ // public Base Sepolia, so the network id alone cannot say which
2285
+ // one a client is talking to. This can.
2286
+ mode: resolveDeploymentMode(
2287
+ requireNetworkProfile(x402.network, "payments.x402.network"),
2288
+ x402.facilitator.mode
2289
+ )
2178
2290
  }
2179
2291
  } : {}
2180
2292
  }
@@ -2309,7 +2421,12 @@ async function handleInvoke(request, reply, options) {
2309
2421
  };
2310
2422
  const outcome = await options.pipeline.execute(canonicalRequest);
2311
2423
  if (outcome.kind === "payment-required") {
2312
- reply.status(402).send(toPaymentRequiredEnvelope(outcome));
2424
+ const envelope = toPaymentRequiredEnvelope(outcome);
2425
+ if (envelope.payment.envelope !== void 0) {
2426
+ reply.header(PAYMENT_REQUIRED_HEADER, encodeHeaderDocument(envelope.payment.envelope));
2427
+ }
2428
+ reply.header("cache-control", "no-store");
2429
+ reply.status(402).send(envelope);
2313
2430
  return;
2314
2431
  }
2315
2432
  if (outcome.payment) {
@@ -2329,7 +2446,7 @@ function errorPaymentSummary(error) {
2329
2446
  const payment = error.details?.["payment"];
2330
2447
  if (typeof payment !== "object" || payment === null) return void 0;
2331
2448
  const rec = payment;
2332
- const { status, provider, amount, currency, externalReference } = rec;
2449
+ const { status, provider, amount, currency, network, externalReference } = rec;
2333
2450
  if (typeof status !== "string" || typeof provider !== "string" || typeof amount !== "string" || typeof currency !== "string") {
2334
2451
  return void 0;
2335
2452
  }
@@ -2338,18 +2455,24 @@ function errorPaymentSummary(error) {
2338
2455
  provider,
2339
2456
  amount,
2340
2457
  currency,
2458
+ ...typeof network === "string" ? { network } : {},
2341
2459
  ...typeof externalReference === "string" ? { externalReference } : {}
2342
2460
  };
2343
2461
  }
2462
+ function encodeHeaderDocument(document) {
2463
+ return Buffer.from(JSON.stringify(document), "utf8").toString("base64");
2464
+ }
2344
2465
  function encodePaymentSummary(payment) {
2345
- const summary = {
2466
+ return encodeHeaderDocument({
2467
+ success: payment.status === "settled",
2468
+ transaction: payment.externalReference ?? "",
2469
+ network: payment.network ?? "",
2346
2470
  status: payment.status,
2347
2471
  provider: payment.provider,
2348
2472
  amount: payment.amount,
2349
2473
  currency: payment.currency,
2350
2474
  ...payment.externalReference !== void 0 ? { externalReference: payment.externalReference } : {}
2351
- };
2352
- return Buffer.from(JSON.stringify(summary), "utf8").toString("base64");
2475
+ });
2353
2476
  }
2354
2477
  function parseLimit(value) {
2355
2478
  if (Array.isArray(value)) return parseLimit(value[0]);
@@ -2683,6 +2806,12 @@ function createSqliteReceiptStore(options) {
2683
2806
  );
2684
2807
  }
2685
2808
  }
2809
+ if (isFileBacked) {
2810
+ try {
2811
+ closeSync(openSync(path2, "a", 384));
2812
+ } catch {
2813
+ }
2814
+ }
2686
2815
  const db = new Database(path2);
2687
2816
  db.pragma("journal_mode = WAL");
2688
2817
  db.pragma("foreign_keys = ON");