@devlab.group/agent-commerce 1.4.1 → 1.6.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.
Files changed (44) hide show
  1. package/README.md +168 -183
  2. package/dist/ap2.d.ts +21 -24
  3. package/dist/ap2.js +68 -82
  4. package/dist/ap2.js.map +1 -1
  5. package/dist/authorization-BpbWGGrf.d.ts +115 -0
  6. package/dist/{chunk-NWIQLJNU.js → chunk-74BSEXB5.js} +37 -34
  7. package/dist/chunk-74BSEXB5.js.map +1 -0
  8. package/dist/chunk-AFI2CEZS.js +10 -0
  9. package/dist/chunk-AFI2CEZS.js.map +1 -0
  10. package/dist/chunk-ATTWSFQP.js +680 -0
  11. package/dist/chunk-ATTWSFQP.js.map +1 -0
  12. package/dist/{chunk-ZEGFLRGV.js → chunk-IVJC5YLE.js} +63 -26
  13. package/dist/chunk-IVJC5YLE.js.map +1 -0
  14. package/dist/{chunk-GF6KHHKL.js → chunk-RAJGPGJG.js} +5 -8
  15. package/dist/chunk-RAJGPGJG.js.map +1 -0
  16. package/dist/chunk-V2V2E7HX.js +27 -0
  17. package/dist/chunk-V2V2E7HX.js.map +1 -0
  18. package/dist/cli/index.js +812 -573
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/constants-Bkq04FRZ.d.ts +48 -0
  21. package/dist/guardrails-71XwtXlT.d.ts +31 -0
  22. package/dist/index.d.ts +245 -265
  23. package/dist/index.js +1222 -626
  24. package/dist/index.js.map +1 -1
  25. package/dist/mcp.d.ts +6 -59
  26. package/dist/mcp.js +64 -53
  27. package/dist/mcp.js.map +1 -1
  28. package/dist/mpp.d.ts +50 -0
  29. package/dist/mpp.js +353 -0
  30. package/dist/mpp.js.map +1 -0
  31. package/dist/protocol-adapter-DTkrXTev.d.ts +238 -0
  32. package/dist/runtime-CiYbgGpi.d.ts +303 -0
  33. package/dist/{types-CJtwLu1c.d.ts → types-O6lg3Q5V.d.ts} +17 -23
  34. package/dist/x402.d.ts +43 -75
  35. package/dist/x402.js +13 -693
  36. package/dist/x402.js.map +1 -1
  37. package/package.json +13 -2
  38. package/dist/authorization-CPDcpJVa.d.ts +0 -123
  39. package/dist/chunk-GF6KHHKL.js.map +0 -1
  40. package/dist/chunk-NWIQLJNU.js.map +0 -1
  41. package/dist/chunk-ZEGFLRGV.js.map +0 -1
  42. package/dist/guardrails-Dmwi63KB.d.ts +0 -38
  43. package/dist/protocol-adapter-Bebmggf_.d.ts +0 -273
  44. package/dist/runtime-CKq3d_3K.d.ts +0 -340
