@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.
- package/README.md +168 -183
- package/dist/ap2.d.ts +21 -24
- package/dist/ap2.js +68 -82
- package/dist/ap2.js.map +1 -1
- package/dist/authorization-BpbWGGrf.d.ts +115 -0
- package/dist/{chunk-NWIQLJNU.js → chunk-74BSEXB5.js} +37 -34
- package/dist/chunk-74BSEXB5.js.map +1 -0
- package/dist/chunk-AFI2CEZS.js +10 -0
- package/dist/chunk-AFI2CEZS.js.map +1 -0
- package/dist/chunk-ATTWSFQP.js +680 -0
- package/dist/chunk-ATTWSFQP.js.map +1 -0
- package/dist/{chunk-ZEGFLRGV.js → chunk-IVJC5YLE.js} +63 -26
- package/dist/chunk-IVJC5YLE.js.map +1 -0
- package/dist/{chunk-GF6KHHKL.js → chunk-RAJGPGJG.js} +5 -8
- package/dist/chunk-RAJGPGJG.js.map +1 -0
- package/dist/chunk-V2V2E7HX.js +27 -0
- package/dist/chunk-V2V2E7HX.js.map +1 -0
- package/dist/cli/index.js +812 -573
- package/dist/cli/index.js.map +1 -1
- package/dist/constants-Bkq04FRZ.d.ts +48 -0
- package/dist/guardrails-71XwtXlT.d.ts +31 -0
- package/dist/index.d.ts +245 -265
- package/dist/index.js +1222 -626
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +6 -59
- package/dist/mcp.js +64 -53
- package/dist/mcp.js.map +1 -1
- package/dist/mpp.d.ts +50 -0
- package/dist/mpp.js +353 -0
- package/dist/mpp.js.map +1 -0
- package/dist/protocol-adapter-DTkrXTev.d.ts +238 -0
- package/dist/runtime-CiYbgGpi.d.ts +303 -0
- package/dist/{types-CJtwLu1c.d.ts → types-O6lg3Q5V.d.ts} +17 -23
- package/dist/x402.d.ts +43 -75
- package/dist/x402.js +13 -693
- package/dist/x402.js.map +1 -1
- package/package.json +13 -2
- package/dist/authorization-CPDcpJVa.d.ts +0 -123
- package/dist/chunk-GF6KHHKL.js.map +0 -1
- package/dist/chunk-NWIQLJNU.js.map +0 -1
- package/dist/chunk-ZEGFLRGV.js.map +0 -1
- package/dist/guardrails-Dmwi63KB.d.ts +0 -38
- package/dist/protocol-adapter-Bebmggf_.d.ts +0 -273
- 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-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 =
|
|
120
|
+
const headers = buildHeaders(handler, context);
|
|
130
121
|
if (request.idempotencyKey !== void 0) {
|
|
131
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
|
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 =
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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 (
|
|
321
|
-
throw new CommerceError("INPUT_INVALID", `Path parameter "${
|
|
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:
|
|
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
|
|
339
|
-
|
|
340
|
-
|
|
324
|
+
function buildHeaders(handler, context) {
|
|
325
|
+
try {
|
|
326
|
+
return new Headers(handler.headers);
|
|
327
|
+
} catch {
|
|
328
|
+
throw invalidHeaderError(context);
|
|
329
|
+
}
|
|
341
330
|
}
|
|
342
|
-
function
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
517
|
-
if (types !== void 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) &&
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
1122
|
+
var RESERVED_INPUT_KEYS = [...RESERVED_INPUT_FIELDS, "__proto__"];
|
|
1166
1123
|
function stripReservedFields(input) {
|
|
1167
|
-
if (
|
|
1168
|
-
|
|
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(
|
|
1173
|
-
if (!RESERVED_INPUT_KEYS.
|
|
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
|
-
|
|
1224
|
+
// Shared secret gating /api/receipts and /api/events. Without it both
|
|
1225
|
+
// routes 404.
|
|
1256
1226
|
adminToken: z.string().min(1).optional(),
|
|
1257
|
-
|
|
1258
|
-
//
|
|
1259
|
-
// `Origin` header, so `"*"` and a trailing slash
|
|
1260
|
-
//
|
|
1261
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
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
|
|
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
|
|
1403
|
-
//
|
|
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
|
-
|
|
1383
|
+
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
1421
1384
|
allowMainnet: BooleanOrString.optional(),
|
|
1422
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
1540
|
+
var SUPPORTED_PAYMENT_METHODS = new Set(PAYMENT_METHOD_NAMES);
|
|
1563
1541
|
var SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
|
|
1564
|
-
function
|
|
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:
|
|
1553
|
+
acp: normalizeAcp(raw.protocols.acp)
|
|
1576
1554
|
};
|
|
1577
1555
|
validateMountPaths(protocols);
|
|
1578
1556
|
const x402Raw = raw.payments.x402;
|
|
1579
|
-
const
|
|
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
|
|
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]) =>
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 =
|
|
1678
|
+
const mandateIssuers = normalizeIssuers(
|
|
1705
1679
|
raw.trust?.mandateIssuers,
|
|
1706
1680
|
"authorization.ap2.trust.mandateIssuers"
|
|
1707
1681
|
);
|
|
1708
|
-
const checkoutIssuers =
|
|
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
|
|
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
|
|
1785
|
+
const normalized = {};
|
|
1812
1786
|
for (const member of AP2_JWK_MEMBERS) {
|
|
1813
|
-
if (jwk[member] !== void 0)
|
|
1787
|
+
if (jwk[member] !== void 0) normalized[member] = value(member);
|
|
1814
1788
|
}
|
|
1815
|
-
return
|
|
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
|
|
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
|
|
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:
|
|
1900
|
-
// Built key by key
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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}"
|
|
2104
|
-
{ details: { path: `resources.${id}.
|
|
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 =
|
|
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 (
|
|
2194
|
+
if (isRecord(properties)) {
|
|
2141
2195
|
result["properties"] = Object.fromEntries(
|
|
2142
2196
|
Object.entries(properties).map(([key, sub]) => [
|
|
2143
2197
|
key,
|
|
2144
|
-
|
|
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 (
|
|
2207
|
+
if (isRecord(items)) {
|
|
2154
2208
|
result["items"] = defaultClosedObjectSchema(items);
|
|
2155
2209
|
}
|
|
2156
2210
|
const additional = schema["additionalProperties"];
|
|
2157
|
-
if (
|
|
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 =
|
|
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 (
|
|
2279
|
+
if (isRecord(properties)) {
|
|
2229
2280
|
for (const [key, sub] of Object.entries(properties)) {
|
|
2230
|
-
if (
|
|
2281
|
+
if (isRecord(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
|
|
2231
2282
|
}
|
|
2232
2283
|
}
|
|
2233
2284
|
const additional = schema["additionalProperties"];
|
|
2234
|
-
if (
|
|
2285
|
+
if (isRecord(additional)) {
|
|
2235
2286
|
validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
|
|
2236
2287
|
}
|
|
2237
2288
|
const items = schema["items"];
|
|
2238
|
-
if (
|
|
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,
|
|
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 (
|
|
2315
|
+
if (decimals !== void 0) {
|
|
2265
2316
|
const fractionalDigits = amount.includes(".") ? amount.split(".")[1]?.length ?? 0 : 0;
|
|
2266
|
-
if (fractionalDigits >
|
|
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 (${
|
|
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 =
|
|
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" &&
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
2467
|
-
//
|
|
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
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2582
|
-
|
|
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
|
-
|
|
2592
|
-
|
|
2593
|
-
var
|
|
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 (
|
|
2639
|
+
if (isValidAdminToken === void 0) {
|
|
2623
2640
|
reply.status(404).send();
|
|
2624
2641
|
return;
|
|
2625
2642
|
}
|
|
2626
|
-
if (!isValidAdminToken(request.headers.authorization
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
2976
|
+
async function probe(target, kind, details, options) {
|
|
2881
2977
|
let health;
|
|
2978
|
+
let threw = false;
|
|
2882
2979
|
try {
|
|
2883
|
-
health = await
|
|
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,
|
|
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
|
-
{
|
|
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
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
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
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
)
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
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
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
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
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
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:
|
|
2961
|
-
|
|
2962
|
-
|
|
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
|
|
2971
|
-
|
|
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
|
-
|
|
3000
|
-
const adapters =
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
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
|
-
//
|
|
3030
|
-
// a
|
|
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
|
|
3035
|
-
//
|
|
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
|
-
|
|
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
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
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 (
|
|
3206
|
-
const
|
|
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(
|
|
3266
|
-
|
|
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
|
|
3364
|
+
events,
|
|
3274
3365
|
logger,
|
|
3275
3366
|
clock,
|
|
3276
3367
|
ids
|
|
3277
3368
|
});
|
|
3278
3369
|
const server = Fastify({
|
|
3279
|
-
//
|
|
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: (
|
|
3283
|
-
|
|
3284
|
-
const
|
|
3285
|
-
return
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
//
|
|
3428
|
-
//
|
|
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
|
|
4102
|
+
return sha2562(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
|
|
3466
4103
|
}
|
|
3467
4104
|
function requestFingerprint(body) {
|
|
3468
|
-
return
|
|
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
|
|
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
|
|
7628
|
-
// are
|
|
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
|
-
//
|
|
7631
|
-
//
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
8662
|
+
isAuthorized;
|
|
8043
8663
|
operations;
|
|
8044
8664
|
idempotencyOptions;
|
|
8045
8665
|
/**
|
|
8046
|
-
*
|
|
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
|
|
8059
|
-
//
|
|
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.
|
|
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 =
|
|
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
|
|
8086
|
-
if (
|
|
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 "${
|
|
8090
|
-
{ details: { path:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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:
|
|
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
|
|
8402
|
-
protocol: receipt.protocol
|
|
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
|
|
9033
|
+
resource_id: event.resourceId ?? null,
|
|
8430
9034
|
at: event.at,
|
|
8431
|
-
adapter: event.adapter
|
|
8432
|
-
payment_provider: event.paymentProvider
|
|
8433
|
-
duration_ms: event.durationMs
|
|
8434
|
-
status: event.status
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|