@devlab.group/agent-commerce 1.5.0 → 1.6.1
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 +73 -71
- package/dist/ap2.d.ts +20 -23
- package/dist/ap2.js +53 -75
- package/dist/ap2.js.map +1 -1
- package/dist/authorization-BpbWGGrf.d.ts +115 -0
- package/dist/chunk-AFI2CEZS.js +10 -0
- package/dist/chunk-AFI2CEZS.js.map +1 -0
- package/dist/{chunk-PBYN7EK2.js → chunk-CBJIT7QP.js} +10 -9
- package/dist/chunk-CBJIT7QP.js.map +1 -0
- package/dist/{chunk-NKNXVDHN.js → chunk-JKEKKXRJ.js} +45 -66
- package/dist/chunk-JKEKKXRJ.js.map +1 -0
- package/dist/{chunk-5GEWKNIH.js → chunk-N5KRK7YA.js} +63 -27
- package/dist/chunk-N5KRK7YA.js.map +1 -0
- package/dist/{chunk-6D2EZ5ME.js → chunk-V2V2E7HX.js} +2 -2
- package/dist/{chunk-6D2EZ5ME.js.map → chunk-V2V2E7HX.js.map} +1 -1
- package/dist/{chunk-US622BKF.js → chunk-VKDXMXHL.js} +5 -8
- package/dist/chunk-VKDXMXHL.js.map +1 -0
- package/dist/cli/index.js +501 -494
- package/dist/cli/index.js.map +1 -1
- package/dist/{constants-6mypC60h.d.ts → constants-Bkq04FRZ.d.ts} +1 -3
- package/dist/guardrails-71XwtXlT.d.ts +31 -0
- package/dist/index.d.ts +230 -265
- package/dist/index.js +1050 -607
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +6 -59
- package/dist/mcp.js +61 -53
- package/dist/mcp.js.map +1 -1
- package/dist/mpp.d.ts +4 -4
- package/dist/mpp.js +21 -19
- package/dist/mpp.js.map +1 -1
- 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 +10 -9
- package/dist/x402.js.map +1 -1
- package/package.json +7 -5
- package/dist/authorization-DA7mjy_O.d.ts +0 -123
- package/dist/chunk-5GEWKNIH.js.map +0 -1
- package/dist/chunk-NKNXVDHN.js.map +0 -1
- package/dist/chunk-PBYN7EK2.js.map +0 -1
- package/dist/chunk-US622BKF.js.map +0 -1
- package/dist/guardrails-Dmwi63KB.d.ts +0 -38
- package/dist/protocol-adapter-APRjs1E2.d.ts +0 -273
- package/dist/runtime-DC5XQ9Cc.d.ts +0 -351
package/dist/index.js
CHANGED
|
@@ -1,16 +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
|
-
import {
|
|
5
|
-
|
|
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-VKDXMXHL.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-CBJIT7QP.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-N5KRK7YA.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-N5KRK7YA.js';
|
|
6
7
|
import { promises, accessSync, constants } from 'fs';
|
|
7
8
|
import * as path from 'path';
|
|
8
9
|
import { parse, YAMLError } from 'yaml';
|
|
9
10
|
import { z } from 'zod';
|
|
11
|
+
import { isDeepStrictEqual } from 'util';
|
|
12
|
+
import Fastify from 'fastify';
|
|
10
13
|
import { randomUUID, timingSafeEqual, createHash } from 'crypto';
|
|
11
14
|
import { createRequire } from 'module';
|
|
12
15
|
import pino from 'pino';
|
|
13
|
-
import Fastify from 'fastify';
|
|
14
16
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
15
17
|
import addFormats from 'ajv-formats';
|
|
16
18
|
|
|
@@ -26,7 +28,7 @@ function substituteEnv(value, env, path2 = "$") {
|
|
|
26
28
|
if (Array.isArray(value)) {
|
|
27
29
|
return value.map((entry, index) => substituteEnv(entry, env, `${path2}[${index}]`));
|
|
28
30
|
}
|
|
29
|
-
if (
|
|
31
|
+
if (isRecord(value)) {
|
|
30
32
|
const result = {};
|
|
31
33
|
for (const [key, entry] of Object.entries(value)) {
|
|
32
34
|
result[key] = substituteEnv(entry, env, `${path2}.${key}`);
|
|
@@ -43,16 +45,15 @@ function assertTemplateIsSupported(value, path2) {
|
|
|
43
45
|
if (NESTED_PLACEHOLDER.test(value)) {
|
|
44
46
|
throw new CommerceError(
|
|
45
47
|
"CONFIG_INVALID",
|
|
46
|
-
`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.`,
|
|
47
49
|
{ details: { path: path2 } }
|
|
48
50
|
);
|
|
49
51
|
}
|
|
50
|
-
BRACE_TOKEN.lastIndex = 0;
|
|
51
52
|
for (const [token] of value.matchAll(BRACE_TOKEN)) {
|
|
52
53
|
if (SUPPORTED_TOKEN.test(token)) continue;
|
|
53
54
|
throw new CommerceError(
|
|
54
55
|
"CONFIG_INVALID",
|
|
55
|
-
`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.`,
|
|
56
57
|
{ details: { path: path2, token } }
|
|
57
58
|
);
|
|
58
59
|
}
|
|
@@ -77,9 +78,6 @@ function substituteOnce(value, env, path2) {
|
|
|
77
78
|
}
|
|
78
79
|
);
|
|
79
80
|
}
|
|
80
|
-
function isPlainObject(value) {
|
|
81
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
|
-
}
|
|
83
81
|
|
|
84
82
|
// src/config/filename.ts
|
|
85
83
|
var DEFAULT_CONFIG_FILENAME = "config.yaml";
|
|
@@ -98,7 +96,7 @@ var HttpBackendExecutor = class {
|
|
|
98
96
|
}
|
|
99
97
|
async call(handler, request) {
|
|
100
98
|
const timeoutMs = handler.timeoutMs ?? DEFAULT_BACKEND_TIMEOUT_MS;
|
|
101
|
-
const inputRecord =
|
|
99
|
+
const inputRecord = isRecord(request.input) ? request.input : {};
|
|
102
100
|
const context = { requestId: request.requestId, resourceId: request.resourceId };
|
|
103
101
|
const parts = buildBackendRequestParts(handler, inputRecord, context);
|
|
104
102
|
let target;
|
|
@@ -111,26 +109,17 @@ var HttpBackendExecutor = class {
|
|
|
111
109
|
cause: error
|
|
112
110
|
});
|
|
113
111
|
}
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
requestId: request.requestId,
|
|
123
|
-
resourceId: request.resourceId
|
|
124
|
-
}
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
} catch (error) {
|
|
128
|
-
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
|
+
);
|
|
129
119
|
}
|
|
130
|
-
const headers =
|
|
120
|
+
const headers = buildHeaders(handler, context);
|
|
131
121
|
if (request.idempotencyKey !== void 0) {
|
|
132
|
-
|
|
133
|
-
headers[IDEMPOTENCY_KEY_HEADER] = request.idempotencyKey;
|
|
122
|
+
setHeader(headers, IDEMPOTENCY_KEY_HEADER, request.idempotencyKey, context);
|
|
134
123
|
}
|
|
135
124
|
let body;
|
|
136
125
|
checkQueryCollision(target, parts.query, context);
|
|
@@ -139,9 +128,7 @@ var HttpBackendExecutor = class {
|
|
|
139
128
|
}
|
|
140
129
|
if (parts.body !== void 0) {
|
|
141
130
|
body = JSON.stringify(parts.body.value);
|
|
142
|
-
if (!
|
|
143
|
-
headers["content-type"] = "application/json";
|
|
144
|
-
}
|
|
131
|
+
if (!headers.has("content-type")) headers.set("content-type", "application/json");
|
|
145
132
|
}
|
|
146
133
|
const started = performance.now();
|
|
147
134
|
let response;
|
|
@@ -187,13 +174,13 @@ var HttpBackendExecutor = class {
|
|
|
187
174
|
}
|
|
188
175
|
);
|
|
189
176
|
}
|
|
190
|
-
const responseHeaders =
|
|
177
|
+
const responseHeaders = Object.fromEntries(response.headers);
|
|
191
178
|
const contentType = response.headers.get("content-type") ?? "";
|
|
192
179
|
let parsedBody;
|
|
193
180
|
try {
|
|
194
181
|
parsedBody = await parseBody(response, contentType);
|
|
195
182
|
} catch (error) {
|
|
196
|
-
const tooLarge = error
|
|
183
|
+
const tooLarge = isCommerceError(error);
|
|
197
184
|
throw new CommerceError(
|
|
198
185
|
"BACKEND_ERROR",
|
|
199
186
|
tooLarge ? "Backend response exceeded the maximum allowed size" : "Backend response could not be read",
|
|
@@ -240,8 +227,9 @@ function findUnparsedBraceToken(url) {
|
|
|
240
227
|
return token?.[0] ?? residue[index];
|
|
241
228
|
}
|
|
242
229
|
function validateBackendRequestShape(handler, input, context) {
|
|
243
|
-
const inputRecord =
|
|
230
|
+
const inputRecord = isRecord(input) ? input : {};
|
|
244
231
|
const parts = buildBackendRequestParts(handler, inputRecord, context);
|
|
232
|
+
buildHeaders(handler, context);
|
|
245
233
|
let target;
|
|
246
234
|
try {
|
|
247
235
|
target = new URL(parts.url);
|
|
@@ -269,7 +257,7 @@ function buildBackendRequestParts(handler, input, context) {
|
|
|
269
257
|
function resolveBoundGroup(input, key, kind, context) {
|
|
270
258
|
const value = input[key];
|
|
271
259
|
if (value === void 0) return {};
|
|
272
|
-
if (!
|
|
260
|
+
if (!isRecord(value)) {
|
|
273
261
|
throw new CommerceError(
|
|
274
262
|
"INPUT_INVALID",
|
|
275
263
|
`Input "${key}" must be an object of ${kind} parameters`,
|
|
@@ -290,14 +278,11 @@ function checkQueryCollision(target, remaining, context) {
|
|
|
290
278
|
}
|
|
291
279
|
}
|
|
292
280
|
}
|
|
293
|
-
function isPlainObject2(value) {
|
|
294
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
295
|
-
}
|
|
296
281
|
var TRAVERSAL_PATH_VALUES = /* @__PURE__ */ new Set(["", ".", ".."]);
|
|
297
282
|
function applyPathTemplate(template, input, context) {
|
|
298
283
|
const remaining = { ...input };
|
|
299
284
|
let missing;
|
|
300
|
-
let
|
|
285
|
+
let invalid2;
|
|
301
286
|
const url = template.replace(PATH_PARAM_PATTERN, (_match, key) => {
|
|
302
287
|
if (!Object.hasOwn(remaining, key)) {
|
|
303
288
|
missing = key;
|
|
@@ -307,7 +292,7 @@ function applyPathTemplate(template, input, context) {
|
|
|
307
292
|
delete remaining[key];
|
|
308
293
|
const raw = stringifyPrimitive(value);
|
|
309
294
|
if (TRAVERSAL_PATH_VALUES.has(raw)) {
|
|
310
|
-
|
|
295
|
+
invalid2 = key;
|
|
311
296
|
return "";
|
|
312
297
|
}
|
|
313
298
|
return encodeURIComponent(raw);
|
|
@@ -318,10 +303,10 @@ function applyPathTemplate(template, input, context) {
|
|
|
318
303
|
details: { field: missing }
|
|
319
304
|
});
|
|
320
305
|
}
|
|
321
|
-
if (
|
|
322
|
-
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`, {
|
|
323
308
|
...context,
|
|
324
|
-
details: { field:
|
|
309
|
+
details: { field: invalid2 }
|
|
325
310
|
});
|
|
326
311
|
}
|
|
327
312
|
return { url, remaining };
|
|
@@ -336,22 +321,38 @@ function stringifyPrimitive(value) {
|
|
|
336
321
|
return String(value);
|
|
337
322
|
}
|
|
338
323
|
}
|
|
339
|
-
function
|
|
340
|
-
|
|
341
|
-
|
|
324
|
+
function buildHeaders(handler, context) {
|
|
325
|
+
try {
|
|
326
|
+
return new Headers(handler.headers);
|
|
327
|
+
} catch {
|
|
328
|
+
throw invalidHeaderError(context);
|
|
329
|
+
}
|
|
342
330
|
}
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
331
|
+
function setHeader(headers, name, value, context) {
|
|
332
|
+
try {
|
|
333
|
+
headers.set(name, value);
|
|
334
|
+
} catch {
|
|
335
|
+
throw invalidHeaderError(context);
|
|
347
336
|
}
|
|
348
337
|
}
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
338
|
+
function invalidHeaderError(context) {
|
|
339
|
+
return new CommerceError("BACKEND_ERROR", "Backend request headers are invalid", {
|
|
340
|
+
...context,
|
|
341
|
+
details: { reason: "invalid-header" }
|
|
353
342
|
});
|
|
354
|
-
|
|
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;
|
|
355
356
|
}
|
|
356
357
|
async function readBodyCapped(response, maxBytes) {
|
|
357
358
|
if (!response.body) return "";
|
|
@@ -365,7 +366,7 @@ async function readBodyCapped(response, maxBytes) {
|
|
|
365
366
|
total += value.byteLength;
|
|
366
367
|
if (total > maxBytes) {
|
|
367
368
|
await reader.cancel();
|
|
368
|
-
throw new
|
|
369
|
+
throw new CommerceError("BACKEND_ERROR", `response body exceeded ${maxBytes} bytes`);
|
|
369
370
|
}
|
|
370
371
|
chunks.push(value);
|
|
371
372
|
}
|
|
@@ -400,47 +401,6 @@ function safeStringify(value) {
|
|
|
400
401
|
function isAbortError(error) {
|
|
401
402
|
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
402
403
|
}
|
|
403
|
-
|
|
404
|
-
// src/core/execution/event-bus.ts
|
|
405
|
-
function createEventBus(options) {
|
|
406
|
-
const subscribers = /* @__PURE__ */ new Set();
|
|
407
|
-
return {
|
|
408
|
-
async emit(event) {
|
|
409
|
-
try {
|
|
410
|
-
await options.store.appendEvent(event);
|
|
411
|
-
} catch (error) {
|
|
412
|
-
options.logger.error(
|
|
413
|
-
{ err: describeError(error), eventType: event.type, requestId: event.requestId },
|
|
414
|
-
"Failed to persist commerce event"
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
for (const subscriber of subscribers) {
|
|
418
|
-
try {
|
|
419
|
-
subscriber(event);
|
|
420
|
-
} catch (error) {
|
|
421
|
-
options.logger.error(
|
|
422
|
-
{ err: describeError(error), eventType: event.type, requestId: event.requestId },
|
|
423
|
-
"Event subscriber threw"
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
},
|
|
428
|
-
subscribe(fn) {
|
|
429
|
-
subscribers.add(fn);
|
|
430
|
-
return () => {
|
|
431
|
-
subscribers.delete(fn);
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
function describeError(error) {
|
|
437
|
-
if (error instanceof Error) {
|
|
438
|
-
return { message: error.message, name: error.name };
|
|
439
|
-
}
|
|
440
|
-
return { message: String(error) };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// src/core/execution/validation.ts
|
|
444
404
|
var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
445
405
|
"object",
|
|
446
406
|
"string",
|
|
@@ -457,13 +417,10 @@ function compileJsonSchema(schema) {
|
|
|
457
417
|
return errors.length === 0 ? { valid: true, value: input } : { valid: false, errors };
|
|
458
418
|
};
|
|
459
419
|
}
|
|
460
|
-
function isPlainObject3(value) {
|
|
461
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
462
|
-
}
|
|
463
420
|
function matchesType(type, value) {
|
|
464
421
|
switch (type) {
|
|
465
422
|
case "object":
|
|
466
|
-
return
|
|
423
|
+
return isRecord(value);
|
|
467
424
|
case "array":
|
|
468
425
|
return Array.isArray(value);
|
|
469
426
|
case "string":
|
|
@@ -486,15 +443,7 @@ function describeType(value) {
|
|
|
486
443
|
return typeof value;
|
|
487
444
|
}
|
|
488
445
|
function deepEqual(a, b) {
|
|
489
|
-
|
|
490
|
-
if (typeof a !== typeof b) return false;
|
|
491
|
-
if (a === null || b === null) return false;
|
|
492
|
-
if (typeof a !== "object") return false;
|
|
493
|
-
try {
|
|
494
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
495
|
-
} catch {
|
|
496
|
-
return false;
|
|
497
|
-
}
|
|
446
|
+
return a === b || isDeepStrictEqual(a, b);
|
|
498
447
|
}
|
|
499
448
|
function own(obj, key) {
|
|
500
449
|
return Object.hasOwn(obj, key) ? obj[key] : void 0;
|
|
@@ -514,8 +463,8 @@ function isObjectSchemaNode(schema) {
|
|
|
514
463
|
function validateNode(schema, value, path2) {
|
|
515
464
|
const errors = [];
|
|
516
465
|
const types = typeList(own(schema, "type"));
|
|
517
|
-
const
|
|
518
|
-
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) {
|
|
519
468
|
if (!types.some((entry) => matchesType(entry, value))) {
|
|
520
469
|
errors.push({
|
|
521
470
|
path: path2,
|
|
@@ -531,7 +480,7 @@ function validateNode(schema, value, path2) {
|
|
|
531
480
|
errors.push({ path: path2, message: `must be one of ${JSON.stringify(allowed)}` });
|
|
532
481
|
}
|
|
533
482
|
}
|
|
534
|
-
if (isObjectSchemaNode(schema) &&
|
|
483
|
+
if (isObjectSchemaNode(schema) && isRecord(value)) {
|
|
535
484
|
errors.push(...validateObject(schema, value, path2));
|
|
536
485
|
} else if ((types?.includes("array") ?? false) && Array.isArray(value)) {
|
|
537
486
|
errors.push(...validateArray(schema, value, path2));
|
|
@@ -541,7 +490,7 @@ function validateNode(schema, value, path2) {
|
|
|
541
490
|
function validateObject(schema, value, path2) {
|
|
542
491
|
const errors = [];
|
|
543
492
|
const propsRaw = own(schema, "properties");
|
|
544
|
-
const properties =
|
|
493
|
+
const properties = isRecord(propsRaw) ? propsRaw : {};
|
|
545
494
|
const requiredRaw = own(schema, "required");
|
|
546
495
|
const required = Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : [];
|
|
547
496
|
for (const key of required) {
|
|
@@ -553,14 +502,14 @@ function validateObject(schema, value, path2) {
|
|
|
553
502
|
for (const key of Object.keys(value)) {
|
|
554
503
|
const propSchemaRaw = own(properties, key);
|
|
555
504
|
if (propSchemaRaw !== void 0) {
|
|
556
|
-
if (
|
|
505
|
+
if (isRecord(propSchemaRaw)) {
|
|
557
506
|
errors.push(...validateNode(propSchemaRaw, value[key], `${path2}.${key}`));
|
|
558
507
|
}
|
|
559
508
|
continue;
|
|
560
509
|
}
|
|
561
510
|
if (additional === false) {
|
|
562
511
|
errors.push({ path: `${path2}.${key}`, message: "additional property not allowed" });
|
|
563
|
-
} else if (
|
|
512
|
+
} else if (isRecord(additional)) {
|
|
564
513
|
errors.push(...validateNode(additional, value[key], `${path2}.${key}`));
|
|
565
514
|
}
|
|
566
515
|
}
|
|
@@ -569,7 +518,7 @@ function validateObject(schema, value, path2) {
|
|
|
569
518
|
function validateArray(schema, value, path2) {
|
|
570
519
|
const errors = [];
|
|
571
520
|
const itemsRaw = own(schema, "items");
|
|
572
|
-
if (
|
|
521
|
+
if (isRecord(itemsRaw)) {
|
|
573
522
|
value.forEach((item, index) => {
|
|
574
523
|
errors.push(...validateNode(itemsRaw, item, `${path2}[${index}]`));
|
|
575
524
|
});
|
|
@@ -592,7 +541,7 @@ function createExecutionPipeline(options) {
|
|
|
592
541
|
await options.events.emit(event);
|
|
593
542
|
} catch (error) {
|
|
594
543
|
options.logger.error(
|
|
595
|
-
{ err:
|
|
544
|
+
{ err: describeError(error), eventType: event.type, requestId: event.requestId },
|
|
596
545
|
"Event sink failed; commerce flow continues"
|
|
597
546
|
);
|
|
598
547
|
}
|
|
@@ -602,7 +551,7 @@ function createExecutionPipeline(options) {
|
|
|
602
551
|
await op();
|
|
603
552
|
} catch (error) {
|
|
604
553
|
options.logger.error(
|
|
605
|
-
{ err:
|
|
554
|
+
{ err: describeError(error), requestId },
|
|
606
555
|
`Persistence failed: ${label}`
|
|
607
556
|
);
|
|
608
557
|
}
|
|
@@ -689,7 +638,7 @@ function createExecutionPipeline(options) {
|
|
|
689
638
|
await provider[action](verification.reservationId, context);
|
|
690
639
|
} catch (error) {
|
|
691
640
|
options.logger.error(
|
|
692
|
-
{ err:
|
|
641
|
+
{ err: describeError(error), requestId: request.requestId, action },
|
|
693
642
|
"Authorization finalization failed; the reservation stays reserved"
|
|
694
643
|
);
|
|
695
644
|
}
|
|
@@ -699,7 +648,7 @@ function createExecutionPipeline(options) {
|
|
|
699
648
|
async function execute(request) {
|
|
700
649
|
const pipelineStart = options.clock.monotonicMs();
|
|
701
650
|
const resource = options.resources.get(request.resourceId);
|
|
702
|
-
if (!resource
|
|
651
|
+
if (!resource?.exposedVia.includes(request.protocol)) {
|
|
703
652
|
throw new CommerceError(
|
|
704
653
|
"RESOURCE_NOT_FOUND",
|
|
705
654
|
`Resource "${request.resourceId}" was not found`,
|
|
@@ -1170,22 +1119,16 @@ function pickProvider(providers, methods) {
|
|
|
1170
1119
|
}
|
|
1171
1120
|
return void 0;
|
|
1172
1121
|
}
|
|
1173
|
-
var RESERVED_INPUT_KEYS =
|
|
1122
|
+
var RESERVED_INPUT_KEYS = [...RESERVED_INPUT_FIELDS, "__proto__"];
|
|
1174
1123
|
function stripReservedFields(input) {
|
|
1175
|
-
if (
|
|
1176
|
-
|
|
1177
|
-
const hasReserved = [...RESERVED_INPUT_KEYS].some((key) => Object.hasOwn(record, key));
|
|
1178
|
-
if (!hasReserved) return input;
|
|
1124
|
+
if (!isRecord(input)) return input;
|
|
1125
|
+
if (!RESERVED_INPUT_KEYS.some((key) => Object.hasOwn(input, key))) return input;
|
|
1179
1126
|
const rest = {};
|
|
1180
|
-
for (const key of Object.keys(
|
|
1181
|
-
if (!RESERVED_INPUT_KEYS.
|
|
1127
|
+
for (const key of Object.keys(input)) {
|
|
1128
|
+
if (!RESERVED_INPUT_KEYS.includes(key)) rest[key] = input[key];
|
|
1182
1129
|
}
|
|
1183
1130
|
return rest;
|
|
1184
1131
|
}
|
|
1185
|
-
function describeError2(error) {
|
|
1186
|
-
if (error instanceof Error) return { message: error.message, name: error.name };
|
|
1187
|
-
return { message: String(error) };
|
|
1188
|
-
}
|
|
1189
1132
|
|
|
1190
1133
|
// src/core/execution/registry.ts
|
|
1191
1134
|
var ResourceRegistryImpl = class {
|
|
@@ -1219,6 +1162,22 @@ function createResourceRegistry(resources) {
|
|
|
1219
1162
|
return new ResourceRegistryImpl(resources);
|
|
1220
1163
|
}
|
|
1221
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
|
+
|
|
1222
1181
|
// src/protocols/acp/constants.ts
|
|
1223
1182
|
var ACP_SPEC_VERSION = "2026-04-17";
|
|
1224
1183
|
var ACP_API_VERSION = "2026-04-17";
|
|
@@ -1237,6 +1196,9 @@ var ACP_OPERATION_INPUT_KEYS = {
|
|
|
1237
1196
|
completeCheckoutSession: ["path", "body"],
|
|
1238
1197
|
cancelCheckoutSession: ["path"]
|
|
1239
1198
|
};
|
|
1199
|
+
var ACP_OPERATION_OPTIONAL_INPUT_KEYS = {
|
|
1200
|
+
cancelCheckoutSession: ["body"]
|
|
1201
|
+
};
|
|
1240
1202
|
var ACP_JSON_MEDIA_TYPE = "application/json";
|
|
1241
1203
|
var ACP_API_VERSION_HEADER = "api-version";
|
|
1242
1204
|
var ACP_REQUEST_ID_HEADER = "request-id";
|
|
@@ -1249,7 +1211,6 @@ var ACP_MAX_REQUEST_ID_LENGTH = 128;
|
|
|
1249
1211
|
var SUPPORTED_CONFIG_VERSION = 1;
|
|
1250
1212
|
var ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
|
|
1251
1213
|
var ZERO_ADDRESS_PATTERN = /^0x0{40}$/i;
|
|
1252
|
-
var MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
|
|
1253
1214
|
var NumberOrString = z.union([z.number(), z.string()]);
|
|
1254
1215
|
var BooleanOrString = z.union([z.boolean(), z.string()]);
|
|
1255
1216
|
var MerchantSchema = z.object({
|
|
@@ -1260,14 +1221,14 @@ var MerchantSchema = z.object({
|
|
|
1260
1221
|
var ServerSchema = z.object({
|
|
1261
1222
|
port: NumberOrString,
|
|
1262
1223
|
host: z.string().min(1),
|
|
1263
|
-
|
|
1224
|
+
// Shared secret gating /api/receipts and /api/events. Without it both
|
|
1225
|
+
// routes 404.
|
|
1264
1226
|
adminToken: z.string().min(1).optional(),
|
|
1265
|
-
|
|
1266
|
-
//
|
|
1267
|
-
// `Origin` header, so `"*"` and a trailing slash
|
|
1268
|
-
//
|
|
1269
|
-
//
|
|
1270
|
-
// 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.
|
|
1271
1232
|
allowedOrigins: z.array(
|
|
1272
1233
|
z.string().min(1).refine((origin) => origin !== "*", {
|
|
1273
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.`
|
|
@@ -1286,16 +1247,14 @@ var RESERVED_GATEWAY_PATHS = [
|
|
|
1286
1247
|
"/health",
|
|
1287
1248
|
"/ready",
|
|
1288
1249
|
"/.well-known/agent-commerce",
|
|
1289
|
-
// Fixed by the A2A specification
|
|
1290
|
-
// a configurable mount's to claim.
|
|
1250
|
+
// Fixed by the A2A specification and served by the adapter
|
|
1291
1251
|
"/.well-known/agent-card.json",
|
|
1292
|
-
//
|
|
1252
|
+
// Fixed by the ACP specification
|
|
1293
1253
|
ACP_WELL_KNOWN_PATH,
|
|
1294
1254
|
"/api/resources",
|
|
1295
1255
|
"/api/resources/:id/invoke",
|
|
1296
1256
|
"/api/receipts",
|
|
1297
|
-
"/api/events"
|
|
1298
|
-
"/api/events/stream"
|
|
1257
|
+
"/api/events"
|
|
1299
1258
|
];
|
|
1300
1259
|
var MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
|
|
1301
1260
|
message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
|
|
@@ -1327,16 +1286,14 @@ var ProtocolsSchema = z.object({
|
|
|
1327
1286
|
enabled: BooleanOrString,
|
|
1328
1287
|
mountPath: MountPathSchema
|
|
1329
1288
|
}).strict(),
|
|
1330
|
-
// Optional
|
|
1331
|
-
// existing config predating the adapter stays valid.
|
|
1289
|
+
// Optional; A2A is off unless configured
|
|
1332
1290
|
a2a: z.object({
|
|
1333
1291
|
enabled: BooleanOrString,
|
|
1334
1292
|
mountPath: MountPathSchema.optional()
|
|
1335
1293
|
}).strict().optional(),
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1339
|
-
// 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.
|
|
1340
1297
|
acp: z.object({
|
|
1341
1298
|
enabled: BooleanOrString,
|
|
1342
1299
|
mountPath: MountPathSchema.optional(),
|
|
@@ -1392,7 +1349,7 @@ var FacilitatorAuthSchema = z.discriminatedUnion("type", [
|
|
|
1392
1349
|
z.object({ type: z.literal("none") }).strict(),
|
|
1393
1350
|
z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
|
|
1394
1351
|
// Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
|
|
1395
|
-
// which signs a fresh JWT per request
|
|
1352
|
+
// which signs a fresh JWT per request.
|
|
1396
1353
|
z.object({
|
|
1397
1354
|
type: z.literal("cdp"),
|
|
1398
1355
|
apiKeyId: z.string().min(1),
|
|
@@ -1407,10 +1364,8 @@ var FacilitatorSchema = z.discriminatedUnion("mode", [
|
|
|
1407
1364
|
z.object({
|
|
1408
1365
|
mode: z.literal("remote"),
|
|
1409
1366
|
url: z.string().min(1),
|
|
1410
|
-
// Absent
|
|
1411
|
-
//
|
|
1412
|
-
// combination is refused outright, so the default can never quietly
|
|
1413
|
-
// become an unauthenticated production facilitator.
|
|
1367
|
+
// Absent auth sends no credential and normalizes to `{ type: 'none' }`,
|
|
1368
|
+
// which on mainnet requires `allowUnauthenticatedFacilitator`
|
|
1414
1369
|
auth: FacilitatorAuthSchema.optional()
|
|
1415
1370
|
}).strict()
|
|
1416
1371
|
]);
|
|
@@ -1425,9 +1380,9 @@ var X402Schema = z.object({
|
|
|
1425
1380
|
payTo: z.string().min(1),
|
|
1426
1381
|
maxTimeoutSeconds: NumberOrString,
|
|
1427
1382
|
facilitator: FacilitatorSchema,
|
|
1428
|
-
|
|
1383
|
+
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
1429
1384
|
allowMainnet: BooleanOrString.optional(),
|
|
1430
|
-
|
|
1385
|
+
// Accepts credential-free facilitator access on mainnet. Never defaulted
|
|
1431
1386
|
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
1432
1387
|
}).strict();
|
|
1433
1388
|
var MppSchema = z.object({
|
|
@@ -1442,9 +1397,9 @@ var MppSchema = z.object({
|
|
|
1442
1397
|
challengeSecret: z.string().min(1),
|
|
1443
1398
|
challengeTtlSeconds: NumberOrString.optional(),
|
|
1444
1399
|
facilitator: FacilitatorSchema,
|
|
1445
|
-
// Real funds. Never defaulted
|
|
1400
|
+
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
1446
1401
|
allowMainnet: BooleanOrString.optional(),
|
|
1447
|
-
// Accepts
|
|
1402
|
+
// Accepts credential-free facilitator access on mainnet. Never defaulted
|
|
1448
1403
|
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
1449
1404
|
}).strict();
|
|
1450
1405
|
var PaymentsSchema = z.object({
|
|
@@ -1457,11 +1412,8 @@ var Ap2KeySchema = z.object({
|
|
|
1457
1412
|
}).strict();
|
|
1458
1413
|
var Ap2IssuerSchema = z.object({
|
|
1459
1414
|
issuer: z.string().min(1),
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
* merchant would verify here, and there is no value worth guessing for
|
|
1463
|
-
* something that decides that.
|
|
1464
|
-
*/
|
|
1415
|
+
// Required, not defaulted: without it a mandate minted for another
|
|
1416
|
+
// merchant would verify here
|
|
1465
1417
|
audience: z.string().min(1),
|
|
1466
1418
|
keys: z.array(Ap2KeySchema).min(1)
|
|
1467
1419
|
}).strict();
|
|
@@ -1488,7 +1440,7 @@ var RawConfigSchema = z.object({
|
|
|
1488
1440
|
authorization: AuthorizationSchema.optional()
|
|
1489
1441
|
}).strict();
|
|
1490
1442
|
function parseConfig(raw, env) {
|
|
1491
|
-
if (
|
|
1443
|
+
if (!isRecord(raw)) {
|
|
1492
1444
|
throw new CommerceError(
|
|
1493
1445
|
"CONFIG_INVALID",
|
|
1494
1446
|
'Configuration root must be a mapping (object) at "$"',
|
|
@@ -1500,7 +1452,7 @@ function parseConfig(raw, env) {
|
|
|
1500
1452
|
checkVersion(raw);
|
|
1501
1453
|
const substituted = substituteEnv(raw, env);
|
|
1502
1454
|
const parsed = parseWithZod(RawConfigSchema, substituted);
|
|
1503
|
-
return
|
|
1455
|
+
return normalize(parsed);
|
|
1504
1456
|
}
|
|
1505
1457
|
function checkVersion(raw) {
|
|
1506
1458
|
if (!("version" in raw)) {
|
|
@@ -1508,12 +1460,12 @@ function checkVersion(raw) {
|
|
|
1508
1460
|
details: { path: "version" }
|
|
1509
1461
|
});
|
|
1510
1462
|
}
|
|
1511
|
-
const
|
|
1512
|
-
const version = typeof rawVersion === "string" ? Number(rawVersion) : rawVersion;
|
|
1463
|
+
const version = raw["version"];
|
|
1513
1464
|
if (version !== SUPPORTED_CONFIG_VERSION) {
|
|
1465
|
+
const hint = typeof version === "string" ? ", written as a number without quotes" : "";
|
|
1514
1466
|
throw new CommerceError(
|
|
1515
1467
|
"CONFIG_INVALID",
|
|
1516
|
-
`Unsupported config version "${String(
|
|
1468
|
+
`Unsupported config version "${String(version)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}${hint}`,
|
|
1517
1469
|
{ details: { path: "version", supported: SUPPORTED_CONFIG_VERSION } }
|
|
1518
1470
|
);
|
|
1519
1471
|
}
|
|
@@ -1587,7 +1539,7 @@ function toBoolean(value, path2) {
|
|
|
1587
1539
|
var SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
|
|
1588
1540
|
var SUPPORTED_PAYMENT_METHODS = new Set(PAYMENT_METHOD_NAMES);
|
|
1589
1541
|
var SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
|
|
1590
|
-
function
|
|
1542
|
+
function normalize(raw) {
|
|
1591
1543
|
const protocols = {
|
|
1592
1544
|
http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
|
|
1593
1545
|
mcp: {
|
|
@@ -1598,7 +1550,7 @@ function normalise(raw) {
|
|
|
1598
1550
|
enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
|
|
1599
1551
|
mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
|
|
1600
1552
|
},
|
|
1601
|
-
acp:
|
|
1553
|
+
acp: normalizeAcp(raw.protocols.acp)
|
|
1602
1554
|
};
|
|
1603
1555
|
validateMountPaths(protocols);
|
|
1604
1556
|
const x402Raw = raw.payments.x402;
|
|
@@ -1621,7 +1573,7 @@ function normalise(raw) {
|
|
|
1621
1573
|
min: 1
|
|
1622
1574
|
}
|
|
1623
1575
|
),
|
|
1624
|
-
facilitator:
|
|
1576
|
+
facilitator: normalizeFacilitator(x402Raw.facilitator),
|
|
1625
1577
|
...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
|
|
1626
1578
|
...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
1627
1579
|
allowUnauthenticatedFacilitator: toBoolean(
|
|
@@ -1644,13 +1596,13 @@ function normalise(raw) {
|
|
|
1644
1596
|
...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
|
|
1645
1597
|
});
|
|
1646
1598
|
}
|
|
1647
|
-
const mpp =
|
|
1648
|
-
const ap2 =
|
|
1599
|
+
const mpp = normalizeMpp(raw.payments.mpp);
|
|
1600
|
+
const ap2 = normalizeAp2(raw.authorization?.ap2);
|
|
1649
1601
|
if (ap2?.enabled) {
|
|
1650
1602
|
validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
|
|
1651
1603
|
}
|
|
1652
1604
|
const resources = Object.entries(raw.resources).map(
|
|
1653
|
-
([id, entry]) =>
|
|
1605
|
+
([id, entry]) => normalizeResource(id, entry, protocols, x402, mpp, ap2)
|
|
1654
1606
|
);
|
|
1655
1607
|
if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
|
|
1656
1608
|
return {
|
|
@@ -1660,8 +1612,7 @@ function normalise(raw) {
|
|
|
1660
1612
|
name: raw.merchant.name,
|
|
1661
1613
|
publicBaseUrl: raw.merchant.publicBaseUrl
|
|
1662
1614
|
},
|
|
1663
|
-
//
|
|
1664
|
-
// convention, useful for tests and ephemeral dev/demo instances.
|
|
1615
|
+
// Port 0 lets the OS pick a free port, for tests and throwaway instances
|
|
1665
1616
|
server: {
|
|
1666
1617
|
port: toNumber(raw.server.port, "server.port", { min: 0, max: 65535 }),
|
|
1667
1618
|
host: raw.server.host,
|
|
@@ -1701,7 +1652,7 @@ function validateMountPaths(protocols) {
|
|
|
1701
1652
|
function ap2Invalid(path2, message, extra = {}) {
|
|
1702
1653
|
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
|
|
1703
1654
|
}
|
|
1704
|
-
function
|
|
1655
|
+
function normalizeAp2(raw) {
|
|
1705
1656
|
if (raw === void 0) return void 0;
|
|
1706
1657
|
if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
|
|
1707
1658
|
const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
|
|
@@ -1721,14 +1672,14 @@ function normaliseAp2(raw) {
|
|
|
1721
1672
|
if (raw.replay === void 0) {
|
|
1722
1673
|
throw ap2Invalid(
|
|
1723
1674
|
"authorization.ap2.replay",
|
|
1724
|
-
"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"
|
|
1725
1676
|
);
|
|
1726
1677
|
}
|
|
1727
|
-
const mandateIssuers =
|
|
1678
|
+
const mandateIssuers = normalizeIssuers(
|
|
1728
1679
|
raw.trust?.mandateIssuers,
|
|
1729
1680
|
"authorization.ap2.trust.mandateIssuers"
|
|
1730
1681
|
);
|
|
1731
|
-
const checkoutIssuers =
|
|
1682
|
+
const checkoutIssuers = normalizeIssuers(
|
|
1732
1683
|
raw.trust?.checkoutIssuers,
|
|
1733
1684
|
"authorization.ap2.trust.checkoutIssuers"
|
|
1734
1685
|
);
|
|
@@ -1745,7 +1696,7 @@ function normaliseAp2(raw) {
|
|
|
1745
1696
|
replay: { path: raw.replay.path }
|
|
1746
1697
|
};
|
|
1747
1698
|
}
|
|
1748
|
-
function
|
|
1699
|
+
function normalizeIssuers(raw, path2) {
|
|
1749
1700
|
if (raw === void 0 || raw.length === 0) {
|
|
1750
1701
|
throw ap2Invalid(
|
|
1751
1702
|
path2,
|
|
@@ -1831,11 +1782,11 @@ function validateJwk(jwk, kid, path2) {
|
|
|
1831
1782
|
if (jwk["kid"] !== void 0 && value("kid") !== kid) {
|
|
1832
1783
|
fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
|
|
1833
1784
|
}
|
|
1834
|
-
const
|
|
1785
|
+
const normalized = {};
|
|
1835
1786
|
for (const member of AP2_JWK_MEMBERS) {
|
|
1836
|
-
if (jwk[member] !== void 0)
|
|
1787
|
+
if (jwk[member] !== void 0) normalized[member] = value(member);
|
|
1837
1788
|
}
|
|
1838
|
-
return
|
|
1789
|
+
return normalized;
|
|
1839
1790
|
}
|
|
1840
1791
|
function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
|
|
1841
1792
|
if (replayPath === ":memory:") return;
|
|
@@ -1849,7 +1800,7 @@ function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
|
|
|
1849
1800
|
);
|
|
1850
1801
|
}
|
|
1851
1802
|
}
|
|
1852
|
-
function
|
|
1803
|
+
function normalizeResourceAuthorization(id, entry, pricing, ap2) {
|
|
1853
1804
|
const required = entry.authorization?.required;
|
|
1854
1805
|
if (required === void 0) return void 0;
|
|
1855
1806
|
const path2 = `resources.${id}.authorization.required`;
|
|
@@ -1889,7 +1840,7 @@ function normaliseResourceAuthorization(id, entry, pricing, ap2) {
|
|
|
1889
1840
|
function acpInvalid(path2, message, extra = {}) {
|
|
1890
1841
|
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
|
|
1891
1842
|
}
|
|
1892
|
-
function
|
|
1843
|
+
function normalizeAcp(raw) {
|
|
1893
1844
|
const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
|
|
1894
1845
|
if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
|
|
1895
1846
|
return { enabled: false, mountPath };
|
|
@@ -1919,9 +1870,8 @@ function normaliseAcp(raw) {
|
|
|
1919
1870
|
{ min: ACP_RETENTION_HOURS }
|
|
1920
1871
|
)
|
|
1921
1872
|
},
|
|
1922
|
-
checkout: { operations:
|
|
1923
|
-
// Built key by key
|
|
1924
|
-
// 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`
|
|
1925
1875
|
...discovery !== void 0 ? {
|
|
1926
1876
|
discovery: {
|
|
1927
1877
|
...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
|
|
@@ -1932,7 +1882,7 @@ function normaliseAcp(raw) {
|
|
|
1932
1882
|
} : {}
|
|
1933
1883
|
};
|
|
1934
1884
|
}
|
|
1935
|
-
function
|
|
1885
|
+
function normalizeAcpOperations(checkout) {
|
|
1936
1886
|
const configured = checkout?.operations ?? {};
|
|
1937
1887
|
for (const key of Object.keys(configured)) {
|
|
1938
1888
|
if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
|
|
@@ -2000,13 +1950,15 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
2000
1950
|
const schema = resource.inputSchema;
|
|
2001
1951
|
if (schema === void 0) return;
|
|
2002
1952
|
const keys = ACP_OPERATION_INPUT_KEYS[operation];
|
|
1953
|
+
const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
|
|
2003
1954
|
const properties = schema["properties"];
|
|
2004
|
-
if (schema["additionalProperties"] === false &&
|
|
2005
|
-
for (const key of keys) {
|
|
1955
|
+
if (schema["additionalProperties"] === false && isRecord(properties)) {
|
|
1956
|
+
for (const key of [...keys, ...optionalKeys]) {
|
|
2006
1957
|
if (!Object.hasOwn(properties, key)) {
|
|
1958
|
+
const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
|
|
2007
1959
|
throw acpInvalid(
|
|
2008
1960
|
path2,
|
|
2009
|
-
`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`,
|
|
2010
1962
|
{ resourceId: resource.id }
|
|
2011
1963
|
);
|
|
2012
1964
|
}
|
|
@@ -2018,17 +1970,17 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
2018
1970
|
if (typeof entry === "string" && !keys.includes(entry)) {
|
|
2019
1971
|
throw acpInvalid(
|
|
2020
1972
|
path2,
|
|
2021
|
-
`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(", ")})`,
|
|
2022
1974
|
{ resourceId: resource.id }
|
|
2023
1975
|
);
|
|
2024
1976
|
}
|
|
2025
1977
|
}
|
|
2026
1978
|
}
|
|
2027
1979
|
}
|
|
2028
|
-
function
|
|
1980
|
+
function normalizeFacilitator(raw) {
|
|
2029
1981
|
return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
|
|
2030
1982
|
}
|
|
2031
|
-
function
|
|
1983
|
+
function normalizeMpp(raw) {
|
|
2032
1984
|
if (raw === void 0) return void 0;
|
|
2033
1985
|
const network = raw.network ?? MPP_DEFAULT_NETWORK;
|
|
2034
1986
|
if (!isMppNetwork(network)) {
|
|
@@ -2055,7 +2007,7 @@ function normaliseMpp(raw) {
|
|
|
2055
2007
|
{ min: 1 }
|
|
2056
2008
|
)
|
|
2057
2009
|
} : {},
|
|
2058
|
-
facilitator:
|
|
2010
|
+
facilitator: normalizeFacilitator(raw.facilitator),
|
|
2059
2011
|
...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
|
|
2060
2012
|
...raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
2061
2013
|
allowUnauthenticatedFacilitator: toBoolean(
|
|
@@ -2092,7 +2044,7 @@ function normaliseMpp(raw) {
|
|
|
2092
2044
|
});
|
|
2093
2045
|
return mpp;
|
|
2094
2046
|
}
|
|
2095
|
-
function
|
|
2047
|
+
function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
|
|
2096
2048
|
if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
|
|
2097
2049
|
const inputProperties = entry.input?.["properties"];
|
|
2098
2050
|
if (inputProperties && typeof inputProperties === "object") {
|
|
@@ -2158,6 +2110,7 @@ function normaliseResource(id, entry, protocols, x402, mpp, ap2) {
|
|
|
2158
2110
|
);
|
|
2159
2111
|
}
|
|
2160
2112
|
validateBackendUrl(id, entry.backend.url);
|
|
2113
|
+
validateBackendHeaders(id, entry.backend.headers);
|
|
2161
2114
|
const pathScope = validateInputBindings(id, entry.backend, entry.input);
|
|
2162
2115
|
validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
|
|
2163
2116
|
if (entry.pricing.type === "dynamic") {
|
|
@@ -2208,7 +2161,7 @@ function normaliseResource(id, entry, protocols, x402, mpp, ap2) {
|
|
|
2208
2161
|
}
|
|
2209
2162
|
}
|
|
2210
2163
|
const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
|
|
2211
|
-
const authorization =
|
|
2164
|
+
const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
|
|
2212
2165
|
return {
|
|
2213
2166
|
id,
|
|
2214
2167
|
name: entry.name,
|
|
@@ -2238,11 +2191,11 @@ function defaultClosedObjectSchema(schema) {
|
|
|
2238
2191
|
const result = { ...schema };
|
|
2239
2192
|
if (isObjectSchema) {
|
|
2240
2193
|
const properties = schema["properties"];
|
|
2241
|
-
if (
|
|
2194
|
+
if (isRecord(properties)) {
|
|
2242
2195
|
result["properties"] = Object.fromEntries(
|
|
2243
2196
|
Object.entries(properties).map(([key, sub]) => [
|
|
2244
2197
|
key,
|
|
2245
|
-
|
|
2198
|
+
isRecord(sub) ? defaultClosedObjectSchema(sub) : sub
|
|
2246
2199
|
])
|
|
2247
2200
|
);
|
|
2248
2201
|
}
|
|
@@ -2251,11 +2204,11 @@ function defaultClosedObjectSchema(schema) {
|
|
|
2251
2204
|
}
|
|
2252
2205
|
}
|
|
2253
2206
|
const items = schema["items"];
|
|
2254
|
-
if (
|
|
2207
|
+
if (isRecord(items)) {
|
|
2255
2208
|
result["items"] = defaultClosedObjectSchema(items);
|
|
2256
2209
|
}
|
|
2257
2210
|
const additional = schema["additionalProperties"];
|
|
2258
|
-
if (
|
|
2211
|
+
if (isRecord(additional)) {
|
|
2259
2212
|
result["additionalProperties"] = defaultClosedObjectSchema(additional);
|
|
2260
2213
|
}
|
|
2261
2214
|
return result;
|
|
@@ -2286,9 +2239,6 @@ var UNSUPPORTED_SCHEMA_KEYWORDS = [
|
|
|
2286
2239
|
"maxItems",
|
|
2287
2240
|
"uniqueItems"
|
|
2288
2241
|
];
|
|
2289
|
-
function isPlainObject4(value) {
|
|
2290
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2291
|
-
}
|
|
2292
2242
|
function excludesObjectType(schema) {
|
|
2293
2243
|
const type = schema["type"];
|
|
2294
2244
|
if (type === void 0 || type === "object") return false;
|
|
@@ -2313,7 +2263,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
2313
2263
|
const requiredRaw = schema["required"];
|
|
2314
2264
|
const closed = !Object.hasOwn(schema, "additionalProperties") ? true : schema["additionalProperties"] === false;
|
|
2315
2265
|
if (Array.isArray(requiredRaw) && closed && isObjectSchemaNode(schema)) {
|
|
2316
|
-
const declared =
|
|
2266
|
+
const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
2317
2267
|
const undeclared = requiredRaw.filter(
|
|
2318
2268
|
(name) => typeof name === "string" && !Object.hasOwn(declared, name)
|
|
2319
2269
|
);
|
|
@@ -2326,17 +2276,17 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
2326
2276
|
}
|
|
2327
2277
|
}
|
|
2328
2278
|
const properties = schema["properties"];
|
|
2329
|
-
if (
|
|
2279
|
+
if (isRecord(properties)) {
|
|
2330
2280
|
for (const [key, sub] of Object.entries(properties)) {
|
|
2331
|
-
if (
|
|
2281
|
+
if (isRecord(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
|
|
2332
2282
|
}
|
|
2333
2283
|
}
|
|
2334
2284
|
const additional = schema["additionalProperties"];
|
|
2335
|
-
if (
|
|
2285
|
+
if (isRecord(additional)) {
|
|
2336
2286
|
validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
|
|
2337
2287
|
}
|
|
2338
2288
|
const items = schema["items"];
|
|
2339
|
-
if (
|
|
2289
|
+
if (isRecord(items)) {
|
|
2340
2290
|
validateResourceSchemaKeywords(id, `${path2}.items`, items);
|
|
2341
2291
|
} else if (Array.isArray(items)) {
|
|
2342
2292
|
console.warn(
|
|
@@ -2403,7 +2353,7 @@ function validateInputBindings(id, backend, input) {
|
|
|
2403
2353
|
);
|
|
2404
2354
|
}
|
|
2405
2355
|
const seen = /* @__PURE__ */ new Map();
|
|
2406
|
-
const properties =
|
|
2356
|
+
const properties = isRecord(input?.["properties"]) ? input["properties"] : {};
|
|
2407
2357
|
const required = new Set(
|
|
2408
2358
|
Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
|
|
2409
2359
|
);
|
|
@@ -2426,7 +2376,7 @@ function validateInputBindings(id, backend, input) {
|
|
|
2426
2376
|
);
|
|
2427
2377
|
}
|
|
2428
2378
|
const declared = properties[property];
|
|
2429
|
-
if (location !== "body" &&
|
|
2379
|
+
if (location !== "body" && isRecord(declared) && !isObjectSchemaNode(declared)) {
|
|
2430
2380
|
fail(
|
|
2431
2381
|
`binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
|
|
2432
2382
|
{ location, property }
|
|
@@ -2442,7 +2392,7 @@ function validateInputBindings(id, backend, input) {
|
|
|
2442
2392
|
if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
|
|
2443
2393
|
const group = properties[bindings.path];
|
|
2444
2394
|
return {
|
|
2445
|
-
schema:
|
|
2395
|
+
schema: isRecord(group) ? group : void 0,
|
|
2446
2396
|
where: `input.properties.${bindings.path}`
|
|
2447
2397
|
};
|
|
2448
2398
|
}
|
|
@@ -2454,7 +2404,7 @@ function validatePathParametersDeclared(id, url, input, where) {
|
|
|
2454
2404
|
if (stray !== void 0) {
|
|
2455
2405
|
throw new CommerceError(
|
|
2456
2406
|
"CONFIG_INVALID",
|
|
2457
|
-
`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`,
|
|
2458
2408
|
{ details: { path: `resources.${id}.backend.url`, resourceId: id, token: stray } }
|
|
2459
2409
|
);
|
|
2460
2410
|
}
|
|
@@ -2472,13 +2422,13 @@ function validatePathParametersDeclared(id, url, input, where) {
|
|
|
2472
2422
|
if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
|
|
2473
2423
|
throw new CommerceError(
|
|
2474
2424
|
"CONFIG_INVALID",
|
|
2475
|
-
`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`,
|
|
2476
2426
|
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
2477
2427
|
);
|
|
2478
2428
|
}
|
|
2479
2429
|
}
|
|
2480
2430
|
const propertiesRaw = input?.["properties"];
|
|
2481
|
-
const properties =
|
|
2431
|
+
const properties = isRecord(propertiesRaw) ? propertiesRaw : {};
|
|
2482
2432
|
const requiredRaw = input?.["required"];
|
|
2483
2433
|
const required = new Set(
|
|
2484
2434
|
Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : []
|
|
@@ -2488,14 +2438,14 @@ function validatePathParametersDeclared(id, url, input, where) {
|
|
|
2488
2438
|
if (!Object.hasOwn(properties, param)) {
|
|
2489
2439
|
throw new CommerceError(
|
|
2490
2440
|
"CONFIG_INVALID",
|
|
2491
|
-
`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`,
|
|
2492
2442
|
{ details: { path: path2, resourceId: id, param } }
|
|
2493
2443
|
);
|
|
2494
2444
|
}
|
|
2495
2445
|
if (!required.has(param)) {
|
|
2496
2446
|
throw new CommerceError(
|
|
2497
2447
|
"CONFIG_INVALID",
|
|
2498
|
-
`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`,
|
|
2499
2449
|
{ details: { path: path2, resourceId: id, param } }
|
|
2500
2450
|
);
|
|
2501
2451
|
}
|
|
@@ -2508,7 +2458,7 @@ function validateBackendUrl(id, url) {
|
|
|
2508
2458
|
} catch {
|
|
2509
2459
|
throw new CommerceError(
|
|
2510
2460
|
"CONFIG_INVALID",
|
|
2511
|
-
`Resource "${id}" has an invalid backend.url
|
|
2461
|
+
`Resource "${id}" has an invalid backend.url: must be an absolute http:// or https:// URL`,
|
|
2512
2462
|
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
2513
2463
|
);
|
|
2514
2464
|
}
|
|
@@ -2520,6 +2470,19 @@ function validateBackendUrl(id, url) {
|
|
|
2520
2470
|
);
|
|
2521
2471
|
}
|
|
2522
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
|
+
}
|
|
2523
2486
|
function validateAddress(path2, value) {
|
|
2524
2487
|
if (!ADDRESS_PATTERN.test(value)) {
|
|
2525
2488
|
throw new CommerceError(
|
|
@@ -2564,9 +2527,8 @@ async function loadConfig(options = {}) {
|
|
|
2564
2527
|
`Configuration file "${configPath}" is not valid YAML: ${reason}`,
|
|
2565
2528
|
{
|
|
2566
2529
|
details: { path: configPath, ...details },
|
|
2567
|
-
//
|
|
2568
|
-
//
|
|
2569
|
-
// `.message` carries the source excerpt (see below).
|
|
2530
|
+
// Kept for programmatic callers. Never print it: its `.message` quotes
|
|
2531
|
+
// the source line (see describeYamlError).
|
|
2570
2532
|
cause: error
|
|
2571
2533
|
}
|
|
2572
2534
|
);
|
|
@@ -2604,95 +2566,47 @@ function resolveConfigPath(explicitPath, env, cwd) {
|
|
|
2604
2566
|
if (fromEnv) return path.resolve(cwd, fromEnv);
|
|
2605
2567
|
return path.resolve(cwd, DEFAULT_CONFIG_FILENAME);
|
|
2606
2568
|
}
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
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);
|
|
2613
2576
|
};
|
|
2614
2577
|
}
|
|
2615
|
-
|
|
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;
|
|
2616
2584
|
try {
|
|
2617
|
-
|
|
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
|
+
}
|
|
2618
2591
|
} catch {
|
|
2619
|
-
return
|
|
2592
|
+
return { kind: "unreadable" };
|
|
2620
2593
|
}
|
|
2621
|
-
|
|
2622
|
-
var SECRET_FIELD_NAMES = [
|
|
2623
|
-
"privateKey",
|
|
2624
|
-
"signerPrivateKey",
|
|
2625
|
-
"signature",
|
|
2626
|
-
"seed",
|
|
2627
|
-
"mnemonic",
|
|
2628
|
-
"secret",
|
|
2629
|
-
"challengeSecret",
|
|
2630
|
-
"apiKey",
|
|
2631
|
-
"adminToken",
|
|
2632
|
-
"token"
|
|
2633
|
-
];
|
|
2634
|
-
var SECRET_HEADERS = ["authorization", PAYMENT_HEADER, AUTHORIZATION_HEADER];
|
|
2635
|
-
var REDACT_PATHS = [
|
|
2636
|
-
...SECRET_HEADERS.map((name) => `req.headers[${JSON.stringify(name)}]`),
|
|
2637
|
-
...SECRET_FIELD_NAMES,
|
|
2638
|
-
...SECRET_FIELD_NAMES.map((name) => `*.${name}`)
|
|
2639
|
-
];
|
|
2640
|
-
function baseLoggerOptions(options) {
|
|
2641
|
-
const nodeEnv = options.nodeEnv ?? process.env["NODE_ENV"] ?? "development";
|
|
2642
|
-
const wantPretty = options.prettyPrint ?? (nodeEnv !== "production" && nodeEnv !== "test");
|
|
2643
|
-
const usePretty = wantPretty && PINO_PRETTY_PATH !== void 0;
|
|
2644
|
-
return {
|
|
2645
|
-
level: options.level ?? process.env["LOG_LEVEL"] ?? (nodeEnv === "test" ? "silent" : "info"),
|
|
2646
|
-
...options.name !== void 0 ? { name: options.name } : {},
|
|
2647
|
-
redact: { paths: [...REDACT_PATHS], censor: "[REDACTED]" },
|
|
2648
|
-
...usePretty ? { transport: { target: PINO_PRETTY_PATH } } : {}
|
|
2649
|
-
};
|
|
2650
|
-
}
|
|
2651
|
-
function sanitizeUrl(url) {
|
|
2652
|
-
if (url === void 0) return void 0;
|
|
2653
|
-
const queryIndex = url.indexOf("?");
|
|
2654
|
-
return queryIndex === -1 ? url : `${url.slice(0, queryIndex)}?[REDACTED]`;
|
|
2655
|
-
}
|
|
2656
|
-
function redactedReqSerializer(req) {
|
|
2657
|
-
return {
|
|
2658
|
-
method: req.method,
|
|
2659
|
-
url: sanitizeUrl(req.url),
|
|
2660
|
-
host: req.host,
|
|
2661
|
-
remoteAddress: req.ip,
|
|
2662
|
-
remotePort: req.socket?.remotePort
|
|
2663
|
-
};
|
|
2664
|
-
}
|
|
2665
|
-
function fastifyLoggerOptions(options = {}) {
|
|
2666
|
-
return {
|
|
2667
|
-
...baseLoggerOptions(options),
|
|
2668
|
-
serializers: { req: redactedReqSerializer }
|
|
2669
|
-
};
|
|
2670
|
-
}
|
|
2671
|
-
function buildNotFoundHandler() {
|
|
2672
|
-
return (request, reply) => {
|
|
2673
|
-
const safeUrl = sanitizeUrl(request.raw.url) ?? "";
|
|
2674
|
-
const message = `Route ${request.method}:${safeUrl} not found`;
|
|
2675
|
-
request.log.info(message);
|
|
2676
|
-
reply.code(404).send({ message, error: "Not Found", statusCode: 404 });
|
|
2677
|
-
};
|
|
2678
|
-
}
|
|
2679
|
-
function createGatewayLogger(options = {}) {
|
|
2680
|
-
const instance = pino(baseLoggerOptions(options));
|
|
2681
|
-
return { pino: instance, core: wrapPino(instance) };
|
|
2594
|
+
return { kind: "ok", text: Buffer.concat(chunks).toString("utf8") };
|
|
2682
2595
|
}
|
|
2683
|
-
function
|
|
2684
|
-
|
|
2685
|
-
debug: (obj, msg) => instance.debug(obj, msg),
|
|
2686
|
-
info: (obj, msg) => instance.info(obj, msg),
|
|
2687
|
-
warn: (obj, msg) => instance.warn(obj, msg),
|
|
2688
|
-
error: (obj, msg) => instance.error(obj, msg),
|
|
2689
|
-
child: (bindings) => wrapPino(instance.child(bindings))
|
|
2690
|
-
};
|
|
2691
|
-
return logger;
|
|
2596
|
+
function joinUrl(baseUrl, mountPath) {
|
|
2597
|
+
return `${baseUrl.replace(/\/+$/, "")}/${mountPath.replace(/^\/+|\/+$/g, "")}`;
|
|
2692
2598
|
}
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
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";
|
|
2696
2610
|
function buildAccessControlHook(options) {
|
|
2697
2611
|
const allowedOrigins = new Set(options.allowedOrigins.map((origin) => origin.toLowerCase()));
|
|
2698
2612
|
const publicHostname = hostnameOf(options.publicBaseUrl);
|
|
@@ -2720,12 +2634,13 @@ function buildAccessControlHook(options) {
|
|
|
2720
2634
|
};
|
|
2721
2635
|
}
|
|
2722
2636
|
function buildOperatorTokenHook(adminToken) {
|
|
2637
|
+
const isValidAdminToken = adminToken ? createBearerCheck(adminToken) : void 0;
|
|
2723
2638
|
return async (request, reply) => {
|
|
2724
|
-
if (
|
|
2639
|
+
if (isValidAdminToken === void 0) {
|
|
2725
2640
|
reply.status(404).send();
|
|
2726
2641
|
return;
|
|
2727
2642
|
}
|
|
2728
|
-
if (!isValidAdminToken(request.headers.authorization
|
|
2643
|
+
if (!isValidAdminToken(request.headers.authorization)) {
|
|
2729
2644
|
reply.status(401).send({ status: "error", code: "UNAUTHORIZED", message: "Admin token required" });
|
|
2730
2645
|
}
|
|
2731
2646
|
};
|
|
@@ -2746,14 +2661,6 @@ function isAllowedHost(hostHeader, publicHostname) {
|
|
|
2746
2661
|
const hostname = hostnameOf(hostHeader);
|
|
2747
2662
|
return hostname !== "" && (hostname === publicHostname || LOOPBACK_HOSTNAMES.has(hostname));
|
|
2748
2663
|
}
|
|
2749
|
-
function isValidAdminToken(authHeader, adminToken) {
|
|
2750
|
-
const provided = authHeader?.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : void 0;
|
|
2751
|
-
if (provided === void 0) return false;
|
|
2752
|
-
return timingSafeEqual(hashToken(provided), hashToken(adminToken));
|
|
2753
|
-
}
|
|
2754
|
-
function hashToken(value) {
|
|
2755
|
-
return createHash("sha256").update(value, "utf8").digest();
|
|
2756
|
-
}
|
|
2757
2664
|
|
|
2758
2665
|
// src/gateway/adapters.ts
|
|
2759
2666
|
var MOUNT_BODY_LIMIT_BYTES = 256 * 1024;
|
|
@@ -2766,7 +2673,7 @@ async function startAndMountAdapters(options) {
|
|
|
2766
2673
|
try {
|
|
2767
2674
|
await adapter.start(adapterContext);
|
|
2768
2675
|
} catch (error) {
|
|
2769
|
-
const detail =
|
|
2676
|
+
const detail = errorMessage(error);
|
|
2770
2677
|
adapterLogger.error(
|
|
2771
2678
|
{ err: detail },
|
|
2772
2679
|
"Protocol adapter failed to start; gateway continues without it"
|
|
@@ -2860,7 +2767,7 @@ function mountHttpAdapter(server, adapter, logger) {
|
|
|
2860
2767
|
try {
|
|
2861
2768
|
await handleHttp(request.raw, reply.raw);
|
|
2862
2769
|
} catch (error) {
|
|
2863
|
-
logger.error({ err:
|
|
2770
|
+
logger.error({ err: errorMessage(error) }, "Protocol adapter request handler threw");
|
|
2864
2771
|
if (!reply.raw.headersSent) {
|
|
2865
2772
|
reply.raw.statusCode = 500;
|
|
2866
2773
|
reply.raw.end();
|
|
@@ -2927,7 +2834,7 @@ async function getAdapterHealth(runtime, clock) {
|
|
|
2927
2834
|
try {
|
|
2928
2835
|
return await runtime.adapter.health();
|
|
2929
2836
|
} catch (error) {
|
|
2930
|
-
return { status: "fail", detail:
|
|
2837
|
+
return { status: "fail", detail: errorMessage(error), checkedAt: clock.nowIso() };
|
|
2931
2838
|
}
|
|
2932
2839
|
}
|
|
2933
2840
|
async function stopAdapters(runtimes, logger) {
|
|
@@ -2935,13 +2842,99 @@ async function stopAdapters(runtimes, logger) {
|
|
|
2935
2842
|
try {
|
|
2936
2843
|
await runtime.adapter.stop();
|
|
2937
2844
|
} catch (error) {
|
|
2938
|
-
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");
|
|
2939
2846
|
}
|
|
2940
2847
|
}
|
|
2941
2848
|
}
|
|
2942
|
-
function
|
|
2849
|
+
function errorMessage(error) {
|
|
2943
2850
|
return error instanceof Error ? error.message : String(error);
|
|
2944
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
|
+
}
|
|
2945
2938
|
|
|
2946
2939
|
// src/gateway/public-resource.ts
|
|
2947
2940
|
function toPublicResource(resource) {
|
|
@@ -2962,7 +2955,8 @@ function toPublicResource(resource) {
|
|
|
2962
2955
|
var STORE_DETAIL = {
|
|
2963
2956
|
pass: void 0,
|
|
2964
2957
|
warn: "store-degraded",
|
|
2965
|
-
fail: "store-unwritable"
|
|
2958
|
+
fail: "store-unwritable",
|
|
2959
|
+
threw: "store-unreachable"
|
|
2966
2960
|
};
|
|
2967
2961
|
var ADAPTER_DETAIL = {
|
|
2968
2962
|
pass: void 0,
|
|
@@ -2979,108 +2973,102 @@ var AUTHORIZATION_PROVIDER_DETAIL = {
|
|
|
2979
2973
|
warn: "authorization-provider-degraded",
|
|
2980
2974
|
fail: "authorization-provider-unreachable"
|
|
2981
2975
|
};
|
|
2982
|
-
async function
|
|
2976
|
+
async function probe(target, kind, details, options) {
|
|
2983
2977
|
let health;
|
|
2978
|
+
let threw = false;
|
|
2984
2979
|
try {
|
|
2985
|
-
health = await
|
|
2980
|
+
health = await target.health();
|
|
2986
2981
|
} catch (error) {
|
|
2987
2982
|
const message = error instanceof Error ? error.message : String(error);
|
|
2988
|
-
options.logger.error({ err: message,
|
|
2983
|
+
options.logger.error({ err: message, name: target.name }, `${kind} health() threw`);
|
|
2989
2984
|
health = { status: "fail", checkedAt: options.clock.nowIso() };
|
|
2985
|
+
threw = true;
|
|
2990
2986
|
}
|
|
2991
2987
|
if (health.detail !== void 0) {
|
|
2992
2988
|
options.logger.debug(
|
|
2993
|
-
{
|
|
2989
|
+
{ name: target.name, detail: health.detail },
|
|
2994
2990
|
`${kind} health detail (not sent to the client)`
|
|
2995
2991
|
);
|
|
2996
2992
|
}
|
|
2997
|
-
const detail = details[health.status];
|
|
2993
|
+
const detail = threw ? details.threw ?? details.fail : details[health.status];
|
|
2998
2994
|
return {
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
2995
|
+
check: {
|
|
2996
|
+
name: target.name,
|
|
2997
|
+
status: health.status,
|
|
2998
|
+
...detail !== void 0 ? { detail } : {}
|
|
2999
|
+
},
|
|
3000
|
+
health
|
|
3002
3001
|
};
|
|
3003
3002
|
}
|
|
3004
|
-
async function
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
)
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
name: runtime.adapter.name,
|
|
3033
|
-
status: health.status,
|
|
3034
|
-
...detail !== void 0 ? { detail } : {}
|
|
3035
|
-
};
|
|
3036
|
-
})
|
|
3037
|
-
);
|
|
3038
|
-
const paymentProviderChecks = await Promise.all(
|
|
3039
|
-
options.paymentProviders.map(
|
|
3040
|
-
(provider) => probeProvider(provider, "payment provider", PAYMENT_PROVIDER_DETAIL, options)
|
|
3041
|
-
)
|
|
3042
|
-
);
|
|
3043
|
-
const authorizationProviderChecks = await Promise.all(
|
|
3044
|
-
options.authorizationProviders.map(
|
|
3045
|
-
(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
|
+
)
|
|
3046
3031
|
)
|
|
3047
|
-
);
|
|
3048
|
-
const
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
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
|
+
];
|
|
3055
3039
|
return {
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
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)
|
|
3061
3046
|
},
|
|
3062
|
-
adapters:
|
|
3063
|
-
|
|
3064
|
-
|
|
3047
|
+
adapters: adapters.map(({ runtime, probed }) => ({
|
|
3048
|
+
descriptor: runtime.adapter.descriptor,
|
|
3049
|
+
health: probed.health
|
|
3050
|
+
}))
|
|
3065
3051
|
};
|
|
3066
3052
|
}
|
|
3067
3053
|
var READINESS_TTL_MS = 2e3;
|
|
3068
3054
|
function createReadinessProbe(options, ttlMs = READINESS_TTL_MS) {
|
|
3069
3055
|
let cached;
|
|
3070
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
|
+
};
|
|
3071
3069
|
return {
|
|
3072
|
-
async
|
|
3073
|
-
|
|
3074
|
-
if (cached && now - cached.at < ttlMs) return cached.result;
|
|
3075
|
-
if (inFlight) return inFlight;
|
|
3076
|
-
inFlight = checkReadiness(options).then((result) => {
|
|
3077
|
-
cached = { result, at: options.clock.monotonicMs() };
|
|
3078
|
-
return result;
|
|
3079
|
-
}).finally(() => {
|
|
3080
|
-
inFlight = void 0;
|
|
3081
|
-
});
|
|
3082
|
-
return inFlight;
|
|
3083
|
-
}
|
|
3070
|
+
check: async () => (await current()).result,
|
|
3071
|
+
adapterHealth: async () => (await current()).adapters
|
|
3084
3072
|
};
|
|
3085
3073
|
}
|
|
3086
3074
|
|
|
@@ -3098,13 +3086,11 @@ function publicProtocols(protocols) {
|
|
|
3098
3086
|
acp: { enabled: protocols.acp.enabled, mountPath: protocols.acp.mountPath }
|
|
3099
3087
|
};
|
|
3100
3088
|
}
|
|
3101
|
-
|
|
3102
|
-
const adapters =
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
})
|
|
3107
|
-
);
|
|
3089
|
+
function buildWellKnownDocument(options) {
|
|
3090
|
+
const adapters = options.adapters.map(({ descriptor, health }) => ({
|
|
3091
|
+
...descriptor,
|
|
3092
|
+
health: publicHealth(health)
|
|
3093
|
+
}));
|
|
3108
3094
|
const { x402, mpp } = options.config.payments;
|
|
3109
3095
|
return {
|
|
3110
3096
|
gateway: {
|
|
@@ -3128,14 +3114,11 @@ async function buildWellKnownDocument(options) {
|
|
|
3128
3114
|
assetDecimals: x402.assetDecimals,
|
|
3129
3115
|
payTo: x402.payTo,
|
|
3130
3116
|
maxTimeoutSeconds: x402.maxTimeoutSeconds,
|
|
3131
|
-
//
|
|
3132
|
-
// a
|
|
3133
|
-
// that can carry a tenant path or an API key, exactly like
|
|
3134
|
-
// `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
|
|
3135
3119
|
facilitator: { mode: x402.facilitator.mode },
|
|
3136
|
-
// Chain id 84532 is
|
|
3137
|
-
//
|
|
3138
|
-
// 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
|
|
3139
3122
|
mode: resolveDeploymentMode(
|
|
3140
3123
|
requireNetworkProfile(x402.network, "payments.x402.network"),
|
|
3141
3124
|
x402.facilitator.mode
|
|
@@ -3163,8 +3146,6 @@ async function buildWellKnownDocument(options) {
|
|
|
3163
3146
|
}
|
|
3164
3147
|
|
|
3165
3148
|
// src/gateway/routes.ts
|
|
3166
|
-
var SSE_HEARTBEAT_MS = 15e3;
|
|
3167
|
-
var MAX_SSE_SUBSCRIBERS = 32;
|
|
3168
3149
|
function registerRoutes(options) {
|
|
3169
3150
|
const { server } = options;
|
|
3170
3151
|
const readinessProbe = createReadinessProbe({
|
|
@@ -3188,8 +3169,7 @@ function registerRoutes(options) {
|
|
|
3188
3169
|
paymentProviders: options.paymentProviders,
|
|
3189
3170
|
authorizationProviders: options.authorizationProviders,
|
|
3190
3171
|
store: options.store,
|
|
3191
|
-
|
|
3192
|
-
clock: options.clock
|
|
3172
|
+
adapters: await readinessProbe.adapterHealth()
|
|
3193
3173
|
})
|
|
3194
3174
|
);
|
|
3195
3175
|
server.get("/api/resources", async () => ({
|
|
@@ -3198,81 +3178,35 @@ function registerRoutes(options) {
|
|
|
3198
3178
|
server.post("/api/resources/:id/invoke", async (request, reply) => {
|
|
3199
3179
|
await handleInvoke(request, reply, options);
|
|
3200
3180
|
});
|
|
3201
|
-
const
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
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) => {
|
|
3215
3195
|
try {
|
|
3216
3196
|
const limit = parseLimit(request.query["limit"]);
|
|
3217
|
-
return {
|
|
3197
|
+
return { [key]: await list(limit !== void 0 ? { limit } : {}) };
|
|
3218
3198
|
} catch (error) {
|
|
3219
3199
|
const commerceError = toCommerceError(error);
|
|
3220
3200
|
reply.status(commerceError.httpStatus).send(toErrorEnvelope(commerceError));
|
|
3221
3201
|
return void 0;
|
|
3222
3202
|
}
|
|
3223
|
-
}
|
|
3224
|
-
let activeSseSubscribers = 0;
|
|
3225
|
-
server.get("/api/events/stream", { onRequest: eventsStreamTokenHook }, (request, reply) => {
|
|
3226
|
-
if (activeSseSubscribers >= MAX_SSE_SUBSCRIBERS) {
|
|
3227
|
-
reply.status(503).send({
|
|
3228
|
-
status: "error",
|
|
3229
|
-
code: "UNAVAILABLE",
|
|
3230
|
-
message: "Too many event-stream subscribers"
|
|
3231
|
-
});
|
|
3232
|
-
return;
|
|
3233
|
-
}
|
|
3234
|
-
reply.raw.writeHead(200, {
|
|
3235
|
-
"Content-Type": "text/event-stream",
|
|
3236
|
-
"Cache-Control": "no-cache",
|
|
3237
|
-
Connection: "keep-alive"
|
|
3238
|
-
});
|
|
3239
|
-
activeSseSubscribers += 1;
|
|
3240
|
-
reply.raw.write(": connected\n\n");
|
|
3241
|
-
let writable = true;
|
|
3242
|
-
reply.raw.on("drain", () => {
|
|
3243
|
-
writable = true;
|
|
3244
|
-
});
|
|
3245
|
-
const unsubscribe = options.eventBus.subscribe((event) => {
|
|
3246
|
-
queueMicrotask(() => {
|
|
3247
|
-
if (!writable) return;
|
|
3248
|
-
writable = reply.raw.write(`data: ${JSON.stringify(event)}
|
|
3249
|
-
|
|
3250
|
-
`);
|
|
3251
|
-
});
|
|
3252
|
-
});
|
|
3253
|
-
const heartbeat = setInterval(() => {
|
|
3254
|
-
if (!writable) return;
|
|
3255
|
-
writable = reply.raw.write(": heartbeat\n\n");
|
|
3256
|
-
}, SSE_HEARTBEAT_MS);
|
|
3257
|
-
heartbeat.unref();
|
|
3258
|
-
let cleanedUp = false;
|
|
3259
|
-
const cleanup = () => {
|
|
3260
|
-
if (cleanedUp) return;
|
|
3261
|
-
cleanedUp = true;
|
|
3262
|
-
clearInterval(heartbeat);
|
|
3263
|
-
unsubscribe();
|
|
3264
|
-
activeSseSubscribers -= 1;
|
|
3265
|
-
};
|
|
3266
|
-
request.raw.on("close", cleanup);
|
|
3267
|
-
reply.raw.on("error", cleanup);
|
|
3268
|
-
reply.hijack();
|
|
3269
|
-
});
|
|
3203
|
+
};
|
|
3270
3204
|
}
|
|
3271
3205
|
async function handleInvoke(request, reply, options) {
|
|
3272
3206
|
const resourceId = request.params.id;
|
|
3273
3207
|
try {
|
|
3274
3208
|
const resource = options.resources.get(resourceId);
|
|
3275
|
-
if (!resource
|
|
3209
|
+
if (!resource?.exposedVia.includes("http")) {
|
|
3276
3210
|
throw new CommerceError("RESOURCE_NOT_FOUND", `Resource "${resourceId}" was not found`, {
|
|
3277
3211
|
requestId: request.id,
|
|
3278
3212
|
resourceId
|
|
@@ -3342,9 +3276,8 @@ function paymentProof(request, method) {
|
|
|
3342
3276
|
}
|
|
3343
3277
|
function errorPaymentSummary(error) {
|
|
3344
3278
|
const payment = error.details?.["payment"];
|
|
3345
|
-
if (
|
|
3346
|
-
const
|
|
3347
|
-
const { status, provider, amount, currency, network, externalReference, receipt } = rec;
|
|
3279
|
+
if (!isRecord(payment)) return void 0;
|
|
3280
|
+
const { status, provider, amount, currency, network, externalReference, receipt } = payment;
|
|
3348
3281
|
if (typeof status !== "string" || typeof provider !== "string" || typeof amount !== "string" || typeof currency !== "string") {
|
|
3349
3282
|
return void 0;
|
|
3350
3283
|
}
|
|
@@ -3397,6 +3330,11 @@ function parseLimit(value) {
|
|
|
3397
3330
|
// src/gateway/server.ts
|
|
3398
3331
|
var REQUEST_ID_HEADER = "x-request-id";
|
|
3399
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
|
+
}
|
|
3400
3338
|
function withProviderBackedMethodsFirst(resource, providers) {
|
|
3401
3339
|
const backed = (method) => providers.some((provider) => provider.name === method);
|
|
3402
3340
|
const ordered = [
|
|
@@ -3416,26 +3354,28 @@ async function createGateway(options) {
|
|
|
3416
3354
|
(resource) => withProviderBackedMethodsFirst(resource, options.paymentProviders)
|
|
3417
3355
|
)
|
|
3418
3356
|
);
|
|
3419
|
-
const
|
|
3357
|
+
const events = createStoreEventSink({ store: options.store, logger });
|
|
3420
3358
|
const pipeline = createExecutionPipeline({
|
|
3421
3359
|
resources,
|
|
3422
3360
|
paymentProviders: options.paymentProviders,
|
|
3423
3361
|
authorizationProviders,
|
|
3424
3362
|
store: options.store,
|
|
3425
3363
|
backend,
|
|
3426
|
-
events
|
|
3364
|
+
events,
|
|
3427
3365
|
logger,
|
|
3428
3366
|
clock,
|
|
3429
3367
|
ids
|
|
3430
3368
|
});
|
|
3431
3369
|
const server = Fastify({
|
|
3432
|
-
//
|
|
3433
|
-
// 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
|
|
3434
3371
|
bodyLimit: MOUNT_BODY_LIMIT_BYTES,
|
|
3435
|
-
genReqId: (
|
|
3436
|
-
|
|
3437
|
-
const
|
|
3438
|
-
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
|
+
);
|
|
3439
3379
|
},
|
|
3440
3380
|
logger: fastifyLoggerOptions({ name: `${options.config.merchant.id}-http` })
|
|
3441
3381
|
});
|
|
@@ -3456,7 +3396,6 @@ async function createGateway(options) {
|
|
|
3456
3396
|
store: options.store,
|
|
3457
3397
|
paymentProviders: options.paymentProviders,
|
|
3458
3398
|
authorizationProviders,
|
|
3459
|
-
eventBus,
|
|
3460
3399
|
clock,
|
|
3461
3400
|
adapterRuntimes,
|
|
3462
3401
|
logger
|
|
@@ -3464,7 +3403,7 @@ async function createGateway(options) {
|
|
|
3464
3403
|
const context = {
|
|
3465
3404
|
pipeline,
|
|
3466
3405
|
resources,
|
|
3467
|
-
events
|
|
3406
|
+
events,
|
|
3468
3407
|
logger,
|
|
3469
3408
|
clock,
|
|
3470
3409
|
ids,
|
|
@@ -3497,6 +3436,554 @@ async function createGateway(options) {
|
|
|
3497
3436
|
};
|
|
3498
3437
|
}
|
|
3499
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
|
+
|
|
3500
3987
|
// src/protocols/acp/checkout-mapping.ts
|
|
3501
3988
|
function toCanonicalRequest(options) {
|
|
3502
3989
|
const { request, resourceId, requestId, receivedAt, idempotencyKey } = options;
|
|
@@ -3511,13 +3998,14 @@ function toCanonicalRequest(options) {
|
|
|
3511
3998
|
}
|
|
3512
3999
|
function canonicalInput(request) {
|
|
3513
4000
|
const keys = ACP_OPERATION_INPUT_KEYS[request.route.operation];
|
|
4001
|
+
const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[request.route.operation] ?? [];
|
|
3514
4002
|
const input = {};
|
|
3515
4003
|
if (keys.includes("path") && request.route.sessionId !== void 0) {
|
|
3516
4004
|
input["path"] = { checkout_session_id: request.route.sessionId };
|
|
3517
4005
|
}
|
|
3518
4006
|
if (keys.includes("body")) {
|
|
3519
4007
|
input["body"] = request.body;
|
|
3520
|
-
} else if (
|
|
4008
|
+
} else if (optionalKeys.includes("body") && Object.keys(request.body).length > 0) {
|
|
3521
4009
|
input["body"] = request.body;
|
|
3522
4010
|
}
|
|
3523
4011
|
return input;
|
|
@@ -3540,21 +4028,21 @@ var ACP_UNSUPPORTED = [
|
|
|
3540
4028
|
"standalone orders service",
|
|
3541
4029
|
"delegate_payment",
|
|
3542
4030
|
"delegate_authentication",
|
|
3543
|
-
// Bindings and delivery directions
|
|
4031
|
+
// Bindings and delivery directions
|
|
3544
4032
|
"ACP MCP transport binding",
|
|
3545
4033
|
"webhooks",
|
|
3546
4034
|
"outbound merchant-to-agent delivery",
|
|
3547
4035
|
"ACP client role",
|
|
3548
|
-
// Request authentication beyond the bearer token
|
|
4036
|
+
// Request authentication beyond the bearer token
|
|
3549
4037
|
"Signature header verification",
|
|
3550
4038
|
"Timestamp replay-window verification",
|
|
3551
|
-
// Extensions and
|
|
4039
|
+
// Extensions, versioning and payment handlers
|
|
3552
4040
|
"discount extension",
|
|
3553
4041
|
"general ACP extension framework",
|
|
3554
4042
|
"multiple ACP API versions",
|
|
3555
4043
|
"seller-backed payment handler integration"
|
|
3556
4044
|
];
|
|
3557
|
-
function
|
|
4045
|
+
function buildDescriptor2(implementationVersion) {
|
|
3558
4046
|
return {
|
|
3559
4047
|
name: "acp",
|
|
3560
4048
|
kind: "protocol",
|
|
@@ -3577,9 +4065,8 @@ function buildAcpDiscoveryDocument(options) {
|
|
|
3577
4065
|
...metadata.documentationUrl !== void 0 ? { documentation_url: metadata.documentationUrl } : {}
|
|
3578
4066
|
},
|
|
3579
4067
|
api_base_url: joinUrl(options.publicBaseUrl, options.mountPath),
|
|
3580
|
-
//
|
|
3581
|
-
//
|
|
3582
|
-
// 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
|
|
3583
4070
|
transports: ["rest"],
|
|
3584
4071
|
capabilities: {
|
|
3585
4072
|
services: ["checkout"],
|
|
@@ -3589,9 +4076,6 @@ function buildAcpDiscoveryDocument(options) {
|
|
|
3589
4076
|
}
|
|
3590
4077
|
};
|
|
3591
4078
|
}
|
|
3592
|
-
function joinUrl(baseUrl, mountPath) {
|
|
3593
|
-
return `${baseUrl.replace(/\/+$/, "")}/${mountPath.replace(/^\/+|\/+$/g, "")}`;
|
|
3594
|
-
}
|
|
3595
4079
|
|
|
3596
4080
|
// src/protocols/acp/errors.ts
|
|
3597
4081
|
function acpFailure(status, type, code, message, extra = {}) {
|
|
@@ -3615,10 +4099,10 @@ function writeAcpFailure(res, failure, headers = {}) {
|
|
|
3615
4099
|
writeAcpJson(res, failure.status, failure.error, headers);
|
|
3616
4100
|
}
|
|
3617
4101
|
function operationKey(scope) {
|
|
3618
|
-
return
|
|
4102
|
+
return sha2562(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
|
|
3619
4103
|
}
|
|
3620
4104
|
function requestFingerprint(body) {
|
|
3621
|
-
return
|
|
4105
|
+
return sha2562(canonicalize(body));
|
|
3622
4106
|
}
|
|
3623
4107
|
function canonicalize(value) {
|
|
3624
4108
|
if (value === null) return "null";
|
|
@@ -3629,7 +4113,7 @@ function canonicalize(value) {
|
|
|
3629
4113
|
}
|
|
3630
4114
|
return JSON.stringify(value) ?? "null";
|
|
3631
4115
|
}
|
|
3632
|
-
function
|
|
4116
|
+
function sha2562(value) {
|
|
3633
4117
|
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
3634
4118
|
}
|
|
3635
4119
|
|
|
@@ -3646,6 +4130,7 @@ function createAcpIdempotencyStore(options) {
|
|
|
3646
4130
|
logger
|
|
3647
4131
|
});
|
|
3648
4132
|
migrate(db, logger);
|
|
4133
|
+
resolveOrphanedClaims(db, logger);
|
|
3649
4134
|
const selectStmt = db.prepare(
|
|
3650
4135
|
`SELECT state, fingerprint, status, body_json FROM acp_idempotency
|
|
3651
4136
|
WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
|
|
@@ -3727,6 +4212,15 @@ function createAcpIdempotencyStore(options) {
|
|
|
3727
4212
|
}
|
|
3728
4213
|
};
|
|
3729
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
|
+
}
|
|
3730
4224
|
function migrate(db, logger) {
|
|
3731
4225
|
const current = db.pragma("user_version", { simple: true });
|
|
3732
4226
|
if (current >= SCHEMA_VERSION) return;
|
|
@@ -3761,17 +4255,6 @@ function migrate(db, logger) {
|
|
|
3761
4255
|
});
|
|
3762
4256
|
apply();
|
|
3763
4257
|
}
|
|
3764
|
-
var BEARER_PREFIX = /^Bearer +/i;
|
|
3765
|
-
function isAuthorizedBearer(header2, expectedToken) {
|
|
3766
|
-
if (header2 === void 0) return false;
|
|
3767
|
-
if (!BEARER_PREFIX.test(header2)) return false;
|
|
3768
|
-
const presented = header2.replace(BEARER_PREFIX, "");
|
|
3769
|
-
if (presented.length === 0) return false;
|
|
3770
|
-
return timingSafeEqual(sha2562(presented), sha2562(expectedToken));
|
|
3771
|
-
}
|
|
3772
|
-
function sha2562(value) {
|
|
3773
|
-
return createHash("sha256").update(value, "utf8").digest();
|
|
3774
|
-
}
|
|
3775
4258
|
|
|
3776
4259
|
// src/protocols/acp/router.ts
|
|
3777
4260
|
var MAX_SESSION_ID_LENGTH = 128;
|
|
@@ -7777,12 +8260,11 @@ var validators;
|
|
|
7777
8260
|
function acpValidators() {
|
|
7778
8261
|
if (validators !== void 0) return validators;
|
|
7779
8262
|
const ajv = new Ajv2020({
|
|
7780
|
-
// The vendored document is the released spec
|
|
7781
|
-
// are
|
|
8263
|
+
// The vendored document is the released spec; unknown annotations in it
|
|
8264
|
+
// are no reason to refuse to start
|
|
7782
8265
|
strict: false,
|
|
7783
|
-
//
|
|
7784
|
-
//
|
|
7785
|
-
// 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
|
|
7786
8268
|
allErrors: false
|
|
7787
8269
|
});
|
|
7788
8270
|
addFormats(ajv);
|
|
@@ -7864,7 +8346,7 @@ async function guardAcpRequest(req, options) {
|
|
|
7864
8346
|
);
|
|
7865
8347
|
}
|
|
7866
8348
|
const route = matched.route;
|
|
7867
|
-
if (!
|
|
8349
|
+
if (!options.isAuthorized(header(req, "authorization"))) {
|
|
7868
8350
|
return failed(
|
|
7869
8351
|
acpFailure(
|
|
7870
8352
|
401,
|
|
@@ -7922,7 +8404,7 @@ async function guardAcpRequest(req, options) {
|
|
|
7922
8404
|
}
|
|
7923
8405
|
idempotencyKey = presented;
|
|
7924
8406
|
}
|
|
7925
|
-
const read = await
|
|
8407
|
+
const read = await readCappedBody(req, options.maxBodyBytes ?? ACP_MAX_REQUEST_BODY_BYTES);
|
|
7926
8408
|
if (read.kind === "too-large") {
|
|
7927
8409
|
return failed(
|
|
7928
8410
|
acpFailure(
|
|
@@ -8020,21 +8502,6 @@ function header(req, name) {
|
|
|
8020
8502
|
const value = req.headers[name];
|
|
8021
8503
|
return Array.isArray(value) ? value[0] : value;
|
|
8022
8504
|
}
|
|
8023
|
-
async function readBody(req, maxBytes) {
|
|
8024
|
-
const chunks = [];
|
|
8025
|
-
let total = 0;
|
|
8026
|
-
try {
|
|
8027
|
-
for await (const chunk of req) {
|
|
8028
|
-
const buffer = chunk;
|
|
8029
|
-
total += buffer.length;
|
|
8030
|
-
if (total > maxBytes) return { kind: "too-large" };
|
|
8031
|
-
chunks.push(buffer);
|
|
8032
|
-
}
|
|
8033
|
-
} catch {
|
|
8034
|
-
return { kind: "unreadable" };
|
|
8035
|
-
}
|
|
8036
|
-
return { kind: "ok", text: Buffer.concat(chunks).toString("utf8") };
|
|
8037
|
-
}
|
|
8038
8505
|
|
|
8039
8506
|
// src/protocols/acp/response-mapping.ts
|
|
8040
8507
|
var ACP_SUCCESS_STATUS = {
|
|
@@ -8192,15 +8659,12 @@ var AcpProtocolAdapter = class {
|
|
|
8192
8659
|
mountPath;
|
|
8193
8660
|
descriptor;
|
|
8194
8661
|
additionalHttpRoutes;
|
|
8195
|
-
|
|
8662
|
+
isAuthorized;
|
|
8196
8663
|
operations;
|
|
8197
8664
|
idempotencyOptions;
|
|
8198
8665
|
/**
|
|
8199
|
-
*
|
|
8200
|
-
*
|
|
8201
|
-
* Not readonly because, unlike the token, it is known only once the adapter
|
|
8202
|
-
* starts. Worth that: scoping by the credential meant a rotation freed
|
|
8203
|
-
* 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.
|
|
8204
8668
|
*/
|
|
8205
8669
|
deployment;
|
|
8206
8670
|
discoveryMetadata;
|
|
@@ -8208,17 +8672,16 @@ var AcpProtocolAdapter = class {
|
|
|
8208
8672
|
resources = /* @__PURE__ */ new Map();
|
|
8209
8673
|
context;
|
|
8210
8674
|
started = false;
|
|
8211
|
-
// Built once at start: the document is fixed by config, and
|
|
8212
|
-
//
|
|
8213
|
-
// 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
|
|
8214
8677
|
discovery;
|
|
8215
8678
|
constructor(options) {
|
|
8216
8679
|
this.mountPath = options.mountPath;
|
|
8217
|
-
this.
|
|
8680
|
+
this.isAuthorized = createBearerCheck(options.token);
|
|
8218
8681
|
this.operations = options.operations;
|
|
8219
8682
|
this.idempotencyOptions = options.idempotency;
|
|
8220
8683
|
this.discoveryMetadata = options.discovery;
|
|
8221
|
-
this.descriptor =
|
|
8684
|
+
this.descriptor = buildDescriptor2(PACKAGE_VERSION);
|
|
8222
8685
|
this.additionalHttpRoutes = [
|
|
8223
8686
|
{
|
|
8224
8687
|
method: "GET",
|
|
@@ -8235,12 +8698,12 @@ var AcpProtocolAdapter = class {
|
|
|
8235
8698
|
mountPath: this.mountPath,
|
|
8236
8699
|
...this.discoveryMetadata !== void 0 ? { metadata: this.discoveryMetadata } : {}
|
|
8237
8700
|
});
|
|
8238
|
-
const
|
|
8239
|
-
if (
|
|
8701
|
+
const invalid2 = validateAcpDocument("discoveryResponse", document);
|
|
8702
|
+
if (invalid2 !== void 0) {
|
|
8240
8703
|
throw new CommerceError(
|
|
8241
8704
|
"CONFIG_INVALID",
|
|
8242
|
-
`ACP discovery document does not conform to the ${this.descriptor.supportedSpec} snapshot at "${
|
|
8243
|
-
{ 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 } }
|
|
8244
8707
|
);
|
|
8245
8708
|
}
|
|
8246
8709
|
const exposed = new Map(
|
|
@@ -8277,7 +8740,7 @@ var AcpProtocolAdapter = class {
|
|
|
8277
8740
|
"acp adapter started"
|
|
8278
8741
|
);
|
|
8279
8742
|
}
|
|
8280
|
-
/** `GET /.well-known/acp.json
|
|
8743
|
+
/** `GET /.well-known/acp.json`: public, unauthenticated, cacheable */
|
|
8281
8744
|
async handleDiscovery(req, res) {
|
|
8282
8745
|
try {
|
|
8283
8746
|
if (!this.started || this.discovery === void 0) {
|
|
@@ -8309,7 +8772,7 @@ var AcpProtocolAdapter = class {
|
|
|
8309
8772
|
this.fail(res, err, "acp adapter: discovery request failed");
|
|
8310
8773
|
}
|
|
8311
8774
|
}
|
|
8312
|
-
/** `<mountPath>/checkout_sessions
|
|
8775
|
+
/** `<mountPath>/checkout_sessions...`: the five stable checkout routes */
|
|
8313
8776
|
async handleHttp(req, res) {
|
|
8314
8777
|
try {
|
|
8315
8778
|
if (!this.started) {
|
|
@@ -8326,7 +8789,7 @@ var AcpProtocolAdapter = class {
|
|
|
8326
8789
|
}
|
|
8327
8790
|
const guarded = await guardAcpRequest(req, {
|
|
8328
8791
|
mountPath: this.mountPath,
|
|
8329
|
-
|
|
8792
|
+
isAuthorized: this.isAuthorized
|
|
8330
8793
|
});
|
|
8331
8794
|
if (!guarded.ok) {
|
|
8332
8795
|
writeAcpFailure(res, guarded);
|
|
@@ -8340,12 +8803,9 @@ var AcpProtocolAdapter = class {
|
|
|
8340
8803
|
}
|
|
8341
8804
|
}
|
|
8342
8805
|
/**
|
|
8343
|
-
* One request, at most one execution.
|
|
8344
|
-
*
|
|
8345
|
-
*
|
|
8346
|
-
* so a retry that arrives while the first one is still running finds the
|
|
8347
|
-
* claim instead of starting a second checkout. A route with no key (the GET)
|
|
8348
|
-
* 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.
|
|
8349
8809
|
*/
|
|
8350
8810
|
async dispatch(request) {
|
|
8351
8811
|
const store = this.idempotency;
|
|
@@ -8406,13 +8866,7 @@ var AcpProtocolAdapter = class {
|
|
|
8406
8866
|
}
|
|
8407
8867
|
return outcome.response;
|
|
8408
8868
|
}
|
|
8409
|
-
|
|
8410
|
-
* One accepted request, one `pipeline.execute()`.
|
|
8411
|
-
*
|
|
8412
|
-
* Nothing here prices a resource, inspects a payment proof or calls a
|
|
8413
|
-
* merchant backend: the adapter builds a canonical request and reads back
|
|
8414
|
-
* what the pipeline decided.
|
|
8415
|
-
*/
|
|
8869
|
+
// One accepted request, one `pipeline.execute()`
|
|
8416
8870
|
async runCheckout(request, idempotencyKey) {
|
|
8417
8871
|
const context = this.context;
|
|
8418
8872
|
const resource = this.resources.get(request.route.operation);
|
|
@@ -8464,7 +8918,7 @@ var AcpProtocolAdapter = class {
|
|
|
8464
8918
|
resourceId: resource.id,
|
|
8465
8919
|
requestId: canonical.requestId,
|
|
8466
8920
|
reached: reachedFor(error.code),
|
|
8467
|
-
err: error
|
|
8921
|
+
err: toLogInfo(error)
|
|
8468
8922
|
},
|
|
8469
8923
|
"acp adapter: checkout execution failed"
|
|
8470
8924
|
);
|
|
@@ -8474,10 +8928,7 @@ var AcpProtocolAdapter = class {
|
|
|
8474
8928
|
};
|
|
8475
8929
|
}
|
|
8476
8930
|
}
|
|
8477
|
-
|
|
8478
|
-
* Protocol-owned headers only. A merchant backend's own headers are never
|
|
8479
|
-
* proxied onto an ACP response.
|
|
8480
|
-
*/
|
|
8931
|
+
// Protocol-owned headers only; a merchant backend's headers are never proxied
|
|
8481
8932
|
responseHeaders(request, response) {
|
|
8482
8933
|
const replayed = response?.replayed === true;
|
|
8483
8934
|
return {
|
|
@@ -8488,7 +8939,7 @@ var AcpProtocolAdapter = class {
|
|
|
8488
8939
|
};
|
|
8489
8940
|
}
|
|
8490
8941
|
fail(res, err, message) {
|
|
8491
|
-
this.context?.logger.error({ err:
|
|
8942
|
+
this.context?.logger.error({ err: toLogInfo(err) }, message);
|
|
8492
8943
|
writeAcpFailure(
|
|
8493
8944
|
res,
|
|
8494
8945
|
acpFailure(500, "processing_error", "internal_error", "Internal server error.")
|
|
@@ -8551,8 +9002,8 @@ function receiptToRow(receipt) {
|
|
|
8551
9002
|
payment_json: receipt.payment !== void 0 ? JSON.stringify(redact(receipt.payment)) : null,
|
|
8552
9003
|
delivered_at: receipt.deliveredAt,
|
|
8553
9004
|
backend_status: receipt.backendStatus,
|
|
8554
|
-
duration_ms: receipt.durationMs
|
|
8555
|
-
protocol: receipt.protocol
|
|
9005
|
+
duration_ms: receipt.durationMs ?? null,
|
|
9006
|
+
protocol: receipt.protocol ?? null,
|
|
8556
9007
|
metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
|
|
8557
9008
|
authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
|
|
8558
9009
|
};
|
|
@@ -8579,12 +9030,12 @@ function eventToRow(event) {
|
|
|
8579
9030
|
id: event.id,
|
|
8580
9031
|
type: event.type,
|
|
8581
9032
|
request_id: event.requestId,
|
|
8582
|
-
resource_id: event.resourceId
|
|
9033
|
+
resource_id: event.resourceId ?? null,
|
|
8583
9034
|
at: event.at,
|
|
8584
|
-
adapter: event.adapter
|
|
8585
|
-
payment_provider: event.paymentProvider
|
|
8586
|
-
duration_ms: event.durationMs
|
|
8587
|
-
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,
|
|
8588
9039
|
data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null
|
|
8589
9040
|
};
|
|
8590
9041
|
}
|
|
@@ -8688,7 +9139,7 @@ var MIGRATIONS = [
|
|
|
8688
9139
|
}
|
|
8689
9140
|
}
|
|
8690
9141
|
];
|
|
8691
|
-
var SCHEMA_VERSION2 = MIGRATIONS
|
|
9142
|
+
var SCHEMA_VERSION2 = MIGRATIONS.at(-1)?.version ?? 0;
|
|
8692
9143
|
function migrate2(db) {
|
|
8693
9144
|
const current = db.pragma("user_version", { simple: true });
|
|
8694
9145
|
for (const migration of MIGRATIONS) {
|
|
@@ -8751,10 +9202,8 @@ function createSqliteReceiptStore(options) {
|
|
|
8751
9202
|
);
|
|
8752
9203
|
const insertPaymentAttemptStmt = db.prepare(
|
|
8753
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)
|
|
8754
|
-
VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)
|
|
8755
|
-
|
|
8756
|
-
const getPaymentAttemptByIdStmt = db.prepare(
|
|
8757
|
-
"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 *`
|
|
8758
9207
|
);
|
|
8759
9208
|
const updatePaymentAttemptStmt = db.prepare(
|
|
8760
9209
|
`UPDATE payment_attempts SET status = @status,
|
|
@@ -8805,8 +9254,9 @@ function createSqliteReceiptStore(options) {
|
|
|
8805
9254
|
async reservePaymentAttempt(reservation) {
|
|
8806
9255
|
const now = clock.nowIso();
|
|
8807
9256
|
const id = ids.next("attempt");
|
|
9257
|
+
let row;
|
|
8808
9258
|
try {
|
|
8809
|
-
insertPaymentAttemptStmt.
|
|
9259
|
+
row = insertPaymentAttemptStmt.get({
|
|
8810
9260
|
id,
|
|
8811
9261
|
request_id: reservation.requestId,
|
|
8812
9262
|
resource_id: reservation.resourceId,
|
|
@@ -8826,19 +9276,12 @@ function createSqliteReceiptStore(options) {
|
|
|
8826
9276
|
if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
|
|
8827
9277
|
throw new CommerceError(
|
|
8828
9278
|
"PAYMENT_REPLAYED",
|
|
8829
|
-
`Payment
|
|
9279
|
+
`Payment authorization has already been used (replayKey=${reservation.replayKey})`,
|
|
8830
9280
|
{ requestId: reservation.requestId, resourceId: reservation.resourceId }
|
|
8831
9281
|
);
|
|
8832
9282
|
}
|
|
8833
9283
|
throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");
|
|
8834
9284
|
}
|
|
8835
|
-
const row = getPaymentAttemptByIdStmt.get(id);
|
|
8836
|
-
if (row === void 0) {
|
|
8837
|
-
throw new CommerceError(
|
|
8838
|
-
"STORAGE_ERROR",
|
|
8839
|
-
"Payment attempt vanished immediately after insert"
|
|
8840
|
-
);
|
|
8841
|
-
}
|
|
8842
9285
|
return rowToPaymentAttempt(row);
|
|
8843
9286
|
},
|
|
8844
9287
|
async updatePaymentAttempt(update) {
|
|
@@ -8876,11 +9319,11 @@ function createSqliteReceiptStore(options) {
|
|
|
8876
9319
|
const rows = listOptions.requestId !== void 0 ? listReceiptsByRequestStmt.all(listOptions.requestId, limit) : listReceiptsStmt.all(limit);
|
|
8877
9320
|
return rows.map(rowToReceipt);
|
|
8878
9321
|
},
|
|
8879
|
-
|
|
9322
|
+
// Exact total, not bounded by the listReceipts clamp (see ReceiptStore.countReceipts)
|
|
8880
9323
|
async countReceipts() {
|
|
8881
9324
|
return countReceiptsStmt.get()?.count ?? 0;
|
|
8882
9325
|
},
|
|
8883
|
-
|
|
9326
|
+
// Exact count of non-2xx receipts (see ReceiptStore.countUndeliveredReceipts)
|
|
8884
9327
|
async countUndeliveredReceipts() {
|
|
8885
9328
|
return countUndeliveredReceiptsStmt.get()?.count ?? 0;
|
|
8886
9329
|
},
|
|
@@ -8959,6 +9402,6 @@ function createSqliteReceiptStore(options) {
|
|
|
8959
9402
|
return store;
|
|
8960
9403
|
}
|
|
8961
9404
|
|
|
8962
|
-
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 };
|
|
8963
9406
|
//# sourceMappingURL=index.js.map
|
|
8964
9407
|
//# sourceMappingURL=index.js.map
|