package/dist/index.js CHANGED
@@ -1,15 +1,18 @@
1
- import { AP2_SPEC_VERSION, AP2_MODES, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_JWK_MEMBERS, AP2_KEY_TYPE, AP2_SIGNING_ALGORITHM, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, openSqliteDatabase, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-GF6KHHKL.js';
2
- import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-NWIQLJNU.js';
3
- import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER } from './chunk-ZEGFLRGV.js';
4
- export { AUTHORIZATION_HEADER, AUTHORIZATION_INPUT_FIELD, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, MAX_AUTHORIZATION_HEADER_BYTES, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PROTOCOL_NAMES, RESERVED_INPUT_FIELDS, RETRYABLE_ERROR_CODES, extractReservedInputFields, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, parseAuthorizationHeader, parseAuthorizationSubmission, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-ZEGFLRGV.js';
1
+ import { AP2_SPEC_VERSION, AP2_MODES, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_JWK_MEMBERS, AP2_KEY_TYPE, AP2_SIGNING_ALGORITHM, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, openSqliteDatabase, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-RAJGPGJG.js';
2
+ import { MCP_TOOL_NAME_PATTERN } from './chunk-AFI2CEZS.js';
3
+ import { MPP_DEFAULT_NETWORK, isMppNetwork, MPP_NETWORKS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPP_PROFILE } from './chunk-V2V2E7HX.js';
4
+ import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-74BSEXB5.js';
5
+ import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_METHOD_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, isRecord, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, isCommerceError, describeError, isHttpProtocolAdapter, toLogInfo, extractReservedInputFields, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, toErrorEnvelope, DELIVERY_SUMMARY_META_KEY, toDeliverySummary } from './chunk-IVJC5YLE.js';
6
+ export { AUTHORIZATION_HEADER, AUTHORIZATION_INPUT_FIELD, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, MAX_AUTHORIZATION_HEADER_BYTES, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_METHOD_NAMES, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PROTOCOL_NAMES, RESERVED_INPUT_FIELDS, RETRYABLE_ERROR_CODES, extractReservedInputFields, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, parseAuthorizationHeader, parseAuthorizationSubmission, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-IVJC5YLE.js';
5
7
  import { promises, accessSync, constants } from 'fs';
6
8
  import * as path from 'path';
7
9
  import { parse, YAMLError } from 'yaml';
8
10
  import { z } from 'zod';
11
+ import { isDeepStrictEqual } from 'util';
12
+ import Fastify from 'fastify';
9
13
  import { randomUUID, timingSafeEqual, createHash } from 'crypto';
10
14
  import { createRequire } from 'module';
11
15
  import pino from 'pino';
12
- import Fastify from 'fastify';
13
16
  import Ajv2020 from 'ajv/dist/2020.js';
14
17
  import addFormats from 'ajv-formats';
15
18
 
@@ -25,7 +28,7 @@ function substituteEnv(value, env, path2 = "$") {
25
28
  if (Array.isArray(value)) {
26
29
  return value.map((entry, index) => substituteEnv(entry, env, `${path2}[${index}]`));
27
30
  }
28
- if (isPlainObject(value)) {
31
+ if (isRecord(value)) {
29
32
  const result = {};
30
33
  for (const [key, entry] of Object.entries(value)) {
31
34
  result[key] = substituteEnv(entry, env, `${path2}.${key}`);
@@ -42,16 +45,15 @@ function assertTemplateIsSupported(value, path2) {
42
45
  if (NESTED_PLACEHOLDER.test(value)) {
43
46
  throw new CommerceError(
44
47
  "CONFIG_INVALID",
45
- `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.`,
48
+ `Configuration value at "${path2}" nests placeholders (e.g. "\${A:-\${B}}"), which is not supported: the inner one is not resolved and its closing brace ends up in the value. Use a plain default, or set the variable.`,
46
49
  { details: { path: path2 } }
47
50
  );
48
51
  }
49
- BRACE_TOKEN.lastIndex = 0;
50
52
  for (const [token] of value.matchAll(BRACE_TOKEN)) {
51
53
  if (SUPPORTED_TOKEN.test(token)) continue;
52
54
  throw new CommerceError(
53
55
  "CONFIG_INVALID",
54
- `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.`,
56
+ `Configuration value at "${path2}" contains "${token}", which is not a supported placeholder. Only "\${VAR}" and "\${VAR:-default}" are recognized; shell forms such as "\${VAR-default}", "\${VAR:=default}" and "\${VAR:?message}" would load as literal text.`,
55
57
  { details: { path: path2, token } }
56
58
  );
57
59
  }
@@ -76,9 +78,6 @@ function substituteOnce(value, env, path2) {
76
78
  }
77
79
  );
78
80
  }
79
- function isPlainObject(value) {
80
- return typeof value === "object" && value !== null && !Array.isArray(value);
81
- }
82
81
 
83
82
  // src/config/filename.ts
84
83
  var DEFAULT_CONFIG_FILENAME = "config.yaml";
@@ -97,7 +96,7 @@ var HttpBackendExecutor = class {
97
96
  }
98
97
  async call(handler, request) {
99
98
  const timeoutMs = handler.timeoutMs ?? DEFAULT_BACKEND_TIMEOUT_MS;
100
- const inputRecord = isPlainObject2(request.input) ? request.input : {};
99
+ const inputRecord = isRecord(request.input) ? request.input : {};
101
100
  const context = { requestId: request.requestId, resourceId: request.resourceId };
102
101
  const parts = buildBackendRequestParts(handler, inputRecord, context);
103
102
  let target;
@@ -110,26 +109,17 @@ var HttpBackendExecutor = class {
110
109
  cause: error
111
110
  });
112
111
  }
113
- const literalPrefix = handler.url.split("{")[0] ?? handler.url;
114
- try {
115
- const literalPrefixPathname = new URL(literalPrefix).pathname;
116
- if (!target.pathname.startsWith(literalPrefixPathname)) {
117
- throw new CommerceError(
118
- "INPUT_INVALID",
119
- "Path parameters resolved outside the configured backend path",
120
- {
121
- requestId: request.requestId,
122
- resourceId: request.resourceId
123
- }
124
- );
125
- }
126
- } catch (error) {
127
- if (error instanceof CommerceError) throw error;
112
+ const prefixPathname = literalPrefixPathname(handler);
113
+ if (prefixPathname !== void 0 && !target.pathname.startsWith(prefixPathname)) {
114
+ throw new CommerceError(
115
+ "INPUT_INVALID",
116
+ "Path parameters resolved outside the configured backend path",
117
+ context
118
+ );
128
119
  }
129
- const headers = { ...handler.headers ?? {} };
120
+ const headers = buildHeaders(handler, context);
130
121
  if (request.idempotencyKey !== void 0) {
131
- deleteHeader(headers, IDEMPOTENCY_KEY_HEADER);
132
- headers[IDEMPOTENCY_KEY_HEADER] = request.idempotencyKey;
122
+ setHeader(headers, IDEMPOTENCY_KEY_HEADER, request.idempotencyKey, context);
133
123
  }
134
124
  let body;
135
125
  checkQueryCollision(target, parts.query, context);
@@ -138,9 +128,7 @@ var HttpBackendExecutor = class {
138
128
  }
139
129
  if (parts.body !== void 0) {
140
130
  body = JSON.stringify(parts.body.value);
141
- if (!hasHeader(headers, "content-type")) {
142
- headers["content-type"] = "application/json";
143
- }
131
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
144
132
  }
145
133
  const started = performance.now();
146
134
  let response;
@@ -186,13 +174,13 @@ var HttpBackendExecutor = class {
186
174
  }
187
175
  );
188
176
  }
189
- const responseHeaders = headersToRecord(response.headers);
177
+ const responseHeaders = Object.fromEntries(response.headers);
190
178
  const contentType = response.headers.get("content-type") ?? "";
191
179
  let parsedBody;
192
180
  try {
193
181
  parsedBody = await parseBody(response, contentType);
194
182
  } catch (error) {
195
- const tooLarge = error instanceof Error && error.message.startsWith("response-body-too-large");
183
+ const tooLarge = isCommerceError(error);
196
184
  throw new CommerceError(
197
185
  "BACKEND_ERROR",
198
186
  tooLarge ? "Backend response exceeded the maximum allowed size" : "Backend response could not be read",
@@ -239,8 +227,9 @@ function findUnparsedBraceToken(url) {
239
227
  return token?.[0] ?? residue[index];
240
228
  }
241
229
  function validateBackendRequestShape(handler, input, context) {
242
- const inputRecord = isPlainObject2(input) ? input : {};
230
+ const inputRecord = isRecord(input) ? input : {};
243
231
  const parts = buildBackendRequestParts(handler, inputRecord, context);
232
+ buildHeaders(handler, context);
244
233
  let target;
245
234
  try {
246
235
  target = new URL(parts.url);
@@ -268,7 +257,7 @@ function buildBackendRequestParts(handler, input, context) {
268
257
  function resolveBoundGroup(input, key, kind, context) {
269
258
  const value = input[key];
270
259
  if (value === void 0) return {};
271
- if (!isPlainObject2(value)) {
260
+ if (!isRecord(value)) {
272
261
  throw new CommerceError(
273
262
  "INPUT_INVALID",
274
263
  `Input "${key}" must be an object of ${kind} parameters`,
@@ -289,14 +278,11 @@ function checkQueryCollision(target, remaining, context) {
289
278
  }
290
279
  }
291
280
  }
292
- function isPlainObject2(value) {
293
- return typeof value === "object" && value !== null && !Array.isArray(value);
294
- }
295
281
  var TRAVERSAL_PATH_VALUES = /* @__PURE__ */ new Set(["", ".", ".."]);
296
282
  function applyPathTemplate(template, input, context) {
297
283
  const remaining = { ...input };
298
284
  let missing;
299
- let invalid;
285
+ let invalid2;
300
286
  const url = template.replace(PATH_PARAM_PATTERN, (_match, key) => {
301
287
  if (!Object.hasOwn(remaining, key)) {
302
288
  missing = key;
@@ -306,7 +292,7 @@ function applyPathTemplate(template, input, context) {
306
292
  delete remaining[key];
307
293
  const raw = stringifyPrimitive(value);
308
294
  if (TRAVERSAL_PATH_VALUES.has(raw)) {
309
- invalid = key;
295
+ invalid2 = key;
310
296
  return "";
311
297
  }
312
298
  return encodeURIComponent(raw);
@@ -317,10 +303,10 @@ function applyPathTemplate(template, input, context) {
317
303
  details: { field: missing }
318
304
  });
319
305
  }
320
- if (invalid !== void 0) {
321
- throw new CommerceError("INPUT_INVALID", `Path parameter "${invalid}" is not a valid value`, {
306
+ if (invalid2 !== void 0) {
307
+ throw new CommerceError("INPUT_INVALID", `Path parameter "${invalid2}" is not a valid value`, {
322
308
  ...context,
323
- details: { field: invalid }
309
+ details: { field: invalid2 }
324
310
  });
325
311
  }
326
312
  return { url, remaining };
@@ -335,22 +321,38 @@ function stringifyPrimitive(value) {
335
321
  return String(value);
336
322
  }
337
323
  }
338
- function hasHeader(headers, name) {
339
- const target = name.toLowerCase();
340
- return Object.keys(headers).some((key) => key.toLowerCase() === target);
324
+ function buildHeaders(handler, context) {
325
+ try {
326
+ return new Headers(handler.headers);
327
+ } catch {
328
+ throw invalidHeaderError(context);
329
+ }
341
330
  }
342
- function deleteHeader(headers, name) {
343
- const target = name.toLowerCase();
344
- for (const key of Object.keys(headers)) {
345
- if (key.toLowerCase() === target) delete headers[key];
331
+ function setHeader(headers, name, value, context) {
332
+ try {
333
+ headers.set(name, value);
334
+ } catch {
335
+ throw invalidHeaderError(context);
346
336
  }
347
337
  }
348
- function headersToRecord(headers) {
349
- const record = {};
350
- headers.forEach((value, key) => {
351
- record[key] = value;
338
+ function invalidHeaderError(context) {
339
+ return new CommerceError("BACKEND_ERROR", "Backend request headers are invalid", {
340
+ ...context,
341
+ details: { reason: "invalid-header" }
352
342
  });
353
- return record;
343
+ }
344
+ var prefixPathnames = /* @__PURE__ */ new WeakMap();
345
+ function literalPrefixPathname(handler) {
346
+ let cached = prefixPathnames.get(handler);
347
+ if (cached === void 0) {
348
+ try {
349
+ cached = new URL(handler.url.split("{")[0] ?? handler.url).pathname;
350
+ } catch {
351
+ cached = null;
352
+ }
353
+ prefixPathnames.set(handler, cached);
354
+ }
355
+ return cached ?? void 0;
354
356
  }
355
357
  async function readBodyCapped(response, maxBytes) {
356
358
  if (!response.body) return "";
@@ -364,7 +366,7 @@ async function readBodyCapped(response, maxBytes) {
364
366
  total += value.byteLength;
365
367
  if (total > maxBytes) {
366
368
  await reader.cancel();
367
- throw new Error(`response-body-too-large:${maxBytes}`);
369
+ throw new CommerceError("BACKEND_ERROR", `response body exceeded ${maxBytes} bytes`);
368
370
  }
369
371
  chunks.push(value);
370
372
  }
@@ -399,47 +401,6 @@ function safeStringify(value) {
399
401
  function isAbortError(error) {
400
402
  return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
401
403
  }
402
-
403
- // src/core/execution/event-bus.ts
404
- function createEventBus(options) {
405
- const subscribers = /* @__PURE__ */ new Set();
406
- return {
407
- async emit(event) {
408
- try {
409
- await options.store.appendEvent(event);
410
- } catch (error) {
411
- options.logger.error(
412
- { err: describeError(error), eventType: event.type, requestId: event.requestId },
413
- "Failed to persist commerce event"
414
- );
415
- }
416
- for (const subscriber of subscribers) {
417
- try {
418
- subscriber(event);
419
- } catch (error) {
420
- options.logger.error(
421
- { err: describeError(error), eventType: event.type, requestId: event.requestId },
422
- "Event subscriber threw"
423
- );
424
- }
425
- }
426
- },
427
- subscribe(fn) {
428
- subscribers.add(fn);
429
- return () => {
430
- subscribers.delete(fn);
431
- };
432
- }
433
- };
434
- }
435
- function describeError(error) {
436
- if (error instanceof Error) {
437
- return { message: error.message, name: error.name };
438
- }
439
- return { message: String(error) };
440
- }
441
-
442
- // src/core/execution/validation.ts
443
404
  var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
444
405
  "object",
445
406
  "string",
@@ -456,13 +417,10 @@ function compileJsonSchema(schema) {
456
417
  return errors.length === 0 ? { valid: true, value: input } : { valid: false, errors };
457
418
  };
458
419
  }
459
- function isPlainObject3(value) {
460
- return typeof value === "object" && value !== null && !Array.isArray(value);
461
- }
462
420
  function matchesType(type, value) {
463
421
  switch (type) {
464
422
  case "object":
465
- return isPlainObject3(value);
423
+ return isRecord(value);
466
424
  case "array":
467
425
  return Array.isArray(value);
468
426
  case "string":
@@ -485,15 +443,7 @@ function describeType(value) {
485
443
  return typeof value;
486
444
  }
487
445
  function deepEqual(a, b) {
488
- if (a === b) return true;
489
- if (typeof a !== typeof b) return false;
490
- if (a === null || b === null) return false;
491
- if (typeof a !== "object") return false;
492
- try {
493
- return JSON.stringify(a) === JSON.stringify(b);
494
- } catch {
495
- return false;
496
- }
446
+ return a === b || isDeepStrictEqual(a, b);
497
447
  }
498
448
  function own(obj, key) {
499
449
  return Object.hasOwn(obj, key) ? obj[key] : void 0;
@@ -513,8 +463,8 @@ function isObjectSchemaNode(schema) {
513
463
  function validateNode(schema, value, path2) {
514
464
  const errors = [];
515
465
  const types = typeList(own(schema, "type"));
516
- const recognisedTypes = types?.filter((entry) => SUPPORTED_TYPES.has(entry));
517
- if (types !== void 0 && recognisedTypes !== void 0 && recognisedTypes.length === types.length && types.length > 0) {
466
+ const recognizedTypes = types?.filter((entry) => SUPPORTED_TYPES.has(entry));
467
+ if (types !== void 0 && recognizedTypes !== void 0 && recognizedTypes.length === types.length && types.length > 0) {
518
468
  if (!types.some((entry) => matchesType(entry, value))) {
519
469
  errors.push({
520
470
  path: path2,
@@ -530,7 +480,7 @@ function validateNode(schema, value, path2) {
530
480
  errors.push({ path: path2, message: `must be one of ${JSON.stringify(allowed)}` });
531
481
  }
532
482
  }
533
- if (isObjectSchemaNode(schema) && isPlainObject3(value)) {
483
+ if (isObjectSchemaNode(schema) && isRecord(value)) {
534
484
  errors.push(...validateObject(schema, value, path2));
535
485
  } else if ((types?.includes("array") ?? false) && Array.isArray(value)) {
536
486
  errors.push(...validateArray(schema, value, path2));
@@ -540,7 +490,7 @@ function validateNode(schema, value, path2) {
540
490
  function validateObject(schema, value, path2) {
541
491
  const errors = [];
542
492
  const propsRaw = own(schema, "properties");
543
- const properties = isPlainObject3(propsRaw) ? propsRaw : {};
493
+ const properties = isRecord(propsRaw) ? propsRaw : {};
544
494
  const requiredRaw = own(schema, "required");
545
495
  const required = Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : [];
546
496
  for (const key of required) {
@@ -552,14 +502,14 @@ function validateObject(schema, value, path2) {
552
502
  for (const key of Object.keys(value)) {
553
503
  const propSchemaRaw = own(properties, key);
554
504
  if (propSchemaRaw !== void 0) {
555
- if (isPlainObject3(propSchemaRaw)) {
505
+ if (isRecord(propSchemaRaw)) {
556
506
  errors.push(...validateNode(propSchemaRaw, value[key], `${path2}.${key}`));
557
507
  }
558
508
  continue;
559
509
  }
560
510
  if (additional === false) {
561
511
  errors.push({ path: `${path2}.${key}`, message: "additional property not allowed" });
562
- } else if (isPlainObject3(additional)) {
512
+ } else if (isRecord(additional)) {
563
513
  errors.push(...validateNode(additional, value[key], `${path2}.${key}`));
564
514
  }
565
515
  }
@@ -568,7 +518,7 @@ function validateObject(schema, value, path2) {
568
518
  function validateArray(schema, value, path2) {
569
519
  const errors = [];
570
520
  const itemsRaw = own(schema, "items");
571
- if (isPlainObject3(itemsRaw)) {
521
+ if (isRecord(itemsRaw)) {
572
522
  value.forEach((item, index) => {
573
523
  errors.push(...validateNode(itemsRaw, item, `${path2}[${index}]`));
574
524
  });
@@ -591,7 +541,7 @@ function createExecutionPipeline(options) {
591
541
  await options.events.emit(event);
592
542
  } catch (error) {
593
543
  options.logger.error(
594
- { err: describeError2(error), eventType: event.type, requestId: event.requestId },
544
+ { err: describeError(error), eventType: event.type, requestId: event.requestId },
595
545
  "Event sink failed; commerce flow continues"
596
546
  );
597
547
  }
@@ -601,7 +551,7 @@ function createExecutionPipeline(options) {
601
551
  await op();
602
552
  } catch (error) {
603
553
  options.logger.error(
604
- { err: describeError2(error), requestId },
554
+ { err: describeError(error), requestId },
605
555
  `Persistence failed: ${label}`
606
556
  );
607
557
  }
@@ -688,7 +638,7 @@ function createExecutionPipeline(options) {
688
638
  await provider[action](verification.reservationId, context);
689
639
  } catch (error) {
690
640
  options.logger.error(
691
- { err: describeError2(error), requestId: request.requestId, action },
641
+ { err: describeError(error), requestId: request.requestId, action },
692
642
  "Authorization finalization failed; the reservation stays reserved"
693
643
  );
694
644
  }
@@ -698,7 +648,7 @@ function createExecutionPipeline(options) {
698
648
  async function execute(request) {
699
649
  const pipelineStart = options.clock.monotonicMs();
700
650
  const resource = options.resources.get(request.resourceId);
701
- if (!resource || !resource.exposedVia.includes(request.protocol)) {
651
+ if (!resource?.exposedVia.includes(request.protocol)) {
702
652
  throw new CommerceError(
703
653
  "RESOURCE_NOT_FOUND",
704
654
  `Resource "${request.resourceId}" was not found`,
@@ -822,7 +772,8 @@ function createExecutionPipeline(options) {
822
772
  const mapped = isCommerceError(error) ? error : new CommerceError("PAYMENT_INVALID", "Payment verification failed", {
823
773
  requestId: request.requestId,
824
774
  resourceId: resource.id,
825
- cause: error
775
+ cause: error,
776
+ ...retryChallenge(requirement)
826
777
  });
827
778
  await safeEmit(
828
779
  buildEvent({
@@ -854,7 +805,8 @@ function createExecutionPipeline(options) {
854
805
  verification.rejectionReason ?? "Payment was rejected",
855
806
  {
856
807
  requestId: request.requestId,
857
- resourceId: resource.id
808
+ resourceId: resource.id,
809
+ ...retryChallenge(requirement)
858
810
  }
859
811
  );
860
812
  }
@@ -1077,7 +1029,8 @@ function createExecutionPipeline(options) {
1077
1029
  amount: paymentResult.amount,
1078
1030
  currency: paymentResult.currency,
1079
1031
  ...paymentResult.network !== void 0 ? { network: paymentResult.network } : {},
1080
- ...paymentResult.externalReference !== void 0 ? { externalReference: paymentResult.externalReference } : {}
1032
+ ...paymentResult.externalReference !== void 0 ? { externalReference: paymentResult.externalReference } : {},
1033
+ ...typeof paymentResult.metadata?.["receipt"] === "string" ? { receipt: paymentResult.metadata["receipt"] } : {}
1081
1034
  }
1082
1035
  }
1083
1036
  }
@@ -1155,6 +1108,10 @@ function resolveAuthorizationProviders(providers, resource, requestId) {
1155
1108
  return found;
1156
1109
  });
1157
1110
  }
1111
+ function retryChallenge(requirement) {
1112
+ const envelope = requirement.challenge.envelope;
1113
+ return envelope === void 0 ? {} : { details: { challenge: envelope } };
1114
+ }
1158
1115
  function pickProvider(providers, methods) {
1159
1116
  for (const method of methods) {
1160
1117
  const found = providers.find((provider) => provider.name === method);
@@ -1162,22 +1119,16 @@ function pickProvider(providers, methods) {
1162
1119
  }
1163
1120
  return void 0;
1164
1121
  }
1165
- var RESERVED_INPUT_KEYS = /* @__PURE__ */ new Set([...RESERVED_INPUT_FIELDS, "__proto__"]);
1122
+ var RESERVED_INPUT_KEYS = [...RESERVED_INPUT_FIELDS, "__proto__"];
1166
1123
  function stripReservedFields(input) {
1167
- if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
1168
- const record = input;
1169
- const hasReserved = [...RESERVED_INPUT_KEYS].some((key) => Object.hasOwn(record, key));
1170
- if (!hasReserved) return input;
1124
+ if (!isRecord(input)) return input;
1125
+ if (!RESERVED_INPUT_KEYS.some((key) => Object.hasOwn(input, key))) return input;
1171
1126
  const rest = {};
1172
- for (const key of Object.keys(record)) {
1173
- if (!RESERVED_INPUT_KEYS.has(key)) rest[key] = record[key];
1127
+ for (const key of Object.keys(input)) {
1128
+ if (!RESERVED_INPUT_KEYS.includes(key)) rest[key] = input[key];
1174
1129
  }
1175
1130
  return rest;
1176
1131
  }
1177
- function describeError2(error) {
1178
- if (error instanceof Error) return { message: error.message, name: error.name };
1179
- return { message: String(error) };
1180
- }
1181
1132
 
1182
1133
  // src/core/execution/registry.ts
1183
1134
  var ResourceRegistryImpl = class {
@@ -1211,6 +1162,22 @@ function createResourceRegistry(resources) {
1211
1162
  return new ResourceRegistryImpl(resources);
1212
1163
  }
1213
1164
 
1165
+ // src/core/execution/store-event-sink.ts
1166
+ function createStoreEventSink(options) {
1167
+ return {
1168
+ async emit(event) {
1169
+ try {
1170
+ await options.store.appendEvent(event);
1171
+ } catch (error) {
1172
+ options.logger.error(
1173
+ { err: describeError(error), eventType: event.type, requestId: event.requestId },
1174
+ "Failed to persist commerce event"
1175
+ );
1176
+ }
1177
+ }
1178
+ };
1179
+ }
1180
+
1214
1181
  // src/protocols/acp/constants.ts
1215
1182
  var ACP_SPEC_VERSION = "2026-04-17";
1216
1183
  var ACP_API_VERSION = "2026-04-17";
@@ -1229,6 +1196,9 @@ var ACP_OPERATION_INPUT_KEYS = {
1229
1196
  completeCheckoutSession: ["path", "body"],
1230
1197
  cancelCheckoutSession: ["path"]
1231
1198
  };
1199
+ var ACP_OPERATION_OPTIONAL_INPUT_KEYS = {
1200
+ cancelCheckoutSession: ["body"]
1201
+ };
1232
1202
  var ACP_JSON_MEDIA_TYPE = "application/json";
1233
1203
  var ACP_API_VERSION_HEADER = "api-version";
1234
1204
  var ACP_REQUEST_ID_HEADER = "request-id";
@@ -1241,7 +1211,6 @@ var ACP_MAX_REQUEST_ID_LENGTH = 128;
1241
1211
  var SUPPORTED_CONFIG_VERSION = 1;
1242
1212
  var ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
1243
1213
  var ZERO_ADDRESS_PATTERN = /^0x0{40}$/i;
1244
- var MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
1245
1214
  var NumberOrString = z.union([z.number(), z.string()]);
1246
1215
  var BooleanOrString = z.union([z.boolean(), z.string()]);
1247
1216
  var MerchantSchema = z.object({
@@ -1252,14 +1221,14 @@ var MerchantSchema = z.object({
1252
1221
  var ServerSchema = z.object({
1253
1222
  port: NumberOrString,
1254
1223
  host: z.string().min(1),
1255
- /** Shared secret gating /api/receipts, /api/events, /api/events/stream. No token -> those routes 404. */
1224
+ // Shared secret gating /api/receipts and /api/events. Without it both
1225
+ // routes 404.
1256
1226
  adminToken: z.string().min(1).optional(),
1257
- /** Browser origins allowed to read the dashboard-facing routes. Empty by default: closed. */
1258
- // Entries are matched literally against the browser's
1259
- // `Origin` header, so `"*"` and a trailing slash match nothing at all -
1260
- // fail-closed (a lockout, not a bypass), but silently, and a lockout with
1261
- // no explanation is the kind of thing an operator "fixes" by disabling the
1262
- // check. Reject those two shapes with a pointer instead.
1227
+ // Browser origins allowed to call the gateway; none by default. A request
1228
+ // with any other `Origin` gets 403 on every route. Entries match the
1229
+ // `Origin` header exactly (ignoring case), so `"*"` and a trailing slash
1230
+ // would match nothing: a silent lockout that an operator might "fix" by
1231
+ // disabling the check. Both are refused with a pointer.
1263
1232
  allowedOrigins: z.array(
1264
1233
  z.string().min(1).refine((origin) => origin !== "*", {
1265
1234
  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.`
@@ -1278,16 +1247,14 @@ var RESERVED_GATEWAY_PATHS = [
1278
1247
  "/health",
1279
1248
  "/ready",
1280
1249
  "/.well-known/agent-commerce",
1281
- // Fixed by the A2A specification, so it is the adapter's to serve and never
1282
- // a configurable mount's to claim.
1250
+ // Fixed by the A2A specification and served by the adapter
1283
1251
  "/.well-known/agent-card.json",
1284
- // Likewise fixed by the ACP specification.
1252
+ // Fixed by the ACP specification
1285
1253
  ACP_WELL_KNOWN_PATH,
1286
1254
  "/api/resources",
1287
1255
  "/api/resources/:id/invoke",
1288
1256
  "/api/receipts",
1289
- "/api/events",
1290
- "/api/events/stream"
1257
+ "/api/events"
1291
1258
  ];
1292
1259
  var MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
1293
1260
  message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
@@ -1319,16 +1286,14 @@ var ProtocolsSchema = z.object({
1319
1286
  enabled: BooleanOrString,
1320
1287
  mountPath: MountPathSchema
1321
1288
  }).strict(),
1322
- // Optional block: A2A is off unless an operator asks for it, and an
1323
- // existing config predating the adapter stays valid.
1289
+ // Optional; A2A is off unless configured
1324
1290
  a2a: z.object({
1325
1291
  enabled: BooleanOrString,
1326
1292
  mountPath: MountPathSchema.optional()
1327
1293
  }).strict().optional(),
1328
- // Likewise optional and off by default. The sub-blocks are optional here
1329
- // and required by the business pass only when ACP is enabled, so a
1330
- // disabled placeholder block stays writable and an enabled one gets a
1331
- // message naming the piece it is missing.
1294
+ // Optional and off by default. The business pass requires the sub-blocks
1295
+ // only when ACP is enabled, so a disabled placeholder stays valid and an
1296
+ // enabled block gets a message naming what it lacks.
1332
1297
  acp: z.object({
1333
1298
  enabled: BooleanOrString,
1334
1299
  mountPath: MountPathSchema.optional(),
@@ -1384,7 +1349,7 @@ var FacilitatorAuthSchema = z.discriminatedUnion("type", [
1384
1349
  z.object({ type: z.literal("none") }).strict(),
1385
1350
  z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
1386
1351
  // Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
1387
- // which signs a fresh JWT per request; a static header cannot express it.
1352
+ // which signs a fresh JWT per request.
1388
1353
  z.object({
1389
1354
  type: z.literal("cdp"),
1390
1355
  apiKeyId: z.string().min(1),
@@ -1399,10 +1364,8 @@ var FacilitatorSchema = z.discriminatedUnion("mode", [
1399
1364
  z.object({
1400
1365
  mode: z.literal("remote"),
1401
1366
  url: z.string().min(1),
1402
- // Absent means "this facilitator takes no credential" - an explicit
1403
- // statement, normalised to `{ type: 'none' }` below. On a mainnet that
1404
- // combination is refused outright, so the default can never quietly
1405
- // become an unauthenticated production facilitator.
1367
+ // Absent auth sends no credential and normalizes to `{ type: 'none' }`,
1368
+ // which on mainnet requires `allowUnauthenticatedFacilitator`
1406
1369
  auth: FacilitatorAuthSchema.optional()
1407
1370
  }).strict()
1408
1371
  ]);
@@ -1417,13 +1380,31 @@ var X402Schema = z.object({
1417
1380
  payTo: z.string().min(1),
1418
1381
  maxTimeoutSeconds: NumberOrString,
1419
1382
  facilitator: FacilitatorSchema,
1420
- /** Real funds. Never defaulted - see src/payments/x402/guardrails.ts. */
1383
+ // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
1421
1384
  allowMainnet: BooleanOrString.optional(),
1422
- /** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
1385
+ // Accepts credential-free facilitator access on mainnet. Never defaulted
1386
+ allowUnauthenticatedFacilitator: BooleanOrString.optional()
1387
+ }).strict();
1388
+ var MppSchema = z.object({
1389
+ enabled: BooleanOrString,
1390
+ network: z.string().min(1).optional(),
1391
+ rpcUrl: z.string().min(1),
1392
+ asset: z.string().min(1),
1393
+ assetName: z.string().min(1),
1394
+ assetVersion: z.string().min(1),
1395
+ recipient: z.string().min(1),
1396
+ realm: z.string().min(1),
1397
+ challengeSecret: z.string().min(1),
1398
+ challengeTtlSeconds: NumberOrString.optional(),
1399
+ facilitator: FacilitatorSchema,
1400
+ // Real funds. Never defaulted; see src/payments/x402/guardrails.ts
1401
+ allowMainnet: BooleanOrString.optional(),
1402
+ // Accepts credential-free facilitator access on mainnet. Never defaulted
1423
1403
  allowUnauthenticatedFacilitator: BooleanOrString.optional()
1424
1404
  }).strict();
1425
1405
  var PaymentsSchema = z.object({
1426
- x402: X402Schema.optional()
1406
+ x402: X402Schema.optional(),
1407
+ mpp: MppSchema.optional()
1427
1408
  }).strict();
1428
1409
  var Ap2KeySchema = z.object({
1429
1410
  kid: z.string().min(1),
@@ -1431,11 +1412,8 @@ var Ap2KeySchema = z.object({
1431
1412
  }).strict();
1432
1413
  var Ap2IssuerSchema = z.object({
1433
1414
  issuer: z.string().min(1),
1434
- /**
1435
- * Required, not defaulted. Without it, a mandate minted for another
1436
- * merchant would verify here, and there is no value worth guessing for
1437
- * something that decides that.
1438
- */
1415
+ // Required, not defaulted: without it a mandate minted for another
1416
+ // merchant would verify here
1439
1417
  audience: z.string().min(1),
1440
1418
  keys: z.array(Ap2KeySchema).min(1)
1441
1419
  }).strict();
@@ -1462,7 +1440,7 @@ var RawConfigSchema = z.object({
1462
1440
  authorization: AuthorizationSchema.optional()
1463
1441
  }).strict();
1464
1442
  function parseConfig(raw, env) {
1465
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1443
+ if (!isRecord(raw)) {
1466
1444
  throw new CommerceError(
1467
1445
  "CONFIG_INVALID",
1468
1446
  'Configuration root must be a mapping (object) at "$"',
@@ -1474,7 +1452,7 @@ function parseConfig(raw, env) {
1474
1452
  checkVersion(raw);
1475
1453
  const substituted = substituteEnv(raw, env);
1476
1454
  const parsed = parseWithZod(RawConfigSchema, substituted);
1477
- return normalise(parsed);
1455
+ return normalize(parsed);
1478
1456
  }
1479
1457
  function checkVersion(raw) {
1480
1458
  if (!("version" in raw)) {
@@ -1482,12 +1460,12 @@ function checkVersion(raw) {
1482
1460
  details: { path: "version" }
1483
1461
  });
1484
1462
  }
1485
- const rawVersion = raw["version"];
1486
- const version = typeof rawVersion === "string" ? Number(rawVersion) : rawVersion;
1463
+ const version = raw["version"];
1487
1464
  if (version !== SUPPORTED_CONFIG_VERSION) {
1465
+ const hint = typeof version === "string" ? ", written as a number without quotes" : "";
1488
1466
  throw new CommerceError(
1489
1467
  "CONFIG_INVALID",
1490
- `Unsupported config version "${String(rawVersion)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}`,
1468
+ `Unsupported config version "${String(version)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}${hint}`,
1491
1469
  { details: { path: "version", supported: SUPPORTED_CONFIG_VERSION } }
1492
1470
  );
1493
1471
  }
@@ -1559,9 +1537,9 @@ function toBoolean(value, path2) {
1559
1537
  );
1560
1538
  }
1561
1539
  var SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
1562
- var SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
1540
+ var SUPPORTED_PAYMENT_METHODS = new Set(PAYMENT_METHOD_NAMES);
1563
1541
  var SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
1564
- function normalise(raw) {
1542
+ function normalize(raw) {
1565
1543
  const protocols = {
1566
1544
  http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
1567
1545
  mcp: {
@@ -1572,16 +1550,11 @@ function normalise(raw) {
1572
1550
  enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
1573
1551
  mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
1574
1552
  },
1575
- acp: normaliseAcp(raw.protocols.acp)
1553
+ acp: normalizeAcp(raw.protocols.acp)
1576
1554
  };
1577
1555
  validateMountPaths(protocols);
1578
1556
  const x402Raw = raw.payments.x402;
1579
- const facilitator = x402Raw === void 0 ? void 0 : x402Raw.facilitator.mode === "local" ? { mode: "local", signerPrivateKey: x402Raw.facilitator.signerPrivateKey } : {
1580
- mode: "remote",
1581
- url: x402Raw.facilitator.url,
1582
- auth: x402Raw.facilitator.auth ?? { type: "none" }
1583
- };
1584
- const x402 = x402Raw && facilitator ? {
1557
+ const x402 = x402Raw !== void 0 ? {
1585
1558
  enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
1586
1559
  network: x402Raw.network,
1587
1560
  rpcUrl: x402Raw.rpcUrl,
@@ -1600,7 +1573,7 @@ function normalise(raw) {
1600
1573
  min: 1
1601
1574
  }
1602
1575
  ),
1603
- facilitator,
1576
+ facilitator: normalizeFacilitator(x402Raw.facilitator),
1604
1577
  ...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
1605
1578
  ...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
1606
1579
  allowUnauthenticatedFacilitator: toBoolean(
@@ -1623,12 +1596,13 @@ function normalise(raw) {
1623
1596
  ...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
1624
1597
  });
1625
1598
  }
1626
- const ap2 = normaliseAp2(raw.authorization?.ap2);
1599
+ const mpp = normalizeMpp(raw.payments.mpp);
1600
+ const ap2 = normalizeAp2(raw.authorization?.ap2);
1627
1601
  if (ap2?.enabled) {
1628
1602
  validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
1629
1603
  }
1630
1604
  const resources = Object.entries(raw.resources).map(
1631
- ([id, entry]) => normaliseResource(id, entry, protocols, x402, ap2)
1605
+ ([id, entry]) => normalizeResource(id, entry, protocols, x402, mpp, ap2)
1632
1606
  );
1633
1607
  if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
1634
1608
  return {
@@ -1638,8 +1612,7 @@ function normalise(raw) {
1638
1612
  name: raw.merchant.name,
1639
1613
  publicBaseUrl: raw.merchant.publicBaseUrl
1640
1614
  },
1641
- // min 0, not 1: port 0 is the standard "let the OS pick a free port"
1642
- // convention, useful for tests and ephemeral dev/demo instances.
1615
+ // Port 0 lets the OS pick a free port, for tests and throwaway instances
1643
1616
  server: {
1644
1617
  port: toNumber(raw.server.port, "server.port", { min: 0, max: 65535 }),
1645
1618
  host: raw.server.host,
@@ -1650,7 +1623,8 @@ function normalise(raw) {
1650
1623
  protocols,
1651
1624
  resources,
1652
1625
  payments: {
1653
- ...x402 !== void 0 ? { x402 } : {}
1626
+ ...x402 !== void 0 ? { x402 } : {},
1627
+ ...mpp !== void 0 ? { mpp } : {}
1654
1628
  },
1655
1629
  ...ap2 !== void 0 ? { authorization: { ap2 } } : {}
1656
1630
  };
@@ -1678,7 +1652,7 @@ function validateMountPaths(protocols) {
1678
1652
  function ap2Invalid(path2, message, extra = {}) {
1679
1653
  return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
1680
1654
  }
1681
- function normaliseAp2(raw) {
1655
+ function normalizeAp2(raw) {
1682
1656
  if (raw === void 0) return void 0;
1683
1657
  if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
1684
1658
  const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
@@ -1698,14 +1672,14 @@ function normaliseAp2(raw) {
1698
1672
  if (raw.replay === void 0) {
1699
1673
  throw ap2Invalid(
1700
1674
  "authorization.ap2.replay",
1701
- "authorization.ap2.replay is required when AP2 is enabled - it names the SQLite file recording which mandates have been spent, and without it a verified mandate could authorise a second settlement"
1675
+ "authorization.ap2.replay is required when AP2 is enabled - it names the SQLite file recording which mandates have been spent, and without it a verified mandate could authorize a second settlement"
1702
1676
  );
1703
1677
  }
1704
- const mandateIssuers = normaliseIssuers(
1678
+ const mandateIssuers = normalizeIssuers(
1705
1679
  raw.trust?.mandateIssuers,
1706
1680
  "authorization.ap2.trust.mandateIssuers"
1707
1681
  );
1708
- const checkoutIssuers = normaliseIssuers(
1682
+ const checkoutIssuers = normalizeIssuers(
1709
1683
  raw.trust?.checkoutIssuers,
1710
1684
  "authorization.ap2.trust.checkoutIssuers"
1711
1685
  );
@@ -1722,7 +1696,7 @@ function normaliseAp2(raw) {
1722
1696
  replay: { path: raw.replay.path }
1723
1697
  };
1724
1698
  }
1725
- function normaliseIssuers(raw, path2) {
1699
+ function normalizeIssuers(raw, path2) {
1726
1700
  if (raw === void 0 || raw.length === 0) {
1727
1701
  throw ap2Invalid(
1728
1702
  path2,
@@ -1808,11 +1782,11 @@ function validateJwk(jwk, kid, path2) {
1808
1782
  if (jwk["kid"] !== void 0 && value("kid") !== kid) {
1809
1783
  fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
1810
1784
  }
1811
- const normalised = {};
1785
+ const normalized = {};
1812
1786
  for (const member of AP2_JWK_MEMBERS) {
1813
- if (jwk[member] !== void 0) normalised[member] = value(member);
1787
+ if (jwk[member] !== void 0) normalized[member] = value(member);
1814
1788
  }
1815
- return normalised;
1789
+ return normalized;
1816
1790
  }
1817
1791
  function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
1818
1792
  if (replayPath === ":memory:") return;
@@ -1826,7 +1800,7 @@ function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
1826
1800
  );
1827
1801
  }
1828
1802
  }
1829
- function normaliseResourceAuthorization(id, entry, pricing, ap2) {
1803
+ function normalizeResourceAuthorization(id, entry, pricing, ap2) {
1830
1804
  const required = entry.authorization?.required;
1831
1805
  if (required === void 0) return void 0;
1832
1806
  const path2 = `resources.${id}.authorization.required`;
@@ -1866,7 +1840,7 @@ function normaliseResourceAuthorization(id, entry, pricing, ap2) {
1866
1840
  function acpInvalid(path2, message, extra = {}) {
1867
1841
  return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
1868
1842
  }
1869
- function normaliseAcp(raw) {
1843
+ function normalizeAcp(raw) {
1870
1844
  const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
1871
1845
  if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
1872
1846
  return { enabled: false, mountPath };
@@ -1896,9 +1870,8 @@ function normaliseAcp(raw) {
1896
1870
  { min: ACP_RETENTION_HOURS }
1897
1871
  )
1898
1872
  },
1899
- checkout: { operations: normaliseAcpOperations(raw.checkout) },
1900
- // Built key by key rather than passed through: absent metadata must stay
1901
- // absent from the discovery document, not appear there as `undefined`.
1873
+ checkout: { operations: normalizeAcpOperations(raw.checkout) },
1874
+ // Built key by key so absent metadata stays absent rather than `undefined`
1902
1875
  ...discovery !== void 0 ? {
1903
1876
  discovery: {
1904
1877
  ...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
@@ -1909,7 +1882,7 @@ function normaliseAcp(raw) {
1909
1882
  } : {}
1910
1883
  };
1911
1884
  }
1912
- function normaliseAcpOperations(checkout) {
1885
+ function normalizeAcpOperations(checkout) {
1913
1886
  const configured = checkout?.operations ?? {};
1914
1887
  for (const key of Object.keys(configured)) {
1915
1888
  if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
@@ -1977,13 +1950,15 @@ function validateAcpOperationInput(path2, operation, resource) {
1977
1950
  const schema = resource.inputSchema;
1978
1951
  if (schema === void 0) return;
1979
1952
  const keys = ACP_OPERATION_INPUT_KEYS[operation];
1953
+ const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
1980
1954
  const properties = schema["properties"];
1981
- if (schema["additionalProperties"] === false && isPlainObject4(properties)) {
1982
- for (const key of keys) {
1955
+ if (schema["additionalProperties"] === false && isRecord(properties)) {
1956
+ for (const key of [...keys, ...optionalKeys]) {
1983
1957
  if (!Object.hasOwn(properties, key)) {
1958
+ const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
1984
1959
  throw acpInvalid(
1985
1960
  path2,
1986
- `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter always sends for this operation`,
1961
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter ${when} for this operation`,
1987
1962
  { resourceId: resource.id }
1988
1963
  );
1989
1964
  }
@@ -1995,14 +1970,81 @@ function validateAcpOperationInput(path2, operation, resource) {
1995
1970
  if (typeof entry === "string" && !keys.includes(entry)) {
1996
1971
  throw acpInvalid(
1997
1972
  path2,
1998
- `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema requires "${entry}", which that operation never supplies (it sends: ${keys.join(", ")})`,
1973
+ `Resource "${resource.id}" implements ACP operation "${operation}" but its input schema requires "${entry}", which that operation does not always supply (it always sends: ${keys.join(", ")})`,
1999
1974
  { resourceId: resource.id }
2000
1975
  );
2001
1976
  }
2002
1977
  }
2003
1978
  }
2004
1979
  }
2005
- function normaliseResource(id, entry, protocols, x402, ap2) {
1980
+ function normalizeFacilitator(raw) {
1981
+ return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
1982
+ }
1983
+ function normalizeMpp(raw) {
1984
+ if (raw === void 0) return void 0;
1985
+ const network = raw.network ?? MPP_DEFAULT_NETWORK;
1986
+ if (!isMppNetwork(network)) {
1987
+ throw new CommerceError(
1988
+ "CONFIG_INVALID",
1989
+ `payments.mpp.network must be one of ${MPP_NETWORKS.join(", ")}`,
1990
+ { details: { path: "payments.mpp.network" } }
1991
+ );
1992
+ }
1993
+ const mpp = {
1994
+ enabled: toBoolean(raw.enabled, "payments.mpp.enabled"),
1995
+ network,
1996
+ rpcUrl: raw.rpcUrl,
1997
+ asset: raw.asset,
1998
+ assetName: raw.assetName,
1999
+ assetVersion: raw.assetVersion,
2000
+ recipient: raw.recipient,
2001
+ realm: raw.realm,
2002
+ challengeSecret: raw.challengeSecret,
2003
+ ...raw.challengeTtlSeconds !== void 0 ? {
2004
+ challengeTtlSeconds: toNumber(
2005
+ raw.challengeTtlSeconds,
2006
+ "payments.mpp.challengeTtlSeconds",
2007
+ { min: 1 }
2008
+ )
2009
+ } : {},
2010
+ facilitator: normalizeFacilitator(raw.facilitator),
2011
+ ...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
2012
+ ...raw.allowUnauthenticatedFacilitator !== void 0 ? {
2013
+ allowUnauthenticatedFacilitator: toBoolean(
2014
+ raw.allowUnauthenticatedFacilitator,
2015
+ "payments.mpp.allowUnauthenticatedFacilitator"
2016
+ )
2017
+ } : {}
2018
+ };
2019
+ validateAddress("payments.mpp.recipient", mpp.recipient);
2020
+ validateAddress("payments.mpp.asset", mpp.asset);
2021
+ if (/[\r\n]/.test(mpp.realm)) {
2022
+ throw new CommerceError("CONFIG_INVALID", "payments.mpp.realm must be a single line", {
2023
+ details: { path: "payments.mpp.realm" }
2024
+ });
2025
+ }
2026
+ if (mpp.challengeSecret.length < MPP_MIN_CHALLENGE_SECRET_LENGTH) {
2027
+ throw new CommerceError(
2028
+ "CONFIG_INVALID",
2029
+ `payments.mpp.challengeSecret must have length at least ${MPP_MIN_CHALLENGE_SECRET_LENGTH}`,
2030
+ { details: { path: "payments.mpp.challengeSecret" } }
2031
+ );
2032
+ }
2033
+ resolveX402Deployment({
2034
+ network: mpp.network,
2035
+ payTo: mpp.recipient,
2036
+ asset: mpp.asset,
2037
+ assetName: mpp.assetName,
2038
+ assetVersion: mpp.assetVersion,
2039
+ facilitator: mpp.facilitator,
2040
+ ...mpp.allowMainnet !== void 0 ? { allowMainnet: mpp.allowMainnet } : {},
2041
+ ...mpp.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: mpp.allowUnauthenticatedFacilitator } : {},
2042
+ configPath: "payments.mpp",
2043
+ payToField: "recipient"
2044
+ });
2045
+ return mpp;
2046
+ }
2047
+ function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
2006
2048
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
2007
2049
  const inputProperties = entry.input?.["properties"];
2008
2050
  if (inputProperties && typeof inputProperties === "object") {
@@ -2068,6 +2110,7 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
2068
2110
  );
2069
2111
  }
2070
2112
  validateBackendUrl(id, entry.backend.url);
2113
+ validateBackendHeaders(id, entry.backend.headers);
2071
2114
  const pathScope = validateInputBindings(id, entry.backend, entry.input);
2072
2115
  validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
2073
2116
  if (entry.pricing.type === "dynamic") {
@@ -2078,7 +2121,7 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
2078
2121
  );
2079
2122
  }
2080
2123
  if (entry.pricing.type === "fixed") {
2081
- validatePricingAmount(id, entry.pricing.amount, x402);
2124
+ validatePricingAmount(id, entry.pricing.amount, x402?.assetDecimals);
2082
2125
  }
2083
2126
  const paymentMethods = entry.payments ?? [];
2084
2127
  if (entry.pricing.type === "fixed") {
@@ -2093,21 +2136,32 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
2093
2136
  if (!SUPPORTED_PAYMENT_METHODS.has(method)) {
2094
2137
  throw new CommerceError(
2095
2138
  "CONFIG_INVALID",
2096
- `Resource "${id}" names unsupported payment method "${method}". Supported: x402.`,
2139
+ `Resource "${id}" names unsupported payment method "${method}". Supported: ${PAYMENT_METHOD_NAMES.join(", ")}.`,
2097
2140
  { details: { path: `resources.${id}.payments`, resourceId: id, method } }
2098
2141
  );
2099
2142
  }
2100
- if (method === "x402" && (!x402 || !x402.enabled)) {
2143
+ }
2144
+ const enabledRails = [...x402?.enabled ? ["x402"] : [], ...mpp?.enabled ? ["mpp"] : []];
2145
+ if (!paymentMethods.some((method) => enabledRails.includes(method))) {
2146
+ throw new CommerceError(
2147
+ "CONFIG_INVALID",
2148
+ `Resource "${id}" names payment method(s) "${paymentMethods.join(", ")}", none of which is configured and enabled under "payments"`,
2149
+ { details: { path: `resources.${id}.payments`, resourceId: id, paymentMethods } }
2150
+ );
2151
+ }
2152
+ if (mpp?.enabled && paymentMethods.includes("mpp")) {
2153
+ if (entry.pricing.currency !== MPP_PROFILE.assetSymbol) {
2101
2154
  throw new CommerceError(
2102
2155
  "CONFIG_INVALID",
2103
- `Resource "${id}" names payment method "x402" which is not configured or not enabled under payments.x402`,
2104
- { details: { path: `resources.${id}.payments`, resourceId: id, method } }
2156
+ `Resource "${id}" is priced in ${entry.pricing.currency}, but MPP charges ${MPP_PROFILE.assetSymbol}`,
2157
+ { details: { path: `resources.${id}.pricing.currency`, resourceId: id } }
2105
2158
  );
2106
2159
  }
2160
+ validatePricingAmount(id, entry.pricing.amount, MPP_PROFILE.assetDecimals);
2107
2161
  }
2108
2162
  }
2109
2163
  const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
2110
- const authorization = normaliseResourceAuthorization(id, entry, pricing, ap2);
2164
+ const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
2111
2165
  return {
2112
2166
  id,
2113
2167
  name: entry.name,
@@ -2137,11 +2191,11 @@ function defaultClosedObjectSchema(schema) {
2137
2191
  const result = { ...schema };
2138
2192
  if (isObjectSchema) {
2139
2193
  const properties = schema["properties"];
2140
- if (isPlainObject4(properties)) {
2194
+ if (isRecord(properties)) {
2141
2195
  result["properties"] = Object.fromEntries(
2142
2196
  Object.entries(properties).map(([key, sub]) => [
2143
2197
  key,
2144
- isPlainObject4(sub) ? defaultClosedObjectSchema(sub) : sub
2198
+ isRecord(sub) ? defaultClosedObjectSchema(sub) : sub
2145
2199
  ])
2146
2200
  );
2147
2201
  }
@@ -2150,11 +2204,11 @@ function defaultClosedObjectSchema(schema) {
2150
2204
  }
2151
2205
  }
2152
2206
  const items = schema["items"];
2153
- if (isPlainObject4(items)) {
2207
+ if (isRecord(items)) {
2154
2208
  result["items"] = defaultClosedObjectSchema(items);
2155
2209
  }
2156
2210
  const additional = schema["additionalProperties"];
2157
- if (isPlainObject4(additional)) {
2211
+ if (isRecord(additional)) {
2158
2212
  result["additionalProperties"] = defaultClosedObjectSchema(additional);
2159
2213
  }
2160
2214
  return result;
@@ -2185,9 +2239,6 @@ var UNSUPPORTED_SCHEMA_KEYWORDS = [
2185
2239
  "maxItems",
2186
2240
  "uniqueItems"
2187
2241
  ];
2188
- function isPlainObject4(value) {
2189
- return typeof value === "object" && value !== null && !Array.isArray(value);
2190
- }
2191
2242
  function excludesObjectType(schema) {
2192
2243
  const type = schema["type"];
2193
2244
  if (type === void 0 || type === "object") return false;
@@ -2212,7 +2263,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
2212
2263
  const requiredRaw = schema["required"];
2213
2264
  const closed = !Object.hasOwn(schema, "additionalProperties") ? true : schema["additionalProperties"] === false;
2214
2265
  if (Array.isArray(requiredRaw) && closed && isObjectSchemaNode(schema)) {
2215
- const declared = isPlainObject4(schema["properties"]) ? schema["properties"] : {};
2266
+ const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
2216
2267
  const undeclared = requiredRaw.filter(
2217
2268
  (name) => typeof name === "string" && !Object.hasOwn(declared, name)
2218
2269
  );
@@ -2225,17 +2276,17 @@ function validateResourceSchemaKeywords(id, path2, schema) {
2225
2276
  }
2226
2277
  }
2227
2278
  const properties = schema["properties"];
2228
- if (isPlainObject4(properties)) {
2279
+ if (isRecord(properties)) {
2229
2280
  for (const [key, sub] of Object.entries(properties)) {
2230
- if (isPlainObject4(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
2281
+ if (isRecord(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
2231
2282
  }
2232
2283
  }
2233
2284
  const additional = schema["additionalProperties"];
2234
- if (isPlainObject4(additional)) {
2285
+ if (isRecord(additional)) {
2235
2286
  validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
2236
2287
  }
2237
2288
  const items = schema["items"];
2238
- if (isPlainObject4(items)) {
2289
+ if (isRecord(items)) {
2239
2290
  validateResourceSchemaKeywords(id, `${path2}.items`, items);
2240
2291
  } else if (Array.isArray(items)) {
2241
2292
  console.warn(
@@ -2245,7 +2296,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
2245
2296
  }
2246
2297
  var PRICING_AMOUNT_PATTERN = /^\d+(?:\.\d+)?$/;
2247
2298
  var ZERO_AMOUNT_PATTERN = /^0(?:\.0+)?$/;
2248
- function validatePricingAmount(id, amount, x402) {
2299
+ function validatePricingAmount(id, amount, decimals) {
2249
2300
  const path2 = `resources.${id}.pricing.amount`;
2250
2301
  if (!PRICING_AMOUNT_PATTERN.test(amount)) {
2251
2302
  throw new CommerceError(
@@ -2261,12 +2312,12 @@ function validatePricingAmount(id, amount, x402) {
2261
2312
  { details: { path: path2, resourceId: id } }
2262
2313
  );
2263
2314
  }
2264
- if (x402 !== void 0) {
2315
+ if (decimals !== void 0) {
2265
2316
  const fractionalDigits = amount.includes(".") ? amount.split(".")[1]?.length ?? 0 : 0;
2266
- if (fractionalDigits > x402.assetDecimals) {
2317
+ if (fractionalDigits > decimals) {
2267
2318
  throw new CommerceError(
2268
2319
  "CONFIG_INVALID",
2269
- `Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${x402.assetDecimals} decimals)`,
2320
+ `Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${decimals} decimals)`,
2270
2321
  { details: { path: path2, resourceId: id } }
2271
2322
  );
2272
2323
  }
@@ -2302,7 +2353,7 @@ function validateInputBindings(id, backend, input) {
2302
2353
  );
2303
2354
  }
2304
2355
  const seen = /* @__PURE__ */ new Map();
2305
- const properties = isPlainObject4(input?.["properties"]) ? input["properties"] : {};
2356
+ const properties = isRecord(input?.["properties"]) ? input["properties"] : {};
2306
2357
  const required = new Set(
2307
2358
  Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
2308
2359
  );
@@ -2325,7 +2376,7 @@ function validateInputBindings(id, backend, input) {
2325
2376
  );
2326
2377
  }
2327
2378
  const declared = properties[property];
2328
- if (location !== "body" && isPlainObject4(declared) && !isObjectSchemaNode(declared)) {
2379
+ if (location !== "body" && isRecord(declared) && !isObjectSchemaNode(declared)) {
2329
2380
  fail(
2330
2381
  `binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
2331
2382
  { location, property }
@@ -2341,7 +2392,7 @@ function validateInputBindings(id, backend, input) {
2341
2392
  if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
2342
2393
  const group = properties[bindings.path];
2343
2394
  return {
2344
- schema: isPlainObject4(group) ? group : void 0,
2395
+ schema: isRecord(group) ? group : void 0,
2345
2396
  where: `input.properties.${bindings.path}`
2346
2397
  };
2347
2398
  }
@@ -2353,7 +2404,7 @@ function validatePathParametersDeclared(id, url, input, where) {
2353
2404
  if (stray !== void 0) {
2354
2405
  throw new CommerceError(
2355
2406
  "CONFIG_INVALID",
2356
- `Resource "${id}" has backend.url containing "${stray}", which is not a valid path parameter (allowed characters: A-Z a-z 0-9 _ . -). It would be sent to the backend literally, so a paid resource would settle payment and then never reach the backend`,
2407
+ `Resource "${id}" has backend.url containing "${stray}", which is not a valid path parameter (allowed characters: A-Z a-z 0-9 _ . -). It would reach the backend as literal text, so a paid resource would take payment for a request the backend cannot serve`,
2357
2408
  { details: { path: `resources.${id}.backend.url`, resourceId: id, token: stray } }
2358
2409
  );
2359
2410
  }
@@ -2371,13 +2422,13 @@ function validatePathParametersDeclared(id, url, input, where) {
2371
2422
  if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
2372
2423
  throw new CommerceError(
2373
2424
  "CONFIG_INVALID",
2374
- `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`,
2425
+ `Resource "${id}" has a backend.url parameter before the end of the host. 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`,
2375
2426
  { details: { path: `resources.${id}.backend.url`, resourceId: id } }
2376
2427
  );
2377
2428
  }
2378
2429
  }
2379
2430
  const propertiesRaw = input?.["properties"];
2380
- const properties = isPlainObject4(propertiesRaw) ? propertiesRaw : {};
2431
+ const properties = isRecord(propertiesRaw) ? propertiesRaw : {};
2381
2432
  const requiredRaw = input?.["required"];
2382
2433
  const required = new Set(
2383
2434
  Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : []
@@ -2387,14 +2438,14 @@ function validatePathParametersDeclared(id, url, input, where) {
2387
2438
  if (!Object.hasOwn(properties, param)) {
2388
2439
  throw new CommerceError(
2389
2440
  "CONFIG_INVALID",
2390
- `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`,
2441
+ `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 be refused`,
2391
2442
  { details: { path: path2, resourceId: id, param } }
2392
2443
  );
2393
2444
  }
2394
2445
  if (!required.has(param)) {
2395
2446
  throw new CommerceError(
2396
2447
  "CONFIG_INVALID",
2397
- `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`,
2448
+ `Resource "${id}" has backend.url path parameter "{${param}}" declared in ${where} but not listed in its "required" - a call that omits it would be refused`,
2398
2449
  { details: { path: path2, resourceId: id, param } }
2399
2450
  );
2400
2451
  }
@@ -2407,7 +2458,7 @@ function validateBackendUrl(id, url) {
2407
2458
  } catch {
2408
2459
  throw new CommerceError(
2409
2460
  "CONFIG_INVALID",
2410
- `Resource "${id}" has an invalid backend.url "${url}": must be an absolute http:// or https:// URL`,
2461
+ `Resource "${id}" has an invalid backend.url: must be an absolute http:// or https:// URL`,
2411
2462
  { details: { path: `resources.${id}.backend.url`, resourceId: id } }
2412
2463
  );
2413
2464
  }
@@ -2419,6 +2470,19 @@ function validateBackendUrl(id, url) {
2419
2470
  );
2420
2471
  }
2421
2472
  }
2473
+ function validateBackendHeaders(id, headers) {
2474
+ for (const [name, value] of Object.entries(headers ?? {})) {
2475
+ try {
2476
+ new Headers([[name, value]]);
2477
+ } catch {
2478
+ throw new CommerceError(
2479
+ "CONFIG_INVALID",
2480
+ `Resource "${id}" has backend header "${name}" with an illegal name or value (a name must be an HTTP token; a value must not contain NUL, a character above U+00FF, or a line break except at either end, which fetch trims)`,
2481
+ { details: { path: `resources.${id}.backend.headers.${name}`, resourceId: id } }
2482
+ );
2483
+ }
2484
+ }
2485
+ }
2422
2486
  function validateAddress(path2, value) {
2423
2487
  if (!ADDRESS_PATTERN.test(value)) {
2424
2488
  throw new CommerceError(
@@ -2463,9 +2527,8 @@ async function loadConfig(options = {}) {
2463
2527
  `Configuration file "${configPath}" is not valid YAML: ${reason}`,
2464
2528
  {
2465
2529
  details: { path: configPath, ...details },
2466
- // Retained for programmatic callers. Nothing in the CLI prints a
2467
- // `cause`, and nothing should start: this is the object whose
2468
- // `.message` carries the source excerpt (see below).
2530
+ // Kept for programmatic callers. Never print it: its `.message` quotes
2531
+ // the source line (see describeYamlError).
2469
2532
  cause: error
2470
2533
  }
2471
2534
  );
@@ -2503,94 +2566,47 @@ function resolveConfigPath(explicitPath, env, cwd) {
2503
2566
  if (fromEnv) return path.resolve(cwd, fromEnv);
2504
2567
  return path.resolve(cwd, DEFAULT_CONFIG_FILENAME);
2505
2568
  }
2506
- function createDefaultIdGenerator() {
2507
- return {
2508
- next(prefix) {
2509
- const id = randomUUID();
2510
- return prefix ? `${prefix}_${id}` : id;
2511
- }
2569
+ var BEARER_PREFIX = /^Bearer +/i;
2570
+ function createBearerCheck(expectedToken) {
2571
+ const expected = sha256(expectedToken);
2572
+ return (header2) => {
2573
+ if (header2 === void 0 || !BEARER_PREFIX.test(header2)) return false;
2574
+ const presented = header2.replace(BEARER_PREFIX, "");
2575
+ return presented.length > 0 && timingSafeEqual(sha256(presented), expected);
2512
2576
  };
2513
2577
  }
2514
- var PINO_PRETTY_PATH = (() => {
2578
+ function sha256(value) {
2579
+ return createHash("sha256").update(value, "utf8").digest();
2580
+ }
2581
+ async function readCappedBody(req, maxBytes) {
2582
+ const chunks = [];
2583
+ let total = 0;
2515
2584
  try {
2516
- return createRequire(import.meta.url).resolve("pino-pretty");
2585
+ for await (const chunk of req) {
2586
+ const buffer = chunk;
2587
+ total += buffer.length;
2588
+ if (total > maxBytes) return { kind: "too-large" };
2589
+ chunks.push(buffer);
2590
+ }
2517
2591
  } catch {
2518
- return void 0;
2592
+ return { kind: "unreadable" };
2519
2593
  }
2520
- })();
2521
- var SECRET_FIELD_NAMES = [
2522
- "privateKey",
2523
- "signerPrivateKey",
2524
- "signature",
2525
- "seed",
2526
- "mnemonic",
2527
- "secret",
2528
- "apiKey",
2529
- "adminToken",
2530
- "token"
2531
- ];
2532
- var SECRET_HEADERS = ["authorization", PAYMENT_HEADER, AUTHORIZATION_HEADER];
2533
- var REDACT_PATHS = [
2534
- ...SECRET_HEADERS.map((name) => `req.headers[${JSON.stringify(name)}]`),
2535
- ...SECRET_FIELD_NAMES,
2536
- ...SECRET_FIELD_NAMES.map((name) => `*.${name}`)
2537
- ];
2538
- function baseLoggerOptions(options) {
2539
- const nodeEnv = options.nodeEnv ?? process.env["NODE_ENV"] ?? "development";
2540
- const wantPretty = options.prettyPrint ?? (nodeEnv !== "production" && nodeEnv !== "test");
2541
- const usePretty = wantPretty && PINO_PRETTY_PATH !== void 0;
2542
- return {
2543
- level: options.level ?? process.env["LOG_LEVEL"] ?? (nodeEnv === "test" ? "silent" : "info"),
2544
- ...options.name !== void 0 ? { name: options.name } : {},
2545
- redact: { paths: [...REDACT_PATHS], censor: "[REDACTED]" },
2546
- ...usePretty ? { transport: { target: PINO_PRETTY_PATH } } : {}
2547
- };
2548
- }
2549
- function sanitizeUrl(url) {
2550
- if (url === void 0) return void 0;
2551
- const queryIndex = url.indexOf("?");
2552
- return queryIndex === -1 ? url : `${url.slice(0, queryIndex)}?[REDACTED]`;
2553
- }
2554
- function redactedReqSerializer(req) {
2555
- return {
2556
- method: req.method,
2557
- url: sanitizeUrl(req.url),
2558
- host: req.host,
2559
- remoteAddress: req.ip,
2560
- remotePort: req.socket?.remotePort
2561
- };
2562
- }
2563
- function fastifyLoggerOptions(options = {}) {
2564
- return {
2565
- ...baseLoggerOptions(options),
2566
- serializers: { req: redactedReqSerializer }
2567
- };
2568
- }
2569
- function buildNotFoundHandler() {
2570
- return (request, reply) => {
2571
- const safeUrl = sanitizeUrl(request.raw.url) ?? "";
2572
- const message = `Route ${request.method}:${safeUrl} not found`;
2573
- request.log.info(message);
2574
- reply.code(404).send({ message, error: "Not Found", statusCode: 404 });
2575
- };
2576
- }
2577
- function createGatewayLogger(options = {}) {
2578
- const instance = pino(baseLoggerOptions(options));
2579
- return { pino: instance, core: wrapPino(instance) };
2594
+ return { kind: "ok", text: Buffer.concat(chunks).toString("utf8") };
2580
2595
  }
2581
- function wrapPino(instance) {
2582
- const logger = {
2583
- debug: (obj, msg) => instance.debug(obj, msg),
2584
- info: (obj, msg) => instance.info(obj, msg),
2585
- warn: (obj, msg) => instance.warn(obj, msg),
2586
- error: (obj, msg) => instance.error(obj, msg),
2587
- child: (bindings) => wrapPino(instance.child(bindings))
2588
- };
2589
- return logger;
2596
+ function joinUrl(baseUrl, mountPath) {
2597
+ return `${baseUrl.replace(/\/+$/, "")}/${mountPath.replace(/^\/+|\/+$/g, "")}`;
2590
2598
  }
2591
- var CORS_METHODS = "GET,POST,DELETE,OPTIONS";
2592
- var CORS_HEADERS = "content-type,payment-signature,x-request-id,authorization";
2593
- var CORS_EXPOSED_HEADERS = "payment-required,payment-response,x-request-id";
2599
+
2600
+ // src/gateway/access-control.ts
2601
+ var CORS_METHODS = "GET,POST,OPTIONS";
2602
+ var CORS_HEADERS = [
2603
+ "content-type",
2604
+ PAYMENT_HEADER,
2605
+ AUTHORIZATION_HEADER,
2606
+ "x-request-id",
2607
+ "authorization"
2608
+ ].join(",");
2609
+ var CORS_EXPOSED_HEADERS = "payment-required,payment-response,www-authenticate,payment-receipt";
2594
2610
  function buildAccessControlHook(options) {
2595
2611
  const allowedOrigins = new Set(options.allowedOrigins.map((origin) => origin.toLowerCase()));
2596
2612
  const publicHostname = hostnameOf(options.publicBaseUrl);
@@ -2618,12 +2634,13 @@ function buildAccessControlHook(options) {
2618
2634
  };
2619
2635
  }
2620
2636
  function buildOperatorTokenHook(adminToken) {
2637
+ const isValidAdminToken = adminToken ? createBearerCheck(adminToken) : void 0;
2621
2638
  return async (request, reply) => {
2622
- if (!adminToken) {
2639
+ if (isValidAdminToken === void 0) {
2623
2640
  reply.status(404).send();
2624
2641
  return;
2625
2642
  }
2626
- if (!isValidAdminToken(request.headers.authorization, adminToken)) {
2643
+ if (!isValidAdminToken(request.headers.authorization)) {
2627
2644
  reply.status(401).send({ status: "error", code: "UNAUTHORIZED", message: "Admin token required" });
2628
2645
  }
2629
2646
  };
@@ -2644,14 +2661,6 @@ function isAllowedHost(hostHeader, publicHostname) {
2644
2661
  const hostname = hostnameOf(hostHeader);
2645
2662
  return hostname !== "" && (hostname === publicHostname || LOOPBACK_HOSTNAMES.has(hostname));
2646
2663
  }
2647
- function isValidAdminToken(authHeader, adminToken) {
2648
- const provided = authHeader?.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : void 0;
2649
- if (provided === void 0) return false;
2650
- return timingSafeEqual(hashToken(provided), hashToken(adminToken));
2651
- }
2652
- function hashToken(value) {
2653
- return createHash("sha256").update(value, "utf8").digest();
2654
- }
2655
2664
 
2656
2665
  // src/gateway/adapters.ts
2657
2666
  var MOUNT_BODY_LIMIT_BYTES = 256 * 1024;
@@ -2664,7 +2673,7 @@ async function startAndMountAdapters(options) {
2664
2673
  try {
2665
2674
  await adapter.start(adapterContext);
2666
2675
  } catch (error) {
2667
- const detail = describeError3(error);
2676
+ const detail = errorMessage(error);
2668
2677
  adapterLogger.error(
2669
2678
  { err: detail },
2670
2679
  "Protocol adapter failed to start; gateway continues without it"
@@ -2758,7 +2767,7 @@ function mountHttpAdapter(server, adapter, logger) {
2758
2767
  try {
2759
2768
  await handleHttp(request.raw, reply.raw);
2760
2769
  } catch (error) {
2761
- logger.error({ err: describeError3(error) }, "Protocol adapter request handler threw");
2770
+ logger.error({ err: errorMessage(error) }, "Protocol adapter request handler threw");
2762
2771
  if (!reply.raw.headersSent) {
2763
2772
  reply.raw.statusCode = 500;
2764
2773
  reply.raw.end();
@@ -2825,7 +2834,7 @@ async function getAdapterHealth(runtime, clock) {
2825
2834
  try {
2826
2835
  return await runtime.adapter.health();
2827
2836
  } catch (error) {
2828
- return { status: "fail", detail: describeError3(error), checkedAt: clock.nowIso() };
2837
+ return { status: "fail", detail: errorMessage(error), checkedAt: clock.nowIso() };
2829
2838
  }
2830
2839
  }
2831
2840
  async function stopAdapters(runtimes, logger) {
@@ -2833,13 +2842,99 @@ async function stopAdapters(runtimes, logger) {
2833
2842
  try {
2834
2843
  await runtime.adapter.stop();
2835
2844
  } catch (error) {
2836
- logger.child({ adapter: runtime.adapter.name }).error({ err: describeError3(error) }, "Protocol adapter failed to stop cleanly");
2845
+ logger.child({ adapter: runtime.adapter.name }).error({ err: errorMessage(error) }, "Protocol adapter failed to stop cleanly");
2837
2846
  }
2838
2847
  }
2839
2848
  }
2840
- function describeError3(error) {
2849
+ function errorMessage(error) {
2841
2850
  return error instanceof Error ? error.message : String(error);
2842
2851
  }
2852
+ function createDefaultIdGenerator() {
2853
+ return {
2854
+ next(prefix) {
2855
+ const id = randomUUID();
2856
+ return prefix ? `${prefix}_${id}` : id;
2857
+ }
2858
+ };
2859
+ }
2860
+ var PINO_PRETTY_PATH = (() => {
2861
+ try {
2862
+ return createRequire(import.meta.url).resolve("pino-pretty");
2863
+ } catch {
2864
+ return void 0;
2865
+ }
2866
+ })();
2867
+ var SECRET_FIELD_NAMES = [
2868
+ "privateKey",
2869
+ "signerPrivateKey",
2870
+ "signature",
2871
+ "seed",
2872
+ "mnemonic",
2873
+ "secret",
2874
+ "challengeSecret",
2875
+ "apiKey",
2876
+ "adminToken",
2877
+ "token"
2878
+ ];
2879
+ var SECRET_HEADERS = ["authorization", PAYMENT_HEADER, AUTHORIZATION_HEADER];
2880
+ var REDACT_PATHS = [
2881
+ ...SECRET_HEADERS.map((name) => `req.headers[${JSON.stringify(name)}]`),
2882
+ ...SECRET_FIELD_NAMES,
2883
+ ...SECRET_FIELD_NAMES.map((name) => `*.${name}`)
2884
+ ];
2885
+ function baseLoggerOptions(options) {
2886
+ const nodeEnv = options.nodeEnv ?? process.env["NODE_ENV"] ?? "development";
2887
+ const wantPretty = options.prettyPrint ?? (nodeEnv !== "production" && nodeEnv !== "test");
2888
+ const usePretty = wantPretty && PINO_PRETTY_PATH !== void 0;
2889
+ return {
2890
+ level: options.level ?? process.env["LOG_LEVEL"] ?? (nodeEnv === "test" ? "silent" : "info"),
2891
+ ...options.name !== void 0 ? { name: options.name } : {},
2892
+ redact: { paths: [...REDACT_PATHS], censor: "[REDACTED]" },
2893
+ ...usePretty ? { transport: { target: PINO_PRETTY_PATH } } : {}
2894
+ };
2895
+ }
2896
+ function sanitizeUrl(url) {
2897
+ if (url === void 0) return void 0;
2898
+ const queryIndex = url.indexOf("?");
2899
+ return queryIndex === -1 ? url : `${url.slice(0, queryIndex)}?[REDACTED]`;
2900
+ }
2901
+ function redactedReqSerializer(req) {
2902
+ return {
2903
+ method: req.method,
2904
+ url: sanitizeUrl(req.url),
2905
+ host: req.host,
2906
+ remoteAddress: req.ip,
2907
+ remotePort: req.socket?.remotePort
2908
+ };
2909
+ }
2910
+ function fastifyLoggerOptions(options = {}) {
2911
+ return {
2912
+ ...baseLoggerOptions(options),
2913
+ serializers: { req: redactedReqSerializer }
2914
+ };
2915
+ }
2916
+ function buildNotFoundHandler() {
2917
+ return (request, reply) => {
2918
+ const safeUrl = sanitizeUrl(request.raw.url) ?? "";
2919
+ const message = `Route ${request.method}:${safeUrl} not found`;
2920
+ request.log.info(message);
2921
+ reply.code(404).send({ message, error: "Not Found", statusCode: 404 });
2922
+ };
2923
+ }
2924
+ function createGatewayLogger(options = {}) {
2925
+ const instance = pino(baseLoggerOptions(options));
2926
+ return { pino: instance, core: wrapPino(instance) };
2927
+ }
2928
+ function wrapPino(instance) {
2929
+ const logger = {
2930
+ debug: (obj, msg) => instance.debug(obj, msg),
2931
+ info: (obj, msg) => instance.info(obj, msg),
2932
+ warn: (obj, msg) => instance.warn(obj, msg),
2933
+ error: (obj, msg) => instance.error(obj, msg),
2934
+ child: (bindings) => wrapPino(instance.child(bindings))
2935
+ };
2936
+ return logger;
2937
+ }
2843
2938
 
2844
2939
  // src/gateway/public-resource.ts
2845
2940
  function toPublicResource(resource) {
@@ -2860,7 +2955,8 @@ function toPublicResource(resource) {
2860
2955
  var STORE_DETAIL = {
2861
2956
  pass: void 0,
2862
2957
  warn: "store-degraded",
2863
- fail: "store-unwritable"
2958
+ fail: "store-unwritable",
2959
+ threw: "store-unreachable"
2864
2960
  };
2865
2961
  var ADAPTER_DETAIL = {
2866
2962
  pass: void 0,
@@ -2877,108 +2973,102 @@ var AUTHORIZATION_PROVIDER_DETAIL = {
2877
2973
  warn: "authorization-provider-degraded",
2878
2974
  fail: "authorization-provider-unreachable"
2879
2975
  };
2880
- async function probeProvider(provider, kind, details, options) {
2976
+ async function probe(target, kind, details, options) {
2881
2977
  let health;
2978
+ let threw = false;
2882
2979
  try {
2883
- health = await provider.health();
2980
+ health = await target.health();
2884
2981
  } catch (error) {
2885
2982
  const message = error instanceof Error ? error.message : String(error);
2886
- options.logger.error({ err: message, provider: provider.name }, `${kind} health() threw`);
2983
+ options.logger.error({ err: message, name: target.name }, `${kind} health() threw`);
2887
2984
  health = { status: "fail", checkedAt: options.clock.nowIso() };
2985
+ threw = true;
2888
2986
  }
2889
2987
  if (health.detail !== void 0) {
2890
2988
  options.logger.debug(
2891
- { provider: provider.name, detail: health.detail },
2989
+ { name: target.name, detail: health.detail },
2892
2990
  `${kind} health detail (not sent to the client)`
2893
2991
  );
2894
2992
  }
2895
- const detail = details[health.status];
2993
+ const detail = threw ? details.threw ?? details.fail : details[health.status];
2896
2994
  return {
2897
- name: provider.name,
2898
- status: health.status,
2899
- ...detail !== void 0 ? { detail } : {}
2995
+ check: {
2996
+ name: target.name,
2997
+ status: health.status,
2998
+ ...detail !== void 0 ? { detail } : {}
2999
+ },
3000
+ health
2900
3001
  };
2901
3002
  }
2902
- async function checkReadiness(options) {
2903
- let storeHealth;
2904
- let storeThrew = false;
2905
- try {
2906
- storeHealth = await options.store.health();
2907
- } catch (error) {
2908
- const message = error instanceof Error ? error.message : String(error);
2909
- options.logger.error({ err: message }, "store.health() threw");
2910
- storeHealth = { status: "fail", checkedAt: options.clock.nowIso() };
2911
- storeThrew = true;
2912
- }
2913
- if (storeHealth.detail !== void 0) {
2914
- options.logger.debug(
2915
- { detail: storeHealth.detail },
2916
- "store health detail (not sent to the client)"
2917
- );
2918
- }
2919
- const adapterChecks = await Promise.all(
2920
- options.adapterRuntimes.map(async (runtime) => {
2921
- const health = await getAdapterHealth(runtime, options.clock);
2922
- if (health.detail !== void 0) {
2923
- options.logger.debug(
2924
- { adapter: runtime.adapter.name, detail: health.detail },
2925
- "adapter health detail (not sent to the client)"
2926
- );
2927
- }
2928
- const detail = ADAPTER_DETAIL[health.status];
2929
- return {
2930
- name: runtime.adapter.name,
2931
- status: health.status,
2932
- ...detail !== void 0 ? { detail } : {}
2933
- };
2934
- })
2935
- );
2936
- const paymentProviderChecks = await Promise.all(
2937
- options.paymentProviders.map(
2938
- (provider) => probeProvider(provider, "payment provider", PAYMENT_PROVIDER_DETAIL, options)
2939
- )
2940
- );
2941
- const authorizationProviderChecks = await Promise.all(
2942
- options.authorizationProviders.map(
2943
- (provider) => probeProvider(provider, "authorization provider", AUTHORIZATION_PROVIDER_DETAIL, options)
3003
+ async function evaluate(options) {
3004
+ const [store, adapters, paymentProviders, authorizationProviders] = await Promise.all([
3005
+ probe({ name: "store", health: () => options.store.health() }, "store", STORE_DETAIL, options),
3006
+ Promise.all(
3007
+ options.adapterRuntimes.map(async (runtime) => ({
3008
+ runtime,
3009
+ probed: await probe(
3010
+ // getAdapterHealth turns a throw into a failed result, so an adapter
3011
+ // failure is logged at debug as a detail rather than at error
3012
+ { name: runtime.adapter.name, health: () => getAdapterHealth(runtime, options.clock) },
3013
+ "adapter",
3014
+ ADAPTER_DETAIL,
3015
+ options
3016
+ )
3017
+ }))
3018
+ ),
3019
+ Promise.all(
3020
+ options.paymentProviders.map(
3021
+ (provider) => probe(provider, "payment provider", PAYMENT_PROVIDER_DETAIL, options)
3022
+ )
3023
+ ),
3024
+ // An unusable authorization provider blocks readiness on the same threshold
3025
+ // as a payment one: a resource that requires a mandate cannot be served
3026
+ // without it, and serving the challenge anyway promises what we cannot honor
3027
+ Promise.all(
3028
+ options.authorizationProviders.map(
3029
+ (provider) => probe(provider, "authorization provider", AUTHORIZATION_PROVIDER_DETAIL, options)
3030
+ )
2944
3031
  )
2945
- );
2946
- const storeReady = storeHealth.status !== "fail";
2947
- const adaptersReady = adapterChecks.every((check) => check.status !== "fail");
2948
- const paymentProvidersReady = paymentProviderChecks.every((check) => check.status !== "fail");
2949
- const authorizationProvidersReady = authorizationProviderChecks.every(
2950
- (check) => check.status !== "fail"
2951
- );
2952
- const storeDetail = storeThrew ? "store-unreachable" : STORE_DETAIL[storeHealth.status];
3032
+ ]);
3033
+ const checks = [
3034
+ store,
3035
+ ...adapters.map((a) => a.probed),
3036
+ ...paymentProviders,
3037
+ ...authorizationProviders
3038
+ ];
2953
3039
  return {
2954
- ready: storeReady && adaptersReady && paymentProvidersReady && authorizationProvidersReady,
2955
- store: {
2956
- name: "store",
2957
- status: storeHealth.status,
2958
- ...storeDetail !== void 0 ? { detail: storeDetail } : {}
3040
+ result: {
3041
+ ready: checks.every((probed) => probed.check.status !== "fail"),
3042
+ store: store.check,
3043
+ adapters: adapters.map((a) => a.probed.check),
3044
+ paymentProviders: paymentProviders.map((p) => p.check),
3045
+ authorizationProviders: authorizationProviders.map((p) => p.check)
2959
3046
  },
2960
- adapters: adapterChecks,
2961
- paymentProviders: paymentProviderChecks,
2962
- authorizationProviders: authorizationProviderChecks
3047
+ adapters: adapters.map(({ runtime, probed }) => ({
3048
+ descriptor: runtime.adapter.descriptor,
3049
+ health: probed.health
3050
+ }))
2963
3051
  };
2964
3052
  }
2965
3053
  var READINESS_TTL_MS = 2e3;
2966
3054
  function createReadinessProbe(options, ttlMs = READINESS_TTL_MS) {
2967
3055
  let cached;
2968
3056
  let inFlight;
3057
+ const current = async () => {
3058
+ const now = options.clock.monotonicMs();
3059
+ if (cached && now - cached.at < ttlMs) return cached.evaluation;
3060
+ if (inFlight) return inFlight;
3061
+ inFlight = evaluate(options).then((evaluation) => {
3062
+ cached = { evaluation, at: options.clock.monotonicMs() };
3063
+ return evaluation;
3064
+ }).finally(() => {
3065
+ inFlight = void 0;
3066
+ });
3067
+ return inFlight;
3068
+ };
2969
3069
  return {
2970
- async check() {
2971
- const now = options.clock.monotonicMs();
2972
- if (cached && now - cached.at < ttlMs) return cached.result;
2973
- if (inFlight) return inFlight;
2974
- inFlight = checkReadiness(options).then((result) => {
2975
- cached = { result, at: options.clock.monotonicMs() };
2976
- return result;
2977
- }).finally(() => {
2978
- inFlight = void 0;
2979
- });
2980
- return inFlight;
2981
- }
3070
+ check: async () => (await current()).result,
3071
+ adapterHealth: async () => (await current()).adapters
2982
3072
  };
2983
3073
  }
2984
3074
 
@@ -2996,14 +3086,12 @@ function publicProtocols(protocols) {
2996
3086
  acp: { enabled: protocols.acp.enabled, mountPath: protocols.acp.mountPath }
2997
3087
  };
2998
3088
  }
2999
- async function buildWellKnownDocument(options) {
3000
- const adapters = await Promise.all(
3001
- options.adapterRuntimes.map(async (runtime) => {
3002
- const health = await getAdapterHealth(runtime, options.clock);
3003
- return { ...runtime.adapter.descriptor, health: publicHealth(health) };
3004
- })
3005
- );
3006
- const x402 = options.config.payments.x402;
3089
+ function buildWellKnownDocument(options) {
3090
+ const adapters = options.adapters.map(({ descriptor, health }) => ({
3091
+ ...descriptor,
3092
+ health: publicHealth(health)
3093
+ }));
3094
+ const { x402, mpp } = options.config.payments;
3007
3095
  return {
3008
3096
  gateway: {
3009
3097
  implementationVersion: PACKAGE_VERSION,
@@ -3026,27 +3114,38 @@ async function buildWellKnownDocument(options) {
3026
3114
  assetDecimals: x402.assetDecimals,
3027
3115
  payTo: x402.payTo,
3028
3116
  maxTimeoutSeconds: x402.maxTimeoutSeconds,
3029
- // Never the signerPrivateKey, and never the facilitator URL:
3030
- // a facilitator endpoint is a per-deployment operational detail
3031
- // that can carry a tenant path or an API key, exactly like
3032
- // `rpcUrl` above. Only whether it is local or remote is public.
3117
+ // Only local or remote is public: the facilitator URL can carry
3118
+ // a tenant path or an API key, and the signer key is secret
3033
3119
  facilitator: { mode: x402.facilitator.mode },
3034
- // Chain id 84532 is shared between the local dev chain and
3035
- // public Base Sepolia, so the network id alone cannot say which
3036
- // one a client is talking to. This can.
3120
+ // Chain id 84532 is both the local dev chain and public Base
3121
+ // Sepolia, so the network id alone cannot say which one this is
3037
3122
  mode: resolveDeploymentMode(
3038
3123
  requireNetworkProfile(x402.network, "payments.x402.network"),
3039
3124
  x402.facilitator.mode
3040
3125
  )
3041
3126
  }
3127
+ } : {},
3128
+ ...mpp !== void 0 ? {
3129
+ mpp: {
3130
+ enabled: mpp.enabled,
3131
+ network: mpp.network,
3132
+ asset: mpp.asset,
3133
+ assetName: mpp.assetName,
3134
+ assetVersion: mpp.assetVersion,
3135
+ assetDecimals: MPP_PROFILE.assetDecimals,
3136
+ recipient: mpp.recipient,
3137
+ facilitator: { mode: mpp.facilitator.mode },
3138
+ mode: resolveDeploymentMode(
3139
+ requireNetworkProfile(mpp.network, "payments.mpp.network"),
3140
+ mpp.facilitator.mode
3141
+ )
3142
+ }
3042
3143
  } : {}
3043
3144
  }
3044
3145
  };
3045
3146
  }
3046
3147
 
3047
3148
  // src/gateway/routes.ts
3048
- var SSE_HEARTBEAT_MS = 15e3;
3049
- var MAX_SSE_SUBSCRIBERS = 32;
3050
3149
  function registerRoutes(options) {
3051
3150
  const { server } = options;
3052
3151
  const readinessProbe = createReadinessProbe({
@@ -3070,8 +3169,7 @@ function registerRoutes(options) {
3070
3169
  paymentProviders: options.paymentProviders,
3071
3170
  authorizationProviders: options.authorizationProviders,
3072
3171
  store: options.store,
3073
- adapterRuntimes: options.adapterRuntimes,
3074
- clock: options.clock
3172
+ adapters: await readinessProbe.adapterHealth()
3075
3173
  })
3076
3174
  );
3077
3175
  server.get("/api/resources", async () => ({
@@ -3080,89 +3178,42 @@ function registerRoutes(options) {
3080
3178
  server.post("/api/resources/:id/invoke", async (request, reply) => {
3081
3179
  await handleInvoke(request, reply, options);
3082
3180
  });
3083
- const receiptsTokenHook = buildOperatorTokenHook(options.config.server.adminToken);
3084
- const eventsTokenHook = buildOperatorTokenHook(options.config.server.adminToken);
3085
- const eventsStreamTokenHook = buildOperatorTokenHook(options.config.server.adminToken);
3086
- server.get("/api/receipts", { onRequest: receiptsTokenHook }, async (request, reply) => {
3087
- try {
3088
- const limit = parseLimit(request.query["limit"]);
3089
- return { receipts: await options.store.listReceipts(limit !== void 0 ? { limit } : {}) };
3090
- } catch (error) {
3091
- const commerceError = toCommerceError(error);
3092
- reply.status(commerceError.httpStatus).send(toErrorEnvelope(commerceError));
3093
- return void 0;
3094
- }
3095
- });
3096
- server.get("/api/events", { onRequest: eventsTokenHook }, async (request, reply) => {
3181
+ const tokenHook = buildOperatorTokenHook(options.config.server.adminToken);
3182
+ server.get(
3183
+ "/api/receipts",
3184
+ { onRequest: tokenHook },
3185
+ ledgerHandler("receipts", (list) => options.store.listReceipts(list))
3186
+ );
3187
+ server.get(
3188
+ "/api/events",
3189
+ { onRequest: tokenHook },
3190
+ ledgerHandler("events", (list) => options.store.listEvents(list))
3191
+ );
3192
+ }
3193
+ function ledgerHandler(key, list) {
3194
+ return async (request, reply) => {
3097
3195
  try {
3098
3196
  const limit = parseLimit(request.query["limit"]);
3099
- return { events: await options.store.listEvents(limit !== void 0 ? { limit } : {}) };
3197
+ return { [key]: await list(limit !== void 0 ? { limit } : {}) };
3100
3198
  } catch (error) {
3101
3199
  const commerceError = toCommerceError(error);
3102
3200
  reply.status(commerceError.httpStatus).send(toErrorEnvelope(commerceError));
3103
3201
  return void 0;
3104
3202
  }
3105
- });
3106
- let activeSseSubscribers = 0;
3107
- server.get("/api/events/stream", { onRequest: eventsStreamTokenHook }, (request, reply) => {
3108
- if (activeSseSubscribers >= MAX_SSE_SUBSCRIBERS) {
3109
- reply.status(503).send({
3110
- status: "error",
3111
- code: "UNAVAILABLE",
3112
- message: "Too many event-stream subscribers"
3113
- });
3114
- return;
3115
- }
3116
- reply.raw.writeHead(200, {
3117
- "Content-Type": "text/event-stream",
3118
- "Cache-Control": "no-cache",
3119
- Connection: "keep-alive"
3120
- });
3121
- activeSseSubscribers += 1;
3122
- reply.raw.write(": connected\n\n");
3123
- let writable = true;
3124
- reply.raw.on("drain", () => {
3125
- writable = true;
3126
- });
3127
- const unsubscribe = options.eventBus.subscribe((event) => {
3128
- queueMicrotask(() => {
3129
- if (!writable) return;
3130
- writable = reply.raw.write(`data: ${JSON.stringify(event)}
3131
-
3132
- `);
3133
- });
3134
- });
3135
- const heartbeat = setInterval(() => {
3136
- if (!writable) return;
3137
- writable = reply.raw.write(": heartbeat\n\n");
3138
- }, SSE_HEARTBEAT_MS);
3139
- heartbeat.unref();
3140
- let cleanedUp = false;
3141
- const cleanup = () => {
3142
- if (cleanedUp) return;
3143
- cleanedUp = true;
3144
- clearInterval(heartbeat);
3145
- unsubscribe();
3146
- activeSseSubscribers -= 1;
3147
- };
3148
- request.raw.on("close", cleanup);
3149
- reply.raw.on("error", cleanup);
3150
- reply.hijack();
3151
- });
3203
+ };
3152
3204
  }
3153
3205
  async function handleInvoke(request, reply, options) {
3154
3206
  const resourceId = request.params.id;
3155
3207
  try {
3156
3208
  const resource = options.resources.get(resourceId);
3157
- if (!resource || !resource.exposedVia.includes("http")) {
3209
+ if (!resource?.exposedVia.includes("http")) {
3158
3210
  throw new CommerceError("RESOURCE_NOT_FOUND", `Resource "${resourceId}" was not found`, {
3159
3211
  requestId: request.id,
3160
3212
  resourceId
3161
3213
  });
3162
3214
  }
3163
- const paymentHeader = request.headers[PAYMENT_HEADER];
3164
- const paymentValue = Array.isArray(paymentHeader) ? paymentHeader[0] : paymentHeader;
3165
3215
  const paymentMethod = resource.paymentMethods[0];
3216
+ const paymentValue = paymentProof(request, paymentMethod);
3166
3217
  const payment = paymentValue !== void 0 && paymentMethod !== void 0 ? { method: paymentMethod, payload: paymentValue } : void 0;
3167
3218
  const authorization = parseAuthorizationHeader(
3168
3219
  request.headers[AUTHORIZATION_HEADER],
@@ -3180,7 +3231,10 @@ async function handleInvoke(request, reply, options) {
3180
3231
  const outcome = await options.pipeline.execute(canonicalRequest);
3181
3232
  if (outcome.kind === "payment-required") {
3182
3233
  const envelope = toPaymentRequiredEnvelope(outcome);
3183
- if (envelope.payment.envelope !== void 0) {
3234
+ if (envelope.payment.provider === "mpp") {
3235
+ const challenge = envelope.payment.envelope?.["wwwAuthenticate"];
3236
+ if (typeof challenge === "string") reply.header("www-authenticate", challenge);
3237
+ } else if (envelope.payment.envelope !== void 0) {
3184
3238
  reply.header(PAYMENT_REQUIRED_HEADER, encodeHeaderDocument(envelope.payment.envelope));
3185
3239
  }
3186
3240
  reply.header("cache-control", "no-store");
@@ -3189,6 +3243,10 @@ async function handleInvoke(request, reply, options) {
3189
3243
  }
3190
3244
  if (outcome.payment) {
3191
3245
  reply.header(PAYMENT_RESPONSE_HEADER, encodePaymentSummary(outcome.payment));
3246
+ const receipt = outcome.payment.metadata?.["receipt"];
3247
+ if (outcome.payment.provider === "mpp" && typeof receipt === "string") {
3248
+ reply.header("payment-receipt", receipt);
3249
+ }
3192
3250
  }
3193
3251
  reply.status(outcome.backendStatus).send(outcome.body);
3194
3252
  } catch (error) {
@@ -3196,15 +3254,30 @@ async function handleInvoke(request, reply, options) {
3196
3254
  const settledPayment = errorPaymentSummary(commerceError);
3197
3255
  if (settledPayment !== void 0) {
3198
3256
  reply.header(PAYMENT_RESPONSE_HEADER, encodePaymentSummary(settledPayment));
3257
+ if (settledPayment.provider === "mpp" && settledPayment.receipt !== void 0) {
3258
+ reply.header("payment-receipt", settledPayment.receipt);
3259
+ }
3260
+ }
3261
+ const challenge = commerceError.details?.["challenge"];
3262
+ if (commerceError.code === "PAYMENT_INVALID" && typeof challenge?.["wwwAuthenticate"] === "string") {
3263
+ reply.header("www-authenticate", challenge["wwwAuthenticate"]);
3264
+ reply.header("cache-control", "no-store");
3199
3265
  }
3200
3266
  reply.status(commerceError.httpStatus).send(toErrorEnvelope(commerceError));
3201
3267
  }
3202
3268
  }
3269
+ function paymentProof(request, method) {
3270
+ if (method === "mpp") {
3271
+ const value2 = request.headers.authorization;
3272
+ return value2 !== void 0 && /^Payment\s+\S/i.test(value2) ? value2 : void 0;
3273
+ }
3274
+ const value = request.headers[PAYMENT_HEADER];
3275
+ return Array.isArray(value) ? value[0] : value;
3276
+ }
3203
3277
  function errorPaymentSummary(error) {
3204
3278
  const payment = error.details?.["payment"];
3205
- if (typeof payment !== "object" || payment === null) return void 0;
3206
- const rec = payment;
3207
- const { status, provider, amount, currency, network, externalReference } = rec;
3279
+ if (!isRecord(payment)) return void 0;
3280
+ const { status, provider, amount, currency, network, externalReference, receipt } = payment;
3208
3281
  if (typeof status !== "string" || typeof provider !== "string" || typeof amount !== "string" || typeof currency !== "string") {
3209
3282
  return void 0;
3210
3283
  }
@@ -3214,7 +3287,8 @@ function errorPaymentSummary(error) {
3214
3287
  amount,
3215
3288
  currency,
3216
3289
  ...typeof network === "string" ? { network } : {},
3217
- ...typeof externalReference === "string" ? { externalReference } : {}
3290
+ ...typeof externalReference === "string" ? { externalReference } : {},
3291
+ ...typeof receipt === "string" ? { receipt } : {}
3218
3292
  };
3219
3293
  }
3220
3294
  function encodeHeaderDocument(document) {
@@ -3256,33 +3330,52 @@ function parseLimit(value) {
3256
3330
  // src/gateway/server.ts
3257
3331
  var REQUEST_ID_HEADER = "x-request-id";
3258
3332
  var CLIENT_REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
3333
+ function clientRequestIdOf(raw) {
3334
+ const header2 = raw.headers[REQUEST_ID_HEADER];
3335
+ const value = Array.isArray(header2) ? header2[0] : header2;
3336
+ return value !== void 0 && CLIENT_REQUEST_ID_PATTERN.test(value) ? value : void 0;
3337
+ }
3338
+ function withProviderBackedMethodsFirst(resource, providers) {
3339
+ const backed = (method) => providers.some((provider) => provider.name === method);
3340
+ const ordered = [
3341
+ ...resource.paymentMethods.filter(backed),
3342
+ ...resource.paymentMethods.filter((method) => !backed(method))
3343
+ ];
3344
+ return ordered.some((method, i) => method !== resource.paymentMethods[i]) ? { ...resource, paymentMethods: ordered } : resource;
3345
+ }
3259
3346
  async function createGateway(options) {
3260
3347
  const clock = options.clock ?? systemClock;
3261
3348
  const ids = options.ids ?? createDefaultIdGenerator();
3262
3349
  const logger = options.logger ?? createGatewayLogger({ name: options.config.merchant.id }).core;
3263
3350
  const backend = options.backend ?? new HttpBackendExecutor({ logger });
3264
3351
  const authorizationProviders = options.authorizationProviders ?? [];
3265
- const resources = createResourceRegistry(options.config.resources);
3266
- const eventBus = createEventBus({ store: options.store, logger });
3352
+ const resources = createResourceRegistry(
3353
+ options.config.resources.map(
3354
+ (resource) => withProviderBackedMethodsFirst(resource, options.paymentProviders)
3355
+ )
3356
+ );
3357
+ const events = createStoreEventSink({ store: options.store, logger });
3267
3358
  const pipeline = createExecutionPipeline({
3268
3359
  resources,
3269
3360
  paymentProviders: options.paymentProviders,
3270
3361
  authorizationProviders,
3271
3362
  store: options.store,
3272
3363
  backend,
3273
- events: eventBus,
3364
+ events,
3274
3365
  logger,
3275
3366
  clock,
3276
3367
  ids
3277
3368
  });
3278
3369
  const server = Fastify({
3279
- // One number for both surfaces — see adapters.ts's doc comment
3280
- // on MOUNT_BODY_LIMIT_BYTES for why they're enforced two different ways.
3370
+ // Same cap as adapter mounts, which enforce it on the socket instead
3281
3371
  bodyLimit: MOUNT_BODY_LIMIT_BYTES,
3282
- genReqId: (rawReq) => {
3283
- const header2 = rawReq.headers[REQUEST_ID_HEADER];
3284
- const value = Array.isArray(header2) ? header2[0] : header2;
3285
- return value !== void 0 && CLIENT_REQUEST_ID_PATTERN.test(value) ? value : ids.next("req");
3372
+ genReqId: () => ids.next("req"),
3373
+ childLoggerFactory: (parent, bindings, childOptions, rawReq) => {
3374
+ const clientRequestId = clientRequestIdOf(rawReq);
3375
+ return parent.child(
3376
+ clientRequestId !== void 0 ? { ...bindings, clientRequestId } : bindings,
3377
+ childOptions
3378
+ );
3286
3379
  },
3287
3380
  logger: fastifyLoggerOptions({ name: `${options.config.merchant.id}-http` })
3288
3381
  });
@@ -3303,7 +3396,6 @@ async function createGateway(options) {
3303
3396
  store: options.store,
3304
3397
  paymentProviders: options.paymentProviders,
3305
3398
  authorizationProviders,
3306
- eventBus,
3307
3399
  clock,
3308
3400
  adapterRuntimes,
3309
3401
  logger
@@ -3311,7 +3403,7 @@ async function createGateway(options) {
3311
3403
  const context = {
3312
3404
  pipeline,
3313
3405
  resources,
3314
- events: eventBus,
3406
+ events,
3315
3407
  logger,
3316
3408
  clock,
3317
3409
  ids,
@@ -3344,6 +3436,554 @@ async function createGateway(options) {
3344
3436
  };
3345
3437
  }
3346
3438
 
3439
+ // src/protocols/a2a/constants.ts
3440
+ var A2A_SPEC_VERSION = "1.0.0";
3441
+ var A2A_PROTOCOL_VERSION = "1.0";
3442
+ var A2A_PROTOCOL_BINDING = "JSONRPC";
3443
+ var A2A_AGENT_CARD_PATH = "/.well-known/agent-card.json";
3444
+ var A2A_DEFAULT_MOUNT_PATH = "/a2a";
3445
+ var A2A_JSON_MEDIA_TYPE = "application/json";
3446
+ var A2A_DEFAULT_AGENT_NAME = "agent-commerce";
3447
+ var A2A_VERSION_HEADER = "a2a-version";
3448
+ var A2A_METHOD_SEND_MESSAGE = "SendMessage";
3449
+ var A2A_UNSUPPORTED_METHODS = [
3450
+ "SendStreamingMessage",
3451
+ "GetTask",
3452
+ "ListTasks",
3453
+ "CancelTask",
3454
+ "SubscribeToTask",
3455
+ "CreateTaskPushNotificationConfig",
3456
+ "GetTaskPushNotificationConfig",
3457
+ "ListTaskPushNotificationConfigs",
3458
+ "DeleteTaskPushNotificationConfig",
3459
+ "GetExtendedAgentCard"
3460
+ ];
3461
+ var A2A_TASK_STATE_COMPLETED = "TASK_STATE_COMPLETED";
3462
+ var A2A_TASK_STATE_FAILED = "TASK_STATE_FAILED";
3463
+
3464
+ // src/protocols/a2a/agent-card.ts
3465
+ function skillTags(resource) {
3466
+ return ["agent-commerce", resource.pricing.type === "free" ? "free" : "paid"];
3467
+ }
3468
+ function skillDescription(resource) {
3469
+ const base = resource.description ?? resource.name;
3470
+ if (resource.pricing.type === "fixed") {
3471
+ return `${base} Costs ${resource.pricing.amount} ${resource.pricing.currency} per call.`;
3472
+ }
3473
+ if (resource.pricing.type === "dynamic") {
3474
+ return `${base} Requires payment (amount determined at request time).`;
3475
+ }
3476
+ return base;
3477
+ }
3478
+ function buildAgentSkill(resource) {
3479
+ return {
3480
+ id: resource.id,
3481
+ name: resource.name,
3482
+ description: skillDescription(resource),
3483
+ tags: skillTags(resource),
3484
+ inputModes: [A2A_JSON_MEDIA_TYPE],
3485
+ outputModes: [A2A_JSON_MEDIA_TYPE]
3486
+ };
3487
+ }
3488
+ function buildAgentCard(options) {
3489
+ return {
3490
+ name: options.name,
3491
+ description: options.description,
3492
+ version: options.version,
3493
+ supportedInterfaces: [
3494
+ {
3495
+ url: joinUrl(options.publicBaseUrl, options.mountPath),
3496
+ protocolBinding: A2A_PROTOCOL_BINDING,
3497
+ protocolVersion: A2A_PROTOCOL_VERSION
3498
+ }
3499
+ ],
3500
+ capabilities: { streaming: false, pushNotifications: false, extendedAgentCard: false },
3501
+ defaultInputModes: [A2A_JSON_MEDIA_TYPE],
3502
+ defaultOutputModes: [A2A_JSON_MEDIA_TYPE],
3503
+ skills: options.resources.map(buildAgentSkill)
3504
+ };
3505
+ }
3506
+
3507
+ // src/protocols/a2a/descriptor.ts
3508
+ var A2A_CAPABILITIES = ["agent-card", "jsonrpc", "SendMessage"];
3509
+ var A2A_UNSUPPORTED = [
3510
+ // Methods, as the protocol names them; the transport refuses the same list
3511
+ ...A2A_UNSUPPORTED_METHODS,
3512
+ // Transports other than the one binding served
3513
+ "HTTP+JSON/REST binding",
3514
+ "gRPC binding",
3515
+ // Behaviors
3516
+ "SSE",
3517
+ "long-running task persistence",
3518
+ "task resumption",
3519
+ "push notifications",
3520
+ "multi-turn conversational continuation",
3521
+ "authenticated extended agent cards",
3522
+ "A2A authentication schemes",
3523
+ "artifact types beyond Agent Commerce outcome data"
3524
+ ];
3525
+ function buildDescriptor(implementationVersion) {
3526
+ return {
3527
+ name: "a2a",
3528
+ kind: "protocol",
3529
+ implementationVersion,
3530
+ supportedSpec: A2A_SPEC_VERSION,
3531
+ capabilities: A2A_CAPABILITIES,
3532
+ status: "experimental",
3533
+ unsupported: A2A_UNSUPPORTED
3534
+ };
3535
+ }
3536
+
3537
+ // src/protocols/a2a/jsonrpc.ts
3538
+ var JSONRPC_PARSE_ERROR = -32700;
3539
+ var JSONRPC_INVALID_REQUEST = -32600;
3540
+ var JSONRPC_METHOD_NOT_FOUND = -32601;
3541
+ var JSONRPC_INVALID_PARAMS = -32602;
3542
+ var JSONRPC_INTERNAL_ERROR = -32603;
3543
+ var A2A_ERROR_UNSUPPORTED_OPERATION = -32004;
3544
+ function readId(value) {
3545
+ if (typeof value === "string" || typeof value === "number") return value;
3546
+ return null;
3547
+ }
3548
+ function jsonRpcResult(id, result) {
3549
+ return { jsonrpc: "2.0", id, result };
3550
+ }
3551
+ function jsonRpcError(id, code, message) {
3552
+ return { jsonrpc: "2.0", id, error: { code, message } };
3553
+ }
3554
+ function parseJsonRpcRequest(rawBody) {
3555
+ let payload;
3556
+ try {
3557
+ payload = JSON.parse(rawBody);
3558
+ } catch {
3559
+ return { ok: false, id: null, error: { code: JSONRPC_PARSE_ERROR, message: "Invalid JSON." } };
3560
+ }
3561
+ if (Array.isArray(payload)) {
3562
+ return {
3563
+ ok: false,
3564
+ id: null,
3565
+ error: {
3566
+ code: JSONRPC_INVALID_REQUEST,
3567
+ message: "Batch requests are not supported: send a single JSON-RPC request object."
3568
+ }
3569
+ };
3570
+ }
3571
+ if (!isRecord(payload)) {
3572
+ return {
3573
+ ok: false,
3574
+ id: null,
3575
+ error: { code: JSONRPC_INVALID_REQUEST, message: "Request must be a JSON-RPC 2.0 object." }
3576
+ };
3577
+ }
3578
+ const id = readId(payload["id"]);
3579
+ if (payload["jsonrpc"] !== "2.0") {
3580
+ return {
3581
+ ok: false,
3582
+ id,
3583
+ error: { code: JSONRPC_INVALID_REQUEST, message: 'Request must set "jsonrpc" to "2.0".' }
3584
+ };
3585
+ }
3586
+ const method = payload["method"];
3587
+ if (typeof method !== "string" || method.length === 0) {
3588
+ return {
3589
+ ok: false,
3590
+ id,
3591
+ error: { code: JSONRPC_INVALID_REQUEST, message: 'Request must carry a "method" string.' }
3592
+ };
3593
+ }
3594
+ const params = payload["params"];
3595
+ if (params !== void 0 && !isRecord(params)) {
3596
+ return {
3597
+ ok: false,
3598
+ id,
3599
+ error: {
3600
+ code: JSONRPC_INVALID_PARAMS,
3601
+ message: 'Request "params" must be an object.'
3602
+ }
3603
+ };
3604
+ }
3605
+ return { ok: true, request: { id, method, params: params ?? {} } };
3606
+ }
3607
+ var A2A_USER_ROLE = "ROLE_USER";
3608
+ var UnknownRecord = z.record(z.string(), z.unknown());
3609
+ var MessageSchema = z.object({
3610
+ role: z.string(),
3611
+ messageId: z.string().optional(),
3612
+ parts: z.array(UnknownRecord),
3613
+ taskId: z.string().optional(),
3614
+ contextId: z.string().optional(),
3615
+ referenceTaskIds: z.array(z.string()).optional()
3616
+ });
3617
+ var ParamsSchema = z.object({
3618
+ message: MessageSchema,
3619
+ taskId: z.string().optional(),
3620
+ contextId: z.string().optional()
3621
+ });
3622
+ function invalid(message) {
3623
+ return new CommerceError("INPUT_INVALID", message);
3624
+ }
3625
+ function unsupported(message) {
3626
+ return new CommerceError("PROTOCOL_UNSUPPORTED", message);
3627
+ }
3628
+ function assertNoContinuation(params) {
3629
+ const message = params.message;
3630
+ if (params.taskId !== void 0 || message.taskId !== void 0) {
3631
+ throw unsupported("Task continuation is not supported: send a request with no taskId.");
3632
+ }
3633
+ if (params.contextId !== void 0 || message.contextId !== void 0) {
3634
+ throw unsupported(
3635
+ "Multi-turn conversational continuation is not supported: send a request with no contextId."
3636
+ );
3637
+ }
3638
+ if (message.referenceTaskIds !== void 0 && message.referenceTaskIds.length > 0) {
3639
+ throw unsupported("Referencing previous tasks is not supported.");
3640
+ }
3641
+ }
3642
+ function assertSupportedPart(part) {
3643
+ if ("file" in part || "raw" in part || "url" in part) {
3644
+ throw unsupported("File and URL parts are not supported: send a structured data part.");
3645
+ }
3646
+ if ("text" in part) {
3647
+ throw unsupported("Text parts are not supported: send a structured data part.");
3648
+ }
3649
+ if (!("data" in part)) {
3650
+ throw invalid('Message part carries no "data": send a structured data part.');
3651
+ }
3652
+ const mediaType = part["mediaType"];
3653
+ if (mediaType !== void 0 && mediaType !== A2A_JSON_MEDIA_TYPE) {
3654
+ throw unsupported(
3655
+ `Media type "${String(mediaType)}" is not supported: parts must be ${A2A_JSON_MEDIA_TYPE}.`
3656
+ );
3657
+ }
3658
+ }
3659
+ function parseInvocation(rawParams) {
3660
+ const parsed = ParamsSchema.safeParse(rawParams);
3661
+ if (!parsed.success) {
3662
+ throw invalid(
3663
+ 'Request must carry a "message" with a "role" and a "parts" array of structured data parts.'
3664
+ );
3665
+ }
3666
+ const params = parsed.data;
3667
+ const message = params.message;
3668
+ assertNoContinuation(params);
3669
+ if (message.role !== A2A_USER_ROLE) {
3670
+ throw invalid(`Unsupported message role "${message.role}": only ${A2A_USER_ROLE} is accepted.`);
3671
+ }
3672
+ if (message.parts.length === 0) {
3673
+ throw invalid("Message carries no parts: send exactly one structured data part.");
3674
+ }
3675
+ if (message.parts.length > 1) {
3676
+ throw unsupported(
3677
+ `Multi-part messages are not supported: send exactly one structured data part (received ${message.parts.length}).`
3678
+ );
3679
+ }
3680
+ const part = message.parts[0];
3681
+ if (part === void 0) throw invalid("Message carries no parts.");
3682
+ assertSupportedPart(part);
3683
+ const data = part["data"];
3684
+ if (!isRecord(data)) {
3685
+ throw invalid('Message part "data" must be a JSON object.');
3686
+ }
3687
+ const resourceId = data["resource"];
3688
+ if (typeof resourceId !== "string") {
3689
+ throw invalid('Message part data must carry a "resource" string naming a canonical resource.');
3690
+ }
3691
+ if (resourceId.length === 0) {
3692
+ throw invalid('Message part data "resource" must not be empty.');
3693
+ }
3694
+ const rawInput = data["input"];
3695
+ if (rawInput !== void 0 && !isRecord(rawInput)) {
3696
+ throw invalid('Message part data "input" must be a JSON object.');
3697
+ }
3698
+ return {
3699
+ resourceId,
3700
+ input: rawInput ?? {},
3701
+ ...message.messageId !== void 0 ? { messageId: message.messageId } : {}
3702
+ };
3703
+ }
3704
+
3705
+ // src/protocols/a2a/task-mapping.ts
3706
+ function dataPayload(body) {
3707
+ return isRecord(body) ? body : { value: body ?? null };
3708
+ }
3709
+ function task(identity, state, artifact) {
3710
+ return {
3711
+ id: identity.taskId,
3712
+ contextId: identity.contextId,
3713
+ status: { state, timestamp: identity.timestamp },
3714
+ artifacts: [{ artifactId: identity.artifactId, ...artifact }]
3715
+ };
3716
+ }
3717
+ function completedTask(outcome, identity) {
3718
+ return task(identity, A2A_TASK_STATE_COMPLETED, {
3719
+ name: outcome.resourceId,
3720
+ parts: [{ data: dataPayload(outcome.body), mediaType: A2A_JSON_MEDIA_TYPE }],
3721
+ // Same meta key MCP attaches its summary under, so a buyer reads the
3722
+ // record of their own purchase the same way on either protocol
3723
+ metadata: { [DELIVERY_SUMMARY_META_KEY]: { ...toDeliverySummary(outcome) } }
3724
+ });
3725
+ }
3726
+ function paymentRequiredTask(outcome, identity) {
3727
+ return task(identity, A2A_TASK_STATE_FAILED, {
3728
+ name: outcome.resourceId,
3729
+ parts: [{ data: { ...toPaymentRequiredEnvelope(outcome) }, mediaType: A2A_JSON_MEDIA_TYPE }]
3730
+ });
3731
+ }
3732
+ function failedTask(error, identity) {
3733
+ return task(identity, A2A_TASK_STATE_FAILED, {
3734
+ parts: [{ data: { ...toErrorEnvelope(error) }, mediaType: A2A_JSON_MEDIA_TYPE }]
3735
+ });
3736
+ }
3737
+
3738
+ // src/protocols/a2a/adapter.ts
3739
+ var MAX_REQUEST_BODY_BYTES = 256 * 1024;
3740
+ var DEFAULT_AGENT_DESCRIPTION = "Agent Commerce Gateway: canonical commerce resources exposed as A2A skills.";
3741
+ var A2aProtocolAdapter = class {
3742
+ name = "a2a";
3743
+ mountPath;
3744
+ descriptor;
3745
+ additionalHttpRoutes;
3746
+ agentName;
3747
+ agentDescription;
3748
+ agentVersion;
3749
+ context;
3750
+ started = false;
3751
+ // The card lists only a2a-exposed resources, but a caller can name any id,
3752
+ // so the adapter refuses another protocol's resource itself
3753
+ skillsById = /* @__PURE__ */ new Map();
3754
+ // Built once at start: resources are fixed at config load, and a per-request
3755
+ // build would be work an unauthenticated GET could trigger at will
3756
+ card;
3757
+ constructor(options = {}) {
3758
+ this.mountPath = options.mountPath ?? A2A_DEFAULT_MOUNT_PATH;
3759
+ this.agentName = options.agentName ?? A2A_DEFAULT_AGENT_NAME;
3760
+ this.agentDescription = options.agentDescription ?? DEFAULT_AGENT_DESCRIPTION;
3761
+ this.agentVersion = options.agentVersion ?? PACKAGE_VERSION;
3762
+ this.descriptor = buildDescriptor(PACKAGE_VERSION);
3763
+ this.additionalHttpRoutes = [
3764
+ {
3765
+ method: "GET",
3766
+ path: A2A_AGENT_CARD_PATH,
3767
+ handleHttp: (req, res) => this.handleAgentCard(req, res)
3768
+ }
3769
+ ];
3770
+ }
3771
+ async start(context) {
3772
+ this.context = context;
3773
+ this.started = false;
3774
+ let resources = [];
3775
+ try {
3776
+ resources = context.resources.listExposedVia("a2a");
3777
+ } catch (err) {
3778
+ context.logger.error(
3779
+ { err: toLogInfo(err) },
3780
+ "a2a adapter: failed to list a2a-exposed resources"
3781
+ );
3782
+ resources = [];
3783
+ }
3784
+ this.skillsById = new Map(resources.map((resource) => [resource.id, resource]));
3785
+ this.card = buildAgentCard({
3786
+ name: this.agentName,
3787
+ description: this.agentDescription,
3788
+ version: this.agentVersion,
3789
+ publicBaseUrl: context.publicBaseUrl,
3790
+ mountPath: this.mountPath,
3791
+ resources
3792
+ });
3793
+ this.started = true;
3794
+ context.logger.info(
3795
+ { mountPath: this.mountPath, cardPath: A2A_AGENT_CARD_PATH, skillCount: resources.length },
3796
+ "a2a adapter started"
3797
+ );
3798
+ }
3799
+ /** `GET /.well-known/agent-card.json` */
3800
+ async handleAgentCard(req, res) {
3801
+ try {
3802
+ if (!this.started || this.card === void 0) {
3803
+ this.writeJson(res, 503, { error: "A2A adapter is not running." });
3804
+ return;
3805
+ }
3806
+ if (req.method !== "GET") {
3807
+ this.writeJson(res, 405, { error: "Method not allowed. The Agent Card is read-only." });
3808
+ return;
3809
+ }
3810
+ this.writeJson(res, 200, this.card);
3811
+ } catch (err) {
3812
+ this.context?.logger.error({ err: toLogInfo(err) }, "a2a adapter: agent card request failed");
3813
+ this.writeJson(res, 500, { error: "Internal server error." });
3814
+ }
3815
+ }
3816
+ /** `POST <mountPath>`: the A2A JSON-RPC endpoint */
3817
+ async handleHttp(req, res) {
3818
+ try {
3819
+ if (!this.started) {
3820
+ this.writeJson(
3821
+ res,
3822
+ 503,
3823
+ jsonRpcError(null, JSONRPC_INTERNAL_ERROR, "A2A adapter is not running.")
3824
+ );
3825
+ return;
3826
+ }
3827
+ if (req.method !== "POST") {
3828
+ this.writeJson(
3829
+ res,
3830
+ 405,
3831
+ jsonRpcError(
3832
+ null,
3833
+ JSONRPC_INVALID_REQUEST,
3834
+ "Method not allowed. This endpoint only accepts POST."
3835
+ )
3836
+ );
3837
+ return;
3838
+ }
3839
+ const version = req.headers[A2A_VERSION_HEADER];
3840
+ const declared = Array.isArray(version) ? version[0] : version;
3841
+ if (declared !== A2A_PROTOCOL_VERSION) {
3842
+ this.writeJson(
3843
+ res,
3844
+ 200,
3845
+ jsonRpcError(
3846
+ null,
3847
+ A2A_ERROR_UNSUPPORTED_OPERATION,
3848
+ `Unsupported A2A protocol version. Send the ${A2A_VERSION_HEADER} header with "${A2A_PROTOCOL_VERSION}".`
3849
+ )
3850
+ );
3851
+ return;
3852
+ }
3853
+ const read = await readCappedBody(req, MAX_REQUEST_BODY_BYTES);
3854
+ if (read.kind !== "ok") {
3855
+ this.writeJson(
3856
+ res,
3857
+ 200,
3858
+ jsonRpcError(null, JSONRPC_PARSE_ERROR, "Could not read the request body.")
3859
+ );
3860
+ return;
3861
+ }
3862
+ const parsed = parseJsonRpcRequest(read.text);
3863
+ if (!parsed.ok) {
3864
+ this.writeJson(res, 200, jsonRpcError(parsed.id, parsed.error.code, parsed.error.message));
3865
+ return;
3866
+ }
3867
+ this.writeJson(
3868
+ res,
3869
+ 200,
3870
+ await this.dispatch(parsed.request.id, parsed.request.method, parsed.request.params)
3871
+ );
3872
+ } catch (err) {
3873
+ this.context?.logger.error({ err: toLogInfo(err) }, "a2a adapter: request handling failed");
3874
+ this.writeJson(
3875
+ res,
3876
+ 200,
3877
+ jsonRpcError(null, JSONRPC_INTERNAL_ERROR, "Internal server error.")
3878
+ );
3879
+ }
3880
+ }
3881
+ async dispatch(id, method, params) {
3882
+ if (A2A_UNSUPPORTED_METHODS.includes(method)) {
3883
+ return jsonRpcError(
3884
+ id,
3885
+ A2A_ERROR_UNSUPPORTED_OPERATION,
3886
+ `A2A method "${method}" is not supported by this deployment.`
3887
+ );
3888
+ }
3889
+ if (method !== A2A_METHOD_SEND_MESSAGE) {
3890
+ return jsonRpcError(id, JSONRPC_METHOD_NOT_FOUND, `Unknown method "${method}".`);
3891
+ }
3892
+ let invocation;
3893
+ try {
3894
+ invocation = parseInvocation(params);
3895
+ } catch (err) {
3896
+ const error = toCommerceError(err);
3897
+ const code = error.code === "PROTOCOL_UNSUPPORTED" ? A2A_ERROR_UNSUPPORTED_OPERATION : JSONRPC_INVALID_PARAMS;
3898
+ return jsonRpcError(id, code, error.message);
3899
+ }
3900
+ return this.execute(id, invocation);
3901
+ }
3902
+ // One accepted invocation, one `pipeline.execute()`
3903
+ async execute(id, invocation) {
3904
+ const context = this.context;
3905
+ if (context === void 0) {
3906
+ return jsonRpcError(id, JSONRPC_INTERNAL_ERROR, "A2A adapter is not running.");
3907
+ }
3908
+ const resource = this.skillsById.get(invocation.resourceId);
3909
+ if (resource === void 0) {
3910
+ return this.taskResult(
3911
+ id,
3912
+ failedTask(
3913
+ new CommerceError(
3914
+ "RESOURCE_NOT_FOUND",
3915
+ `Unknown canonical resource "${invocation.resourceId}".`
3916
+ ),
3917
+ this.taskIdentity(context, context.ids.next("a2a"))
3918
+ )
3919
+ );
3920
+ }
3921
+ const requestId = context.ids.next("a2a");
3922
+ const identity = this.taskIdentity(context, requestId);
3923
+ try {
3924
+ const { input, payment, authorization } = extractReservedInputFields(
3925
+ invocation.input,
3926
+ resource,
3927
+ requestId
3928
+ );
3929
+ const request = {
3930
+ requestId,
3931
+ resourceId: invocation.resourceId,
3932
+ input,
3933
+ protocol: "a2a",
3934
+ receivedAt: context.clock.nowIso(),
3935
+ ...payment !== void 0 ? { payment } : {},
3936
+ ...authorization !== void 0 ? { authorization } : {}
3937
+ };
3938
+ const outcome = await context.pipeline.execute(request);
3939
+ return this.taskResult(
3940
+ id,
3941
+ outcome.kind === "payment-required" ? paymentRequiredTask(outcome, identity) : completedTask(outcome, identity)
3942
+ );
3943
+ } catch (err) {
3944
+ const error = toCommerceError(err);
3945
+ context.logger.warn(
3946
+ { resourceId: invocation.resourceId, requestId, err: toLogInfo(error) },
3947
+ "a2a adapter: execution failed"
3948
+ );
3949
+ return this.taskResult(id, failedTask(error, identity));
3950
+ }
3951
+ }
3952
+ taskIdentity(context, requestId) {
3953
+ return {
3954
+ taskId: requestId,
3955
+ contextId: context.ids.next("a2a-ctx"),
3956
+ artifactId: context.ids.next("a2a-artifact"),
3957
+ timestamp: context.clock.nowIso()
3958
+ };
3959
+ }
3960
+ // A2A's JSON-RPC result wraps the terminal task
3961
+ taskResult(id, task2) {
3962
+ return jsonRpcResult(id, { task: task2 });
3963
+ }
3964
+ async health() {
3965
+ const checkedAt = this.context?.clock.nowIso() ?? (/* @__PURE__ */ new Date()).toISOString();
3966
+ if (!this.started || this.card === void 0) {
3967
+ return { status: "fail", detail: "A2A adapter has not been started.", checkedAt };
3968
+ }
3969
+ return { status: "pass", detail: `${this.skillsById.size} skill(s) published.`, checkedAt };
3970
+ }
3971
+ async stop() {
3972
+ this.started = false;
3973
+ this.card = void 0;
3974
+ this.skillsById = /* @__PURE__ */ new Map();
3975
+ this.context = void 0;
3976
+ }
3977
+ writeJson(res, status, body) {
3978
+ if (res.headersSent) return;
3979
+ res.writeHead(status, { "content-type": A2A_JSON_MEDIA_TYPE });
3980
+ res.end(JSON.stringify(body));
3981
+ }
3982
+ };
3983
+ function createA2aAdapter(options = {}) {
3984
+ return new A2aProtocolAdapter(options);
3985
+ }
3986
+
3347
3987
  // src/protocols/acp/checkout-mapping.ts
3348
3988
  function toCanonicalRequest(options) {
3349
3989
  const { request, resourceId, requestId, receivedAt, idempotencyKey } = options;
@@ -3358,13 +3998,14 @@ function toCanonicalRequest(options) {
3358
3998
  }
3359
3999
  function canonicalInput(request) {
3360
4000
  const keys = ACP_OPERATION_INPUT_KEYS[request.route.operation];
4001
+ const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[request.route.operation] ?? [];
3361
4002
  const input = {};
3362
4003
  if (keys.includes("path") && request.route.sessionId !== void 0) {
3363
4004
  input["path"] = { checkout_session_id: request.route.sessionId };
3364
4005
  }
3365
4006
  if (keys.includes("body")) {
3366
4007
  input["body"] = request.body;
3367
- } else if (request.route.acceptsBody && Object.keys(request.body).length > 0) {
4008
+ } else if (optionalKeys.includes("body") && Object.keys(request.body).length > 0) {
3368
4009
  input["body"] = request.body;
3369
4010
  }
3370
4011
  return input;
@@ -3387,21 +4028,21 @@ var ACP_UNSUPPORTED = [
3387
4028
  "standalone orders service",
3388
4029
  "delegate_payment",
3389
4030
  "delegate_authentication",
3390
- // Bindings and delivery directions.
4031
+ // Bindings and delivery directions
3391
4032
  "ACP MCP transport binding",
3392
4033
  "webhooks",
3393
4034
  "outbound merchant-to-agent delivery",
3394
4035
  "ACP client role",
3395
- // Request authentication beyond the bearer token.
4036
+ // Request authentication beyond the bearer token
3396
4037
  "Signature header verification",
3397
4038
  "Timestamp replay-window verification",
3398
- // Extensions and versioning.
4039
+ // Extensions, versioning and payment handlers
3399
4040
  "discount extension",
3400
4041
  "general ACP extension framework",
3401
4042
  "multiple ACP API versions",
3402
4043
  "seller-backed payment handler integration"
3403
4044
  ];
3404
- function buildDescriptor(implementationVersion) {
4045
+ function buildDescriptor2(implementationVersion) {
3405
4046
  return {
3406
4047
  name: "acp",
3407
4048
  kind: "protocol",
@@ -3424,9 +4065,8 @@ function buildAcpDiscoveryDocument(options) {
3424
4065
  ...metadata.documentationUrl !== void 0 ? { documentation_url: metadata.documentationUrl } : {}
3425
4066
  },
3426
4067
  api_base_url: joinUrl(options.publicBaseUrl, options.mountPath),
3427
- // Only the REST binding. ACP's MCP transport is its own fixed checkout
3428
- // binding and is not the generic MCP adapter this gateway also serves;
3429
- // advertising it here because "we do MCP" would be a false claim.
4068
+ // REST only: ACP's MCP transport is a fixed checkout binding, not the
4069
+ // generic MCP adapter this gateway also serves
3430
4070
  transports: ["rest"],
3431
4071
  capabilities: {
3432
4072
  services: ["checkout"],
@@ -3436,9 +4076,6 @@ function buildAcpDiscoveryDocument(options) {
3436
4076
  }
3437
4077
  };
3438
4078
  }
3439
- function joinUrl(baseUrl, mountPath) {
3440
- return `${baseUrl.replace(/\/+$/, "")}/${mountPath.replace(/^\/+|\/+$/g, "")}`;
3441
- }
3442
4079
 
3443
4080
  // src/protocols/acp/errors.ts
3444
4081
  function acpFailure(status, type, code, message, extra = {}) {
@@ -3462,10 +4099,10 @@ function writeAcpFailure(res, failure, headers = {}) {
3462
4099
  writeAcpJson(res, failure.status, failure.error, headers);
3463
4100
  }
3464
4101
  function operationKey(scope) {
3465
- return sha256(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
4102
+ return sha2562(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
3466
4103
  }
3467
4104
  function requestFingerprint(body) {
3468
- return sha256(canonicalize(body));
4105
+ return sha2562(canonicalize(body));
3469
4106
  }
3470
4107
  function canonicalize(value) {
3471
4108
  if (value === null) return "null";
@@ -3476,7 +4113,7 @@ function canonicalize(value) {
3476
4113
  }
3477
4114
  return JSON.stringify(value) ?? "null";
3478
4115
  }
3479
- function sha256(value) {
4116
+ function sha2562(value) {
3480
4117
  return createHash("sha256").update(value, "utf8").digest("hex");
3481
4118
  }
3482
4119
 
@@ -3493,6 +4130,7 @@ function createAcpIdempotencyStore(options) {
3493
4130
  logger
3494
4131
  });
3495
4132
  migrate(db, logger);
4133
+ resolveOrphanedClaims(db, logger);
3496
4134
  const selectStmt = db.prepare(
3497
4135
  `SELECT state, fingerprint, status, body_json FROM acp_idempotency
3498
4136
  WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
@@ -3574,6 +4212,15 @@ function createAcpIdempotencyStore(options) {
3574
4212
  }
3575
4213
  };
3576
4214
  }
4215
+ function resolveOrphanedClaims(db, logger) {
4216
+ const { changes } = db.prepare("UPDATE acp_idempotency SET state = 'unresolved' WHERE state = 'in_flight'").run();
4217
+ if (changes > 0) {
4218
+ logger.warn(
4219
+ { unresolved: changes },
4220
+ "acp idempotency: marked claims left in flight by an earlier process as unresolved; reconcile these operations against the merchant before trusting a retry"
4221
+ );
4222
+ }
4223
+ }
3577
4224
  function migrate(db, logger) {
3578
4225
  const current = db.pragma("user_version", { simple: true });
3579
4226
  if (current >= SCHEMA_VERSION) return;
@@ -3608,17 +4255,6 @@ function migrate(db, logger) {
3608
4255
  });
3609
4256
  apply();
3610
4257
  }
3611
- var BEARER_PREFIX = /^Bearer +/i;
3612
- function isAuthorizedBearer(header2, expectedToken) {
3613
- if (header2 === void 0) return false;
3614
- if (!BEARER_PREFIX.test(header2)) return false;
3615
- const presented = header2.replace(BEARER_PREFIX, "");
3616
- if (presented.length === 0) return false;
3617
- return timingSafeEqual(sha2562(presented), sha2562(expectedToken));
3618
- }
3619
- function sha2562(value) {
3620
- return createHash("sha256").update(value, "utf8").digest();
3621
- }
3622
4258
 
3623
4259
  // src/protocols/acp/router.ts
3624
4260
  var MAX_SESSION_ID_LENGTH = 128;
@@ -7624,12 +8260,11 @@ var validators;
7624
8260
  function acpValidators() {
7625
8261
  if (validators !== void 0) return validators;
7626
8262
  const ajv = new Ajv2020({
7627
- // The vendored document is the released spec: annotations we do not know
7628
- // are the spec's business, not a reason to refuse to start.
8263
+ // The vendored document is the released spec; unknown annotations in it
8264
+ // are no reason to refuse to start
7629
8265
  strict: false,
7630
- // One failure is all that leaves the adapter, and collecting every error
7631
- // in a deeply nested cart is work an unauthenticated caller could ask for
7632
- // repeatedly.
8266
+ // Only one failure leaves the adapter, and collecting every error in a
8267
+ // deeply nested cart is work a caller could request repeatedly
7633
8268
  allErrors: false
7634
8269
  });
7635
8270
  addFormats(ajv);
@@ -7711,7 +8346,7 @@ async function guardAcpRequest(req, options) {
7711
8346
  );
7712
8347
  }
7713
8348
  const route = matched.route;
7714
- if (!isAuthorizedBearer(header(req, "authorization"), options.token)) {
8349
+ if (!options.isAuthorized(header(req, "authorization"))) {
7715
8350
  return failed(
7716
8351
  acpFailure(
7717
8352
  401,
@@ -7769,7 +8404,7 @@ async function guardAcpRequest(req, options) {
7769
8404
  }
7770
8405
  idempotencyKey = presented;
7771
8406
  }
7772
- const read = await readBody(req, options.maxBodyBytes ?? ACP_MAX_REQUEST_BODY_BYTES);
8407
+ const read = await readCappedBody(req, options.maxBodyBytes ?? ACP_MAX_REQUEST_BODY_BYTES);
7773
8408
  if (read.kind === "too-large") {
7774
8409
  return failed(
7775
8410
  acpFailure(
@@ -7867,21 +8502,6 @@ function header(req, name) {
7867
8502
  const value = req.headers[name];
7868
8503
  return Array.isArray(value) ? value[0] : value;
7869
8504
  }
7870
- async function readBody(req, maxBytes) {
7871
- const chunks = [];
7872
- let total = 0;
7873
- try {
7874
- for await (const chunk of req) {
7875
- const buffer = chunk;
7876
- total += buffer.length;
7877
- if (total > maxBytes) return { kind: "too-large" };
7878
- chunks.push(buffer);
7879
- }
7880
- } catch {
7881
- return { kind: "unreadable" };
7882
- }
7883
- return { kind: "ok", text: Buffer.concat(chunks).toString("utf8") };
7884
- }
7885
8505
 
7886
8506
  // src/protocols/acp/response-mapping.ts
7887
8507
  var ACP_SUCCESS_STATUS = {
@@ -8039,15 +8659,12 @@ var AcpProtocolAdapter = class {
8039
8659
  mountPath;
8040
8660
  descriptor;
8041
8661
  additionalHttpRoutes;
8042
- token;
8662
+ isAuthorized;
8043
8663
  operations;
8044
8664
  idempotencyOptions;
8045
8665
  /**
8046
- * What an idempotency claim is scoped to, from `context.publicBaseUrl`.
8047
- *
8048
- * Not readonly because, unlike the token, it is known only once the adapter
8049
- * starts. Worth that: scoping by the credential meant a rotation freed
8050
- * every outstanding claim.
8666
+ * The deployment an idempotency claim is scoped to, `context.publicBaseUrl`.
8667
+ * Not readonly because it is known only once the adapter starts.
8051
8668
  */
8052
8669
  deployment;
8053
8670
  discoveryMetadata;
@@ -8055,17 +8672,16 @@ var AcpProtocolAdapter = class {
8055
8672
  resources = /* @__PURE__ */ new Map();
8056
8673
  context;
8057
8674
  started = false;
8058
- // Built once at start: the document is fixed by config, and rebuilding it
8059
- // per request would let an unauthenticated GET do work a caller controls the
8060
- // cost of.
8675
+ // Built once at start: the document is fixed by config, and a per-request
8676
+ // build would be work an unauthenticated GET could trigger at will
8061
8677
  discovery;
8062
8678
  constructor(options) {
8063
8679
  this.mountPath = options.mountPath;
8064
- this.token = options.token;
8680
+ this.isAuthorized = createBearerCheck(options.token);
8065
8681
  this.operations = options.operations;
8066
8682
  this.idempotencyOptions = options.idempotency;
8067
8683
  this.discoveryMetadata = options.discovery;
8068
- this.descriptor = buildDescriptor(PACKAGE_VERSION);
8684
+ this.descriptor = buildDescriptor2(PACKAGE_VERSION);
8069
8685
  this.additionalHttpRoutes = [
8070
8686
  {
8071
8687
  method: "GET",
@@ -8082,12 +8698,12 @@ var AcpProtocolAdapter = class {
8082
8698
  mountPath: this.mountPath,
8083
8699
  ...this.discoveryMetadata !== void 0 ? { metadata: this.discoveryMetadata } : {}
8084
8700
  });
8085
- const invalid = validateAcpDocument("discoveryResponse", document);
8086
- if (invalid !== void 0) {
8701
+ const invalid2 = validateAcpDocument("discoveryResponse", document);
8702
+ if (invalid2 !== void 0) {
8087
8703
  throw new CommerceError(
8088
8704
  "CONFIG_INVALID",
8089
- `ACP discovery document does not conform to the ${this.descriptor.supportedSpec} snapshot at "${invalid.path ?? "$"}" (${invalid.code}) - check protocols.acp.discovery`,
8090
- { details: { path: invalid.path ?? "$", code: invalid.code } }
8705
+ `ACP discovery document does not conform to the ${this.descriptor.supportedSpec} snapshot at "${invalid2.path ?? "$"}" (${invalid2.code}) - check protocols.acp.discovery`,
8706
+ { details: { path: invalid2.path ?? "$", code: invalid2.code } }
8091
8707
  );
8092
8708
  }
8093
8709
  const exposed = new Map(
@@ -8124,7 +8740,7 @@ var AcpProtocolAdapter = class {
8124
8740
  "acp adapter started"
8125
8741
  );
8126
8742
  }
8127
- /** `GET /.well-known/acp.json` - public, unauthenticated, cacheable. */
8743
+ /** `GET /.well-known/acp.json`: public, unauthenticated, cacheable */
8128
8744
  async handleDiscovery(req, res) {
8129
8745
  try {
8130
8746
  if (!this.started || this.discovery === void 0) {
@@ -8156,7 +8772,7 @@ var AcpProtocolAdapter = class {
8156
8772
  this.fail(res, err, "acp adapter: discovery request failed");
8157
8773
  }
8158
8774
  }
8159
- /** `<mountPath>/checkout_sessions...` - the five stable checkout routes. */
8775
+ /** `<mountPath>/checkout_sessions...`: the five stable checkout routes */
8160
8776
  async handleHttp(req, res) {
8161
8777
  try {
8162
8778
  if (!this.started) {
@@ -8173,7 +8789,7 @@ var AcpProtocolAdapter = class {
8173
8789
  }
8174
8790
  const guarded = await guardAcpRequest(req, {
8175
8791
  mountPath: this.mountPath,
8176
- token: this.token
8792
+ isAuthorized: this.isAuthorized
8177
8793
  });
8178
8794
  if (!guarded.ok) {
8179
8795
  writeAcpFailure(res, guarded);
@@ -8187,12 +8803,9 @@ var AcpProtocolAdapter = class {
8187
8803
  }
8188
8804
  }
8189
8805
  /**
8190
- * One request, at most one execution.
8191
- *
8192
- * A body-bearing route claims its idempotency key *before* the work starts,
8193
- * so a retry that arrives while the first one is still running finds the
8194
- * claim instead of starting a second checkout. A route with no key (the GET)
8195
- * is read-only and needs none.
8806
+ * One request, at most one execution. A body-bearing route claims its
8807
+ * idempotency key before the work starts, so a retry arriving meanwhile finds
8808
+ * the claim; the read-only GET carries no key.
8196
8809
  */
8197
8810
  async dispatch(request) {
8198
8811
  const store = this.idempotency;
@@ -8253,13 +8866,7 @@ var AcpProtocolAdapter = class {
8253
8866
  }
8254
8867
  return outcome.response;
8255
8868
  }
8256
- /**
8257
- * One accepted request, one `pipeline.execute()`.
8258
- *
8259
- * Nothing here prices a resource, inspects a payment proof or calls a
8260
- * merchant backend: the adapter builds a canonical request and reads back
8261
- * what the pipeline decided.
8262
- */
8869
+ // One accepted request, one `pipeline.execute()`
8263
8870
  async runCheckout(request, idempotencyKey) {
8264
8871
  const context = this.context;
8265
8872
  const resource = this.resources.get(request.route.operation);
@@ -8311,7 +8918,7 @@ var AcpProtocolAdapter = class {
8311
8918
  resourceId: resource.id,
8312
8919
  requestId: canonical.requestId,
8313
8920
  reached: reachedFor(error.code),
8314
- err: error.toInfo()
8921
+ err: toLogInfo(error)
8315
8922
  },
8316
8923
  "acp adapter: checkout execution failed"
8317
8924
  );
@@ -8321,10 +8928,7 @@ var AcpProtocolAdapter = class {
8321
8928
  };
8322
8929
  }
8323
8930
  }
8324
- /**
8325
- * Protocol-owned headers only. A merchant backend's own headers are never
8326
- * proxied onto an ACP response.
8327
- */
8931
+ // Protocol-owned headers only; a merchant backend's headers are never proxied
8328
8932
  responseHeaders(request, response) {
8329
8933
  const replayed = response?.replayed === true;
8330
8934
  return {
@@ -8335,7 +8939,7 @@ var AcpProtocolAdapter = class {
8335
8939
  };
8336
8940
  }
8337
8941
  fail(res, err, message) {
8338
- this.context?.logger.error({ err: toCommerceError(err).toInfo() }, message);
8942
+ this.context?.logger.error({ err: toLogInfo(err) }, message);
8339
8943
  writeAcpFailure(
8340
8944
  res,
8341
8945
  acpFailure(500, "processing_error", "internal_error", "Internal server error.")
@@ -8398,8 +9002,8 @@ function receiptToRow(receipt) {
8398
9002
  payment_json: receipt.payment !== void 0 ? JSON.stringify(redact(receipt.payment)) : null,
8399
9003
  delivered_at: receipt.deliveredAt,
8400
9004
  backend_status: receipt.backendStatus,
8401
- duration_ms: receipt.durationMs !== void 0 ? receipt.durationMs : null,
8402
- protocol: receipt.protocol !== void 0 ? receipt.protocol : null,
9005
+ duration_ms: receipt.durationMs ?? null,
9006
+ protocol: receipt.protocol ?? null,
8403
9007
  metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
8404
9008
  authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
8405
9009
  };
@@ -8426,12 +9030,12 @@ function eventToRow(event) {
8426
9030
  id: event.id,
8427
9031
  type: event.type,
8428
9032
  request_id: event.requestId,
8429
- resource_id: event.resourceId !== void 0 ? event.resourceId : null,
9033
+ resource_id: event.resourceId ?? null,
8430
9034
  at: event.at,
8431
- adapter: event.adapter !== void 0 ? event.adapter : null,
8432
- payment_provider: event.paymentProvider !== void 0 ? event.paymentProvider : null,
8433
- duration_ms: event.durationMs !== void 0 ? event.durationMs : null,
8434
- status: event.status !== void 0 ? event.status : null,
9035
+ adapter: event.adapter ?? null,
9036
+ payment_provider: event.paymentProvider ?? null,
9037
+ duration_ms: event.durationMs ?? null,
9038
+ status: event.status ?? null,
8435
9039
  data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null
8436
9040
  };
8437
9041
  }
@@ -8535,7 +9139,7 @@ var MIGRATIONS = [
8535
9139
  }
8536
9140
  }
8537
9141
  ];
8538
- var SCHEMA_VERSION2 = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
9142
+ var SCHEMA_VERSION2 = MIGRATIONS.at(-1)?.version ?? 0;
8539
9143
  function migrate2(db) {
8540
9144
  const current = db.pragma("user_version", { simple: true });
8541
9145
  for (const migration of MIGRATIONS) {
@@ -8598,10 +9202,8 @@ function createSqliteReceiptStore(options) {
8598
9202
  );
8599
9203
  const insertPaymentAttemptStmt = db.prepare(
8600
9204
  `INSERT INTO payment_attempts (id, request_id, resource_id, provider, replay_key, status, amount, currency, payer, payee, external_reference, rejection_reason, created_at, updated_at)
8601
- VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)`
8602
- );
8603
- const getPaymentAttemptByIdStmt = db.prepare(
8604
- "SELECT * FROM payment_attempts WHERE id = ?"
9205
+ VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)
9206
+ RETURNING *`
8605
9207
  );
8606
9208
  const updatePaymentAttemptStmt = db.prepare(
8607
9209
  `UPDATE payment_attempts SET status = @status,
@@ -8652,8 +9254,9 @@ function createSqliteReceiptStore(options) {
8652
9254
  async reservePaymentAttempt(reservation) {
8653
9255
  const now = clock.nowIso();
8654
9256
  const id = ids.next("attempt");
9257
+ let row;
8655
9258
  try {
8656
- insertPaymentAttemptStmt.run({
9259
+ row = insertPaymentAttemptStmt.get({
8657
9260
  id,
8658
9261
  request_id: reservation.requestId,
8659
9262
  resource_id: reservation.resourceId,
@@ -8673,19 +9276,12 @@ function createSqliteReceiptStore(options) {
8673
9276
  if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
8674
9277
  throw new CommerceError(
8675
9278
  "PAYMENT_REPLAYED",
8676
- `Payment authorisation has already been used (replayKey=${reservation.replayKey})`,
9279
+ `Payment authorization has already been used (replayKey=${reservation.replayKey})`,
8677
9280
  { requestId: reservation.requestId, resourceId: reservation.resourceId }
8678
9281
  );
8679
9282
  }
8680
9283
  throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");
8681
9284
  }
8682
- const row = getPaymentAttemptByIdStmt.get(id);
8683
- if (row === void 0) {
8684
- throw new CommerceError(
8685
- "STORAGE_ERROR",
8686
- "Payment attempt vanished immediately after insert"
8687
- );
8688
- }
8689
9285
  return rowToPaymentAttempt(row);
8690
9286
  },
8691
9287
  async updatePaymentAttempt(update) {
@@ -8723,11 +9319,11 @@ function createSqliteReceiptStore(options) {
8723
9319
  const rows = listOptions.requestId !== void 0 ? listReceiptsByRequestStmt.all(listOptions.requestId, limit) : listReceiptsStmt.all(limit);
8724
9320
  return rows.map(rowToReceipt);
8725
9321
  },
8726
- /** Exact total, ignoring the listReceipts clamp — see the interface doc for why this exists. */
9322
+ // Exact total, not bounded by the listReceipts clamp (see ReceiptStore.countReceipts)
8727
9323
  async countReceipts() {
8728
9324
  return countReceiptsStmt.get()?.count ?? 0;
8729
9325
  },
8730
- /** Exact count of non-2xx receipts — see the interface doc for why this exists. */
9326
+ // Exact count of non-2xx receipts (see ReceiptStore.countUndeliveredReceipts)
8731
9327
  async countUndeliveredReceipts() {
8732
9328
  return countUndeliveredReceiptsStmt.get()?.count ?? 0;
8733
9329
  },
@@ -8806,6 +9402,6 @@ function createSqliteReceiptStore(options) {
8806
9402
  return store;
8807
9403
  }
8808
9404
 
8809
- export { ACP_API_VERSION, ACP_SPEC_VERSION, ACP_WELL_KNOWN_PATH, createAcpAdapter, createGateway, createSqliteReceiptStore, loadConfig, parseConfig, createSqliteReceiptStore as receipts };
9405
+ export { A2A_AGENT_CARD_PATH, A2A_PROTOCOL_VERSION, A2A_SPEC_VERSION, ACP_API_VERSION, ACP_SPEC_VERSION, ACP_WELL_KNOWN_PATH, createA2aAdapter, createAcpAdapter, createGateway, createSqliteReceiptStore, loadConfig, parseConfig, createSqliteReceiptStore as receipts };
8810
9406
  //# sourceMappingURL=index.js.map
8811
9407
  //# sourceMappingURL=index.js.map