@devlab.group/agent-commerce 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -56
- package/dist/ap2.d.ts +2 -2
- package/dist/ap2.js +4 -3
- package/dist/ap2.js.map +1 -1
- package/dist/{authorization-BpbWGGrf.d.ts → authorization-CMkIuAkO.d.ts} +1 -1
- package/dist/chunk-2CJNA7MS.js +8 -0
- package/dist/chunk-2CJNA7MS.js.map +1 -0
- package/dist/{chunk-CKJRITWY.js → chunk-5ZYXBCDX.js} +3 -3
- package/dist/{chunk-CKJRITWY.js.map → chunk-5ZYXBCDX.js.map} +1 -1
- package/dist/{chunk-3LUJHVKX.js → chunk-6Z2H53CG.js} +5 -4
- package/dist/chunk-6Z2H53CG.js.map +1 -0
- package/dist/{chunk-53WXXIPR.js → chunk-7ZMMXZ4G.js} +6 -10
- package/dist/chunk-7ZMMXZ4G.js.map +1 -0
- package/dist/{chunk-HOZA3IWP.js → chunk-ES3V6ECS.js} +16 -6
- package/dist/chunk-ES3V6ECS.js.map +1 -0
- package/dist/chunk-HNRDZLSN.js +1398 -0
- package/dist/chunk-HNRDZLSN.js.map +1 -0
- package/dist/{chunk-SYEGCNJZ.js → chunk-IEW7NSP2.js} +23 -5
- package/dist/chunk-IEW7NSP2.js.map +1 -0
- package/dist/chunk-KA2AX5FH.js +44 -0
- package/dist/chunk-KA2AX5FH.js.map +1 -0
- package/dist/chunk-Y4RE3XYF.js +581 -0
- package/dist/chunk-Y4RE3XYF.js.map +1 -0
- package/dist/cli/ap2-MBMQHFX4.js +678 -0
- package/dist/cli/ap2-MBMQHFX4.js.map +1 -0
- package/dist/cli/chunk-2ZZOGI5I.js +1352 -0
- package/dist/cli/chunk-2ZZOGI5I.js.map +1 -0
- package/dist/cli/chunk-3EQBCFZY.js +53 -0
- package/dist/cli/chunk-3EQBCFZY.js.map +1 -0
- package/dist/cli/chunk-3M6U7ILB.js +8 -0
- package/dist/cli/chunk-3M6U7ILB.js.map +1 -0
- package/dist/cli/chunk-3MGAJ5FX.js +70 -0
- package/dist/cli/chunk-3MGAJ5FX.js.map +1 -0
- package/dist/cli/chunk-5K4XEAZV.js +113 -0
- package/dist/cli/chunk-5K4XEAZV.js.map +1 -0
- package/dist/cli/chunk-7JNYFUT3.js +256 -0
- package/dist/cli/chunk-7JNYFUT3.js.map +1 -0
- package/dist/cli/chunk-AA3FIEAR.js +27 -0
- package/dist/cli/chunk-AA3FIEAR.js.map +1 -0
- package/dist/cli/chunk-FHCFR2GS.js +17 -0
- package/dist/cli/chunk-FHCFR2GS.js.map +1 -0
- package/dist/cli/chunk-FRMLWGSS.js +8 -0
- package/dist/cli/chunk-FRMLWGSS.js.map +1 -0
- package/dist/cli/chunk-KLX2YZDG.js +1400 -0
- package/dist/cli/chunk-KLX2YZDG.js.map +1 -0
- package/dist/cli/chunk-NSPPEF5M.js +334 -0
- package/dist/cli/chunk-NSPPEF5M.js.map +1 -0
- package/dist/cli/chunk-OSTCU6ZK.js +44 -0
- package/dist/cli/chunk-OSTCU6ZK.js.map +1 -0
- package/dist/cli/chunk-QQ3WY5L3.js +271 -0
- package/dist/cli/chunk-QQ3WY5L3.js.map +1 -0
- package/dist/cli/chunk-RWVPYAKC.js +1742 -0
- package/dist/cli/chunk-RWVPYAKC.js.map +1 -0
- package/dist/cli/chunk-SPAYKVUP.js +118 -0
- package/dist/cli/chunk-SPAYKVUP.js.map +1 -0
- package/dist/cli/chunk-SS3QFF5N.js +614 -0
- package/dist/cli/chunk-SS3QFF5N.js.map +1 -0
- package/dist/cli/chunk-TPC4EJLT.js +11 -0
- package/dist/cli/chunk-TPC4EJLT.js.map +1 -0
- package/dist/cli/config-4IBGVFSP.js +11 -0
- package/dist/cli/config-4IBGVFSP.js.map +1 -0
- package/dist/cli/index.js +822 -3392
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-CNWQ2MOE.js +559 -0
- package/dist/cli/mcp-CNWQ2MOE.js.map +1 -0
- package/dist/cli/near-5AY3HWYF.js +58 -0
- package/dist/cli/near-5AY3HWYF.js.map +1 -0
- package/dist/cli/provider-4BQDH56Y.js +396 -0
- package/dist/cli/provider-4BQDH56Y.js.map +1 -0
- package/dist/cli/run-5MB2D45X.js +7286 -0
- package/dist/cli/run-5MB2D45X.js.map +1 -0
- package/dist/cli/svm-3MMYFPC7.js +33 -0
- package/dist/cli/svm-3MMYFPC7.js.map +1 -0
- package/dist/cli/x402-NKR3WQDQ.js +96 -0
- package/dist/cli/x402-NKR3WQDQ.js.map +1 -0
- package/dist/{constants-BSBRlKRR.d.ts → constants-nackLItD.d.ts} +2 -1
- package/dist/index.d.ts +61 -40
- package/dist/index.js +2642 -2205
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +15 -20
- package/dist/mcp.js.map +1 -1
- package/dist/mpp.d.ts +12 -5
- package/dist/mpp.js +69 -65
- package/dist/mpp.js.map +1 -1
- package/dist/near-D6C42VPE.js +58 -0
- package/dist/near-D6C42VPE.js.map +1 -0
- package/dist/networks-olfflCI_.d.ts +38 -0
- package/dist/{protocol-adapter-DonxfAwa.d.ts → protocol-adapter-CfxU8Z2T.d.ts} +2 -2
- package/dist/{runtime-CiYbgGpi.d.ts → runtime-7vLNYvmU.d.ts} +25 -3
- package/dist/svm-QEUAUNPQ.js +33 -0
- package/dist/svm-QEUAUNPQ.js.map +1 -0
- package/dist/x402.d.ts +76 -64
- package/dist/x402.js +13 -4
- package/dist/x402.js.map +1 -1
- package/package.json +22 -2
- package/dist/chunk-3LUJHVKX.js.map +0 -1
- package/dist/chunk-53WXXIPR.js.map +0 -1
- package/dist/chunk-HOZA3IWP.js.map +0 -1
- package/dist/chunk-RKBGCPCK.js +0 -733
- package/dist/chunk-RKBGCPCK.js.map +0 -1
- package/dist/chunk-SYEGCNJZ.js.map +0 -1
- package/dist/chunk-TWRUNWDR.js +0 -265
- package/dist/chunk-TWRUNWDR.js.map +0 -1
- package/dist/{payment-flow-DCS882tR.d.ts → guardrails-CX_jYXvZ.d.ts} +8 -8
package/dist/cli/index.js
CHANGED
|
@@ -1,2191 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import '
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
for (var name in all)
|
|
24
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// src/config/filename.ts
|
|
28
|
-
var DEFAULT_CONFIG_FILENAME;
|
|
29
|
-
var init_filename = __esm({
|
|
30
|
-
"src/config/filename.ts"() {
|
|
31
|
-
DEFAULT_CONFIG_FILENAME = "config.yaml";
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// src/core/domain/common.ts
|
|
36
|
-
var PROTOCOL_NAME_KEYS, PROTOCOL_NAMES, PAYMENT_METHOD_NAME_KEYS, PAYMENT_METHOD_NAMES;
|
|
37
|
-
var init_common = __esm({
|
|
38
|
-
"src/core/domain/common.ts"() {
|
|
39
|
-
PROTOCOL_NAME_KEYS = {
|
|
40
|
-
http: true,
|
|
41
|
-
mcp: true,
|
|
42
|
-
a2a: true,
|
|
43
|
-
acp: true
|
|
44
|
-
};
|
|
45
|
-
PROTOCOL_NAMES = Object.keys(
|
|
46
|
-
PROTOCOL_NAME_KEYS
|
|
47
|
-
);
|
|
48
|
-
PAYMENT_METHOD_NAME_KEYS = { x402: true, mpp: true };
|
|
49
|
-
PAYMENT_METHOD_NAMES = Object.keys(
|
|
50
|
-
PAYMENT_METHOD_NAME_KEYS
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// src/core/domain/event.ts
|
|
56
|
-
var init_event = __esm({
|
|
57
|
-
"src/core/domain/event.ts"() {
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// src/core/domain/resource.ts
|
|
62
|
-
var init_resource = __esm({
|
|
63
|
-
"src/core/domain/resource.ts"() {
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// src/core/errors/codes.ts
|
|
68
|
-
var COMMERCE_ERROR_HTTP_STATUS, RETRYABLE_ERROR_CODES;
|
|
69
|
-
var init_codes = __esm({
|
|
70
|
-
"src/core/errors/codes.ts"() {
|
|
71
|
-
COMMERCE_ERROR_HTTP_STATUS = {
|
|
72
|
-
CONFIG_INVALID: 500,
|
|
73
|
-
RESOURCE_NOT_FOUND: 404,
|
|
74
|
-
INPUT_INVALID: 400,
|
|
75
|
-
PAYMENT_REQUIRED: 402,
|
|
76
|
-
PAYMENT_INVALID: 402,
|
|
77
|
-
PAYMENT_REPLAYED: 409,
|
|
78
|
-
PAYMENT_PROVIDER_UNAVAILABLE: 503,
|
|
79
|
-
PAYMENT_SETTLEMENT_FAILED: 502,
|
|
80
|
-
// 403, not 402: a 402 tells a client to pay and retry, which cannot fix a
|
|
81
|
-
// missing or rejected mandate, and a client that auto-pays on 402 would be
|
|
82
|
-
// charged for a request that was never going to be delivered
|
|
83
|
-
AUTHORIZATION_REQUIRED: 403,
|
|
84
|
-
AUTHORIZATION_INVALID: 403,
|
|
85
|
-
AUTHORIZATION_REPLAYED: 409,
|
|
86
|
-
AUTHORIZATION_PROVIDER_UNAVAILABLE: 503,
|
|
87
|
-
BACKEND_TIMEOUT: 504,
|
|
88
|
-
BACKEND_ERROR: 502,
|
|
89
|
-
PROTOCOL_UNSUPPORTED: 501,
|
|
90
|
-
GATEWAY_BUSY: 503,
|
|
91
|
-
STORAGE_ERROR: 500,
|
|
92
|
-
INTERNAL_ERROR: 500
|
|
93
|
-
};
|
|
94
|
-
RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
95
|
-
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
96
|
-
// The verifier or replay store was unreachable, so the mandate got no
|
|
97
|
-
// verdict. The same proof can verify once the outage clears.
|
|
98
|
-
"AUTHORIZATION_PROVIDER_UNAVAILABLE",
|
|
99
|
-
"BACKEND_TIMEOUT",
|
|
100
|
-
// Load shedding is transient: the caller should back off and try again.
|
|
101
|
-
// Clients act on this flag, and leaving it out would make a momentary
|
|
102
|
-
// throttle look like a permanent failure.
|
|
103
|
-
"GATEWAY_BUSY"
|
|
104
|
-
]);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// src/core/errors/commerce-error.ts
|
|
109
|
-
function isCommerceError(value) {
|
|
110
|
-
return value instanceof CommerceError;
|
|
111
|
-
}
|
|
112
|
-
function toCommerceError(value, fallbackCode = "INTERNAL_ERROR", fallbackMessage = "Unexpected internal error") {
|
|
113
|
-
if (isCommerceError(value)) return value;
|
|
114
|
-
return new CommerceError(fallbackCode, fallbackMessage, { cause: value });
|
|
115
|
-
}
|
|
116
|
-
var CommerceError;
|
|
117
|
-
var init_commerce_error = __esm({
|
|
118
|
-
"src/core/errors/commerce-error.ts"() {
|
|
119
|
-
init_codes();
|
|
120
|
-
CommerceError = class _CommerceError extends Error {
|
|
121
|
-
code;
|
|
122
|
-
httpStatus;
|
|
123
|
-
retryable;
|
|
124
|
-
details;
|
|
125
|
-
requestId;
|
|
126
|
-
resourceId;
|
|
127
|
-
constructor(code, message, options = {}) {
|
|
128
|
-
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
129
|
-
this.name = "CommerceError";
|
|
130
|
-
this.code = code;
|
|
131
|
-
this.httpStatus = options.httpStatus ?? COMMERCE_ERROR_HTTP_STATUS[code];
|
|
132
|
-
this.retryable = RETRYABLE_ERROR_CODES.has(code);
|
|
133
|
-
if (options.details !== void 0) this.details = options.details;
|
|
134
|
-
if (options.requestId !== void 0) this.requestId = options.requestId;
|
|
135
|
-
if (options.resourceId !== void 0) this.resourceId = options.resourceId;
|
|
136
|
-
Error.captureStackTrace?.(this, _CommerceError);
|
|
137
|
-
}
|
|
138
|
-
/** Non-secret, client-safe representation. `cause` is never included */
|
|
139
|
-
toInfo() {
|
|
140
|
-
return {
|
|
141
|
-
code: this.code,
|
|
142
|
-
message: this.message,
|
|
143
|
-
httpStatus: this.httpStatus,
|
|
144
|
-
retryable: this.retryable,
|
|
145
|
-
...this.requestId !== void 0 ? { requestId: this.requestId } : {},
|
|
146
|
-
...this.resourceId !== void 0 ? { resourceId: this.resourceId } : {},
|
|
147
|
-
...this.details !== void 0 ? { details: this.details } : {}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// src/core/errors/index.ts
|
|
155
|
-
var init_errors = __esm({
|
|
156
|
-
"src/core/errors/index.ts"() {
|
|
157
|
-
init_codes();
|
|
158
|
-
init_commerce_error();
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// src/core/is-record.ts
|
|
163
|
-
function isRecord(value) {
|
|
164
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
165
|
-
}
|
|
166
|
-
var init_is_record = __esm({
|
|
167
|
-
"src/core/is-record.ts"() {
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// src/core/domain/wire.ts
|
|
172
|
-
var PAYMENT_INPUT_FIELD, AUTHORIZATION_INPUT_FIELD, RESERVED_INPUT_FIELDS;
|
|
173
|
-
var init_wire = __esm({
|
|
174
|
-
"src/core/domain/wire.ts"() {
|
|
175
|
-
init_errors();
|
|
176
|
-
init_is_record();
|
|
177
|
-
PAYMENT_INPUT_FIELD = "_payment";
|
|
178
|
-
AUTHORIZATION_INPUT_FIELD = "_authorization";
|
|
179
|
-
RESERVED_INPUT_FIELDS = [
|
|
180
|
-
PAYMENT_INPUT_FIELD,
|
|
181
|
-
AUTHORIZATION_INPUT_FIELD
|
|
182
|
-
];
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// src/core/interfaces/logger.ts
|
|
187
|
-
var NOOP_LOGGER;
|
|
188
|
-
var init_logger = __esm({
|
|
189
|
-
"src/core/interfaces/logger.ts"() {
|
|
190
|
-
NOOP_LOGGER = {
|
|
191
|
-
debug: () => {
|
|
192
|
-
},
|
|
193
|
-
info: () => {
|
|
194
|
-
},
|
|
195
|
-
warn: () => {
|
|
196
|
-
},
|
|
197
|
-
error: () => {
|
|
198
|
-
},
|
|
199
|
-
child: () => NOOP_LOGGER
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// src/core/interfaces/protocol-adapter.ts
|
|
205
|
-
var init_protocol_adapter = __esm({
|
|
206
|
-
"src/core/interfaces/protocol-adapter.ts"() {
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// src/core/interfaces/runtime.ts
|
|
211
|
-
var systemClock;
|
|
212
|
-
var init_runtime = __esm({
|
|
213
|
-
"src/core/interfaces/runtime.ts"() {
|
|
214
|
-
systemClock = {
|
|
215
|
-
now: () => /* @__PURE__ */ new Date(),
|
|
216
|
-
nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
217
|
-
monotonicMs: () => performance.now()
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// src/core/public-types.ts
|
|
223
|
-
var init_public_types = __esm({
|
|
224
|
-
"src/core/public-types.ts"() {
|
|
225
|
-
init_common();
|
|
226
|
-
init_event();
|
|
227
|
-
init_resource();
|
|
228
|
-
init_wire();
|
|
229
|
-
init_errors();
|
|
230
|
-
init_logger();
|
|
231
|
-
init_protocol_adapter();
|
|
232
|
-
init_runtime();
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// src/core/index.ts
|
|
237
|
-
var init_core = __esm({
|
|
238
|
-
"src/core/index.ts"() {
|
|
239
|
-
init_public_types();
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// src/authorization/ap2/constants.ts
|
|
244
|
-
var AP2_SPEC_VERSION, AP2_MODES, AP2_SIGNING_ALGORITHM, AP2_KEY_TYPE, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, AP2_JWK_MEMBERS, AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_MAX_CLOCK_SKEW_SECONDS;
|
|
245
|
-
var init_constants = __esm({
|
|
246
|
-
"src/authorization/ap2/constants.ts"() {
|
|
247
|
-
AP2_SPEC_VERSION = "0.2.0";
|
|
248
|
-
AP2_MODES = ["direct"];
|
|
249
|
-
AP2_SIGNING_ALGORITHM = "ES256";
|
|
250
|
-
AP2_KEY_TYPE = "EC";
|
|
251
|
-
AP2_JWK_CURVE = "P-256";
|
|
252
|
-
AP2_JWK_COORDINATE_BYTES = 32;
|
|
253
|
-
AP2_JWK_MEMBERS = ["kty", "crv", "x", "y", "kid", "alg", "use"];
|
|
254
|
-
AP2_CHECKOUT_MANDATE_VCT = "mandate.checkout.1";
|
|
255
|
-
AP2_CHECKOUT_PROFILE = "agent-commerce/ap2/checkout/v1";
|
|
256
|
-
AP2_DEFAULT_CLOCK_SKEW_SECONDS = 60;
|
|
257
|
-
AP2_MAX_CLOCK_SKEW_SECONDS = 300;
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// src/core/execution/backend-http.ts
|
|
262
|
-
function extractPathParameterNames(url) {
|
|
263
|
-
return [...url.matchAll(PATH_PARAM_PATTERN)].map((match) => match[1]);
|
|
264
|
-
}
|
|
265
|
-
function findUnparsedBraceToken(url) {
|
|
266
|
-
const residue = url.replace(PATH_PARAM_PATTERN, "");
|
|
267
|
-
const index = residue.search(/[{}]/);
|
|
268
|
-
if (index === -1) return void 0;
|
|
269
|
-
const token = /\{[^{}]*\}?|\}/.exec(residue.slice(index));
|
|
270
|
-
return token?.[0] ?? residue[index];
|
|
271
|
-
}
|
|
272
|
-
var PATH_PARAM_PATTERN;
|
|
273
|
-
var init_backend_http = __esm({
|
|
274
|
-
"src/core/execution/backend-http.ts"() {
|
|
275
|
-
init_resource();
|
|
276
|
-
init_errors();
|
|
277
|
-
init_logger();
|
|
278
|
-
init_is_record();
|
|
279
|
-
PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// src/core/execution/settlement-order.ts
|
|
284
|
-
var init_settlement_order = __esm({
|
|
285
|
-
"src/core/execution/settlement-order.ts"() {
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
function typeList(raw) {
|
|
289
|
-
if (typeof raw === "string") return [raw];
|
|
290
|
-
if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
|
|
291
|
-
return raw;
|
|
292
|
-
}
|
|
293
|
-
return void 0;
|
|
294
|
-
}
|
|
295
|
-
function isObjectSchemaNode(schema) {
|
|
296
|
-
const types = typeList(schema["type"]);
|
|
297
|
-
if (types !== void 0) return types.includes("object");
|
|
298
|
-
return Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required");
|
|
299
|
-
}
|
|
300
|
-
var init_validation = __esm({
|
|
301
|
-
"src/core/execution/validation.ts"() {
|
|
302
|
-
init_is_record();
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
var init_pipeline = __esm({
|
|
306
|
-
"src/core/execution/pipeline.ts"() {
|
|
307
|
-
init_wire();
|
|
308
|
-
init_errors();
|
|
309
|
-
init_is_record();
|
|
310
|
-
init_backend_http();
|
|
311
|
-
init_settlement_order();
|
|
312
|
-
init_validation();
|
|
313
|
-
[...RESERVED_INPUT_FIELDS, "__proto__"];
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
// src/core/execution/registry.ts
|
|
318
|
-
var init_registry = __esm({
|
|
319
|
-
"src/core/execution/registry.ts"() {
|
|
320
|
-
init_errors();
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
// src/core/execution/store-event-sink.ts
|
|
325
|
-
var init_store_event_sink = __esm({
|
|
326
|
-
"src/core/execution/store-event-sink.ts"() {
|
|
327
|
-
init_errors();
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// src/core/execution/index.ts
|
|
332
|
-
var init_execution = __esm({
|
|
333
|
-
"src/core/execution/index.ts"() {
|
|
334
|
-
init_backend_http();
|
|
335
|
-
init_pipeline();
|
|
336
|
-
init_registry();
|
|
337
|
-
init_store_event_sink();
|
|
338
|
-
init_validation();
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// src/payments/mpp/constants.ts
|
|
343
|
-
function isMppNetwork(value) {
|
|
344
|
-
return MPP_NETWORKS.includes(value);
|
|
345
|
-
}
|
|
346
|
-
var MPP_SPEC_COMMIT, MPP_SPEC_DRAFTS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPPX_VERSION, MPP_PROFILE, MPP_NETWORKS, MPP_DEFAULT_NETWORK;
|
|
347
|
-
var init_constants2 = __esm({
|
|
348
|
-
"src/payments/mpp/constants.ts"() {
|
|
349
|
-
MPP_SPEC_COMMIT = "806fdb8b8c92cda5c84b8660558921b6bbeef7e0";
|
|
350
|
-
MPP_SPEC_DRAFTS = {
|
|
351
|
-
core: "draft-httpauth-payment-01",
|
|
352
|
-
intent: "draft-payment-intent-charge-00",
|
|
353
|
-
method: "draft-evm-charge-00"
|
|
354
|
-
};
|
|
355
|
-
MPP_MIN_CHALLENGE_SECRET_LENGTH = 32;
|
|
356
|
-
MPPX_VERSION = "0.13.1";
|
|
357
|
-
MPP_PROFILE = {
|
|
358
|
-
intent: "charge",
|
|
359
|
-
method: "evm",
|
|
360
|
-
credentialType: "authorization",
|
|
361
|
-
assetSymbol: "USDC",
|
|
362
|
-
assetDecimals: 6
|
|
363
|
-
};
|
|
364
|
-
MPP_NETWORKS = ["eip155:84532", "eip155:8453"];
|
|
365
|
-
MPP_DEFAULT_NETWORK = "eip155:84532";
|
|
366
|
-
`mpp/${MPP_SPEC_DRAFTS.core} intent=${MPP_PROFILE.intent} method=${MPP_PROFILE.method}`;
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// src/payments/x402/networks.ts
|
|
371
|
-
function findNetworkProfile(id) {
|
|
372
|
-
return BY_ID.get(id);
|
|
373
|
-
}
|
|
374
|
-
function requireNetworkProfile(id, path2) {
|
|
375
|
-
const profile = BY_ID.get(id);
|
|
376
|
-
if (!profile) {
|
|
377
|
-
throw new CommerceError(
|
|
378
|
-
"CONFIG_INVALID",
|
|
379
|
-
`${path2}: "${id}" is not a supported network. Supported CAIP-2 identifiers: ${SUPPORTED_NETWORK_IDS.join(", ")}`,
|
|
380
|
-
{ details: { path: path2, network: id } }
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
return profile;
|
|
384
|
-
}
|
|
385
|
-
function resolveDeploymentMode(profile, facilitatorMode) {
|
|
386
|
-
if (facilitatorMode === "local") return "local";
|
|
387
|
-
return profile.kind;
|
|
388
|
-
}
|
|
389
|
-
function describeDeploymentMode(mode) {
|
|
390
|
-
return mode === "mainnet" ? LIVE_MAINNET_BANNER : mode.toUpperCase();
|
|
391
|
-
}
|
|
392
|
-
var PROFILES, BY_ID, SUPPORTED_NETWORK_IDS, LIVE_MAINNET_BANNER;
|
|
393
|
-
var init_networks = __esm({
|
|
394
|
-
"src/payments/x402/networks.ts"() {
|
|
395
|
-
init_core();
|
|
396
|
-
PROFILES = [
|
|
397
|
-
{
|
|
398
|
-
id: "eip155:84532",
|
|
399
|
-
chainId: 84532,
|
|
400
|
-
displayName: "Base Sepolia",
|
|
401
|
-
kind: "testnet",
|
|
402
|
-
canonicalAsset: {
|
|
403
|
-
symbol: "USDC",
|
|
404
|
-
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
405
|
-
name: "USDC",
|
|
406
|
-
version: "2"
|
|
407
|
-
}
|
|
408
|
-
},
|
|
409
|
-
{
|
|
410
|
-
id: "eip155:8453",
|
|
411
|
-
chainId: 8453,
|
|
412
|
-
displayName: "Base",
|
|
413
|
-
kind: "mainnet",
|
|
414
|
-
canonicalAsset: {
|
|
415
|
-
symbol: "USDC",
|
|
416
|
-
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
417
|
-
name: "USD Coin",
|
|
418
|
-
version: "2"
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
];
|
|
422
|
-
BY_ID = new Map(PROFILES.map((profile) => [profile.id, profile]));
|
|
423
|
-
SUPPORTED_NETWORK_IDS = PROFILES.map((p) => p.id);
|
|
424
|
-
LIVE_MAINNET_BANNER = "LIVE MAINNET MODE - REAL FUNDS";
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
// src/protocols/acp/constants.ts
|
|
429
|
-
var ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, ACP_CHECKOUT_OPERATIONS, ACP_OPERATION_INPUT_KEYS, ACP_OPERATION_OPTIONAL_INPUT_KEYS, ACP_SESSION_ID_INPUT_KEY;
|
|
430
|
-
var init_constants3 = __esm({
|
|
431
|
-
"src/protocols/acp/constants.ts"() {
|
|
432
|
-
ACP_SPEC_VERSION = "2026-04-17";
|
|
433
|
-
ACP_API_VERSION = "2026-04-17";
|
|
434
|
-
ACP_WELL_KNOWN_PATH = "/.well-known/acp.json";
|
|
435
|
-
ACP_CHECKOUT_OPERATIONS = [
|
|
436
|
-
"createCheckoutSession",
|
|
437
|
-
"updateCheckoutSession",
|
|
438
|
-
"getCheckoutSession",
|
|
439
|
-
"completeCheckoutSession",
|
|
440
|
-
"cancelCheckoutSession"
|
|
441
|
-
];
|
|
442
|
-
ACP_OPERATION_INPUT_KEYS = {
|
|
443
|
-
createCheckoutSession: ["body"],
|
|
444
|
-
updateCheckoutSession: ["path", "body"],
|
|
445
|
-
getCheckoutSession: ["path"],
|
|
446
|
-
completeCheckoutSession: ["path", "body"],
|
|
447
|
-
cancelCheckoutSession: ["path"]
|
|
448
|
-
};
|
|
449
|
-
ACP_OPERATION_OPTIONAL_INPUT_KEYS = {
|
|
450
|
-
cancelCheckoutSession: ["body"]
|
|
451
|
-
};
|
|
452
|
-
ACP_SESSION_ID_INPUT_KEY = "checkout_session_id";
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
// src/config/env.ts
|
|
457
|
-
function substituteEnv(value, env, path2 = "$") {
|
|
458
|
-
if (typeof value === "string") {
|
|
459
|
-
return substituteString(value, env, path2);
|
|
460
|
-
}
|
|
461
|
-
if (Array.isArray(value)) {
|
|
462
|
-
return value.map((entry, index) => substituteEnv(entry, env, `${path2}[${index}]`));
|
|
463
|
-
}
|
|
464
|
-
if (isRecord(value)) {
|
|
465
|
-
const result = {};
|
|
466
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
467
|
-
result[key] = substituteEnv(entry, env, `${path2}.${key}`);
|
|
468
|
-
}
|
|
469
|
-
return result;
|
|
470
|
-
}
|
|
471
|
-
return value;
|
|
472
|
-
}
|
|
473
|
-
function substituteString(value, env, path2) {
|
|
474
|
-
assertTemplateIsSupported(value, path2);
|
|
475
|
-
return substituteOnce(value, env, path2);
|
|
476
|
-
}
|
|
477
|
-
function assertTemplateIsSupported(value, path2) {
|
|
478
|
-
if (NESTED_PLACEHOLDER.test(value)) {
|
|
479
|
-
throw new CommerceError(
|
|
480
|
-
"CONFIG_INVALID",
|
|
481
|
-
`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.`,
|
|
482
|
-
{ details: { path: path2 } }
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
for (const [token] of value.matchAll(BRACE_TOKEN)) {
|
|
486
|
-
if (SUPPORTED_TOKEN.test(token)) continue;
|
|
487
|
-
throw new CommerceError(
|
|
488
|
-
"CONFIG_INVALID",
|
|
489
|
-
`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.`,
|
|
490
|
-
{ details: { path: path2, token } }
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
function substituteOnce(value, env, path2) {
|
|
495
|
-
return value.replace(
|
|
496
|
-
PLACEHOLDER_PATTERN,
|
|
497
|
-
(_match, name, hasDefault, def) => {
|
|
498
|
-
const envValue = env[name];
|
|
499
|
-
if (hasDefault !== void 0) {
|
|
500
|
-
if (envValue === void 0 || envValue === "") return def ?? "";
|
|
501
|
-
return envValue;
|
|
502
|
-
}
|
|
503
|
-
if (envValue === void 0) {
|
|
504
|
-
throw new CommerceError(
|
|
505
|
-
"CONFIG_INVALID",
|
|
506
|
-
`Unresolved environment variable "\${${name}}" referenced at config path "${path2}"`,
|
|
507
|
-
{ details: { variable: name, path: path2 } }
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
return envValue;
|
|
511
|
-
}
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
var PLACEHOLDER_PATTERN, BRACE_TOKEN, SUPPORTED_TOKEN, NESTED_PLACEHOLDER;
|
|
515
|
-
var init_env = __esm({
|
|
516
|
-
"src/config/env.ts"() {
|
|
517
|
-
init_core();
|
|
518
|
-
init_is_record();
|
|
519
|
-
PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
|
|
520
|
-
BRACE_TOKEN = /\$\{[^{}]*\}/g;
|
|
521
|
-
SUPPORTED_TOKEN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}$/;
|
|
522
|
-
NESTED_PLACEHOLDER = /\$\{[^{}]*\$\{/;
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// src/payments/x402/local-chain/accounts.ts
|
|
527
|
-
var ANVIL_WELL_KNOWN_ACCOUNTS;
|
|
528
|
-
var init_accounts = __esm({
|
|
529
|
-
"src/payments/x402/local-chain/accounts.ts"() {
|
|
530
|
-
ANVIL_WELL_KNOWN_ACCOUNTS = [
|
|
531
|
-
{
|
|
532
|
-
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
|
533
|
-
privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
534
|
-
},
|
|
535
|
-
{
|
|
536
|
-
address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
|
|
537
|
-
privateKey: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
|
|
538
|
-
},
|
|
539
|
-
{
|
|
540
|
-
address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
|
|
541
|
-
privateKey: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
|
|
542
|
-
}
|
|
543
|
-
];
|
|
544
|
-
ANVIL_WELL_KNOWN_ACCOUNTS[0];
|
|
545
|
-
ANVIL_WELL_KNOWN_ACCOUNTS[1];
|
|
546
|
-
ANVIL_WELL_KNOWN_ACCOUNTS[2];
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
// src/payments/x402/dev-key-guard.ts
|
|
551
|
-
function isWellKnownDevAddress(address) {
|
|
552
|
-
return WELL_KNOWN_DEV_ADDRESSES.has(address.toLowerCase());
|
|
553
|
-
}
|
|
554
|
-
function isLikelyLocalOrPrivateHost(hostname) {
|
|
555
|
-
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
556
|
-
if (h === "localhost" || h === "::1") return true;
|
|
557
|
-
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
558
|
-
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
559
|
-
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
560
|
-
if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
|
561
|
-
if (!h.includes(".") && !h.includes(":")) return true;
|
|
562
|
-
return false;
|
|
563
|
-
}
|
|
564
|
-
var WELL_KNOWN_DEV_ADDRESSES;
|
|
565
|
-
var init_dev_key_guard = __esm({
|
|
566
|
-
"src/payments/x402/dev-key-guard.ts"() {
|
|
567
|
-
init_core();
|
|
568
|
-
init_accounts();
|
|
569
|
-
new Set(
|
|
570
|
-
ANVIL_WELL_KNOWN_ACCOUNTS.map((a) => a.privateKey.toLowerCase())
|
|
571
|
-
);
|
|
572
|
-
WELL_KNOWN_DEV_ADDRESSES = new Set(
|
|
573
|
-
ANVIL_WELL_KNOWN_ACCOUNTS.map((a) => a.address.toLowerCase())
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
// src/payments/x402/guardrails.ts
|
|
579
|
-
function invalid(message, path2) {
|
|
580
|
-
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2 } });
|
|
581
|
-
}
|
|
582
|
-
function resolveX402Deployment(input) {
|
|
583
|
-
const at = input.configPath ?? "payments.x402";
|
|
584
|
-
const payTo = input.payToField ?? "payTo";
|
|
585
|
-
const profile = requireNetworkProfile(input.network, `${at}.network`);
|
|
586
|
-
const mode = resolveDeploymentMode(profile, input.facilitator.mode);
|
|
587
|
-
if (profile.kind === "mainnet" && input.facilitator.mode === "local") {
|
|
588
|
-
throw invalid(
|
|
589
|
-
`${at}: network "${profile.id}" (${profile.displayName}) is a mainnet and cannot be served by facilitator.mode "local". A mainnet deployment must settle through a remote facilitator.`,
|
|
590
|
-
`${at}.facilitator.mode`
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
if (mode === "mainnet" && input.allowMainnet !== true) {
|
|
594
|
-
throw invalid(
|
|
595
|
-
`${at}: network "${profile.id}" (${profile.displayName}) settles real funds. Set ${at}.allowMainnet: true to acknowledge this explicitly - it is never the default.`,
|
|
596
|
-
`${at}.allowMainnet`
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
if (input.facilitator.mode === "remote") {
|
|
600
|
-
assertFacilitatorUrlIsSafe(input.facilitator.url, mode, at);
|
|
601
|
-
if (mode === "mainnet" && input.facilitator.auth.type === "none" && input.allowUnauthenticatedFacilitator !== true) {
|
|
602
|
-
throw invalid(
|
|
603
|
-
`${at}: no credential is configured for facilitator ${describeOrigin(input.facilitator.url)} on mainnet. Set ${at}.allowUnauthenticatedFacilitator: true to accept anonymous access and the facilitator's limits, or configure facilitator.auth.`,
|
|
604
|
-
`${at}.allowUnauthenticatedFacilitator`
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
for (const [field, value] of credentialFields(input.facilitator.auth)) {
|
|
608
|
-
if (value.trim() === "") {
|
|
609
|
-
throw invalid(
|
|
610
|
-
`${at}: facilitator.auth.type is "${input.facilitator.auth.type}" but ${field} is empty. An empty credential is refused rather than sent.`,
|
|
611
|
-
`${at}.facilitator.auth.${field}`
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
if (/^0x0{40}$/i.test(input.payTo)) {
|
|
617
|
-
throw invalid(
|
|
618
|
-
`${at}: "${payTo}" is the zero address. Every payment settled there is destroyed.`,
|
|
619
|
-
`${at}.${payTo}`
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
if (mode !== "local" && isWellKnownDevAddress(input.payTo)) {
|
|
623
|
-
throw invalid(
|
|
624
|
-
`${at}: "${payTo}" (${input.payTo}) is a well-known Anvil development address and this is a ${mode} deployment. Anyone can spend what settles there. Set ${payTo} to your own merchant wallet.`,
|
|
625
|
-
`${at}.${payTo}`
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
const canonical = profile.canonicalAsset;
|
|
629
|
-
if (mode === "mainnet" && canonical) {
|
|
630
|
-
if (!sameAddress(input.asset, canonical.address)) {
|
|
631
|
-
throw invalid(
|
|
632
|
-
`${at}: asset ${input.asset} is not ${canonical.symbol} on ${profile.displayName} (expected ${canonical.address}). Settling a mainnet payment in an unintended token is refused.`,
|
|
633
|
-
`${at}.asset`
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
if (input.assetName !== void 0 && input.assetName !== canonical.name) {
|
|
637
|
-
throw invalid(
|
|
638
|
-
`${at}: assetName "${input.assetName}" is not the EIP-712 domain name ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.name}"). Every payment would be refused after the buyer signed.`,
|
|
639
|
-
`${at}.assetName`
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
if (input.assetVersion !== void 0 && input.assetVersion !== canonical.version) {
|
|
643
|
-
throw invalid(
|
|
644
|
-
`${at}: assetVersion "${input.assetVersion}" is not the EIP-712 domain version ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.version}").`,
|
|
645
|
-
`${at}.assetVersion`
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return { profile, mode };
|
|
650
|
-
}
|
|
651
|
-
function assertFacilitatorUrlIsSafe(url, mode, at) {
|
|
652
|
-
let parsed;
|
|
653
|
-
try {
|
|
654
|
-
parsed = new URL(url);
|
|
655
|
-
} catch (cause) {
|
|
656
|
-
throw new CommerceError("CONFIG_INVALID", `${at}: facilitator.url is not a valid URL`, {
|
|
657
|
-
cause,
|
|
658
|
-
details: { path: `${at}.facilitator.url` }
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
if (parsed.protocol === "https:") return;
|
|
662
|
-
if (parsed.protocol !== "http:") {
|
|
663
|
-
throw invalid(
|
|
664
|
-
`${at}: facilitator ${describeOrigin(url)} must be reached over https (or http on a local/private host); got "${parsed.protocol}//"`,
|
|
665
|
-
`${at}.facilitator.url`
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
if (mode !== "mainnet" && isLikelyLocalOrPrivateHost(parsed.hostname)) return;
|
|
669
|
-
throw invalid(
|
|
670
|
-
`${at}: facilitator ${describeOrigin(url)} is reached over plain HTTP. Payment authorizations and settlement results would travel unencrypted. Use https, or point at a local/private host on a non-mainnet deployment.`,
|
|
671
|
-
`${at}.facilitator.url`
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
function describeOrigin(url) {
|
|
675
|
-
try {
|
|
676
|
-
return new URL(url).origin;
|
|
677
|
-
} catch {
|
|
678
|
-
return "[unparseable facilitator.url]";
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
function credentialFields(auth) {
|
|
682
|
-
switch (auth.type) {
|
|
683
|
-
case "bearer":
|
|
684
|
-
return [["token", auth.token]];
|
|
685
|
-
case "cdp":
|
|
686
|
-
return [
|
|
687
|
-
["apiKeyId", auth.apiKeyId],
|
|
688
|
-
["apiKeySecret", auth.apiKeySecret]
|
|
689
|
-
];
|
|
690
|
-
default:
|
|
691
|
-
return [];
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
function sameAddress(a, b) {
|
|
695
|
-
return a.toLowerCase() === b.toLowerCase();
|
|
696
|
-
}
|
|
697
|
-
var init_guardrails = __esm({
|
|
698
|
-
"src/payments/x402/guardrails.ts"() {
|
|
699
|
-
init_core();
|
|
700
|
-
init_dev_key_guard();
|
|
701
|
-
init_networks();
|
|
702
|
-
}
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
// src/payments/x402/payment-flow.ts
|
|
706
|
-
var X402_PAYMENT_FLOWS, X402_DEFAULT_PAYMENT_FLOW;
|
|
707
|
-
var init_payment_flow = __esm({
|
|
708
|
-
"src/payments/x402/payment-flow.ts"() {
|
|
709
|
-
X402_PAYMENT_FLOWS = ["authorization", "upfront"];
|
|
710
|
-
X402_DEFAULT_PAYMENT_FLOW = "authorization";
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
// src/protocols/mcp/constants.ts
|
|
715
|
-
var MCP_TOOL_NAME_CHARS, MCP_TOOL_NAME_MAX_LENGTH, MCP_TOOL_NAME_PATTERN;
|
|
716
|
-
var init_constants4 = __esm({
|
|
717
|
-
"src/protocols/mcp/constants.ts"() {
|
|
718
|
-
MCP_TOOL_NAME_CHARS = "A-Za-z0-9._-";
|
|
719
|
-
MCP_TOOL_NAME_MAX_LENGTH = 128;
|
|
720
|
-
MCP_TOOL_NAME_PATTERN = new RegExp(
|
|
721
|
-
`^[${MCP_TOOL_NAME_CHARS}]{1,${MCP_TOOL_NAME_MAX_LENGTH}}$`
|
|
722
|
-
);
|
|
723
|
-
}
|
|
724
|
-
});
|
|
725
|
-
function parseConfig(raw, env) {
|
|
726
|
-
if (!isRecord(raw)) {
|
|
727
|
-
throw new CommerceError(
|
|
728
|
-
"CONFIG_INVALID",
|
|
729
|
-
'Configuration root must be a mapping (object) at "$"',
|
|
730
|
-
{
|
|
731
|
-
details: { path: "$" }
|
|
732
|
-
}
|
|
733
|
-
);
|
|
734
|
-
}
|
|
735
|
-
checkVersion(raw);
|
|
736
|
-
const substituted = substituteEnv(raw, env);
|
|
737
|
-
const parsed = parseWithZod(RawConfigSchema, substituted);
|
|
738
|
-
return normalize(parsed);
|
|
739
|
-
}
|
|
740
|
-
function checkVersion(raw) {
|
|
741
|
-
if (!("version" in raw)) {
|
|
742
|
-
throw new CommerceError("CONFIG_INVALID", 'Configuration is missing required field "version"', {
|
|
743
|
-
details: { path: "version" }
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
const version = raw["version"];
|
|
747
|
-
if (version !== SUPPORTED_CONFIG_VERSION) {
|
|
748
|
-
const hint = typeof version === "string" ? ", written as a number without quotes" : "";
|
|
749
|
-
throw new CommerceError(
|
|
750
|
-
"CONFIG_INVALID",
|
|
751
|
-
`Unsupported config version "${String(version)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}${hint}`,
|
|
752
|
-
{ details: { path: "version", supported: SUPPORTED_CONFIG_VERSION } }
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
function parseWithZod(schema, value) {
|
|
757
|
-
const result = schema.safeParse(value);
|
|
758
|
-
if (!result.success) {
|
|
759
|
-
throw zodErrorToConfigError(result.error);
|
|
760
|
-
}
|
|
761
|
-
return result.data;
|
|
762
|
-
}
|
|
763
|
-
function zodErrorToConfigError(error) {
|
|
764
|
-
const issues = error.issues.map(describeIssue);
|
|
765
|
-
const first = issues[0];
|
|
766
|
-
const message = first ? `Configuration invalid at "${first.path}": ${first.message}` : "Configuration invalid";
|
|
767
|
-
return new CommerceError("CONFIG_INVALID", message, { details: { issues } });
|
|
768
|
-
}
|
|
769
|
-
function describeIssue(issue) {
|
|
770
|
-
const path2 = issue.path.length > 0 ? issue.path.join(".") : "$";
|
|
771
|
-
return { path: path2, message: issue.message, code: issue.code };
|
|
772
|
-
}
|
|
773
|
-
function toNumber(value, path2, bounds = {}) {
|
|
774
|
-
if (typeof value === "string" && !/^\s*\d+\s*$/.test(value)) {
|
|
775
|
-
throw new CommerceError(
|
|
776
|
-
"CONFIG_INVALID",
|
|
777
|
-
`Configuration value at "${path2}" must be an integer written in decimal digits (got ${value === "" ? "an empty string" : `"${value}"`})`,
|
|
778
|
-
{ details: { path: path2 } }
|
|
779
|
-
);
|
|
780
|
-
}
|
|
781
|
-
const n = typeof value === "number" ? value : Number(value);
|
|
782
|
-
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
783
|
-
throw new CommerceError(
|
|
784
|
-
"CONFIG_INVALID",
|
|
785
|
-
`Configuration value at "${path2}" must be an integer`,
|
|
786
|
-
{
|
|
787
|
-
details: { path: path2 }
|
|
788
|
-
}
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
if (bounds.min !== void 0 && n < bounds.min) {
|
|
792
|
-
throw new CommerceError(
|
|
793
|
-
"CONFIG_INVALID",
|
|
794
|
-
`Configuration value at "${path2}" must be >= ${bounds.min}`,
|
|
795
|
-
{
|
|
796
|
-
details: { path: path2 }
|
|
797
|
-
}
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
if (bounds.max !== void 0 && n > bounds.max) {
|
|
801
|
-
throw new CommerceError(
|
|
802
|
-
"CONFIG_INVALID",
|
|
803
|
-
`Configuration value at "${path2}" must be <= ${bounds.max}`,
|
|
804
|
-
{
|
|
805
|
-
details: { path: path2 }
|
|
806
|
-
}
|
|
807
|
-
);
|
|
808
|
-
}
|
|
809
|
-
return n;
|
|
810
|
-
}
|
|
811
|
-
function toBoolean(value, path2) {
|
|
812
|
-
if (typeof value === "boolean") return value;
|
|
813
|
-
const normalized = value.trim().toLowerCase();
|
|
814
|
-
if (normalized === "true") return true;
|
|
815
|
-
if (normalized === "false") return false;
|
|
816
|
-
throw new CommerceError(
|
|
817
|
-
"CONFIG_INVALID",
|
|
818
|
-
`Configuration value at "${path2}" must be a boolean ("true" or "false")`,
|
|
819
|
-
{ details: { path: path2 } }
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
function normalize(raw) {
|
|
823
|
-
const protocols = {
|
|
824
|
-
http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
|
|
825
|
-
mcp: {
|
|
826
|
-
enabled: toBoolean(raw.protocols.mcp.enabled, "protocols.mcp.enabled"),
|
|
827
|
-
mountPath: raw.protocols.mcp.mountPath
|
|
828
|
-
},
|
|
829
|
-
a2a: {
|
|
830
|
-
enabled: toBoolean(raw.protocols.a2a?.enabled ?? false, "protocols.a2a.enabled"),
|
|
831
|
-
mountPath: raw.protocols.a2a?.mountPath ?? DEFAULT_A2A_MOUNT_PATH
|
|
832
|
-
},
|
|
833
|
-
acp: normalizeAcp(raw.protocols.acp)
|
|
834
|
-
};
|
|
835
|
-
validateMountPaths(protocols);
|
|
836
|
-
const x402Raw = raw.payments.x402;
|
|
837
|
-
const x402 = x402Raw !== void 0 ? {
|
|
838
|
-
enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
|
|
839
|
-
network: x402Raw.network,
|
|
840
|
-
rpcUrl: x402Raw.rpcUrl,
|
|
841
|
-
asset: x402Raw.asset,
|
|
842
|
-
assetName: x402Raw.assetName,
|
|
843
|
-
assetVersion: x402Raw.assetVersion,
|
|
844
|
-
assetDecimals: toNumber(x402Raw.assetDecimals, "payments.x402.assetDecimals", {
|
|
845
|
-
min: 0,
|
|
846
|
-
max: 36
|
|
847
|
-
}),
|
|
848
|
-
payTo: x402Raw.payTo,
|
|
849
|
-
maxTimeoutSeconds: toNumber(
|
|
850
|
-
x402Raw.maxTimeoutSeconds,
|
|
851
|
-
"payments.x402.maxTimeoutSeconds",
|
|
852
|
-
{
|
|
853
|
-
min: 1
|
|
854
|
-
}
|
|
855
|
-
),
|
|
856
|
-
paymentFlow: x402Raw.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
|
|
857
|
-
facilitator: normalizeFacilitator(x402Raw.facilitator),
|
|
858
|
-
...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
|
|
859
|
-
...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
860
|
-
allowUnauthenticatedFacilitator: toBoolean(
|
|
861
|
-
x402Raw.allowUnauthenticatedFacilitator,
|
|
862
|
-
"payments.x402.allowUnauthenticatedFacilitator"
|
|
863
|
-
)
|
|
864
|
-
} : {}
|
|
865
|
-
} : void 0;
|
|
866
|
-
if (x402) {
|
|
867
|
-
validateAddress("payments.x402.payTo", x402.payTo);
|
|
868
|
-
validateAddress("payments.x402.asset", x402.asset);
|
|
869
|
-
resolveX402Deployment({
|
|
870
|
-
network: x402.network,
|
|
871
|
-
payTo: x402.payTo,
|
|
872
|
-
asset: x402.asset,
|
|
873
|
-
assetName: x402.assetName,
|
|
874
|
-
assetVersion: x402.assetVersion,
|
|
875
|
-
facilitator: x402.facilitator,
|
|
876
|
-
...x402.allowMainnet !== void 0 ? { allowMainnet: x402.allowMainnet } : {},
|
|
877
|
-
...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
const mpp = normalizeMpp(raw.payments.mpp);
|
|
881
|
-
const ap2 = normalizeAp2(raw.authorization?.ap2);
|
|
882
|
-
if (ap2?.enabled) {
|
|
883
|
-
validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
|
|
884
|
-
}
|
|
885
|
-
const resources = Object.entries(raw.resources).map(
|
|
886
|
-
([id, entry]) => normalizeResource(id, entry, protocols, x402, mpp, ap2)
|
|
887
|
-
);
|
|
888
|
-
const resourcePaymentFlows = Object.fromEntries(
|
|
889
|
-
Object.entries(raw.resources).flatMap(
|
|
890
|
-
([id, entry]) => entry.paymentFlow !== void 0 ? [[id, entry.paymentFlow]] : []
|
|
891
|
-
)
|
|
892
|
-
);
|
|
893
|
-
const hasFlowOverrides = Object.keys(resourcePaymentFlows).length > 0;
|
|
894
|
-
const x402WithFlows = x402 !== void 0 && hasFlowOverrides ? { ...x402, resourcePaymentFlows } : x402;
|
|
895
|
-
const mppWithFlows = mpp !== void 0 && hasFlowOverrides ? { ...mpp, resourcePaymentFlows } : mpp;
|
|
896
|
-
if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
|
|
897
|
-
if (protocols.a2a.enabled && !resources.some((r) => r.exposedVia.includes("a2a"))) {
|
|
898
|
-
throw new CommerceError(
|
|
899
|
-
"CONFIG_INVALID",
|
|
900
|
-
'protocols.a2a.enabled is true but no resource lists "a2a" in its expose',
|
|
901
|
-
{ details: { path: "protocols.a2a.enabled" } }
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
return {
|
|
905
|
-
version: SUPPORTED_CONFIG_VERSION,
|
|
906
|
-
merchant: {
|
|
907
|
-
id: raw.merchant.id,
|
|
908
|
-
name: raw.merchant.name,
|
|
909
|
-
publicBaseUrl: raw.merchant.publicBaseUrl
|
|
910
|
-
},
|
|
911
|
-
// Port 0 lets the OS pick a free port, for tests and throwaway instances
|
|
912
|
-
server: {
|
|
913
|
-
port: toNumber(raw.server.port, "server.port", { min: 0, max: 65535 }),
|
|
914
|
-
host: raw.server.host,
|
|
915
|
-
...raw.server.adminToken !== void 0 ? { adminToken: raw.server.adminToken } : {},
|
|
916
|
-
allowedOrigins: raw.server.allowedOrigins ?? []
|
|
917
|
-
},
|
|
918
|
-
storage: { receipts: { driver: "sqlite", path: raw.storage.receipts.path } },
|
|
919
|
-
protocols,
|
|
920
|
-
resources,
|
|
921
|
-
payments: {
|
|
922
|
-
...x402WithFlows !== void 0 ? { x402: x402WithFlows } : {},
|
|
923
|
-
...mppWithFlows !== void 0 ? { mpp: mppWithFlows } : {}
|
|
924
|
-
},
|
|
925
|
-
...ap2 !== void 0 ? { authorization: { ap2 } } : {}
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
function validateMountPaths(protocols) {
|
|
929
|
-
const mounts = [
|
|
930
|
-
["mcp", protocols.mcp],
|
|
931
|
-
["a2a", protocols.a2a],
|
|
932
|
-
["acp", protocols.acp]
|
|
933
|
-
].filter(([, p]) => p.enabled);
|
|
934
|
-
for (const [index, [nameA, a]] of mounts.entries()) {
|
|
935
|
-
for (const [nameB, b] of mounts.slice(index + 1)) {
|
|
936
|
-
const baseA = a.mountPath.replace(/\/+$/, "");
|
|
937
|
-
const baseB = b.mountPath.replace(/\/+$/, "");
|
|
938
|
-
if (baseA === baseB || baseA.startsWith(`${baseB}/`) || baseB.startsWith(`${baseA}/`)) {
|
|
939
|
-
throw new CommerceError(
|
|
940
|
-
"CONFIG_INVALID",
|
|
941
|
-
`protocols.${nameA}.mountPath ("${a.mountPath}") collides with protocols.${nameB}.mountPath ("${b.mountPath}") - each mount registers a wildcard, so overlapping prefixes cannot both be served`,
|
|
942
|
-
{ details: { path: `protocols.${nameA}.mountPath` } }
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
function ap2Invalid(path2, message, extra = {}) {
|
|
949
|
-
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
|
|
950
|
-
}
|
|
951
|
-
function normalizeAp2(raw) {
|
|
952
|
-
if (raw === void 0) return void 0;
|
|
953
|
-
if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
|
|
954
|
-
const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
|
|
955
|
-
if (specVersion !== AP2_SPEC_VERSION) {
|
|
956
|
-
throw ap2Invalid(
|
|
957
|
-
"authorization.ap2.specVersion",
|
|
958
|
-
`authorization.ap2.specVersion "${specVersion}" is not supported - this gateway verifies against the tagged AP2 release ${AP2_SPEC_VERSION} only`
|
|
959
|
-
);
|
|
960
|
-
}
|
|
961
|
-
const mode = raw.mode ?? AP2_MODES[0];
|
|
962
|
-
if (!AP2_MODES.includes(mode)) {
|
|
963
|
-
throw ap2Invalid(
|
|
964
|
-
"authorization.ap2.mode",
|
|
965
|
-
`authorization.ap2.mode "${mode}" is not supported. Supported: ${AP2_MODES.join(", ")}. Autonomous mode needs open mandates, agent key binding and constraint evaluation, none of which this release implements.`
|
|
966
|
-
);
|
|
967
|
-
}
|
|
968
|
-
if (raw.replay === void 0) {
|
|
969
|
-
throw ap2Invalid(
|
|
970
|
-
"authorization.ap2.replay",
|
|
971
|
-
"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"
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
const mandateIssuers = normalizeIssuers(
|
|
975
|
-
raw.trust?.mandateIssuers,
|
|
976
|
-
"authorization.ap2.trust.mandateIssuers"
|
|
977
|
-
);
|
|
978
|
-
const checkoutIssuers = normalizeIssuers(
|
|
979
|
-
raw.trust?.checkoutIssuers,
|
|
980
|
-
"authorization.ap2.trust.checkoutIssuers"
|
|
981
|
-
);
|
|
982
|
-
return {
|
|
983
|
-
enabled: true,
|
|
984
|
-
specVersion: AP2_SPEC_VERSION,
|
|
985
|
-
mode,
|
|
986
|
-
trust: { mandateIssuers, checkoutIssuers },
|
|
987
|
-
clockSkewSeconds: toNumber(
|
|
988
|
-
raw.clockSkewSeconds ?? AP2_DEFAULT_CLOCK_SKEW_SECONDS,
|
|
989
|
-
"authorization.ap2.clockSkewSeconds",
|
|
990
|
-
{ min: 0, max: AP2_MAX_CLOCK_SKEW_SECONDS }
|
|
991
|
-
),
|
|
992
|
-
requireMandateAudience: toBoolean(
|
|
993
|
-
raw.requireMandateAudience ?? false,
|
|
994
|
-
"authorization.ap2.requireMandateAudience"
|
|
995
|
-
),
|
|
996
|
-
requireMandateExpiry: toBoolean(
|
|
997
|
-
raw.requireMandateExpiry ?? false,
|
|
998
|
-
"authorization.ap2.requireMandateExpiry"
|
|
999
|
-
),
|
|
1000
|
-
replay: { path: raw.replay.path }
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
function normalizeIssuers(raw, path2) {
|
|
1004
|
-
if (raw === void 0 || raw.length === 0) {
|
|
1005
|
-
throw ap2Invalid(
|
|
1006
|
-
path2,
|
|
1007
|
-
`${path2} is required when AP2 is enabled and must name at least one issuer - trust is operator-configured and static, so an empty list means no mandate can ever verify`
|
|
1008
|
-
);
|
|
1009
|
-
}
|
|
1010
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1011
|
-
return raw.map((entry, index) => {
|
|
1012
|
-
const entryPath = `${path2}.${index}`;
|
|
1013
|
-
if (seen.has(entry.issuer)) {
|
|
1014
|
-
throw ap2Invalid(
|
|
1015
|
-
`${entryPath}.issuer`,
|
|
1016
|
-
`${entryPath}.issuer "${entry.issuer}" is listed twice - key lookup resolves by issuer, so a second entry for the same one is silently unreachable. Merge the keys into one entry instead (which is also how a rotation overlaps an old and a new key).`,
|
|
1017
|
-
{ issuer: entry.issuer }
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
seen.add(entry.issuer);
|
|
1021
|
-
const kids = /* @__PURE__ */ new Set();
|
|
1022
|
-
const keys = entry.keys.map((key, keyIndex) => {
|
|
1023
|
-
const keyPath = `${entryPath}.keys.${keyIndex}`;
|
|
1024
|
-
if (kids.has(key.kid)) {
|
|
1025
|
-
throw ap2Invalid(
|
|
1026
|
-
`${keyPath}.kid`,
|
|
1027
|
-
`${keyPath}.kid "${key.kid}" is listed twice for issuer "${entry.issuer}" - a kid selects exactly one key, so which of the two verifies a signature would be undefined`,
|
|
1028
|
-
{ issuer: entry.issuer, kid: key.kid }
|
|
1029
|
-
);
|
|
1030
|
-
}
|
|
1031
|
-
kids.add(key.kid);
|
|
1032
|
-
return { kid: key.kid, jwk: validateJwk(key.jwk, key.kid, keyPath) };
|
|
1033
|
-
});
|
|
1034
|
-
return { issuer: entry.issuer, audience: entry.audience, keys };
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
function validateJwk(jwk, kid, path2) {
|
|
1038
|
-
const fail = (detail) => {
|
|
1039
|
-
throw ap2Invalid(`${path2}.jwk`, `${path2}.jwk (kid "${kid}") ${detail}`, { kid });
|
|
1040
|
-
};
|
|
1041
|
-
for (const member of Object.keys(jwk)) {
|
|
1042
|
-
if (AP2_JWK_MEMBERS.includes(member)) continue;
|
|
1043
|
-
if (member === "d" || member === "k") {
|
|
1044
|
-
fail(
|
|
1045
|
-
`carries private key material ("${member}"). The gateway verifies signatures and never produces them; publish only the public half. Treat the pasted key as compromised and rotate it.`
|
|
1046
|
-
);
|
|
1047
|
-
}
|
|
1048
|
-
fail(
|
|
1049
|
-
`has unsupported member "${member}". Allowed: ${AP2_JWK_MEMBERS.join(", ")}. Members naming a URL are refused on purpose - keys are configured inline and never fetched.`
|
|
1050
|
-
);
|
|
1051
|
-
}
|
|
1052
|
-
const value = (member) => {
|
|
1053
|
-
const raw = jwk[member];
|
|
1054
|
-
if (typeof raw !== "string" || raw.length === 0) {
|
|
1055
|
-
fail(`must give "${member}" as a non-empty string`);
|
|
1056
|
-
}
|
|
1057
|
-
return raw;
|
|
1058
|
-
};
|
|
1059
|
-
if (value("kty") !== AP2_KEY_TYPE) {
|
|
1060
|
-
fail(
|
|
1061
|
-
`must have kty "${AP2_KEY_TYPE}" (got "${value("kty")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
|
|
1062
|
-
);
|
|
1063
|
-
}
|
|
1064
|
-
if (value("crv") !== AP2_JWK_CURVE) {
|
|
1065
|
-
fail(
|
|
1066
|
-
`must be on curve "${AP2_JWK_CURVE}" (got "${value("crv")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
|
|
1067
|
-
);
|
|
1068
|
-
}
|
|
1069
|
-
for (const coordinate of ["x", "y"]) {
|
|
1070
|
-
const encoded = value(coordinate);
|
|
1071
|
-
if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
1072
|
-
fail(`coordinate "${coordinate}" is not base64url (no padding, no "+" or "/")`);
|
|
1073
|
-
}
|
|
1074
|
-
if (Buffer.from(encoded, "base64url").length !== AP2_JWK_COORDINATE_BYTES) {
|
|
1075
|
-
fail(
|
|
1076
|
-
`coordinate "${coordinate}" decodes to ${Buffer.from(encoded, "base64url").length} bytes; a ${AP2_JWK_CURVE} coordinate is ${AP2_JWK_COORDINATE_BYTES}`
|
|
1077
|
-
);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
if (jwk["alg"] !== void 0 && value("alg") !== AP2_SIGNING_ALGORITHM) {
|
|
1081
|
-
fail(`declares alg "${value("alg")}"; only ${AP2_SIGNING_ALGORITHM} is accepted`);
|
|
1082
|
-
}
|
|
1083
|
-
if (jwk["use"] !== void 0 && value("use") !== "sig") {
|
|
1084
|
-
fail(`declares use "${value("use")}"; a verification key must be "sig"`);
|
|
1085
|
-
}
|
|
1086
|
-
if (jwk["kid"] !== void 0 && value("kid") !== kid) {
|
|
1087
|
-
fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
|
|
1088
|
-
}
|
|
1089
|
-
const normalized = {};
|
|
1090
|
-
for (const member of AP2_JWK_MEMBERS) {
|
|
1091
|
-
if (jwk[member] !== void 0) normalized[member] = value(member);
|
|
1092
|
-
}
|
|
1093
|
-
return normalized;
|
|
1094
|
-
}
|
|
1095
|
-
function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
|
|
1096
|
-
if (replayPath === ":memory:") return;
|
|
1097
|
-
const others = [["storage.receipts.path", receiptsPath]];
|
|
1098
|
-
if (acp.enabled) others.push(["protocols.acp.idempotency.path", acp.idempotency.path]);
|
|
1099
|
-
for (const [otherPath, other] of others) {
|
|
1100
|
-
if (replayPath !== other) continue;
|
|
1101
|
-
throw ap2Invalid(
|
|
1102
|
-
"authorization.ap2.replay.path",
|
|
1103
|
-
`authorization.ap2.replay.path is the same file as ${otherPath} ("${replayPath}") - the AP2 replay store keeps its own schema and must not share a database with another store`
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
function normalizeResourceAuthorization(id, entry, pricing, ap2) {
|
|
1108
|
-
const required = entry.authorization?.required;
|
|
1109
|
-
if (required === void 0) return void 0;
|
|
1110
|
-
const path2 = `resources.${id}.authorization.required`;
|
|
1111
|
-
const methods = [];
|
|
1112
|
-
for (const method of required) {
|
|
1113
|
-
if (!SUPPORTED_AUTHORIZATION_METHODS.has(method)) {
|
|
1114
|
-
throw ap2Invalid(
|
|
1115
|
-
path2,
|
|
1116
|
-
`Resource "${id}" requires unsupported authorization method "${method}". Supported: ${[...SUPPORTED_AUTHORIZATION_METHODS].join(", ")}.`,
|
|
1117
|
-
{ resourceId: id, method }
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
if (methods.includes(method)) {
|
|
1121
|
-
throw ap2Invalid(path2, `Resource "${id}" lists authorization method "${method}" twice`, {
|
|
1122
|
-
resourceId: id,
|
|
1123
|
-
method
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
if (method === "ap2" && (ap2 === void 0 || !ap2.enabled)) {
|
|
1127
|
-
throw ap2Invalid(
|
|
1128
|
-
path2,
|
|
1129
|
-
`Resource "${id}" requires authorization method "ap2", which is not configured or not enabled under authorization.ap2`,
|
|
1130
|
-
{ resourceId: id, method }
|
|
1131
|
-
);
|
|
1132
|
-
}
|
|
1133
|
-
methods.push(method);
|
|
1134
|
-
}
|
|
1135
|
-
if (pricing.type !== "fixed") {
|
|
1136
|
-
throw ap2Invalid(
|
|
1137
|
-
path2,
|
|
1138
|
-
`Resource "${id}" requires authorization but its pricing is "${pricing.type}" - authorization proves a purchase was approved and never replaces payment, so it applies to fixed-price paid resources only in this release`,
|
|
1139
|
-
{ resourceId: id }
|
|
1140
|
-
);
|
|
1141
|
-
}
|
|
1142
|
-
return { required: methods };
|
|
1143
|
-
}
|
|
1144
|
-
function acpInvalid(path2, message, extra = {}) {
|
|
1145
|
-
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
|
|
1146
|
-
}
|
|
1147
|
-
function normalizeAcp(raw) {
|
|
1148
|
-
const mountPath = raw?.mountPath ?? DEFAULT_ACP_MOUNT_PATH;
|
|
1149
|
-
if (raw === void 0 || !toBoolean(raw.enabled, "protocols.acp.enabled")) {
|
|
1150
|
-
return { enabled: false, mountPath };
|
|
1151
|
-
}
|
|
1152
|
-
if (raw.auth === void 0) {
|
|
1153
|
-
throw acpInvalid(
|
|
1154
|
-
"protocols.acp.auth",
|
|
1155
|
-
'protocols.acp.auth is required when ACP is enabled - every ACP checkout endpoint is authenticated, and "bearer" is the only scheme supported in this release'
|
|
1156
|
-
);
|
|
1157
|
-
}
|
|
1158
|
-
if (raw.idempotency === void 0) {
|
|
1159
|
-
throw acpInvalid(
|
|
1160
|
-
"protocols.acp.idempotency",
|
|
1161
|
-
"protocols.acp.idempotency is required when ACP is enabled - it names the SQLite file holding checkout idempotency records, and ACP makes the Idempotency-Key mandatory on every checkout POST"
|
|
1162
|
-
);
|
|
1163
|
-
}
|
|
1164
|
-
const discovery = raw.discovery;
|
|
1165
|
-
return {
|
|
1166
|
-
enabled: true,
|
|
1167
|
-
mountPath,
|
|
1168
|
-
auth: raw.auth,
|
|
1169
|
-
idempotency: {
|
|
1170
|
-
path: raw.idempotency.path,
|
|
1171
|
-
retentionHours: toNumber(
|
|
1172
|
-
raw.idempotency.retentionHours ?? ACP_RETENTION_HOURS,
|
|
1173
|
-
"protocols.acp.idempotency.retentionHours",
|
|
1174
|
-
{ min: ACP_RETENTION_HOURS }
|
|
1175
|
-
),
|
|
1176
|
-
merchantIdempotent: raw.idempotency.merchantIdempotent ?? false
|
|
1177
|
-
},
|
|
1178
|
-
checkout: { operations: normalizeAcpOperations(raw.checkout) },
|
|
1179
|
-
// Built key by key so absent metadata stays absent rather than `undefined`
|
|
1180
|
-
...discovery !== void 0 ? {
|
|
1181
|
-
discovery: {
|
|
1182
|
-
...discovery.documentationUrl !== void 0 ? { documentationUrl: discovery.documentationUrl } : {},
|
|
1183
|
-
...discovery.supportedCurrencies !== void 0 ? { supportedCurrencies: discovery.supportedCurrencies } : {},
|
|
1184
|
-
...discovery.supportedLocales !== void 0 ? { supportedLocales: discovery.supportedLocales } : {},
|
|
1185
|
-
...discovery.interventionTypes !== void 0 ? { interventionTypes: discovery.interventionTypes } : {}
|
|
1186
|
-
}
|
|
1187
|
-
} : {}
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
function normalizeAcpOperations(checkout) {
|
|
1191
|
-
const configured = checkout?.operations ?? {};
|
|
1192
|
-
for (const key of Object.keys(configured)) {
|
|
1193
|
-
if (!ACP_CHECKOUT_OPERATIONS.includes(key)) {
|
|
1194
|
-
throw acpInvalid(
|
|
1195
|
-
`protocols.acp.checkout.operations.${key}`,
|
|
1196
|
-
`protocols.acp.checkout.operations names unknown ACP checkout operation "${key}". Supported: ${ACP_CHECKOUT_OPERATIONS.join(", ")}.`
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
const operations = {};
|
|
1201
|
-
const claimedBy = /* @__PURE__ */ new Map();
|
|
1202
|
-
for (const operation of ACP_CHECKOUT_OPERATIONS) {
|
|
1203
|
-
const path2 = `protocols.acp.checkout.operations.${operation}`;
|
|
1204
|
-
const resourceId = configured[operation];
|
|
1205
|
-
if (resourceId === void 0) {
|
|
1206
|
-
throw acpInvalid(
|
|
1207
|
-
path2,
|
|
1208
|
-
`${path2} is required when ACP is enabled - all five checkout operations must be mapped, since ACP discovery advertises the checkout service as a whole`
|
|
1209
|
-
);
|
|
1210
|
-
}
|
|
1211
|
-
const claimed = claimedBy.get(resourceId);
|
|
1212
|
-
if (claimed !== void 0) {
|
|
1213
|
-
throw acpInvalid(
|
|
1214
|
-
path2,
|
|
1215
|
-
`${path2} maps resource "${resourceId}", which is already mapped to "${claimed}" - each ACP checkout operation needs its own resource`,
|
|
1216
|
-
{ resourceId }
|
|
1217
|
-
);
|
|
1218
|
-
}
|
|
1219
|
-
claimedBy.set(resourceId, operation);
|
|
1220
|
-
operations[operation] = resourceId;
|
|
1221
|
-
}
|
|
1222
|
-
return operations;
|
|
1223
|
-
}
|
|
1224
|
-
function validateAcpCheckoutMapping(acp, resources) {
|
|
1225
|
-
const byId = new Map(resources.map((resource) => [resource.id, resource]));
|
|
1226
|
-
for (const operation of ACP_CHECKOUT_OPERATIONS) {
|
|
1227
|
-
const resourceId = acp.checkout.operations[operation];
|
|
1228
|
-
const path2 = `protocols.acp.checkout.operations.${operation}`;
|
|
1229
|
-
const resource = byId.get(resourceId);
|
|
1230
|
-
if (resource === void 0) {
|
|
1231
|
-
throw acpInvalid(
|
|
1232
|
-
path2,
|
|
1233
|
-
`${path2} maps resource "${resourceId}", which is not defined under "resources"`,
|
|
1234
|
-
{ resourceId }
|
|
1235
|
-
);
|
|
1236
|
-
}
|
|
1237
|
-
if (!resource.exposedVia.includes("acp")) {
|
|
1238
|
-
throw acpInvalid(
|
|
1239
|
-
path2,
|
|
1240
|
-
`Resource "${resourceId}" implements ACP operation "${operation}" but does not list "acp" in its expose - the mapping says which resource serves the operation, expose says ACP may invoke it, and both are required`,
|
|
1241
|
-
{ resourceId }
|
|
1242
|
-
);
|
|
1243
|
-
}
|
|
1244
|
-
if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
|
|
1245
|
-
throw acpInvalid(
|
|
1246
|
-
path2,
|
|
1247
|
-
`Resource "${resourceId}" implements ACP operation "${operation}" and must use pricing.type "free" with no "payments" - ACP checkout carries the merchant's own purchase payment, so the gateway does not also charge for the invocation`,
|
|
1248
|
-
{ resourceId }
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1251
|
-
validateAcpOperationInput(path2, operation, resource);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
function validateAcpOperationInput(path2, operation, resource) {
|
|
1255
|
-
const schema = resource.inputSchema;
|
|
1256
|
-
if (schema === void 0) return;
|
|
1257
|
-
const keys = ACP_OPERATION_INPUT_KEYS[operation];
|
|
1258
|
-
const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
|
|
1259
|
-
const sent = [...keys, ...optionalKeys];
|
|
1260
|
-
const topLevel = closedObjectProperties(schema);
|
|
1261
|
-
if (topLevel !== void 0) {
|
|
1262
|
-
for (const key of sent) {
|
|
1263
|
-
if (!Object.hasOwn(topLevel, key)) {
|
|
1264
|
-
const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
|
|
1265
|
-
throw acpInvalid(
|
|
1266
|
-
path2,
|
|
1267
|
-
`Resource "${resource.id}" implements ACP operation "${operation}" but its input schema sets additionalProperties: false without declaring "${key}", which the adapter ${when} for this operation`,
|
|
1268
|
-
{ resourceId: resource.id }
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
1274
|
-
const pathKeys = closedObjectProperties(declared["path"]);
|
|
1275
|
-
if (sent.includes("path") && pathKeys !== void 0 && !Object.hasOwn(pathKeys, ACP_SESSION_ID_INPUT_KEY)) {
|
|
1276
|
-
throw acpInvalid(
|
|
1277
|
-
path2,
|
|
1278
|
-
`Resource "${resource.id}" implements ACP operation "${operation}" but its "path" schema sets additionalProperties: false without declaring "${ACP_SESSION_ID_INPUT_KEY}", which the adapter always sends inside "path"`,
|
|
1279
|
-
{ resourceId: resource.id }
|
|
1280
|
-
);
|
|
1281
|
-
}
|
|
1282
|
-
const bodyKeys = closedObjectProperties(declared["body"]);
|
|
1283
|
-
if (sent.includes("body") && bodyKeys !== void 0 && Object.keys(bodyKeys).length === 0) {
|
|
1284
|
-
throw acpInvalid(
|
|
1285
|
-
path2,
|
|
1286
|
-
`Resource "${resource.id}" implements ACP operation "${operation}" but its "body" schema accepts no keys, so it refuses every ACP document. Config load closes an object schema that omits additionalProperties: declare body as { type: object, additionalProperties: true }`,
|
|
1287
|
-
{ resourceId: resource.id }
|
|
1288
|
-
);
|
|
1289
|
-
}
|
|
1290
|
-
const required = schema["required"];
|
|
1291
|
-
if (Array.isArray(required)) {
|
|
1292
|
-
for (const entry of required) {
|
|
1293
|
-
if (typeof entry === "string" && !keys.includes(entry)) {
|
|
1294
|
-
throw acpInvalid(
|
|
1295
|
-
path2,
|
|
1296
|
-
`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(", ")})`,
|
|
1297
|
-
{ resourceId: resource.id }
|
|
1298
|
-
);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
function closedObjectProperties(schema) {
|
|
1304
|
-
if (!isRecord(schema) || schema["additionalProperties"] !== false) return void 0;
|
|
1305
|
-
return isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
1306
|
-
}
|
|
1307
|
-
function normalizeFacilitator(raw) {
|
|
1308
|
-
return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
|
|
1309
|
-
}
|
|
1310
|
-
function normalizeMpp(raw) {
|
|
1311
|
-
if (raw === void 0) return void 0;
|
|
1312
|
-
const network = raw.network ?? MPP_DEFAULT_NETWORK;
|
|
1313
|
-
if (!isMppNetwork(network)) {
|
|
1314
|
-
throw new CommerceError(
|
|
1315
|
-
"CONFIG_INVALID",
|
|
1316
|
-
`payments.mpp.network must be one of ${MPP_NETWORKS.join(", ")}`,
|
|
1317
|
-
{ details: { path: "payments.mpp.network" } }
|
|
1318
|
-
);
|
|
1319
|
-
}
|
|
1320
|
-
const mpp = {
|
|
1321
|
-
enabled: toBoolean(raw.enabled, "payments.mpp.enabled"),
|
|
1322
|
-
network,
|
|
1323
|
-
rpcUrl: raw.rpcUrl,
|
|
1324
|
-
asset: raw.asset,
|
|
1325
|
-
assetName: raw.assetName,
|
|
1326
|
-
assetVersion: raw.assetVersion,
|
|
1327
|
-
recipient: raw.recipient,
|
|
1328
|
-
realm: raw.realm,
|
|
1329
|
-
challengeSecret: raw.challengeSecret,
|
|
1330
|
-
...raw.challengeTtlSeconds !== void 0 ? {
|
|
1331
|
-
challengeTtlSeconds: toNumber(
|
|
1332
|
-
raw.challengeTtlSeconds,
|
|
1333
|
-
"payments.mpp.challengeTtlSeconds",
|
|
1334
|
-
{ min: 1 }
|
|
1335
|
-
)
|
|
1336
|
-
} : {},
|
|
1337
|
-
paymentFlow: raw.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
|
|
1338
|
-
facilitator: normalizeFacilitator(raw.facilitator),
|
|
1339
|
-
...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
|
|
1340
|
-
...raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
1341
|
-
allowUnauthenticatedFacilitator: toBoolean(
|
|
1342
|
-
raw.allowUnauthenticatedFacilitator,
|
|
1343
|
-
"payments.mpp.allowUnauthenticatedFacilitator"
|
|
1344
|
-
)
|
|
1345
|
-
} : {}
|
|
1346
|
-
};
|
|
1347
|
-
validateAddress("payments.mpp.recipient", mpp.recipient);
|
|
1348
|
-
validateAddress("payments.mpp.asset", mpp.asset);
|
|
1349
|
-
if (/[\r\n]/.test(mpp.realm)) {
|
|
1350
|
-
throw new CommerceError("CONFIG_INVALID", "payments.mpp.realm must be a single line", {
|
|
1351
|
-
details: { path: "payments.mpp.realm" }
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
if (mpp.challengeSecret.length < MPP_MIN_CHALLENGE_SECRET_LENGTH) {
|
|
1355
|
-
throw new CommerceError(
|
|
1356
|
-
"CONFIG_INVALID",
|
|
1357
|
-
`payments.mpp.challengeSecret must have length at least ${MPP_MIN_CHALLENGE_SECRET_LENGTH}`,
|
|
1358
|
-
{ details: { path: "payments.mpp.challengeSecret" } }
|
|
1359
|
-
);
|
|
1360
|
-
}
|
|
1361
|
-
resolveX402Deployment({
|
|
1362
|
-
network: mpp.network,
|
|
1363
|
-
payTo: mpp.recipient,
|
|
1364
|
-
asset: mpp.asset,
|
|
1365
|
-
assetName: mpp.assetName,
|
|
1366
|
-
assetVersion: mpp.assetVersion,
|
|
1367
|
-
facilitator: mpp.facilitator,
|
|
1368
|
-
...mpp.allowMainnet !== void 0 ? { allowMainnet: mpp.allowMainnet } : {},
|
|
1369
|
-
...mpp.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: mpp.allowUnauthenticatedFacilitator } : {},
|
|
1370
|
-
configPath: "payments.mpp",
|
|
1371
|
-
payToField: "recipient"
|
|
1372
|
-
});
|
|
1373
|
-
return mpp;
|
|
1374
|
-
}
|
|
1375
|
-
function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
|
|
1376
|
-
if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
|
|
1377
|
-
const inputProperties = entry.input?.["properties"];
|
|
1378
|
-
if (inputProperties && typeof inputProperties === "object") {
|
|
1379
|
-
for (const reserved of RESERVED_INPUT_FIELDS) {
|
|
1380
|
-
if (!(reserved in inputProperties)) continue;
|
|
1381
|
-
throw new CommerceError(
|
|
1382
|
-
"CONFIG_INVALID",
|
|
1383
|
-
`Resource "${id}" declares an input property "${reserved}", which is reserved by the gateway`,
|
|
1384
|
-
{
|
|
1385
|
-
details: {
|
|
1386
|
-
path: `resources.${id}.input.properties.${reserved}`,
|
|
1387
|
-
resourceId: id
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
for (const protocol of entry.expose) {
|
|
1394
|
-
if (!SUPPORTED_PROTOCOLS.has(protocol)) {
|
|
1395
|
-
const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
|
|
1396
|
-
throw new CommerceError(
|
|
1397
|
-
"CONFIG_INVALID",
|
|
1398
|
-
`Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: ${PROTOCOL_NAMES.join(", ")}.`,
|
|
1399
|
-
{ details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
|
|
1400
|
-
);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
if (entry.expose.includes("mcp")) {
|
|
1404
|
-
if (!protocols.mcp.enabled) {
|
|
1405
|
-
throw new CommerceError(
|
|
1406
|
-
"CONFIG_INVALID",
|
|
1407
|
-
`Resource "${id}" is exposed via "mcp" but protocols.mcp.enabled is false`,
|
|
1408
|
-
{ details: { path: `resources.${id}.expose`, resourceId: id } }
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
if (!MCP_TOOL_NAME_PATTERN.test(id)) {
|
|
1412
|
-
throw new CommerceError(
|
|
1413
|
-
"CONFIG_INVALID",
|
|
1414
|
-
`Resource "${id}" is exposed via "mcp" but its id is not a legal MCP tool name (allowed: A-Z, a-z, 0-9, "_", "-", ".", 1-128 chars)`,
|
|
1415
|
-
{ details: { path: `resources.${id}`, resourceId: id } }
|
|
1416
|
-
);
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
if (entry.expose.includes("a2a") && !protocols.a2a.enabled) {
|
|
1420
|
-
throw new CommerceError(
|
|
1421
|
-
"CONFIG_INVALID",
|
|
1422
|
-
`Resource "${id}" is exposed via "a2a" but protocols.a2a.enabled is false`,
|
|
1423
|
-
{ details: { path: `resources.${id}.expose`, resourceId: id } }
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
if (entry.expose.includes("acp") && !protocols.acp.enabled) {
|
|
1427
|
-
throw new CommerceError(
|
|
1428
|
-
"CONFIG_INVALID",
|
|
1429
|
-
`Resource "${id}" is exposed via "acp" but protocols.acp.enabled is false`,
|
|
1430
|
-
{ details: { path: `resources.${id}.expose`, resourceId: id } }
|
|
1431
|
-
);
|
|
1432
|
-
}
|
|
1433
|
-
if (entry.expose.includes("http") && !protocols.http.enabled) {
|
|
1434
|
-
throw new CommerceError(
|
|
1435
|
-
"CONFIG_INVALID",
|
|
1436
|
-
`Resource "${id}" is exposed via "http" but protocols.http.enabled is false`,
|
|
1437
|
-
{ details: { path: `resources.${id}.expose`, resourceId: id } }
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
1440
|
-
validateBackendUrl(id, entry.backend.url);
|
|
1441
|
-
validateBackendHeaders(id, entry.backend.headers);
|
|
1442
|
-
const pathScope = validateInputBindings(id, entry.backend, entry.input);
|
|
1443
|
-
validatePathParametersDeclared(id, entry.backend.url, pathScope.schema, pathScope.where);
|
|
1444
|
-
if (entry.pricing.type === "dynamic") {
|
|
1445
|
-
throw new CommerceError(
|
|
1446
|
-
"CONFIG_INVALID",
|
|
1447
|
-
`Resource "${id}" uses pricing.type "dynamic", which is not supported in this release`,
|
|
1448
|
-
{ details: { path: `resources.${id}.pricing.type`, resourceId: id } }
|
|
1449
|
-
);
|
|
1450
|
-
}
|
|
1451
|
-
if (entry.pricing.type === "fixed") {
|
|
1452
|
-
validatePricingAmount(id, entry.pricing.amount, x402?.assetDecimals);
|
|
1453
|
-
}
|
|
1454
|
-
const paymentMethods = entry.payments ?? [];
|
|
1455
|
-
if (entry.pricing.type === "fixed") {
|
|
1456
|
-
if (paymentMethods.length === 0) {
|
|
1457
|
-
throw new CommerceError(
|
|
1458
|
-
"CONFIG_INVALID",
|
|
1459
|
-
`Resource "${id}" has fixed pricing but declares no "payments" (a paid resource must name at least one payment method)`,
|
|
1460
|
-
{ details: { path: `resources.${id}.payments`, resourceId: id } }
|
|
1461
|
-
);
|
|
1462
|
-
}
|
|
1463
|
-
for (const method of paymentMethods) {
|
|
1464
|
-
if (!SUPPORTED_PAYMENT_METHODS.has(method)) {
|
|
1465
|
-
throw new CommerceError(
|
|
1466
|
-
"CONFIG_INVALID",
|
|
1467
|
-
`Resource "${id}" names unsupported payment method "${method}". Supported: ${PAYMENT_METHOD_NAMES.join(", ")}.`,
|
|
1468
|
-
{ details: { path: `resources.${id}.payments`, resourceId: id, method } }
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
const enabledRails = [...x402?.enabled ? ["x402"] : [], ...mpp?.enabled ? ["mpp"] : []];
|
|
1473
|
-
if (!paymentMethods.some((method) => enabledRails.includes(method))) {
|
|
1474
|
-
throw new CommerceError(
|
|
1475
|
-
"CONFIG_INVALID",
|
|
1476
|
-
`Resource "${id}" names payment method(s) "${paymentMethods.join(", ")}", none of which is configured and enabled under "payments"`,
|
|
1477
|
-
{ details: { path: `resources.${id}.payments`, resourceId: id, paymentMethods } }
|
|
1478
|
-
);
|
|
1479
|
-
}
|
|
1480
|
-
if (mpp?.enabled && paymentMethods.includes("mpp")) {
|
|
1481
|
-
if (entry.pricing.currency !== MPP_PROFILE.assetSymbol) {
|
|
1482
|
-
throw new CommerceError(
|
|
1483
|
-
"CONFIG_INVALID",
|
|
1484
|
-
`Resource "${id}" is priced in ${entry.pricing.currency}, but MPP charges ${MPP_PROFILE.assetSymbol}`,
|
|
1485
|
-
{ details: { path: `resources.${id}.pricing.currency`, resourceId: id } }
|
|
1486
|
-
);
|
|
1487
|
-
}
|
|
1488
|
-
validatePricingAmount(id, entry.pricing.amount, MPP_PROFILE.assetDecimals);
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
if (entry.paymentFlow !== void 0 && (entry.pricing.type !== "fixed" || !paymentMethods.some((method) => method === "x402" || method === "mpp"))) {
|
|
1492
|
-
throw new CommerceError(
|
|
1493
|
-
"CONFIG_INVALID",
|
|
1494
|
-
`Resource "${id}" sets paymentFlow, which requires fixed pricing and "x402" or "mpp" in payments`,
|
|
1495
|
-
{ details: { path: `resources.${id}.paymentFlow`, resourceId: id } }
|
|
1496
|
-
);
|
|
1497
|
-
}
|
|
1498
|
-
const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
|
|
1499
|
-
const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
|
|
1500
|
-
return {
|
|
1501
|
-
id,
|
|
1502
|
-
name: entry.name,
|
|
1503
|
-
...entry.description !== void 0 ? { description: entry.description } : {},
|
|
1504
|
-
inputSchema: entry.input !== void 0 ? defaultClosedObjectSchema(entry.input) : EMPTY_CLOSED_OBJECT_SCHEMA,
|
|
1505
|
-
...entry.output !== void 0 ? { outputSchema: entry.output } : {},
|
|
1506
|
-
handler: {
|
|
1507
|
-
type: "http",
|
|
1508
|
-
method: entry.backend.method,
|
|
1509
|
-
url: entry.backend.url,
|
|
1510
|
-
...entry.backend.headers !== void 0 ? { headers: entry.backend.headers } : {},
|
|
1511
|
-
...entry.backend.timeoutMs !== void 0 ? {
|
|
1512
|
-
timeoutMs: toNumber(entry.backend.timeoutMs, `resources.${id}.backend.timeoutMs`, {
|
|
1513
|
-
min: 1
|
|
1514
|
-
})
|
|
1515
|
-
} : {},
|
|
1516
|
-
...entry.backend.inputBindings !== void 0 ? { inputBindings: pickDefined(entry.backend.inputBindings) } : {}
|
|
1517
|
-
},
|
|
1518
|
-
pricing,
|
|
1519
|
-
exposedVia: entry.expose,
|
|
1520
|
-
paymentMethods,
|
|
1521
|
-
...authorization !== void 0 ? { authorization } : {}
|
|
1522
|
-
};
|
|
1523
|
-
}
|
|
1524
|
-
function defaultClosedObjectSchema(schema) {
|
|
1525
|
-
const isObjectSchema = isObjectSchemaNode(schema);
|
|
1526
|
-
const result = { ...schema };
|
|
1527
|
-
if (isObjectSchema) {
|
|
1528
|
-
const properties = schema["properties"];
|
|
1529
|
-
if (isRecord(properties)) {
|
|
1530
|
-
result["properties"] = Object.fromEntries(
|
|
1531
|
-
Object.entries(properties).map(([key, sub]) => [
|
|
1532
|
-
key,
|
|
1533
|
-
isRecord(sub) ? defaultClosedObjectSchema(sub) : sub
|
|
1534
|
-
])
|
|
1535
|
-
);
|
|
1536
|
-
}
|
|
1537
|
-
if (!Object.hasOwn(schema, "additionalProperties")) {
|
|
1538
|
-
result["additionalProperties"] = false;
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
const items = schema["items"];
|
|
1542
|
-
if (isRecord(items)) {
|
|
1543
|
-
result["items"] = defaultClosedObjectSchema(items);
|
|
1544
|
-
}
|
|
1545
|
-
const additional = schema["additionalProperties"];
|
|
1546
|
-
if (isRecord(additional)) {
|
|
1547
|
-
result["additionalProperties"] = defaultClosedObjectSchema(additional);
|
|
1548
|
-
}
|
|
1549
|
-
return result;
|
|
1550
|
-
}
|
|
1551
|
-
function excludesObjectType(schema) {
|
|
1552
|
-
const type = schema["type"];
|
|
1553
|
-
if (type === void 0 || type === "object") return false;
|
|
1554
|
-
if (Array.isArray(type)) return !type.includes("object");
|
|
1555
|
-
return true;
|
|
1556
|
-
}
|
|
1557
|
-
function validateResourceSchemaKeywords(id, path2, schema) {
|
|
1558
|
-
for (const keyword of UNSUPPORTED_SCHEMA_KEYWORDS) {
|
|
1559
|
-
if (Object.hasOwn(schema, keyword)) {
|
|
1560
|
-
console.warn(
|
|
1561
|
-
`[agent-commerce] resource "${id}" ${path2} uses JSON Schema keyword "${keyword}", which this gateway does not enforce (see src/core/execution/validation.ts for the supported subset). Remove it or treat it as documentation only.`
|
|
1562
|
-
);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
if ((Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required")) && excludesObjectType(schema)) {
|
|
1566
|
-
throw new CommerceError(
|
|
1567
|
-
"CONFIG_INVALID",
|
|
1568
|
-
`Resource "${id}" ${path2} declares "properties" and/or "required" but its "type" (${JSON.stringify(schema["type"])}) does not include "object" - the validator will never route a value through either check under that type, so this schema claims to constrain input it does not actually enforce. Remove "properties"/"required", or include "object" in "type".`,
|
|
1569
|
-
{ details: { path: `resources.${id}.${path2}`, resourceId: id } }
|
|
1570
|
-
);
|
|
1571
|
-
}
|
|
1572
|
-
const requiredRaw = schema["required"];
|
|
1573
|
-
const closed = !Object.hasOwn(schema, "additionalProperties") ? true : schema["additionalProperties"] === false;
|
|
1574
|
-
if (Array.isArray(requiredRaw) && closed && isObjectSchemaNode(schema)) {
|
|
1575
|
-
const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
1576
|
-
const undeclared = requiredRaw.filter(
|
|
1577
|
-
(name) => typeof name === "string" && !Object.hasOwn(declared, name)
|
|
1578
|
-
);
|
|
1579
|
-
if (undeclared.length > 0) {
|
|
1580
|
-
throw new CommerceError(
|
|
1581
|
-
"CONFIG_INVALID",
|
|
1582
|
-
`Resource "${id}" ${path2} lists ${undeclared.map((n) => `"${n}"`).join(", ")} in "required" but does not declare ${undeclared.length === 1 ? "it" : "them"} in "properties". The schema is closed (additionalProperties: false), so ${undeclared.length === 1 ? "that name is" : "those names are"} rejected as an unknown key and no input can ever satisfy this schema. Declare ${undeclared.length === 1 ? "it" : "them"} in "properties".`,
|
|
1583
|
-
{ details: { path: `resources.${id}.${path2}`, resourceId: id, undeclared } }
|
|
1584
|
-
);
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
const properties = schema["properties"];
|
|
1588
|
-
if (isRecord(properties)) {
|
|
1589
|
-
for (const [key, sub] of Object.entries(properties)) {
|
|
1590
|
-
if (isRecord(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
const additional = schema["additionalProperties"];
|
|
1594
|
-
if (isRecord(additional)) {
|
|
1595
|
-
validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
|
|
1596
|
-
}
|
|
1597
|
-
const items = schema["items"];
|
|
1598
|
-
if (isRecord(items)) {
|
|
1599
|
-
validateResourceSchemaKeywords(id, `${path2}.items`, items);
|
|
1600
|
-
} else if (Array.isArray(items)) {
|
|
1601
|
-
console.warn(
|
|
1602
|
-
`[agent-commerce] resource "${id}" ${path2}.items is a tuple (an array of schemas), which this gateway does not enforce - only a single schema applied to every array element is supported (see src/core/execution/validation.ts). Each position's schema is unenforced; treat it as documentation only.`
|
|
1603
|
-
);
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
function validatePricingAmount(id, amount, decimals) {
|
|
1607
|
-
const path2 = `resources.${id}.pricing.amount`;
|
|
1608
|
-
if (!PRICING_AMOUNT_PATTERN.test(amount)) {
|
|
1609
|
-
throw new CommerceError(
|
|
1610
|
-
"CONFIG_INVALID",
|
|
1611
|
-
`Resource "${id}" has pricing.amount "${amount}", which is not a plain positive decimal (no currency symbol, no thousands separator, no exponent - e.g. "0.01")`,
|
|
1612
|
-
{ details: { path: path2, resourceId: id } }
|
|
1613
|
-
);
|
|
1614
|
-
}
|
|
1615
|
-
if (ZERO_AMOUNT_PATTERN.test(amount)) {
|
|
1616
|
-
throw new CommerceError(
|
|
1617
|
-
"CONFIG_INVALID",
|
|
1618
|
-
`Resource "${id}" has pricing.amount "0" - a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
|
|
1619
|
-
{ details: { path: path2, resourceId: id } }
|
|
1620
|
-
);
|
|
1621
|
-
}
|
|
1622
|
-
if (decimals !== void 0) {
|
|
1623
|
-
const fractionalDigits = amount.includes(".") ? amount.split(".")[1]?.length ?? 0 : 0;
|
|
1624
|
-
if (fractionalDigits > decimals) {
|
|
1625
|
-
throw new CommerceError(
|
|
1626
|
-
"CONFIG_INVALID",
|
|
1627
|
-
`Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${decimals} decimals)`,
|
|
1628
|
-
{ details: { path: path2, resourceId: id } }
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
function validateInputBindings(id, backend, input) {
|
|
1634
|
-
const legacy = { schema: input, where: "its input schema" };
|
|
1635
|
-
const bindings = backend.inputBindings;
|
|
1636
|
-
const templated = extractPathParameterNames(backend.url).length > 0;
|
|
1637
|
-
if (bindings === void 0) return legacy;
|
|
1638
|
-
const path2 = `resources.${id}.backend.inputBindings`;
|
|
1639
|
-
const fail = (message, details = {}) => {
|
|
1640
|
-
throw new CommerceError("CONFIG_INVALID", `Resource "${id}" ${message}`, {
|
|
1641
|
-
details: { path: path2, resourceId: id, ...details }
|
|
1642
|
-
});
|
|
1643
|
-
};
|
|
1644
|
-
const entries = Object.entries(bindings).filter(
|
|
1645
|
-
(entry) => entry[1] !== void 0
|
|
1646
|
-
);
|
|
1647
|
-
if (entries.length === 0) {
|
|
1648
|
-
fail("has an empty backend.inputBindings - remove the block to use the default mapping");
|
|
1649
|
-
}
|
|
1650
|
-
if (backend.method === "GET" || backend.method === "DELETE") {
|
|
1651
|
-
if (bindings.body !== void 0) {
|
|
1652
|
-
fail(
|
|
1653
|
-
`binds a request body on a ${backend.method}, which sends none - the value would be silently dropped`
|
|
1654
|
-
);
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
if (templated && bindings.path === void 0) {
|
|
1658
|
-
fail(
|
|
1659
|
-
'has backend.url path parameters but no "path" binding - in explicit binding mode nothing else supplies them, so every call would fail to reach the backend'
|
|
1660
|
-
);
|
|
1661
|
-
}
|
|
1662
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1663
|
-
const properties = isRecord(input?.["properties"]) ? input["properties"] : {};
|
|
1664
|
-
const required = new Set(
|
|
1665
|
-
Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
|
|
1666
|
-
);
|
|
1667
|
-
for (const [location, property] of entries) {
|
|
1668
|
-
if (RESERVED_INPUT_FIELDS.includes(property)) {
|
|
1669
|
-
fail(`binds "${location}" to "${property}", which is reserved by the gateway`, { location });
|
|
1670
|
-
}
|
|
1671
|
-
const other = seen.get(property);
|
|
1672
|
-
if (other !== void 0) {
|
|
1673
|
-
fail(`binds both "${other}" and "${location}" to the input property "${property}"`, {
|
|
1674
|
-
location,
|
|
1675
|
-
property
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
seen.set(property, location);
|
|
1679
|
-
if (!Object.hasOwn(properties, property)) {
|
|
1680
|
-
fail(
|
|
1681
|
-
`binds "${location}" to input property "${property}", which the input schema does not declare - the schema is closed, so a caller could never supply it`,
|
|
1682
|
-
{ location, property }
|
|
1683
|
-
);
|
|
1684
|
-
}
|
|
1685
|
-
const declared = properties[property];
|
|
1686
|
-
if (location !== "body" && isRecord(declared) && !isObjectSchemaNode(declared)) {
|
|
1687
|
-
fail(
|
|
1688
|
-
`binds "${location}" to input property "${property}", which is not an object schema - ${location} parameters are read as an object of name/value pairs`,
|
|
1689
|
-
{ location, property }
|
|
1690
|
-
);
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
if (templated && bindings.path !== void 0 && !required.has(bindings.path)) {
|
|
1694
|
-
fail(
|
|
1695
|
-
`binds path parameters to input property "${bindings.path}" without listing it in the input schema's "required" - a caller that omits it cannot supply any path parameter, so the request could never be built`,
|
|
1696
|
-
{ property: bindings.path }
|
|
1697
|
-
);
|
|
1698
|
-
}
|
|
1699
|
-
if (bindings.path === void 0) return { schema: void 0, where: "its input schema" };
|
|
1700
|
-
const group = properties[bindings.path];
|
|
1701
|
-
return {
|
|
1702
|
-
schema: isRecord(group) ? group : void 0,
|
|
1703
|
-
where: `input.properties.${bindings.path}`
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
function pickDefined(value) {
|
|
1707
|
-
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
1708
|
-
}
|
|
1709
|
-
function validatePathParametersDeclared(id, url, input, where) {
|
|
1710
|
-
const stray = findUnparsedBraceToken(url);
|
|
1711
|
-
if (stray !== void 0) {
|
|
1712
|
-
throw new CommerceError(
|
|
1713
|
-
"CONFIG_INVALID",
|
|
1714
|
-
`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`,
|
|
1715
|
-
{ details: { path: `resources.${id}.backend.url`, resourceId: id, token: stray } }
|
|
1716
|
-
);
|
|
1717
|
-
}
|
|
1718
|
-
const params = extractPathParameterNames(url);
|
|
1719
|
-
if (params.length === 0) return;
|
|
1720
|
-
const firstBrace = url.indexOf("{");
|
|
1721
|
-
if (firstBrace !== -1) {
|
|
1722
|
-
const prefix = url.slice(0, firstBrace);
|
|
1723
|
-
let origin;
|
|
1724
|
-
try {
|
|
1725
|
-
const parsedPrefix = new URL(prefix);
|
|
1726
|
-
origin = parsedPrefix.hostname === "" ? void 0 : parsedPrefix.origin;
|
|
1727
|
-
} catch {
|
|
1728
|
-
}
|
|
1729
|
-
if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
|
|
1730
|
-
throw new CommerceError(
|
|
1731
|
-
"CONFIG_INVALID",
|
|
1732
|
-
`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`,
|
|
1733
|
-
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
1734
|
-
);
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
const propertiesRaw = input?.["properties"];
|
|
1738
|
-
const properties = isRecord(propertiesRaw) ? propertiesRaw : {};
|
|
1739
|
-
const requiredRaw = input?.["required"];
|
|
1740
|
-
const required = new Set(
|
|
1741
|
-
Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : []
|
|
1742
|
-
);
|
|
1743
|
-
for (const param of params) {
|
|
1744
|
-
const path2 = `resources.${id}.backend.url`;
|
|
1745
|
-
if (!Object.hasOwn(properties, param)) {
|
|
1746
|
-
throw new CommerceError(
|
|
1747
|
-
"CONFIG_INVALID",
|
|
1748
|
-
`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`,
|
|
1749
|
-
{ details: { path: path2, resourceId: id, param } }
|
|
1750
|
-
);
|
|
1751
|
-
}
|
|
1752
|
-
if (!required.has(param)) {
|
|
1753
|
-
throw new CommerceError(
|
|
1754
|
-
"CONFIG_INVALID",
|
|
1755
|
-
`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`,
|
|
1756
|
-
{ details: { path: path2, resourceId: id, param } }
|
|
1757
|
-
);
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
function validateBackendUrl(id, url) {
|
|
1762
|
-
let parsed;
|
|
1763
|
-
try {
|
|
1764
|
-
parsed = new URL(url);
|
|
1765
|
-
} catch {
|
|
1766
|
-
throw new CommerceError(
|
|
1767
|
-
"CONFIG_INVALID",
|
|
1768
|
-
`Resource "${id}" has an invalid backend.url: must be an absolute http:// or https:// URL`,
|
|
1769
|
-
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
1770
|
-
);
|
|
1771
|
-
}
|
|
1772
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1773
|
-
throw new CommerceError(
|
|
1774
|
-
"CONFIG_INVALID",
|
|
1775
|
-
`Resource "${id}" has a backend.url with scheme "${parsed.protocol}": must be http:// or https://`,
|
|
1776
|
-
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
1777
|
-
);
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
function validateBackendHeaders(id, headers) {
|
|
1781
|
-
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
1782
|
-
try {
|
|
1783
|
-
new Headers([[name, value]]);
|
|
1784
|
-
} catch {
|
|
1785
|
-
throw new CommerceError(
|
|
1786
|
-
"CONFIG_INVALID",
|
|
1787
|
-
`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)`,
|
|
1788
|
-
{ details: { path: `resources.${id}.backend.headers.${name}`, resourceId: id } }
|
|
1789
|
-
);
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
function validateAddress(path2, value) {
|
|
1794
|
-
if (!ADDRESS_PATTERN.test(value)) {
|
|
1795
|
-
throw new CommerceError(
|
|
1796
|
-
"CONFIG_INVALID",
|
|
1797
|
-
`Configuration value at "${path2}" is not a plausible address (expected "0x" followed by 40 hex characters)`,
|
|
1798
|
-
{ details: { path: path2 } }
|
|
1799
|
-
);
|
|
1800
|
-
}
|
|
1801
|
-
if (ZERO_ADDRESS_PATTERN.test(value)) {
|
|
1802
|
-
throw new CommerceError(
|
|
1803
|
-
"CONFIG_INVALID",
|
|
1804
|
-
`Configuration value at "${path2}" must not be the zero address`,
|
|
1805
|
-
{
|
|
1806
|
-
details: { path: path2 }
|
|
1807
|
-
}
|
|
1808
|
-
);
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, NumberOrString, BooleanOrString, MerchantSchema, ServerSchema, StorageSchema, RESERVED_GATEWAY_PATHS, MountPathSchema, AcpAuthSchema, AcpIdempotencySchema, AcpCheckoutSchema, AcpDiscoverySchema, ProtocolsSchema, DEFAULT_A2A_MOUNT_PATH, DEFAULT_ACP_MOUNT_PATH, ACP_RETENTION_HOURS, BackendMethodSchema, BackendInputBindingsSchema, BackendHandlerSchema, PricingSchema, JsonSchemaValueSchema, ResourceEntrySchema, ResourcesMapSchema, FacilitatorAuthSchema, FacilitatorSchema, X402Schema, MppSchema, PaymentsSchema, Ap2KeySchema, Ap2IssuerSchema, Ap2Schema, AuthorizationSchema, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, SUPPORTED_AUTHORIZATION_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
|
|
1812
|
-
var init_schema = __esm({
|
|
1813
|
-
"src/config/schema.ts"() {
|
|
1814
|
-
init_constants();
|
|
1815
|
-
init_core();
|
|
1816
|
-
init_execution();
|
|
1817
|
-
init_is_record();
|
|
1818
|
-
init_constants2();
|
|
1819
|
-
init_guardrails();
|
|
1820
|
-
init_payment_flow();
|
|
1821
|
-
init_constants3();
|
|
1822
|
-
init_constants4();
|
|
1823
|
-
init_env();
|
|
1824
|
-
SUPPORTED_CONFIG_VERSION = 1;
|
|
1825
|
-
ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
|
|
1826
|
-
ZERO_ADDRESS_PATTERN = /^0x0{40}$/i;
|
|
1827
|
-
NumberOrString = z.union([z.number(), z.string()]);
|
|
1828
|
-
BooleanOrString = z.union([z.boolean(), z.string()]);
|
|
1829
|
-
MerchantSchema = z.object({
|
|
1830
|
-
id: z.string().min(1),
|
|
1831
|
-
name: z.string().min(1),
|
|
1832
|
-
publicBaseUrl: z.string().min(1)
|
|
1833
|
-
}).strict();
|
|
1834
|
-
ServerSchema = z.object({
|
|
1835
|
-
port: NumberOrString,
|
|
1836
|
-
host: z.string().min(1),
|
|
1837
|
-
// Shared secret gating /api/receipts and /api/events. Without it both
|
|
1838
|
-
// routes 404.
|
|
1839
|
-
adminToken: z.string().min(1).optional(),
|
|
1840
|
-
// Browser origins allowed to call the gateway; none by default. A request
|
|
1841
|
-
// with any other `Origin` gets 403 on every route. Entries match the
|
|
1842
|
-
// `Origin` header exactly (ignoring case), so `"*"` and a trailing slash
|
|
1843
|
-
// would match nothing: a silent lockout that an operator might "fix" by
|
|
1844
|
-
// disabling the check. Both are refused with a pointer.
|
|
1845
|
-
allowedOrigins: z.array(
|
|
1846
|
-
z.string().min(1).refine((origin) => origin !== "*", {
|
|
1847
|
-
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.`
|
|
1848
|
-
}).refine((origin) => !origin.endsWith("/"), {
|
|
1849
|
-
message: 'must not end with "/" - a browser Origin header never has a trailing slash, so this entry would match nothing. Use e.g. "http://localhost:5173".'
|
|
1850
|
-
})
|
|
1851
|
-
).optional()
|
|
1852
|
-
}).strict();
|
|
1853
|
-
StorageSchema = z.object({
|
|
1854
|
-
receipts: z.object({
|
|
1855
|
-
driver: z.literal("sqlite"),
|
|
1856
|
-
path: z.string().min(1)
|
|
1857
|
-
}).strict()
|
|
1858
|
-
}).strict();
|
|
1859
|
-
RESERVED_GATEWAY_PATHS = [
|
|
1860
|
-
"/health",
|
|
1861
|
-
"/ready",
|
|
1862
|
-
"/.well-known/agent-commerce",
|
|
1863
|
-
// Fixed by the A2A specification and served by the adapter
|
|
1864
|
-
"/.well-known/agent-card.json",
|
|
1865
|
-
// Fixed by the ACP specification
|
|
1866
|
-
ACP_WELL_KNOWN_PATH,
|
|
1867
|
-
"/api/resources",
|
|
1868
|
-
"/api/resources/:id/invoke",
|
|
1869
|
-
"/api/receipts",
|
|
1870
|
-
"/api/events"
|
|
1871
|
-
];
|
|
1872
|
-
MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
|
|
1873
|
-
message: 'must start with "/" - it is an absolute gateway path, e.g. "/mcp".'
|
|
1874
|
-
}).refine((value) => !/[:*?\s]/.test(value), {
|
|
1875
|
-
message: 'must not contain ":", "*", "?" or whitespace - the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
|
|
1876
|
-
}).refine(
|
|
1877
|
-
(value) => {
|
|
1878
|
-
const base = value.replace(/\/+$/, "");
|
|
1879
|
-
return !RESERVED_GATEWAY_PATHS.some(
|
|
1880
|
-
(reserved) => reserved === base || reserved.startsWith(`${base}/`)
|
|
1881
|
-
);
|
|
1882
|
-
},
|
|
1883
|
-
{
|
|
1884
|
-
message: `must not collide with a route the gateway already serves (${RESERVED_GATEWAY_PATHS.join(", ")}); pick a dedicated prefix such as "/mcp".`
|
|
1885
|
-
}
|
|
1886
|
-
);
|
|
1887
|
-
AcpAuthSchema = z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict();
|
|
1888
|
-
AcpIdempotencySchema = z.object({
|
|
1889
|
-
path: z.string().min(1),
|
|
1890
|
-
retentionHours: NumberOrString.optional(),
|
|
1891
|
-
merchantIdempotent: z.boolean().optional()
|
|
1892
|
-
}).strict();
|
|
1893
|
-
AcpCheckoutSchema = z.object({ operations: z.record(z.string().min(1)) }).strict();
|
|
1894
|
-
AcpDiscoverySchema = z.object({
|
|
1895
|
-
documentationUrl: z.string().min(1).optional(),
|
|
1896
|
-
supportedCurrencies: z.array(z.string().min(1)).optional(),
|
|
1897
|
-
supportedLocales: z.array(z.string().min(1)).optional(),
|
|
1898
|
-
interventionTypes: z.array(z.string().min(1)).optional()
|
|
1899
|
-
}).strict();
|
|
1900
|
-
ProtocolsSchema = z.object({
|
|
1901
|
-
http: z.object({ enabled: BooleanOrString }).strict(),
|
|
1902
|
-
mcp: z.object({
|
|
1903
|
-
enabled: BooleanOrString,
|
|
1904
|
-
mountPath: MountPathSchema
|
|
1905
|
-
}).strict(),
|
|
1906
|
-
// Optional; A2A is off unless configured
|
|
1907
|
-
a2a: z.object({
|
|
1908
|
-
enabled: BooleanOrString,
|
|
1909
|
-
mountPath: MountPathSchema.optional()
|
|
1910
|
-
}).strict().optional(),
|
|
1911
|
-
// Optional and off by default. The business pass requires the sub-blocks
|
|
1912
|
-
// only when ACP is enabled, so a disabled placeholder stays valid and an
|
|
1913
|
-
// enabled block gets a message naming what it lacks.
|
|
1914
|
-
acp: z.object({
|
|
1915
|
-
enabled: BooleanOrString,
|
|
1916
|
-
mountPath: MountPathSchema.optional(),
|
|
1917
|
-
auth: AcpAuthSchema.optional(),
|
|
1918
|
-
idempotency: AcpIdempotencySchema.optional(),
|
|
1919
|
-
checkout: AcpCheckoutSchema.optional(),
|
|
1920
|
-
discovery: AcpDiscoverySchema.optional()
|
|
1921
|
-
}).strict().optional()
|
|
1922
|
-
}).strict();
|
|
1923
|
-
DEFAULT_A2A_MOUNT_PATH = "/a2a";
|
|
1924
|
-
DEFAULT_ACP_MOUNT_PATH = "/acp";
|
|
1925
|
-
ACP_RETENTION_HOURS = 24;
|
|
1926
|
-
BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
1927
|
-
BackendInputBindingsSchema = z.object({
|
|
1928
|
-
path: z.string().min(1).optional(),
|
|
1929
|
-
query: z.string().min(1).optional(),
|
|
1930
|
-
body: z.string().min(1).optional()
|
|
1931
|
-
}).strict();
|
|
1932
|
-
BackendHandlerSchema = z.object({
|
|
1933
|
-
type: z.literal("http"),
|
|
1934
|
-
method: BackendMethodSchema,
|
|
1935
|
-
url: z.string().min(1),
|
|
1936
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
1937
|
-
timeoutMs: NumberOrString.optional(),
|
|
1938
|
-
inputBindings: BackendInputBindingsSchema.optional()
|
|
1939
|
-
}).strict();
|
|
1940
|
-
PricingSchema = z.discriminatedUnion("type", [
|
|
1941
|
-
z.object({ type: z.literal("free") }).strict(),
|
|
1942
|
-
z.object({
|
|
1943
|
-
type: z.literal("fixed"),
|
|
1944
|
-
amount: z.string().min(1),
|
|
1945
|
-
currency: z.string().min(1)
|
|
1946
|
-
}).strict(),
|
|
1947
|
-
z.object({
|
|
1948
|
-
type: z.literal("dynamic"),
|
|
1949
|
-
resolver: z.string().min(1)
|
|
1950
|
-
}).strict()
|
|
1951
|
-
]);
|
|
1952
|
-
JsonSchemaValueSchema = z.record(z.string(), z.unknown());
|
|
1953
|
-
ResourceEntrySchema = z.object({
|
|
1954
|
-
name: z.string().min(1),
|
|
1955
|
-
description: z.string().optional(),
|
|
1956
|
-
input: JsonSchemaValueSchema.optional(),
|
|
1957
|
-
output: JsonSchemaValueSchema.optional(),
|
|
1958
|
-
backend: BackendHandlerSchema,
|
|
1959
|
-
pricing: PricingSchema,
|
|
1960
|
-
expose: z.array(z.string().min(1)).min(1),
|
|
1961
|
-
payments: z.array(z.string().min(1)).optional(),
|
|
1962
|
-
paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
|
|
1963
|
-
authorization: z.object({ required: z.array(z.string().min(1)).min(1) }).strict().optional()
|
|
1964
|
-
}).strict();
|
|
1965
|
-
ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
|
|
1966
|
-
FacilitatorAuthSchema = z.discriminatedUnion("type", [
|
|
1967
|
-
z.object({ type: z.literal("none") }).strict(),
|
|
1968
|
-
z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
|
|
1969
|
-
// Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
|
|
1970
|
-
// which signs a fresh JWT per request.
|
|
1971
|
-
z.object({
|
|
1972
|
-
type: z.literal("cdp"),
|
|
1973
|
-
apiKeyId: z.string().min(1),
|
|
1974
|
-
apiKeySecret: z.string().min(1)
|
|
1975
|
-
}).strict()
|
|
1976
|
-
]);
|
|
1977
|
-
FacilitatorSchema = z.discriminatedUnion("mode", [
|
|
1978
|
-
z.object({
|
|
1979
|
-
mode: z.literal("local"),
|
|
1980
|
-
signerPrivateKey: z.string().min(1)
|
|
1981
|
-
}).strict(),
|
|
1982
|
-
z.object({
|
|
1983
|
-
mode: z.literal("remote"),
|
|
1984
|
-
url: z.string().min(1),
|
|
1985
|
-
// Absent auth sends no credential and normalizes to `{ type: 'none' }`,
|
|
1986
|
-
// which on mainnet requires `allowUnauthenticatedFacilitator`
|
|
1987
|
-
auth: FacilitatorAuthSchema.optional()
|
|
1988
|
-
}).strict()
|
|
1989
|
-
]);
|
|
1990
|
-
X402Schema = z.object({
|
|
1991
|
-
enabled: BooleanOrString,
|
|
1992
|
-
network: z.string().min(1),
|
|
1993
|
-
rpcUrl: z.string().min(1),
|
|
1994
|
-
asset: z.string().min(1),
|
|
1995
|
-
assetName: z.string().min(1),
|
|
1996
|
-
assetVersion: z.string().min(1),
|
|
1997
|
-
assetDecimals: NumberOrString,
|
|
1998
|
-
payTo: z.string().min(1),
|
|
1999
|
-
maxTimeoutSeconds: NumberOrString,
|
|
2000
|
-
// `authorization` (the x402 default) settles after the backend call
|
|
2001
|
-
paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
|
|
2002
|
-
facilitator: FacilitatorSchema,
|
|
2003
|
-
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
2004
|
-
allowMainnet: BooleanOrString.optional(),
|
|
2005
|
-
// Accepts credential-free facilitator access on mainnet. Never defaulted
|
|
2006
|
-
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
2007
|
-
}).strict();
|
|
2008
|
-
MppSchema = z.object({
|
|
2009
|
-
enabled: BooleanOrString,
|
|
2010
|
-
network: z.string().min(1).optional(),
|
|
2011
|
-
rpcUrl: z.string().min(1),
|
|
2012
|
-
asset: z.string().min(1),
|
|
2013
|
-
assetName: z.string().min(1),
|
|
2014
|
-
assetVersion: z.string().min(1),
|
|
2015
|
-
recipient: z.string().min(1),
|
|
2016
|
-
realm: z.string().min(1),
|
|
2017
|
-
challengeSecret: z.string().min(1),
|
|
2018
|
-
challengeTtlSeconds: NumberOrString.optional(),
|
|
2019
|
-
// Same values and default as `payments.x402.paymentFlow`
|
|
2020
|
-
paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
|
|
2021
|
-
facilitator: FacilitatorSchema,
|
|
2022
|
-
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
2023
|
-
allowMainnet: BooleanOrString.optional(),
|
|
2024
|
-
// Accepts credential-free facilitator access on mainnet. Never defaulted
|
|
2025
|
-
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
2026
|
-
}).strict();
|
|
2027
|
-
PaymentsSchema = z.object({
|
|
2028
|
-
x402: X402Schema.optional(),
|
|
2029
|
-
mpp: MppSchema.optional()
|
|
2030
|
-
}).strict();
|
|
2031
|
-
Ap2KeySchema = z.object({
|
|
2032
|
-
kid: z.string().min(1),
|
|
2033
|
-
jwk: z.record(z.string(), z.unknown())
|
|
2034
|
-
}).strict();
|
|
2035
|
-
Ap2IssuerSchema = z.object({
|
|
2036
|
-
issuer: z.string().min(1),
|
|
2037
|
-
// Required, not defaulted: without it a mandate minted for another
|
|
2038
|
-
// merchant would verify here
|
|
2039
|
-
audience: z.string().min(1),
|
|
2040
|
-
keys: z.array(Ap2KeySchema).min(1)
|
|
2041
|
-
}).strict();
|
|
2042
|
-
Ap2Schema = z.object({
|
|
2043
|
-
enabled: BooleanOrString,
|
|
2044
|
-
specVersion: z.string().min(1).optional(),
|
|
2045
|
-
mode: z.string().min(1).optional(),
|
|
2046
|
-
trust: z.object({
|
|
2047
|
-
mandateIssuers: z.array(Ap2IssuerSchema).optional(),
|
|
2048
|
-
checkoutIssuers: z.array(Ap2IssuerSchema).optional()
|
|
2049
|
-
}).strict().optional(),
|
|
2050
|
-
clockSkewSeconds: NumberOrString.optional(),
|
|
2051
|
-
requireMandateAudience: BooleanOrString.optional(),
|
|
2052
|
-
requireMandateExpiry: BooleanOrString.optional(),
|
|
2053
|
-
replay: z.object({ path: z.string().min(1) }).strict().optional()
|
|
2054
|
-
}).strict();
|
|
2055
|
-
AuthorizationSchema = z.object({ ap2: Ap2Schema.optional() }).strict();
|
|
2056
|
-
RawConfigSchema = z.object({
|
|
2057
|
-
version: z.literal(SUPPORTED_CONFIG_VERSION),
|
|
2058
|
-
merchant: MerchantSchema,
|
|
2059
|
-
server: ServerSchema,
|
|
2060
|
-
storage: StorageSchema,
|
|
2061
|
-
protocols: ProtocolsSchema,
|
|
2062
|
-
resources: ResourcesMapSchema,
|
|
2063
|
-
payments: PaymentsSchema,
|
|
2064
|
-
authorization: AuthorizationSchema.optional()
|
|
2065
|
-
}).strict();
|
|
2066
|
-
SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
|
|
2067
|
-
SUPPORTED_PAYMENT_METHODS = new Set(PAYMENT_METHOD_NAMES);
|
|
2068
|
-
SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
|
|
2069
|
-
EMPTY_CLOSED_OBJECT_SCHEMA = {
|
|
2070
|
-
type: "object",
|
|
2071
|
-
properties: {},
|
|
2072
|
-
additionalProperties: false
|
|
2073
|
-
};
|
|
2074
|
-
UNSUPPORTED_SCHEMA_KEYWORDS = [
|
|
2075
|
-
"pattern",
|
|
2076
|
-
"minLength",
|
|
2077
|
-
"maxLength",
|
|
2078
|
-
"format",
|
|
2079
|
-
"minimum",
|
|
2080
|
-
"maximum",
|
|
2081
|
-
"exclusiveMinimum",
|
|
2082
|
-
"exclusiveMaximum",
|
|
2083
|
-
"multipleOf",
|
|
2084
|
-
"oneOf",
|
|
2085
|
-
"anyOf",
|
|
2086
|
-
"allOf",
|
|
2087
|
-
"not",
|
|
2088
|
-
"$ref",
|
|
2089
|
-
"const",
|
|
2090
|
-
"patternProperties",
|
|
2091
|
-
"minItems",
|
|
2092
|
-
"maxItems",
|
|
2093
|
-
"uniqueItems"
|
|
2094
|
-
];
|
|
2095
|
-
PRICING_AMOUNT_PATTERN = /^\d+(?:\.\d+)?$/;
|
|
2096
|
-
ZERO_AMOUNT_PATTERN = /^0(?:\.0+)?$/;
|
|
2097
|
-
}
|
|
2098
|
-
});
|
|
2099
|
-
async function loadConfig(options = {}) {
|
|
2100
|
-
const env = options.env ?? process.env;
|
|
2101
|
-
const cwd = options.cwd ?? process.cwd();
|
|
2102
|
-
const configPath = resolveConfigPath(options.path, env, cwd);
|
|
2103
|
-
let text2;
|
|
2104
|
-
try {
|
|
2105
|
-
text2 = await promises.readFile(configPath, "utf8");
|
|
2106
|
-
} catch (error) {
|
|
2107
|
-
throw new CommerceError("CONFIG_INVALID", `Could not read configuration file "${configPath}"`, {
|
|
2108
|
-
details: { path: configPath },
|
|
2109
|
-
cause: error
|
|
2110
|
-
});
|
|
2111
|
-
}
|
|
2112
|
-
let raw;
|
|
2113
|
-
try {
|
|
2114
|
-
raw = parse(text2);
|
|
2115
|
-
} catch (error) {
|
|
2116
|
-
const { reason, details } = describeYamlError(error);
|
|
2117
|
-
throw new CommerceError(
|
|
2118
|
-
"CONFIG_INVALID",
|
|
2119
|
-
`Configuration file "${configPath}" is not valid YAML: ${reason}`,
|
|
2120
|
-
{
|
|
2121
|
-
details: { path: configPath, ...details },
|
|
2122
|
-
// Kept for programmatic callers. Never print it: its `.message` quotes
|
|
2123
|
-
// the source line (see describeYamlError).
|
|
2124
|
-
cause: error
|
|
2125
|
-
}
|
|
2126
|
-
);
|
|
2127
|
-
}
|
|
2128
|
-
return parseConfig(raw, env);
|
|
2129
|
-
}
|
|
2130
|
-
function describeYamlError(error) {
|
|
2131
|
-
if (!(error instanceof YAMLError)) {
|
|
2132
|
-
return { reason: "unknown YAML parse error", details: {} };
|
|
2133
|
-
}
|
|
2134
|
-
const start = error.linePos?.[0];
|
|
2135
|
-
const where = start === void 0 ? "" : ` at line ${String(start.line)}, column ${String(start.col)}`;
|
|
2136
|
-
const explanation = YAML_ERROR_EXPLANATIONS[error.code];
|
|
2137
|
-
const what = explanation === void 0 ? error.code : `${error.code} (${explanation})`;
|
|
2138
|
-
return {
|
|
2139
|
-
reason: `${what}${where}`,
|
|
2140
|
-
details: {
|
|
2141
|
-
yamlErrorCode: error.code,
|
|
2142
|
-
...start === void 0 ? {} : { line: start.line, column: start.col }
|
|
2143
|
-
}
|
|
2144
|
-
};
|
|
2145
|
-
}
|
|
2146
|
-
function resolveConfigPath(explicitPath, env, cwd) {
|
|
2147
|
-
if (explicitPath) return path.resolve(cwd, explicitPath);
|
|
2148
|
-
const fromEnv = env[CONFIG_PATH_ENV_VAR];
|
|
2149
|
-
if (fromEnv) return path.resolve(cwd, fromEnv);
|
|
2150
|
-
return path.resolve(cwd, DEFAULT_CONFIG_FILENAME);
|
|
2151
|
-
}
|
|
2152
|
-
var CONFIG_PATH_ENV_VAR, YAML_ERROR_EXPLANATIONS;
|
|
2153
|
-
var init_load = __esm({
|
|
2154
|
-
"src/config/load.ts"() {
|
|
2155
|
-
init_core();
|
|
2156
|
-
init_filename();
|
|
2157
|
-
init_schema();
|
|
2158
|
-
CONFIG_PATH_ENV_VAR = "AGENT_COMMERCE_CONFIG";
|
|
2159
|
-
YAML_ERROR_EXPLANATIONS = {
|
|
2160
|
-
DUPLICATE_KEY: "map keys must be unique",
|
|
2161
|
-
BAD_INDENT: "bad indentation",
|
|
2162
|
-
TAB_AS_INDENT: "tabs cannot be used for indentation",
|
|
2163
|
-
MISSING_CHAR: "a required character is missing",
|
|
2164
|
-
BLOCK_AS_IMPLICIT_KEY: "a block value cannot be used as a key (missing quotes, or a stray colon)",
|
|
2165
|
-
MULTILINE_IMPLICIT_KEY: "a key spans multiple lines (missing quotes, or a stray colon)",
|
|
2166
|
-
UNEXPECTED_TOKEN: "unexpected token"
|
|
2167
|
-
};
|
|
2168
|
-
}
|
|
2169
|
-
});
|
|
2170
|
-
|
|
2171
|
-
// src/config/index.ts
|
|
2172
|
-
var config_exports = {};
|
|
2173
|
-
__export(config_exports, {
|
|
2174
|
-
loadConfig: () => loadConfig,
|
|
2175
|
-
parseConfig: () => parseConfig,
|
|
2176
|
-
substituteEnv: () => substituteEnv
|
|
2177
|
-
});
|
|
2178
|
-
var init_config = __esm({
|
|
2179
|
-
"src/config/index.ts"() {
|
|
2180
|
-
init_env();
|
|
2181
|
-
init_load();
|
|
2182
|
-
init_schema();
|
|
2183
|
-
}
|
|
2184
|
-
});
|
|
2185
|
-
|
|
2186
|
-
// src/cli/program.ts
|
|
2187
|
-
init_filename();
|
|
2188
|
-
init_core();
|
|
2
|
+
import { createSqliteReceiptStore, A2A_SPEC_VERSION, A2A_PROTOCOL_VERSION, A2A_PROTOCOL_BINDING, A2A_AGENT_CARD_PATH, A2A_UNSUPPORTED, ACP_UNSUPPORTED } from './chunk-SS3QFF5N.js';
|
|
3
|
+
import { DEFAULT_CONFIG_FILENAME, extractPathParameterNames, ACP_SPEC_VERSION, ACP_API_VERSION, ACP_WELL_KNOWN_PATH, findUnparsedBraceToken } from './chunk-2ZZOGI5I.js';
|
|
4
|
+
import { MPP_PROFILE, MPP_SPEC_DRAFTS, MPPX_VERSION, MPP_SPEC_COMMIT, MPP_DEFAULT_PAYMENT_FLOW } from './chunk-AA3FIEAR.js';
|
|
5
|
+
import { ANVIL_WELL_KNOWN_ACCOUNTS, describeDeploymentMode, X402_DEFAULT_PAYMENT_FLOW, findX402Network, isEvmAddress, LOCAL_CHAIN_ID, resolveDeploymentMode, sameAddress } from './chunk-7JNYFUT3.js';
|
|
6
|
+
import { AP2_UNSUPPORTED } from './chunk-3EQBCFZY.js';
|
|
7
|
+
import './chunk-3MGAJ5FX.js';
|
|
8
|
+
import { AP2_SPEC_VERSION, AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE } from './chunk-FHCFR2GS.js';
|
|
9
|
+
import { MCP_TOOL_NAME_MAX_LENGTH, MCP_TOOL_NAME_CHARS } from './chunk-TPC4EJLT.js';
|
|
10
|
+
import { PACKAGE_VERSION } from './chunk-FRMLWGSS.js';
|
|
11
|
+
import { PROTOCOL_NAMES, isCommerceError, CommerceError } from './chunk-QQ3WY5L3.js';
|
|
12
|
+
import { isRecord } from './chunk-3M6U7ILB.js';
|
|
13
|
+
import { CommanderError, Command } from 'commander';
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, accessSync, constants } from 'fs';
|
|
15
|
+
import { dirname, resolve, join, basename, extname, win32 } from 'path';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { stringify, parse, Document } from 'yaml';
|
|
18
|
+
import picocolors from 'picocolors';
|
|
19
|
+
import { writeFile, stat, mkdir, rename, rm, open } from 'fs/promises';
|
|
20
|
+
import { isDeepStrictEqual } from 'util';
|
|
21
|
+
import { validate } from '@scalar/openapi-parser';
|
|
22
|
+
import * as clack from '@clack/prompts';
|
|
2189
23
|
|
|
2190
24
|
// src/cli/commands/demo.ts
|
|
2191
25
|
function defaultDemoSteps() {
|
|
@@ -2196,659 +30,55 @@ function defaultDemoSteps() {
|
|
|
2196
30
|
args: ["compose", "up", "--build", "--detach", "--wait"]
|
|
2197
31
|
},
|
|
2198
32
|
{ name: "Run the deterministic buyer agent", command: "npm", args: ["run", "demo:agent"] }
|
|
2199
|
-
];
|
|
2200
|
-
}
|
|
2201
|
-
async function runDemo(io, deps = {}) {
|
|
2202
|
-
const steps = deps.steps ?? defaultDemoSteps();
|
|
2203
|
-
const run = deps.run ?? execCommand;
|
|
2204
|
-
io.stdout("agent-commerce demo: starting the local stack, then running the buyer agent");
|
|
2205
|
-
for (const step of steps) {
|
|
2206
|
-
io.stdout("");
|
|
2207
|
-
io.stdout(`==> ${step.name}`);
|
|
2208
|
-
io.stdout(` $ ${step.command} ${step.args.join(" ")}`);
|
|
2209
|
-
const result = await run(step.command, step.args);
|
|
2210
|
-
if (result.code !== 0) {
|
|
2211
|
-
io.stderr(`FAIL "${step.name}" exited with code ${result.code}.`);
|
|
2212
|
-
if (result.stderrTail !== void 0 && result.stderrTail.length > 0) {
|
|
2213
|
-
io.stderr(result.stderrTail);
|
|
2214
|
-
}
|
|
2215
|
-
io.stderr("");
|
|
2216
|
-
io.stderr("Nothing further was run. To retry manually:");
|
|
2217
|
-
io.stderr(` ${step.command} ${step.args.join(" ")}`);
|
|
2218
|
-
return 1;
|
|
2219
|
-
}
|
|
2220
|
-
io.stdout(`OK ${step.name}`);
|
|
2221
|
-
}
|
|
2222
|
-
io.stdout("");
|
|
2223
|
-
io.stdout("PASS Demo is up. Try: agent-commerce doctor --config config-demo.yaml");
|
|
2224
|
-
return 0;
|
|
2225
|
-
}
|
|
2226
|
-
async function execCommand(command, args) {
|
|
2227
|
-
const { spawn } = await import('child_process');
|
|
2228
|
-
return new Promise((resolvePromise) => {
|
|
2229
|
-
let stderr = "";
|
|
2230
|
-
const child = spawn(command, args, { stdio: ["ignore", "inherit", "pipe"] });
|
|
2231
|
-
child.stderr?.on("data", (chunk) => {
|
|
2232
|
-
stderr += chunk.toString("utf8");
|
|
2233
|
-
});
|
|
2234
|
-
child.on("error", (err) => {
|
|
2235
|
-
resolvePromise({ code: 1, stderrTail: err.message });
|
|
2236
|
-
});
|
|
2237
|
-
child.on("close", (code) => {
|
|
2238
|
-
const tail = stderr.trim().split("\n").slice(-10).join("\n");
|
|
2239
|
-
resolvePromise({ code: code ?? 1, ...tail.length > 0 ? { stderrTail: tail } : {} });
|
|
2240
|
-
});
|
|
2241
|
-
});
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
// src/cli/commands/doctor.ts
|
|
2245
|
-
init_constants();
|
|
2246
|
-
|
|
2247
|
-
// src/authorization/ap2/descriptor.ts
|
|
2248
|
-
init_constants();
|
|
2249
|
-
var AP2_UNSUPPORTED = [
|
|
2250
|
-
// Mandate kinds. Open mandates are refused: see AP2_CHECKOUT_MANDATE_VCT
|
|
2251
|
-
"autonomous mode",
|
|
2252
|
-
"open checkout mandates (mandate.checkout.open.1)",
|
|
2253
|
-
"payment mandates (mandate.payment.1)",
|
|
2254
|
-
"spending constraint evaluation",
|
|
2255
|
-
"cnf-bound agent keys",
|
|
2256
|
-
// Delegation models other than a trusted agent provider
|
|
2257
|
-
"user credential delegation (OpenID4VP)",
|
|
2258
|
-
"delegation chains and key-binding JWTs",
|
|
2259
|
-
// Key handling. Every key is written into config by an operator
|
|
2260
|
-
"JWKS and any key discovery by URL (jku, x5u)",
|
|
2261
|
-
"issuer metadata fetching",
|
|
2262
|
-
"key rotation without a config change",
|
|
2263
|
-
// Algorithms
|
|
2264
|
-
"signature algorithms other than ES256",
|
|
2265
|
-
"digest algorithms other than sha-256",
|
|
2266
|
-
// Roles this gateway does not play
|
|
2267
|
-
"mandate issuance",
|
|
2268
|
-
"checkout receipts",
|
|
2269
|
-
"AP2 over the ACP checkout adapter"
|
|
2270
|
-
];
|
|
2271
|
-
|
|
2272
|
-
// src/cli/commands/doctor.ts
|
|
2273
|
-
init_core();
|
|
2274
|
-
init_execution();
|
|
2275
|
-
init_constants2();
|
|
2276
|
-
init_networks();
|
|
2277
|
-
|
|
2278
|
-
// src/protocols/a2a/constants.ts
|
|
2279
|
-
var A2A_SPEC_VERSION = "1.0.1";
|
|
2280
|
-
var A2A_PROTOCOL_VERSION = "1.0";
|
|
2281
|
-
var A2A_PROTOCOL_BINDING = "JSONRPC";
|
|
2282
|
-
var A2A_AGENT_CARD_PATH = "/.well-known/agent-card.json";
|
|
2283
|
-
var A2A_PUSH_CONFIG_METHODS = [
|
|
2284
|
-
"CreateTaskPushNotificationConfig",
|
|
2285
|
-
"GetTaskPushNotificationConfig",
|
|
2286
|
-
"ListTaskPushNotificationConfigs",
|
|
2287
|
-
"DeleteTaskPushNotificationConfig"
|
|
2288
|
-
];
|
|
2289
|
-
var A2A_UNSUPPORTED_METHODS = [
|
|
2290
|
-
"SendStreamingMessage",
|
|
2291
|
-
"GetTask",
|
|
2292
|
-
"ListTasks",
|
|
2293
|
-
"CancelTask",
|
|
2294
|
-
"SubscribeToTask",
|
|
2295
|
-
...A2A_PUSH_CONFIG_METHODS,
|
|
2296
|
-
"GetExtendedAgentCard"
|
|
2297
|
-
];
|
|
2298
|
-
|
|
2299
|
-
// src/protocols/a2a/descriptor.ts
|
|
2300
|
-
var A2A_UNSUPPORTED = [
|
|
2301
|
-
// Methods, as the protocol names them; the transport refuses the same list
|
|
2302
|
-
...A2A_UNSUPPORTED_METHODS,
|
|
2303
|
-
// Transports other than the one binding served
|
|
2304
|
-
"HTTP+JSON/REST binding",
|
|
2305
|
-
"gRPC binding",
|
|
2306
|
-
// Behaviors
|
|
2307
|
-
"SSE",
|
|
2308
|
-
"long-running task persistence",
|
|
2309
|
-
"task resumption other than an x402 extension payment",
|
|
2310
|
-
"push notifications",
|
|
2311
|
-
"multi-turn conversational continuation",
|
|
2312
|
-
"authenticated extended agent cards",
|
|
2313
|
-
"A2A authentication schemes",
|
|
2314
|
-
"artifact types beyond Agent Commerce outcome data",
|
|
2315
|
-
// Pending x402 payments live in process memory
|
|
2316
|
-
"x402 extension payments across gateway restarts or instances"
|
|
2317
|
-
];
|
|
2318
|
-
|
|
2319
|
-
// src/cli/commands/doctor.ts
|
|
2320
|
-
init_constants3();
|
|
2321
|
-
|
|
2322
|
-
// src/protocols/acp/descriptor.ts
|
|
2323
|
-
init_constants3();
|
|
2324
|
-
[
|
|
2325
|
-
"rest transport",
|
|
2326
|
-
"well-known discovery",
|
|
2327
|
-
"bearer authentication",
|
|
2328
|
-
"API-Version enforcement",
|
|
2329
|
-
// Forwarded as received: the gateway verifies neither signature header
|
|
2330
|
-
"request header forwarding to the merchant",
|
|
2331
|
-
"checkout service",
|
|
2332
|
-
...ACP_CHECKOUT_OPERATIONS
|
|
2333
|
-
];
|
|
2334
|
-
var ACP_UNSUPPORTED = [
|
|
2335
|
-
// Services this seller does not implement, named as ACP names them. The
|
|
2336
|
-
// discovery document advertises `checkout` alone, and these are the reason.
|
|
2337
|
-
"carts service",
|
|
2338
|
-
"feed service",
|
|
2339
|
-
"standalone orders service",
|
|
2340
|
-
"delegate_payment",
|
|
2341
|
-
"delegate_authentication",
|
|
2342
|
-
// Bindings and delivery directions
|
|
2343
|
-
"ACP MCP transport binding",
|
|
2344
|
-
"webhooks",
|
|
2345
|
-
"outbound merchant-to-agent delivery",
|
|
2346
|
-
"ACP client role",
|
|
2347
|
-
// Request authentication beyond the bearer token
|
|
2348
|
-
"Signature header verification",
|
|
2349
|
-
"Timestamp replay-window verification",
|
|
2350
|
-
// Extensions, versioning and payment handlers
|
|
2351
|
-
"discount extension",
|
|
2352
|
-
"general ACP extension framework",
|
|
2353
|
-
"multiple ACP API versions",
|
|
2354
|
-
"seller-backed payment handler integration"
|
|
2355
|
-
];
|
|
2356
|
-
|
|
2357
|
-
// src/storage/receipts/store.ts
|
|
2358
|
-
init_core();
|
|
2359
|
-
var PACKAGE_VERSION = "2.0.0" ;
|
|
2360
|
-
|
|
2361
|
-
// src/storage/sqlite.ts
|
|
2362
|
-
init_core();
|
|
2363
|
-
function openSqliteDatabase(options) {
|
|
2364
|
-
const { path: path2, label, logger } = options;
|
|
2365
|
-
const isFileBacked = path2 !== ":memory:";
|
|
2366
|
-
if (isFileBacked) {
|
|
2367
|
-
mkdirSync(dirname(path2), { recursive: true, mode: 448 });
|
|
2368
|
-
}
|
|
2369
|
-
if (isFileBacked && existsSync(path2)) {
|
|
2370
|
-
try {
|
|
2371
|
-
accessSync(path2, constants.W_OK);
|
|
2372
|
-
} catch {
|
|
2373
|
-
throw new CommerceError(
|
|
2374
|
-
"STORAGE_ERROR",
|
|
2375
|
-
`${label} "${path2}" exists but is not writable by this process (uid ${typeof process.getuid === "function" ? process.getuid() : "unknown"}). If this is a container that recently changed user, an existing volume may still be owned by the previous one - recreate it (docker compose down -v) or fix its ownership.`,
|
|
2376
|
-
{ details: { path: path2 } }
|
|
2377
|
-
);
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
if (isFileBacked) {
|
|
2381
|
-
try {
|
|
2382
|
-
closeSync(openSync(path2, "a", 384));
|
|
2383
|
-
} catch {
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
const db = new Database(path2);
|
|
2387
|
-
db.pragma("journal_mode = WAL");
|
|
2388
|
-
db.pragma("foreign_keys = ON");
|
|
2389
|
-
if (isFileBacked) restrictDatabasePermissions(path2, label, logger);
|
|
2390
|
-
return db;
|
|
2391
|
-
}
|
|
2392
|
-
function restrictDatabasePermissions(path2, label, logger) {
|
|
2393
|
-
for (const file of [path2, `${path2}-wal`, `${path2}-shm`]) {
|
|
2394
|
-
try {
|
|
2395
|
-
chmodSync(file, 384);
|
|
2396
|
-
} catch (error) {
|
|
2397
|
-
const code = error.code;
|
|
2398
|
-
if (code === "ENOENT") continue;
|
|
2399
|
-
logger.warn(
|
|
2400
|
-
{ file, label, error: code ?? "unknown" },
|
|
2401
|
-
"could not restrict database permissions to owner-only"
|
|
2402
|
-
);
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
// src/storage/receipts/redact.ts
|
|
2408
|
-
var SECRET_KEY_PATTERN = /private[-_ ]?key|secret|password|mnemonic|seed[-_ ]?phrase|api[-_ ]?key|authoriz(e|ation)|^auth$|auth[-_]?header|payment[-_ ]?proof|^proof$|x-payment|signature|signed[-_ ]?message|token|bearer|credential|cookie|session|jwt/i;
|
|
2409
|
-
var REDACTED = "[REDACTED]";
|
|
2410
|
-
function isSecretKey(key) {
|
|
2411
|
-
return SECRET_KEY_PATTERN.test(key);
|
|
2412
|
-
}
|
|
2413
|
-
function redact(value) {
|
|
2414
|
-
return redactValue(value);
|
|
33
|
+
];
|
|
2415
34
|
}
|
|
2416
|
-
function
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
35
|
+
async function runDemo(io, deps = {}) {
|
|
36
|
+
const steps = deps.steps ?? defaultDemoSteps();
|
|
37
|
+
const run = deps.run ?? execCommand;
|
|
38
|
+
io.stdout("agent-commerce demo: starting the local stack, then running the buyer agent");
|
|
39
|
+
for (const step of steps) {
|
|
40
|
+
io.stdout("");
|
|
41
|
+
io.stdout(`==> ${step.name}`);
|
|
42
|
+
io.stdout(` $ ${step.command} ${step.args.join(" ")}`);
|
|
43
|
+
const result = await run(step.command, step.args);
|
|
44
|
+
if (result.code !== 0) {
|
|
45
|
+
io.stderr(`FAIL "${step.name}" exited with code ${result.code}.`);
|
|
46
|
+
if (result.stderrTail !== void 0 && result.stderrTail.length > 0) {
|
|
47
|
+
io.stderr(result.stderrTail);
|
|
48
|
+
}
|
|
49
|
+
io.stderr("");
|
|
50
|
+
io.stderr("Nothing further was run. To retry manually:");
|
|
51
|
+
io.stderr(` ${step.command} ${step.args.join(" ")}`);
|
|
52
|
+
return 1;
|
|
2424
53
|
}
|
|
2425
|
-
|
|
54
|
+
io.stdout(`OK ${step.name}`);
|
|
2426
55
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
// src/storage/receipts/rows.ts
|
|
2431
|
-
function receiptToRow(receipt) {
|
|
2432
|
-
return {
|
|
2433
|
-
id: receipt.id,
|
|
2434
|
-
request_id: receipt.requestId,
|
|
2435
|
-
resource_id: receipt.resourceId,
|
|
2436
|
-
payment_json: receipt.payment !== void 0 ? JSON.stringify(redact(receipt.payment)) : null,
|
|
2437
|
-
delivered_at: receipt.deliveredAt,
|
|
2438
|
-
backend_status: receipt.backendStatus,
|
|
2439
|
-
duration_ms: receipt.durationMs ?? null,
|
|
2440
|
-
protocol: receipt.protocol ?? null,
|
|
2441
|
-
metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
|
|
2442
|
-
authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
|
|
2443
|
-
};
|
|
2444
|
-
}
|
|
2445
|
-
function rowToReceipt(row) {
|
|
2446
|
-
const payment = row.payment_json !== null ? JSON.parse(row.payment_json) : void 0;
|
|
2447
|
-
const metadata = row.metadata_json !== null ? JSON.parse(row.metadata_json) : void 0;
|
|
2448
|
-
const authorization = row.authorization_json !== null ? JSON.parse(row.authorization_json) : void 0;
|
|
2449
|
-
return {
|
|
2450
|
-
id: row.id,
|
|
2451
|
-
requestId: row.request_id,
|
|
2452
|
-
resourceId: row.resource_id,
|
|
2453
|
-
deliveredAt: row.delivered_at,
|
|
2454
|
-
backendStatus: row.backend_status,
|
|
2455
|
-
...payment !== void 0 ? { payment } : {},
|
|
2456
|
-
...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
|
|
2457
|
-
...row.protocol !== null ? { protocol: row.protocol } : {},
|
|
2458
|
-
...metadata !== void 0 ? { metadata } : {},
|
|
2459
|
-
...authorization !== void 0 ? { authorization } : {}
|
|
2460
|
-
};
|
|
2461
|
-
}
|
|
2462
|
-
function eventToRow(event) {
|
|
2463
|
-
return {
|
|
2464
|
-
id: event.id,
|
|
2465
|
-
type: event.type,
|
|
2466
|
-
request_id: event.requestId,
|
|
2467
|
-
resource_id: event.resourceId ?? null,
|
|
2468
|
-
at: event.at,
|
|
2469
|
-
adapter: event.adapter ?? null,
|
|
2470
|
-
payment_provider: event.paymentProvider ?? null,
|
|
2471
|
-
duration_ms: event.durationMs ?? null,
|
|
2472
|
-
status: event.status ?? null,
|
|
2473
|
-
data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null
|
|
2474
|
-
};
|
|
2475
|
-
}
|
|
2476
|
-
function rowToEvent(row) {
|
|
2477
|
-
const data = row.data_json !== null ? JSON.parse(row.data_json) : void 0;
|
|
2478
|
-
return {
|
|
2479
|
-
id: row.id,
|
|
2480
|
-
type: row.type,
|
|
2481
|
-
requestId: row.request_id,
|
|
2482
|
-
at: row.at,
|
|
2483
|
-
...row.resource_id !== null ? { resourceId: row.resource_id } : {},
|
|
2484
|
-
...row.adapter !== null ? { adapter: row.adapter } : {},
|
|
2485
|
-
...row.payment_provider !== null ? { paymentProvider: row.payment_provider } : {},
|
|
2486
|
-
...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
|
|
2487
|
-
...row.status !== null ? { status: row.status } : {},
|
|
2488
|
-
...data !== void 0 ? { data } : {}
|
|
2489
|
-
};
|
|
2490
|
-
}
|
|
2491
|
-
function rowToPaymentAttempt(row) {
|
|
2492
|
-
return {
|
|
2493
|
-
id: row.id,
|
|
2494
|
-
requestId: row.request_id,
|
|
2495
|
-
resourceId: row.resource_id,
|
|
2496
|
-
provider: row.provider,
|
|
2497
|
-
replayKey: row.replay_key,
|
|
2498
|
-
status: row.status,
|
|
2499
|
-
amount: row.amount,
|
|
2500
|
-
currency: row.currency,
|
|
2501
|
-
...row.payer !== null ? { payer: row.payer } : {},
|
|
2502
|
-
...row.payee !== null ? { payee: row.payee } : {},
|
|
2503
|
-
...row.external_reference !== null ? { externalReference: row.external_reference } : {},
|
|
2504
|
-
...row.rejection_reason !== null ? { rejectionReason: row.rejection_reason } : {},
|
|
2505
|
-
createdAt: row.created_at,
|
|
2506
|
-
updatedAt: row.updated_at
|
|
2507
|
-
};
|
|
56
|
+
io.stdout("");
|
|
57
|
+
io.stdout("PASS Demo is up. Try: agent-commerce doctor --config config-demo.yaml");
|
|
58
|
+
return 0;
|
|
2508
59
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
CREATE TABLE IF NOT EXISTS receipts (
|
|
2517
|
-
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2518
|
-
id TEXT NOT NULL UNIQUE,
|
|
2519
|
-
request_id TEXT NOT NULL,
|
|
2520
|
-
resource_id TEXT NOT NULL,
|
|
2521
|
-
payment_json TEXT,
|
|
2522
|
-
delivered_at TEXT NOT NULL,
|
|
2523
|
-
backend_status INTEGER NOT NULL,
|
|
2524
|
-
duration_ms INTEGER,
|
|
2525
|
-
protocol TEXT,
|
|
2526
|
-
metadata_json TEXT
|
|
2527
|
-
);
|
|
2528
|
-
CREATE INDEX IF NOT EXISTS idx_receipts_request_id ON receipts(request_id);
|
|
2529
|
-
CREATE INDEX IF NOT EXISTS idx_receipts_delivered_at ON receipts(delivered_at);
|
|
2530
|
-
|
|
2531
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
2532
|
-
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2533
|
-
id TEXT NOT NULL UNIQUE,
|
|
2534
|
-
type TEXT NOT NULL,
|
|
2535
|
-
request_id TEXT NOT NULL,
|
|
2536
|
-
resource_id TEXT,
|
|
2537
|
-
at TEXT NOT NULL,
|
|
2538
|
-
adapter TEXT,
|
|
2539
|
-
payment_provider TEXT,
|
|
2540
|
-
duration_ms INTEGER,
|
|
2541
|
-
status TEXT,
|
|
2542
|
-
data_json TEXT
|
|
2543
|
-
);
|
|
2544
|
-
CREATE INDEX IF NOT EXISTS idx_events_request_id ON events(request_id);
|
|
2545
|
-
CREATE INDEX IF NOT EXISTS idx_events_at ON events(at);
|
|
2546
|
-
|
|
2547
|
-
CREATE TABLE IF NOT EXISTS payment_attempts (
|
|
2548
|
-
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2549
|
-
id TEXT NOT NULL UNIQUE,
|
|
2550
|
-
request_id TEXT NOT NULL,
|
|
2551
|
-
resource_id TEXT NOT NULL,
|
|
2552
|
-
provider TEXT NOT NULL,
|
|
2553
|
-
replay_key TEXT NOT NULL UNIQUE,
|
|
2554
|
-
status TEXT NOT NULL,
|
|
2555
|
-
amount TEXT NOT NULL,
|
|
2556
|
-
currency TEXT NOT NULL,
|
|
2557
|
-
payer TEXT,
|
|
2558
|
-
payee TEXT,
|
|
2559
|
-
external_reference TEXT,
|
|
2560
|
-
rejection_reason TEXT,
|
|
2561
|
-
created_at TEXT NOT NULL,
|
|
2562
|
-
updated_at TEXT NOT NULL
|
|
2563
|
-
);
|
|
2564
|
-
CREATE INDEX IF NOT EXISTS idx_payment_attempts_request_id ON payment_attempts(request_id);
|
|
2565
|
-
CREATE INDEX IF NOT EXISTS idx_payment_attempts_created_at ON payment_attempts(created_at);
|
|
2566
|
-
`);
|
|
2567
|
-
}
|
|
2568
|
-
},
|
|
2569
|
-
{
|
|
2570
|
-
version: 2,
|
|
2571
|
-
up(db) {
|
|
2572
|
-
db.exec(`ALTER TABLE receipts ADD COLUMN authorization_json TEXT;`);
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
];
|
|
2576
|
-
var SCHEMA_VERSION = MIGRATIONS.at(-1)?.version ?? 0;
|
|
2577
|
-
function migrate(db) {
|
|
2578
|
-
const current = db.pragma("user_version", { simple: true });
|
|
2579
|
-
for (const migration of MIGRATIONS) {
|
|
2580
|
-
if (migration.version <= current) continue;
|
|
2581
|
-
const apply = db.transaction(() => {
|
|
2582
|
-
migration.up(db);
|
|
2583
|
-
db.pragma(`user_version = ${migration.version}`);
|
|
60
|
+
async function execCommand(command, args) {
|
|
61
|
+
const { spawn } = await import('child_process');
|
|
62
|
+
return new Promise((resolvePromise) => {
|
|
63
|
+
let stderr = "";
|
|
64
|
+
const child = spawn(command, args, { stdio: ["ignore", "inherit", "pipe"] });
|
|
65
|
+
child.stderr?.on("data", (chunk) => {
|
|
66
|
+
stderr += chunk.toString("utf8");
|
|
2584
67
|
});
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
const base = limit !== void 0 && Number.isFinite(limit) ? limit : DEFAULT_LIST_LIMIT;
|
|
2594
|
-
return Math.min(Math.max(Math.trunc(base), 1), MAX_LIST_LIMIT);
|
|
2595
|
-
}
|
|
2596
|
-
var defaultIds = {
|
|
2597
|
-
next: (prefix) => prefix !== void 0 ? `${prefix}_${randomUUID()}` : randomUUID()
|
|
2598
|
-
};
|
|
2599
|
-
function isUniqueConstraintOn(err, column) {
|
|
2600
|
-
return err instanceof Error && "code" in err && err.code === "SQLITE_CONSTRAINT_UNIQUE" && err.message.includes(column);
|
|
2601
|
-
}
|
|
2602
|
-
function createSqliteReceiptStore(options) {
|
|
2603
|
-
const logger = options.logger ?? NOOP_LOGGER;
|
|
2604
|
-
const clock = options.clock ?? systemClock;
|
|
2605
|
-
const ids = options.ids ?? defaultIds;
|
|
2606
|
-
const path2 = options.path;
|
|
2607
|
-
const isFileBacked = path2 !== ":memory:";
|
|
2608
|
-
const db = openSqliteDatabase({ path: path2, label: "Receipt database", logger });
|
|
2609
|
-
migrate(db);
|
|
2610
|
-
const insertReceiptStmt = db.prepare(
|
|
2611
|
-
`INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json, authorization_json)
|
|
2612
|
-
VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json, @authorization_json)`
|
|
2613
|
-
);
|
|
2614
|
-
const getReceiptStmt = db.prepare("SELECT * FROM receipts WHERE id = ?");
|
|
2615
|
-
const listReceiptsStmt = db.prepare(
|
|
2616
|
-
"SELECT * FROM receipts ORDER BY delivered_at DESC, seq DESC LIMIT ?"
|
|
2617
|
-
);
|
|
2618
|
-
const listReceiptsByRequestStmt = db.prepare(
|
|
2619
|
-
"SELECT * FROM receipts WHERE request_id = ? ORDER BY delivered_at DESC, seq DESC LIMIT ?"
|
|
2620
|
-
);
|
|
2621
|
-
const countReceiptsStmt = db.prepare(
|
|
2622
|
-
"SELECT COUNT(*) AS count FROM receipts"
|
|
2623
|
-
);
|
|
2624
|
-
const countUndeliveredReceiptsStmt = db.prepare(
|
|
2625
|
-
"SELECT COUNT(*) AS count FROM receipts WHERE backend_status NOT BETWEEN 200 AND 299"
|
|
2626
|
-
);
|
|
2627
|
-
const insertEventStmt = db.prepare(
|
|
2628
|
-
`INSERT INTO events (id, type, request_id, resource_id, at, adapter, payment_provider, duration_ms, status, data_json)
|
|
2629
|
-
VALUES (@id, @type, @request_id, @resource_id, @at, @adapter, @payment_provider, @duration_ms, @status, @data_json)`
|
|
2630
|
-
);
|
|
2631
|
-
const listEventsStmt = db.prepare(
|
|
2632
|
-
"SELECT * FROM events ORDER BY at DESC, seq DESC LIMIT ?"
|
|
2633
|
-
);
|
|
2634
|
-
const listEventsByRequestStmt = db.prepare(
|
|
2635
|
-
"SELECT * FROM events WHERE request_id = ? ORDER BY at DESC, seq DESC LIMIT ?"
|
|
2636
|
-
);
|
|
2637
|
-
const insertPaymentAttemptStmt = db.prepare(
|
|
2638
|
-
`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)
|
|
2639
|
-
VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)
|
|
2640
|
-
RETURNING *`
|
|
2641
|
-
);
|
|
2642
|
-
const updatePaymentAttemptStmt = db.prepare(
|
|
2643
|
-
`UPDATE payment_attempts SET status = @status,
|
|
2644
|
-
external_reference = COALESCE(@external_reference, external_reference),
|
|
2645
|
-
rejection_reason = COALESCE(@rejection_reason, rejection_reason),
|
|
2646
|
-
updated_at = @updated_at
|
|
2647
|
-
WHERE replay_key = @replay_key`
|
|
2648
|
-
);
|
|
2649
|
-
const listPaymentAttemptsStmt = db.prepare(
|
|
2650
|
-
"SELECT * FROM payment_attempts ORDER BY created_at DESC, seq DESC LIMIT ?"
|
|
2651
|
-
);
|
|
2652
|
-
const listPaymentAttemptsByRequestStmt = db.prepare(
|
|
2653
|
-
"SELECT * FROM payment_attempts WHERE request_id = ? ORDER BY created_at DESC, seq DESC LIMIT ?"
|
|
2654
|
-
);
|
|
2655
|
-
const paymentAttemptStatusStmt = db.prepare(
|
|
2656
|
-
"SELECT status FROM payment_attempts WHERE replay_key = ?"
|
|
2657
|
-
);
|
|
2658
|
-
let closed = false;
|
|
2659
|
-
const descriptor = {
|
|
2660
|
-
name: "sqlite-receipt-store",
|
|
2661
|
-
kind: "storage",
|
|
2662
|
-
implementationVersion: PACKAGE_VERSION,
|
|
2663
|
-
supportedSpec: `sqlite-schema-v${SCHEMA_VERSION}`,
|
|
2664
|
-
capabilities: ["receipts", "events", "payment-attempts"],
|
|
2665
|
-
status: "stable"
|
|
2666
|
-
};
|
|
2667
|
-
const store = {
|
|
2668
|
-
async init() {
|
|
2669
|
-
const version = db.pragma("user_version", { simple: true });
|
|
2670
|
-
if (version !== SCHEMA_VERSION) {
|
|
2671
|
-
throw new CommerceError(
|
|
2672
|
-
"STORAGE_ERROR",
|
|
2673
|
-
`Receipt store schema version mismatch (found ${version}, expected ${SCHEMA_VERSION})`
|
|
2674
|
-
);
|
|
2675
|
-
}
|
|
2676
|
-
},
|
|
2677
|
-
async appendEvent(event) {
|
|
2678
|
-
try {
|
|
2679
|
-
insertEventStmt.run(eventToRow(event));
|
|
2680
|
-
} catch (err) {
|
|
2681
|
-
logger.warn(
|
|
2682
|
-
{
|
|
2683
|
-
err: err instanceof Error ? err.message : String(err),
|
|
2684
|
-
eventId: event.id,
|
|
2685
|
-
requestId: event.requestId
|
|
2686
|
-
},
|
|
2687
|
-
"receipt-store: failed to persist commerce event"
|
|
2688
|
-
);
|
|
2689
|
-
}
|
|
2690
|
-
},
|
|
2691
|
-
async reservePaymentAttempt(reservation) {
|
|
2692
|
-
const now = clock.nowIso();
|
|
2693
|
-
const id = ids.next("attempt");
|
|
2694
|
-
let row;
|
|
2695
|
-
try {
|
|
2696
|
-
row = insertPaymentAttemptStmt.get({
|
|
2697
|
-
id,
|
|
2698
|
-
request_id: reservation.requestId,
|
|
2699
|
-
resource_id: reservation.resourceId,
|
|
2700
|
-
provider: reservation.provider,
|
|
2701
|
-
replay_key: reservation.replayKey,
|
|
2702
|
-
status: "reserved",
|
|
2703
|
-
amount: reservation.amount,
|
|
2704
|
-
currency: reservation.currency,
|
|
2705
|
-
payer: reservation.payer ?? null,
|
|
2706
|
-
payee: reservation.payee ?? null,
|
|
2707
|
-
external_reference: null,
|
|
2708
|
-
rejection_reason: null,
|
|
2709
|
-
created_at: now,
|
|
2710
|
-
updated_at: now
|
|
2711
|
-
});
|
|
2712
|
-
} catch (err) {
|
|
2713
|
-
if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
|
|
2714
|
-
const existing = paymentAttemptStatusStmt.get(reservation.replayKey);
|
|
2715
|
-
throw new CommerceError(
|
|
2716
|
-
"PAYMENT_REPLAYED",
|
|
2717
|
-
`Payment authorization has already been used (replayKey=${reservation.replayKey})`,
|
|
2718
|
-
{
|
|
2719
|
-
requestId: reservation.requestId,
|
|
2720
|
-
resourceId: reservation.resourceId,
|
|
2721
|
-
...existing !== void 0 ? { details: { attemptStatus: existing.status } } : {}
|
|
2722
|
-
}
|
|
2723
|
-
);
|
|
2724
|
-
}
|
|
2725
|
-
throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");
|
|
2726
|
-
}
|
|
2727
|
-
return rowToPaymentAttempt(row);
|
|
2728
|
-
},
|
|
2729
|
-
async updatePaymentAttempt(update) {
|
|
2730
|
-
try {
|
|
2731
|
-
const result = updatePaymentAttemptStmt.run({
|
|
2732
|
-
replay_key: update.replayKey,
|
|
2733
|
-
status: update.status,
|
|
2734
|
-
external_reference: update.externalReference ?? null,
|
|
2735
|
-
rejection_reason: update.rejectionReason ?? null,
|
|
2736
|
-
updated_at: clock.nowIso()
|
|
2737
|
-
});
|
|
2738
|
-
if (result.changes === 0) {
|
|
2739
|
-
throw new CommerceError(
|
|
2740
|
-
"STORAGE_ERROR",
|
|
2741
|
-
`No payment attempt found for replayKey=${update.replayKey}`
|
|
2742
|
-
);
|
|
2743
|
-
}
|
|
2744
|
-
} catch (err) {
|
|
2745
|
-
throw toCommerceError(err, "STORAGE_ERROR", "Failed to update payment attempt");
|
|
2746
|
-
}
|
|
2747
|
-
},
|
|
2748
|
-
async saveReceipt(receipt) {
|
|
2749
|
-
try {
|
|
2750
|
-
insertReceiptStmt.run(receiptToRow(receipt));
|
|
2751
|
-
} catch (err) {
|
|
2752
|
-
throw toCommerceError(err, "STORAGE_ERROR", "Failed to save receipt");
|
|
2753
|
-
}
|
|
2754
|
-
},
|
|
2755
|
-
async getReceipt(id) {
|
|
2756
|
-
const row = getReceiptStmt.get(id);
|
|
2757
|
-
return row !== void 0 ? rowToReceipt(row) : void 0;
|
|
2758
|
-
},
|
|
2759
|
-
async listReceipts(listOptions = {}) {
|
|
2760
|
-
const limit = clampListLimit(listOptions.limit);
|
|
2761
|
-
const rows = listOptions.requestId !== void 0 ? listReceiptsByRequestStmt.all(listOptions.requestId, limit) : listReceiptsStmt.all(limit);
|
|
2762
|
-
return rows.map(rowToReceipt);
|
|
2763
|
-
},
|
|
2764
|
-
// Exact total, not bounded by the listReceipts clamp (see ReceiptStore.countReceipts)
|
|
2765
|
-
async countReceipts() {
|
|
2766
|
-
return countReceiptsStmt.get()?.count ?? 0;
|
|
2767
|
-
},
|
|
2768
|
-
// Exact count of non-2xx receipts (see ReceiptStore.countUndeliveredReceipts)
|
|
2769
|
-
async countUndeliveredReceipts() {
|
|
2770
|
-
return countUndeliveredReceiptsStmt.get()?.count ?? 0;
|
|
2771
|
-
},
|
|
2772
|
-
async listEvents(listOptions = {}) {
|
|
2773
|
-
const limit = clampListLimit(listOptions.limit);
|
|
2774
|
-
const rows = listOptions.requestId !== void 0 ? listEventsByRequestStmt.all(listOptions.requestId, limit) : listEventsStmt.all(limit);
|
|
2775
|
-
return rows.map(rowToEvent);
|
|
2776
|
-
},
|
|
2777
|
-
async listPaymentAttempts(listOptions = {}) {
|
|
2778
|
-
const limit = clampListLimit(listOptions.limit);
|
|
2779
|
-
const rows = listOptions.requestId !== void 0 ? listPaymentAttemptsByRequestStmt.all(listOptions.requestId, limit) : listPaymentAttemptsStmt.all(limit);
|
|
2780
|
-
return rows.map(rowToPaymentAttempt);
|
|
2781
|
-
},
|
|
2782
|
-
descriptor,
|
|
2783
|
-
async health() {
|
|
2784
|
-
const startedAt = clock.monotonicMs();
|
|
2785
|
-
if (closed) {
|
|
2786
|
-
return { status: "fail", detail: "store-unavailable", checkedAt: clock.nowIso() };
|
|
2787
|
-
}
|
|
2788
|
-
if (isFileBacked) {
|
|
2789
|
-
try {
|
|
2790
|
-
accessSync(path2, constants.W_OK);
|
|
2791
|
-
} catch (err) {
|
|
2792
|
-
logger.warn(
|
|
2793
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
2794
|
-
"receipt-store: health check failed (store not writable)"
|
|
2795
|
-
);
|
|
2796
|
-
return {
|
|
2797
|
-
status: "fail",
|
|
2798
|
-
detail: "store-unwritable",
|
|
2799
|
-
checkedAt: clock.nowIso(),
|
|
2800
|
-
durationMs: clock.monotonicMs() - startedAt
|
|
2801
|
-
};
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
try {
|
|
2805
|
-
const version = db.pragma("user_version", { simple: true });
|
|
2806
|
-
if (version !== SCHEMA_VERSION) {
|
|
2807
|
-
logger.warn(
|
|
2808
|
-
{ found: version, expected: SCHEMA_VERSION },
|
|
2809
|
-
"receipt-store: health check failed (schema version mismatch)"
|
|
2810
|
-
);
|
|
2811
|
-
return {
|
|
2812
|
-
status: "fail",
|
|
2813
|
-
detail: "store-schema-mismatch",
|
|
2814
|
-
checkedAt: clock.nowIso(),
|
|
2815
|
-
durationMs: clock.monotonicMs() - startedAt
|
|
2816
|
-
};
|
|
2817
|
-
}
|
|
2818
|
-
return {
|
|
2819
|
-
status: "pass",
|
|
2820
|
-
detail: `sqlite schema v${SCHEMA_VERSION} writable`,
|
|
2821
|
-
checkedAt: clock.nowIso(),
|
|
2822
|
-
durationMs: clock.monotonicMs() - startedAt
|
|
2823
|
-
};
|
|
2824
|
-
} catch (err) {
|
|
2825
|
-
logger.warn(
|
|
2826
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
2827
|
-
"receipt-store: health check failed (store unavailable)"
|
|
2828
|
-
);
|
|
2829
|
-
return {
|
|
2830
|
-
status: "fail",
|
|
2831
|
-
detail: "store-unavailable",
|
|
2832
|
-
checkedAt: clock.nowIso(),
|
|
2833
|
-
durationMs: clock.monotonicMs() - startedAt
|
|
2834
|
-
};
|
|
2835
|
-
}
|
|
2836
|
-
},
|
|
2837
|
-
async close() {
|
|
2838
|
-
if (!closed) {
|
|
2839
|
-
closed = true;
|
|
2840
|
-
db.close();
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
2843
|
-
};
|
|
2844
|
-
return store;
|
|
68
|
+
child.on("error", (err) => {
|
|
69
|
+
resolvePromise({ code: 1, stderrTail: err.message });
|
|
70
|
+
});
|
|
71
|
+
child.on("close", (code) => {
|
|
72
|
+
const tail = stderr.trim().split("\n").slice(-10).join("\n");
|
|
73
|
+
resolvePromise({ code: code ?? 1, ...tail.length > 0 ? { stderrTail: tail } : {} });
|
|
74
|
+
});
|
|
75
|
+
});
|
|
2845
76
|
}
|
|
2846
77
|
|
|
2847
78
|
// src/cli/lib/config-client.ts
|
|
2848
|
-
init_core();
|
|
2849
79
|
async function importConfig() {
|
|
2850
80
|
try {
|
|
2851
|
-
return await
|
|
81
|
+
return await import('./config-4IBGVFSP.js');
|
|
2852
82
|
} catch (err) {
|
|
2853
83
|
throw new CommerceError(
|
|
2854
84
|
"INTERNAL_ERROR",
|
|
@@ -2871,141 +101,17 @@ async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
|
|
|
2871
101
|
try {
|
|
2872
102
|
body = await res.json();
|
|
2873
103
|
} catch {
|
|
2874
|
-
body = void 0;
|
|
2875
|
-
}
|
|
2876
|
-
return { ok: res.ok, status: res.status, ...body !== void 0 ? { body } : {} };
|
|
2877
|
-
} catch (err) {
|
|
2878
|
-
return {
|
|
2879
|
-
ok: false,
|
|
2880
|
-
status: 0,
|
|
2881
|
-
error: err instanceof Error ? err.message : "unknown fetch error"
|
|
2882
|
-
};
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
|
|
2886
|
-
// src/cli/lib/init-config.ts
|
|
2887
|
-
init_accounts();
|
|
2888
|
-
var LOCAL_DEV_MERCHANT_ADDRESS = ANVIL_WELL_KNOWN_ACCOUNTS[1].address;
|
|
2889
|
-
var LOCAL_DEV_FACILITATOR_PRIVATE_KEY = ANVIL_WELL_KNOWN_ACCOUNTS[0].privateKey;
|
|
2890
|
-
var PLACEHOLDER_ASSET_ADDRESS = "0x000000000000000000000000000000000000dEaD";
|
|
2891
|
-
function defaultInitAnswers() {
|
|
2892
|
-
return {
|
|
2893
|
-
backendBaseUrl: "http://localhost:3000",
|
|
2894
|
-
resources: ["weather", "report"],
|
|
2895
|
-
protocols: ["http", "mcp"],
|
|
2896
|
-
x402Enabled: true,
|
|
2897
|
-
merchantPayTo: LOCAL_DEV_MERCHANT_ADDRESS
|
|
2898
|
-
};
|
|
2899
|
-
}
|
|
2900
|
-
function buildResourcesMap(answers) {
|
|
2901
|
-
const resources = {};
|
|
2902
|
-
const expose = [...answers.protocols];
|
|
2903
|
-
if (answers.resources.includes("weather")) {
|
|
2904
|
-
resources.weather = {
|
|
2905
|
-
name: "Get weather",
|
|
2906
|
-
description: "Deterministic demo weather lookup (free resource).",
|
|
2907
|
-
input: {
|
|
2908
|
-
type: "object",
|
|
2909
|
-
properties: { city: { type: "string" } },
|
|
2910
|
-
required: ["city"],
|
|
2911
|
-
additionalProperties: false
|
|
2912
|
-
},
|
|
2913
|
-
backend: {
|
|
2914
|
-
type: "http",
|
|
2915
|
-
method: "GET",
|
|
2916
|
-
url: `${answers.backendBaseUrl}/api/weather/{city}`
|
|
2917
|
-
},
|
|
2918
|
-
pricing: { type: "free" },
|
|
2919
|
-
expose
|
|
2920
|
-
};
|
|
2921
|
-
}
|
|
2922
|
-
if (answers.resources.includes("report")) {
|
|
2923
|
-
const paid = answers.x402Enabled;
|
|
2924
|
-
resources.report = {
|
|
2925
|
-
name: "Get market report",
|
|
2926
|
-
description: "Deterministic demo premium market report.",
|
|
2927
|
-
// No caller-supplied fields. Config loads a missing `input` as this same
|
|
2928
|
-
// closed schema; writing it out shows that in the generated file
|
|
2929
|
-
input: {
|
|
2930
|
-
type: "object",
|
|
2931
|
-
properties: {},
|
|
2932
|
-
additionalProperties: false
|
|
2933
|
-
},
|
|
2934
|
-
backend: {
|
|
2935
|
-
type: "http",
|
|
2936
|
-
method: "GET",
|
|
2937
|
-
url: `${answers.backendBaseUrl}/api/report`
|
|
2938
|
-
},
|
|
2939
|
-
pricing: paid ? { type: "fixed", amount: "0.01", currency: "USDC" } : { type: "free" },
|
|
2940
|
-
expose,
|
|
2941
|
-
...paid ? { payments: ["x402"] } : {}
|
|
2942
|
-
};
|
|
2943
|
-
}
|
|
2944
|
-
return resources;
|
|
2945
|
-
}
|
|
2946
|
-
function buildInitConfigObject(answers) {
|
|
2947
|
-
return {
|
|
2948
|
-
version: 1,
|
|
2949
|
-
merchant: {
|
|
2950
|
-
id: "demo-merchant",
|
|
2951
|
-
name: "Demo Merchant",
|
|
2952
|
-
publicBaseUrl: "http://localhost:8080"
|
|
2953
|
-
},
|
|
2954
|
-
server: { port: 8080, host: "127.0.0.1" },
|
|
2955
|
-
storage: { receipts: { driver: "sqlite", path: "./data/receipts.db" } },
|
|
2956
|
-
protocols: {
|
|
2957
|
-
http: { enabled: answers.protocols.includes("http") },
|
|
2958
|
-
mcp: { enabled: answers.protocols.includes("mcp"), mountPath: "/mcp" }
|
|
2959
|
-
},
|
|
2960
|
-
resources: buildResourcesMap(answers),
|
|
2961
|
-
payments: answers.x402Enabled ? {
|
|
2962
|
-
x402: {
|
|
2963
|
-
enabled: true,
|
|
2964
|
-
network: "eip155:84532",
|
|
2965
|
-
rpcUrl: "http://127.0.0.1:8545",
|
|
2966
|
-
asset: PLACEHOLDER_ASSET_ADDRESS,
|
|
2967
|
-
assetName: "MockUSDC",
|
|
2968
|
-
assetVersion: "2",
|
|
2969
|
-
assetDecimals: 6,
|
|
2970
|
-
payTo: answers.merchantPayTo,
|
|
2971
|
-
maxTimeoutSeconds: 60,
|
|
2972
|
-
facilitator: { mode: "local", signerPrivateKey: LOCAL_DEV_FACILITATOR_PRIVATE_KEY }
|
|
2973
|
-
}
|
|
2974
|
-
} : {}
|
|
2975
|
-
};
|
|
2976
|
-
}
|
|
2977
|
-
var HEADER = `# Generated by \`agent-commerce init\`.
|
|
2978
|
-
#
|
|
2979
|
-
# With x402 enabled, the facilitator signerPrivateKey is Anvil's public dev
|
|
2980
|
-
# account #0 and the default payTo is account #1.
|
|
2981
|
-
# LOCAL DEVELOPMENT ONLY - DO NOT FUND: never use them outside a local chain.
|
|
2982
|
-
# payments.x402.asset is a placeholder, not a real contract. Before running
|
|
2983
|
-
# against the local chain, run \`npm run chain:deploy\` and set it to the
|
|
2984
|
-
# MockUSDC address in \`.deploy/local.json\`. Before pointing this at a funded
|
|
2985
|
-
# network, set \`payments.x402.payTo\` to your own settlement address.
|
|
2986
|
-
#
|
|
2987
|
-
# server.host is loopback (127.0.0.1): binding every interface should be a
|
|
2988
|
-
# deliberate edit.
|
|
2989
|
-
#
|
|
2990
|
-
# server.adminToken and server.allowedOrigins are left unset, so no secret
|
|
2991
|
-
# lands in a file you might commit. Without a token the operator routes
|
|
2992
|
-
# (/api/receipts, /api/events) return 404. They serve the merchant's commerce
|
|
2993
|
-
# ledger: payer addresses, amounts and settlement hashes. To enable them, add
|
|
2994
|
-
# these keys to the server block below:
|
|
2995
|
-
# server:
|
|
2996
|
-
# adminToken: \${ADMIN_TOKEN}
|
|
2997
|
-
# allowedOrigins:
|
|
2998
|
-
# - http://localhost:5173
|
|
2999
|
-
# See SECURITY.md before exposing this gateway beyond localhost.
|
|
3000
|
-
`;
|
|
3001
|
-
function renderInitConfigYaml(answers) {
|
|
3002
|
-
const body = stringify(buildInitConfigObject(answers), { indent: 2 });
|
|
3003
|
-
return `${HEADER}
|
|
3004
|
-
${body}`;
|
|
104
|
+
body = void 0;
|
|
105
|
+
}
|
|
106
|
+
return { ok: res.ok, status: res.status, ...body !== void 0 ? { body } : {} };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
status: 0,
|
|
111
|
+
error: err instanceof Error ? err.message : "unknown fetch error"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
3005
114
|
}
|
|
3006
|
-
|
|
3007
|
-
// src/payments/x402/local-chain/manifest.ts
|
|
3008
|
-
init_core();
|
|
3009
115
|
var LOCAL_CHAIN_MANIFEST_PATH = ".deploy/local.json";
|
|
3010
116
|
var nonEmpty = z.string().min(1, "must be a non-empty string");
|
|
3011
117
|
var keyedAccount = z.object({ address: z.string(), privateKey: z.string(), note: z.string() });
|
|
@@ -3023,18 +129,18 @@ var MANIFEST_SCHEMA = z.object({
|
|
|
3023
129
|
buyerInitialBalance: nonEmpty
|
|
3024
130
|
});
|
|
3025
131
|
function readLocalChainManifest(cwd = process.cwd()) {
|
|
3026
|
-
const
|
|
3027
|
-
if (!existsSync(
|
|
132
|
+
const path = join(cwd, LOCAL_CHAIN_MANIFEST_PATH);
|
|
133
|
+
if (!existsSync(path)) {
|
|
3028
134
|
throw new CommerceError(
|
|
3029
135
|
"CONFIG_INVALID",
|
|
3030
|
-
`Local chain manifest not found at "${
|
|
136
|
+
`Local chain manifest not found at "${path}". Run "npm run chain:start" then "npm run chain:deploy" first.`
|
|
3031
137
|
);
|
|
3032
138
|
}
|
|
3033
139
|
let raw;
|
|
3034
140
|
try {
|
|
3035
|
-
raw = readFileSync(
|
|
141
|
+
raw = readFileSync(path, "utf8");
|
|
3036
142
|
} catch (cause) {
|
|
3037
|
-
throw new CommerceError("CONFIG_INVALID", `Could not read local chain manifest at "${
|
|
143
|
+
throw new CommerceError("CONFIG_INVALID", `Could not read local chain manifest at "${path}".`, {
|
|
3038
144
|
cause
|
|
3039
145
|
});
|
|
3040
146
|
}
|
|
@@ -3044,7 +150,7 @@ function readLocalChainManifest(cwd = process.cwd()) {
|
|
|
3044
150
|
} catch (cause) {
|
|
3045
151
|
throw new CommerceError(
|
|
3046
152
|
"CONFIG_INVALID",
|
|
3047
|
-
`Local chain manifest at "${
|
|
153
|
+
`Local chain manifest at "${path}" is not valid JSON. Re-run "npm run chain:deploy" to regenerate it.`,
|
|
3048
154
|
{ cause }
|
|
3049
155
|
);
|
|
3050
156
|
}
|
|
@@ -3054,7 +160,7 @@ function readLocalChainManifest(cwd = process.cwd()) {
|
|
|
3054
160
|
const where = issue?.path.length ? `${issue.path.join(".")}: ` : "";
|
|
3055
161
|
throw new CommerceError(
|
|
3056
162
|
"CONFIG_INVALID",
|
|
3057
|
-
`Local chain manifest at "${
|
|
163
|
+
`Local chain manifest at "${path}" is invalid: ${where}${issue?.message ?? "unexpected shape"}. Re-run "npm run chain:deploy".`
|
|
3058
164
|
);
|
|
3059
165
|
}
|
|
3060
166
|
return result.data;
|
|
@@ -3089,11 +195,19 @@ function fillEnvFromLocalChainManifest(baseEnv, cwd = process.cwd()) {
|
|
|
3089
195
|
}
|
|
3090
196
|
return { env, filled, manifestFound: true };
|
|
3091
197
|
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
198
|
+
function hasChainDeployScript(cwd = process.cwd()) {
|
|
199
|
+
try {
|
|
200
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
201
|
+
return pkg.name === "@devlab.group/agent-commerce" && typeof pkg.scripts?.["chain:deploy"] === "string";
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function unresolvedVariableHint(variable, manifestFound, chainDeploy) {
|
|
207
|
+
if (chainDeploy && !manifestFound && MANIFEST_FILLABLE_ENV_VAR_NAMES.has(variable)) {
|
|
208
|
+
return `"npm run chain:deploy" writes ${LOCAL_CHAIN_MANIFEST_PATH}, which validate and doctor read to fill local X402_* and MERCHANT_WALLET placeholders`;
|
|
209
|
+
}
|
|
210
|
+
return `set ${variable} in the environment, or in a file passed with --env-file`;
|
|
3097
211
|
}
|
|
3098
212
|
|
|
3099
213
|
// package.json
|
|
@@ -3115,8 +229,11 @@ var package_default = {
|
|
|
3115
229
|
"@coinbase/x402": "2.1.0",
|
|
3116
230
|
"@modelcontextprotocol/server": "2.3.0",
|
|
3117
231
|
"@sd-jwt/core": "0.20.1",
|
|
232
|
+
"@solana/kit": "5.5.1",
|
|
3118
233
|
"@x402/core": "2.25.0",
|
|
3119
234
|
"@x402/evm": "2.25.0",
|
|
235
|
+
"@x402/near": "2.25.0",
|
|
236
|
+
"@x402/svm": "2.25.0",
|
|
3120
237
|
canonicalize: "5.0.0",
|
|
3121
238
|
jose: "6.2.12",
|
|
3122
239
|
mppx: "0.13.1",
|
|
@@ -3130,6 +247,9 @@ var PINS = [
|
|
|
3130
247
|
["ajv-formats", "protocols/acp"],
|
|
3131
248
|
["@x402/core", "payments/x402"],
|
|
3132
249
|
["@x402/evm", "payments/x402"],
|
|
250
|
+
["@x402/svm", "payments/x402"],
|
|
251
|
+
["@solana/kit", "payments/x402"],
|
|
252
|
+
["@x402/near", "payments/x402"],
|
|
3133
253
|
["@coinbase/x402", "payments/x402"],
|
|
3134
254
|
["viem", "payments/x402"],
|
|
3135
255
|
["mppx", "payments/mpp"],
|
|
@@ -3153,278 +273,99 @@ function readVersionReport() {
|
|
|
3153
273
|
});
|
|
3154
274
|
return { cliVersion: PACKAGE_VERSION, pinned };
|
|
3155
275
|
}
|
|
3156
|
-
|
|
3157
|
-
// src/cli/commands/doctor.ts
|
|
3158
|
-
var CHECK_TIMEOUT_MS = 1500;
|
|
3159
|
-
function deriveGatewayUrl(explicit, config) {
|
|
3160
|
-
if (explicit !== void 0) return explicit.replace(/\/$/, "");
|
|
3161
|
-
if (config !== void 0) {
|
|
3162
|
-
const raw = config.server.host;
|
|
3163
|
-
const host = raw === "0.0.0.0" ? "127.0.0.1" : raw === "::" ? "::1" : raw;
|
|
3164
|
-
const authority = host.includes(":") ? `[${host}]` : host;
|
|
3165
|
-
return `http://${authority}:${config.server.port}`;
|
|
3166
|
-
}
|
|
3167
|
-
return "http://localhost:8080";
|
|
3168
|
-
}
|
|
3169
|
-
function substitutePathParams(url) {
|
|
3170
|
-
let filled = url;
|
|
3171
|
-
for (const name of extractPathParameterNames(url)) {
|
|
3172
|
-
filled = filled.replaceAll(`{${name}}`, "demo-check");
|
|
3173
|
-
}
|
|
3174
|
-
return filled;
|
|
3175
|
-
}
|
|
3176
|
-
function describePaymentFlow(rail) {
|
|
3177
|
-
const overrides = Object.keys(rail.resourcePaymentFlows ?? {}).length;
|
|
3178
|
-
const overrideCount = overrides > 0 ? ` (${overrides} resource ${overrides === 1 ? "override" : "overrides"})` : "";
|
|
3179
|
-
return `paymentFlow=${rail.paymentFlow ?? "authorization"}${overrideCount}`;
|
|
3180
|
-
}
|
|
3181
|
-
function extractWellKnownX402(body, rail = "x402") {
|
|
3182
|
-
const payments = body?.["payments"];
|
|
3183
|
-
if (typeof payments !== "object" || payments === null) return void 0;
|
|
3184
|
-
const block = payments[rail];
|
|
3185
|
-
if (typeof block !== "object" || block === null) return void 0;
|
|
3186
|
-
const rec = block;
|
|
3187
|
-
if (rec["enabled"] === false) return "disabled";
|
|
3188
|
-
if (rec["enabled"] !== true) return void 0;
|
|
3189
|
-
const { asset, network } = rec;
|
|
3190
|
-
const payTo = rec[rail === "mpp" ? "recipient" : "payTo"];
|
|
3191
|
-
if (typeof asset !== "string" || typeof network !== "string" || typeof payTo !== "string") {
|
|
3192
|
-
return void 0;
|
|
3193
|
-
}
|
|
3194
|
-
return { asset, network, payTo };
|
|
3195
|
-
}
|
|
3196
|
-
function sameAddress2(a, b) {
|
|
3197
|
-
return a.toLowerCase() === b.toLowerCase();
|
|
3198
|
-
}
|
|
3199
|
-
function findX402Mismatch(configured, live, payToField = "payTo") {
|
|
3200
|
-
const diffs = [];
|
|
3201
|
-
if (!sameAddress2(configured.asset, live.asset)) {
|
|
3202
|
-
diffs.push(
|
|
3203
|
-
`asset: gateway is using ${live.asset} but local config resolves to ${configured.asset}`
|
|
3204
|
-
);
|
|
3205
|
-
}
|
|
3206
|
-
if (configured.network !== live.network) {
|
|
3207
|
-
diffs.push(
|
|
3208
|
-
`network: gateway is using "${live.network}" but local config resolves to "${configured.network}"`
|
|
3209
|
-
);
|
|
3210
|
-
}
|
|
3211
|
-
if (!sameAddress2(configured.payTo, live.payTo)) {
|
|
3212
|
-
diffs.push(
|
|
3213
|
-
`${payToField}: gateway is using ${live.payTo} but local config resolves to ${configured.payTo}`
|
|
3214
|
-
);
|
|
3215
|
-
}
|
|
3216
|
-
if (diffs.length === 0) return void 0;
|
|
3217
|
-
return `${diffs.join("; ")}; the gateway may be running against an older deployment - restart it or re-run chain:deploy`;
|
|
3218
|
-
}
|
|
3219
|
-
function describeSettlement(network, facilitator) {
|
|
3220
|
-
const profile = findNetworkProfile(network);
|
|
3221
|
-
const mode = profile ? resolveDeploymentMode(profile, facilitator.mode) : void 0;
|
|
3222
|
-
const where = profile === void 0 || mode === void 0 ? `unknown network ${network}` : mode === "local" ? `LOCAL dev chain (${network}, chain id shared with ${profile.displayName})` : `${describeDeploymentMode(mode)} on ${profile.displayName} (${network})`;
|
|
3223
|
-
const via = facilitator.mode === "local" ? "local (in-process)" : `remote (auth=${facilitator.auth.type})`;
|
|
3224
|
-
return { mode, where, via };
|
|
3225
|
-
}
|
|
3226
|
-
function mainnetSafetyCheck(name, payToField, facilitator) {
|
|
3227
|
-
const unauthenticated = facilitator.mode === "remote" && facilitator.auth.type === "none";
|
|
3228
|
-
return {
|
|
3229
|
-
name,
|
|
3230
|
-
status: unauthenticated ? "WARN" : "INFO",
|
|
3231
|
-
detail: unauthenticated ? `${describeDeploymentMode("mainnet")}: no credential is sent to the remote facilitator (accepted via allowUnauthenticatedFacilitator); its anonymous-access limits apply. Config load also requires allowMainnet, HTTPS, a non-development ${payToField} and the canonical asset. Payments fail closed.` : `${describeDeploymentMode("mainnet")}. Enforced at config load: explicit allowMainnet opt-in, remote facilitator over HTTPS with a credential, non-development ${payToField}, and the canonical asset for this network. Payments fail closed.`
|
|
3232
|
-
};
|
|
3233
|
-
}
|
|
3234
|
-
function describeIssuers(label, issuers) {
|
|
3235
|
-
const described = issuers.map(
|
|
3236
|
-
(entry) => `${entry.issuer} (${entry.keys.length} key${entry.keys.length === 1 ? "" : "s"})`
|
|
3237
|
-
).join(", ");
|
|
3238
|
-
return `${label} issuers: ${described}`;
|
|
3239
|
-
}
|
|
3240
|
-
function isWritableFile(path2) {
|
|
276
|
+
function isWritableFile(path) {
|
|
3241
277
|
try {
|
|
3242
|
-
accessSync(
|
|
278
|
+
accessSync(path, constants.W_OK);
|
|
3243
279
|
return true;
|
|
3244
280
|
} catch {
|
|
3245
281
|
return false;
|
|
3246
282
|
}
|
|
3247
283
|
}
|
|
3248
|
-
function storeFileCheck(name,
|
|
3249
|
-
if (
|
|
284
|
+
function storeFileCheck(name, path, risk, suffix = "") {
|
|
285
|
+
if (path === ":memory:") {
|
|
3250
286
|
return {
|
|
3251
287
|
name,
|
|
3252
288
|
status: "WARN",
|
|
3253
289
|
detail: `in-memory store: ${risk.inMemory}; use a file path in production${suffix}`
|
|
3254
290
|
};
|
|
3255
291
|
}
|
|
3256
|
-
if (existsSync(
|
|
3257
|
-
return isWritableFile(
|
|
292
|
+
if (existsSync(path)) {
|
|
293
|
+
return isWritableFile(path) ? { name, status: "PASS", detail: `writable at "${path}"${suffix}` } : {
|
|
3258
294
|
name,
|
|
3259
295
|
status: "FAIL",
|
|
3260
|
-
detail: `"${
|
|
296
|
+
detail: `"${path}" exists but is not writable by this user, so ${risk.unwritable}${suffix}`
|
|
3261
297
|
};
|
|
3262
298
|
}
|
|
3263
|
-
return { name, status: "WARN", detail: `${missingStoreDetail(
|
|
3264
|
-
}
|
|
3265
|
-
function missingStoreDetail(path2) {
|
|
3266
|
-
const directory = dirname(path2);
|
|
3267
|
-
return directory !== "." && !existsSync(directory) ? `no store at "${path2}" and its directory does not exist. Both are created when the gateway starts, provided the path is writable` : `no store at "${path2}" yet. It is created the first time the gateway starts`;
|
|
299
|
+
return { name, status: "WARN", detail: `${missingStoreDetail(path)}${suffix}` };
|
|
3268
300
|
}
|
|
3269
|
-
function
|
|
3270
|
-
const
|
|
3271
|
-
|
|
3272
|
-
for (const [operation, resourceId] of Object.entries(operations)) {
|
|
3273
|
-
const resource = byId.get(resourceId);
|
|
3274
|
-
if (resource === void 0) {
|
|
3275
|
-
problems.push(`${operation} -> "${resourceId}" does not exist`);
|
|
3276
|
-
continue;
|
|
3277
|
-
}
|
|
3278
|
-
if (!resource.exposedVia.includes("acp")) {
|
|
3279
|
-
problems.push(`${operation} -> "${resourceId}" is not exposed via acp`);
|
|
3280
|
-
}
|
|
3281
|
-
if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
|
|
3282
|
-
problems.push(`${operation} -> "${resourceId}" is not free to invoke`);
|
|
3283
|
-
}
|
|
3284
|
-
}
|
|
3285
|
-
if (problems.length > 0) {
|
|
3286
|
-
return { name: "ACP checkout mapping", status: "FAIL", detail: problems.join("; ") };
|
|
3287
|
-
}
|
|
3288
|
-
return {
|
|
3289
|
-
name: "ACP checkout mapping",
|
|
3290
|
-
status: "PASS",
|
|
3291
|
-
detail: `5 operations mapped to free acp-exposed resources (${Object.values(operations).join(", ")})`
|
|
3292
|
-
};
|
|
301
|
+
function missingStoreDetail(path) {
|
|
302
|
+
const directory = dirname(path);
|
|
303
|
+
return directory !== "." && !existsSync(directory) ? `no store at "${path}" and its directory does not exist. Both are created when the gateway starts, provided the path is writable` : `no store at "${path}" yet. It is created the first time the gateway starts`;
|
|
3293
304
|
}
|
|
3294
|
-
async function
|
|
3295
|
-
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
3296
|
-
const loadConfig2 = deps.loadConfig ?? loadConfigDynamic;
|
|
3297
|
-
const createStore = deps.createStore ?? ((path2) => createSqliteReceiptStore({ path: path2 }));
|
|
3298
|
-
const fillEnvFromManifest = deps.fillEnvFromManifest ?? fillEnvFromLocalChainManifest;
|
|
305
|
+
async function storageChecks(config, createStore) {
|
|
3299
306
|
const checks = [];
|
|
3300
|
-
let config;
|
|
3301
|
-
const { env, filled, manifestFound } = fillEnvFromManifest(process.env);
|
|
3302
|
-
const filledSuffix = filled.length > 0 ? ` (using local chain manifest ${LOCAL_CHAIN_MANIFEST_PATH} for ${filled.join(", ")})` : "";
|
|
3303
|
-
try {
|
|
3304
|
-
config = await loadConfig2({
|
|
3305
|
-
...options.configPath !== void 0 ? { path: options.configPath } : {},
|
|
3306
|
-
env
|
|
3307
|
-
});
|
|
3308
|
-
checks.push({
|
|
3309
|
-
name: "Config",
|
|
3310
|
-
status: "PASS",
|
|
3311
|
-
detail: `valid: ${config.resources.length} resource(s), merchant "${config.merchant.name}"${filledSuffix}`
|
|
3312
|
-
});
|
|
3313
|
-
} catch (err) {
|
|
3314
|
-
const variable = isCommerceError(err) ? err.details?.["variable"] : void 0;
|
|
3315
|
-
const hint = !manifestFound && typeof variable === "string" && MANIFEST_FILLABLE_ENV_VAR_NAMES.has(variable) ? `. Run "npm run chain:deploy": it writes ${LOCAL_CHAIN_MANIFEST_PATH}, which fills local X402_*/MERCHANT_WALLET placeholders automatically` : "";
|
|
3316
|
-
checks.push({
|
|
3317
|
-
name: "Config",
|
|
3318
|
-
status: "FAIL",
|
|
3319
|
-
detail: `${err instanceof Error ? err.message : String(err)}${hint}`
|
|
3320
|
-
});
|
|
3321
|
-
}
|
|
3322
|
-
const gatewayUrl = deriveGatewayUrl(options.gatewayUrl, config);
|
|
3323
|
-
const health = await fetchJson(fetchImpl, `${gatewayUrl}/health`, CHECK_TIMEOUT_MS);
|
|
3324
|
-
let gatewayUp = false;
|
|
3325
|
-
if (!health.ok) {
|
|
3326
|
-
checks.push({
|
|
3327
|
-
name: "Gateway",
|
|
3328
|
-
status: "FAIL",
|
|
3329
|
-
detail: `unreachable at ${gatewayUrl} (${health.error ?? `HTTP ${health.status}`})`
|
|
3330
|
-
});
|
|
3331
|
-
} else {
|
|
3332
|
-
gatewayUp = true;
|
|
3333
|
-
const ready = await fetchJson(fetchImpl, `${gatewayUrl}/ready`, CHECK_TIMEOUT_MS);
|
|
3334
|
-
checks.push(
|
|
3335
|
-
ready.ok ? { name: "Gateway", status: "PASS", detail: `healthy and ready at ${gatewayUrl}` } : {
|
|
3336
|
-
name: "Gateway",
|
|
3337
|
-
status: "WARN",
|
|
3338
|
-
detail: `healthy but not ready at ${gatewayUrl} (${ready.error ?? `HTTP ${ready.status}`})`
|
|
3339
|
-
}
|
|
3340
|
-
);
|
|
3341
|
-
}
|
|
3342
|
-
if (config === void 0) {
|
|
3343
|
-
checks.push({ name: "Backend", status: "WARN", detail: "skipped: config invalid" });
|
|
3344
|
-
} else if (config.resources.length === 0) {
|
|
3345
|
-
checks.push({ name: "Backend", status: "INFO", detail: "no resources configured" });
|
|
3346
|
-
} else {
|
|
3347
|
-
const resources = config.resources;
|
|
3348
|
-
const urls = [...new Set(resources.map((r) => substitutePathParams(r.handler.url)))];
|
|
3349
|
-
const results = await Promise.all(
|
|
3350
|
-
urls.map((url) => fetchJson(fetchImpl, url, CHECK_TIMEOUT_MS))
|
|
3351
|
-
);
|
|
3352
|
-
const reachable = results.filter((r) => r.status !== 0).length;
|
|
3353
|
-
const status = reachable === urls.length ? "PASS" : reachable === 0 ? "FAIL" : "WARN";
|
|
3354
|
-
checks.push({
|
|
3355
|
-
name: "Backend",
|
|
3356
|
-
status,
|
|
3357
|
-
detail: `${reachable}/${urls.length} backend host(s) reachable`
|
|
3358
|
-
});
|
|
3359
|
-
}
|
|
3360
|
-
const wellKnown = gatewayUp ? await fetchJson(
|
|
3361
|
-
fetchImpl,
|
|
3362
|
-
`${gatewayUrl}/.well-known/agent-commerce`,
|
|
3363
|
-
CHECK_TIMEOUT_MS
|
|
3364
|
-
) : void 0;
|
|
3365
|
-
if (config === void 0) {
|
|
3366
|
-
checks.push({ name: "Protocols", status: "WARN", detail: "skipped: config invalid" });
|
|
3367
|
-
} else if (wellKnown === void 0 || !wellKnown.ok) {
|
|
3368
|
-
checks.push({ name: "Protocols", status: "FAIL", detail: "well-known document unreachable" });
|
|
3369
|
-
} else {
|
|
3370
|
-
const mcpMountPath = config.protocols.mcp.enabled ? config.protocols.mcp.mountPath : void 0;
|
|
3371
|
-
const a2aMountPath = config.protocols.a2a.enabled ? config.protocols.a2a.mountPath : void 0;
|
|
3372
|
-
checks.push({
|
|
3373
|
-
name: "Protocols",
|
|
3374
|
-
status: "PASS",
|
|
3375
|
-
detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"} a2a=${config.protocols.a2a.enabled ? `on (${a2aMountPath})` : "off"} acp=${config.protocols.acp.enabled ? `on (${config.protocols.acp.mountPath})` : "off"}`
|
|
3376
|
-
});
|
|
3377
|
-
}
|
|
3378
307
|
if (config === void 0) {
|
|
3379
|
-
checks.push({ name: "
|
|
3380
|
-
} else if (
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
308
|
+
checks.push({ name: "Storage", status: "WARN", detail: "skipped: config invalid" });
|
|
309
|
+
} else if (
|
|
310
|
+
// Diagnose without creating: createSqliteReceiptStore creates the file,
|
|
311
|
+
// so opening a wrong path (a typo, or a container path read from the
|
|
312
|
+
// host) would leave an empty database and report it healthy, hiding the
|
|
313
|
+
// real one. `:memory:` never exists on disk, so it takes the open branch
|
|
314
|
+
// below.
|
|
315
|
+
config.storage.receipts.path !== ":memory:" && !existsSync(config.storage.receipts.path)
|
|
316
|
+
) {
|
|
3388
317
|
checks.push({
|
|
3389
|
-
name: "
|
|
3390
|
-
status: "
|
|
3391
|
-
detail:
|
|
318
|
+
name: "Storage",
|
|
319
|
+
status: "WARN",
|
|
320
|
+
detail: `${missingStoreDetail(config.storage.receipts.path)}. If the gateway is already running, this path does not match the one it uses.`
|
|
3392
321
|
});
|
|
3393
|
-
}
|
|
3394
|
-
if (config === void 0) {
|
|
3395
|
-
checks.push({ name: "ACP", status: "WARN", detail: "skipped: config invalid" });
|
|
3396
|
-
} else if (!config.protocols.acp.enabled) {
|
|
3397
|
-
checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
|
|
3398
322
|
} else {
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
323
|
+
let store;
|
|
324
|
+
try {
|
|
325
|
+
store = createStore(config.storage.receipts.path);
|
|
326
|
+
await store.init();
|
|
327
|
+
const storeHealth = await store.health();
|
|
328
|
+
let receiptCount;
|
|
329
|
+
try {
|
|
330
|
+
receiptCount = await store.countReceipts();
|
|
331
|
+
} catch {
|
|
332
|
+
receiptCount = void 0;
|
|
333
|
+
}
|
|
334
|
+
let undeliveredCount;
|
|
335
|
+
try {
|
|
336
|
+
undeliveredCount = await store.countUndeliveredReceipts();
|
|
337
|
+
} catch {
|
|
338
|
+
undeliveredCount = void 0;
|
|
339
|
+
}
|
|
340
|
+
const status = storeHealth.status === "pass" ? "PASS" : storeHealth.status === "warn" ? "WARN" : "FAIL";
|
|
341
|
+
const receiptsSuffix = receiptCount !== void 0 ? `; receipts=${receiptCount}${undeliveredCount !== void 0 && undeliveredCount > 0 ? ` (${undeliveredCount} undelivered)` : ""}` : "";
|
|
342
|
+
checks.push({
|
|
343
|
+
name: "Storage",
|
|
344
|
+
status,
|
|
345
|
+
detail: `${storeHealth.detail ?? "sqlite"}${receiptsSuffix}`
|
|
346
|
+
});
|
|
347
|
+
} catch (err) {
|
|
348
|
+
checks.push({
|
|
349
|
+
name: "Storage",
|
|
350
|
+
status: "FAIL",
|
|
351
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
352
|
+
});
|
|
353
|
+
} finally {
|
|
354
|
+
await store?.close();
|
|
355
|
+
}
|
|
3427
356
|
}
|
|
357
|
+
return checks;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/cli/commands/doctor/ap2.ts
|
|
361
|
+
function describeIssuers(label, issuers) {
|
|
362
|
+
const described = issuers.map(
|
|
363
|
+
(entry) => `${entry.issuer} (${entry.keys.length} key${entry.keys.length === 1 ? "" : "s"})`
|
|
364
|
+
).join(", ");
|
|
365
|
+
return `${label} issuers: ${described}`;
|
|
366
|
+
}
|
|
367
|
+
function ap2Checks(config) {
|
|
368
|
+
const checks = [];
|
|
3428
369
|
const ap2 = config?.authorization?.ap2;
|
|
3429
370
|
if (config === void 0) {
|
|
3430
371
|
checks.push({ name: "AP2", status: "WARN", detail: "skipped: config invalid" });
|
|
@@ -3461,19 +402,331 @@ async function runDoctor(options, deps = {}) {
|
|
|
3461
402
|
detail: AP2_UNSUPPORTED.join(", ")
|
|
3462
403
|
});
|
|
3463
404
|
}
|
|
405
|
+
return checks;
|
|
406
|
+
}
|
|
407
|
+
var LOCAL_DEV_MERCHANT_ADDRESS = ANVIL_WELL_KNOWN_ACCOUNTS[1].address;
|
|
408
|
+
var LOCAL_DEV_FACILITATOR_PRIVATE_KEY = ANVIL_WELL_KNOWN_ACCOUNTS[0].privateKey;
|
|
409
|
+
var PLACEHOLDER_ASSET_ADDRESS = "0x000000000000000000000000000000000000dEaD";
|
|
410
|
+
function defaultInitAnswers() {
|
|
411
|
+
return {
|
|
412
|
+
backendBaseUrl: "http://localhost:3000",
|
|
413
|
+
resources: ["weather", "report"],
|
|
414
|
+
protocols: ["http", "mcp"],
|
|
415
|
+
x402Enabled: true,
|
|
416
|
+
merchantPayTo: LOCAL_DEV_MERCHANT_ADDRESS
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function buildResourcesMap(answers) {
|
|
420
|
+
const resources = {};
|
|
421
|
+
const expose = [...answers.protocols];
|
|
422
|
+
if (answers.resources.includes("weather")) {
|
|
423
|
+
resources.weather = {
|
|
424
|
+
name: "Get weather",
|
|
425
|
+
description: "Deterministic demo weather lookup (free resource).",
|
|
426
|
+
input: {
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: { city: { type: "string" } },
|
|
429
|
+
required: ["city"],
|
|
430
|
+
additionalProperties: false
|
|
431
|
+
},
|
|
432
|
+
backend: {
|
|
433
|
+
type: "http",
|
|
434
|
+
method: "GET",
|
|
435
|
+
url: `${answers.backendBaseUrl}/api/weather/{city}`
|
|
436
|
+
},
|
|
437
|
+
pricing: { type: "free" },
|
|
438
|
+
expose
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (answers.resources.includes("report")) {
|
|
442
|
+
const paid = answers.x402Enabled;
|
|
443
|
+
resources.report = {
|
|
444
|
+
name: "Get market report",
|
|
445
|
+
description: "Deterministic demo premium market report.",
|
|
446
|
+
// No caller-supplied fields. Config loads a missing `input` as this same
|
|
447
|
+
// closed schema; writing it out shows that in the generated file
|
|
448
|
+
input: {
|
|
449
|
+
type: "object",
|
|
450
|
+
properties: {},
|
|
451
|
+
additionalProperties: false
|
|
452
|
+
},
|
|
453
|
+
backend: {
|
|
454
|
+
type: "http",
|
|
455
|
+
method: "GET",
|
|
456
|
+
url: `${answers.backendBaseUrl}/api/report`
|
|
457
|
+
},
|
|
458
|
+
pricing: paid ? { type: "fixed", amount: "0.01", currency: "USDC" } : { type: "free" },
|
|
459
|
+
expose,
|
|
460
|
+
...paid ? { payments: ["x402"] } : {}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
return resources;
|
|
464
|
+
}
|
|
465
|
+
function buildInitConfigObject(answers) {
|
|
466
|
+
return {
|
|
467
|
+
version: 1,
|
|
468
|
+
merchant: {
|
|
469
|
+
id: "demo-merchant",
|
|
470
|
+
name: "Demo Merchant",
|
|
471
|
+
publicBaseUrl: "http://localhost:8080"
|
|
472
|
+
},
|
|
473
|
+
server: { port: 8080, host: "127.0.0.1" },
|
|
474
|
+
storage: { receipts: { driver: "sqlite", path: "./data/receipts.db" } },
|
|
475
|
+
protocols: {
|
|
476
|
+
http: { enabled: answers.protocols.includes("http") },
|
|
477
|
+
mcp: { enabled: answers.protocols.includes("mcp"), mountPath: "/mcp" }
|
|
478
|
+
},
|
|
479
|
+
resources: buildResourcesMap(answers),
|
|
480
|
+
payments: answers.x402Enabled ? {
|
|
481
|
+
x402: {
|
|
482
|
+
enabled: true,
|
|
483
|
+
network: `eip155:${LOCAL_CHAIN_ID}`,
|
|
484
|
+
rpcUrl: "http://127.0.0.1:8545",
|
|
485
|
+
asset: PLACEHOLDER_ASSET_ADDRESS,
|
|
486
|
+
assetName: "MockUSDC",
|
|
487
|
+
assetVersion: "2",
|
|
488
|
+
assetDecimals: 6,
|
|
489
|
+
payTo: answers.merchantPayTo,
|
|
490
|
+
maxTimeoutSeconds: 60,
|
|
491
|
+
facilitator: { mode: "local", signerPrivateKey: LOCAL_DEV_FACILITATOR_PRIVATE_KEY }
|
|
492
|
+
}
|
|
493
|
+
} : {}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
var HEADER = `# Generated by \`agent-commerce init\`.
|
|
497
|
+
#
|
|
498
|
+
# With x402 enabled, the facilitator signerPrivateKey is Anvil's public dev
|
|
499
|
+
# account #0 and the default payTo is account #1.
|
|
500
|
+
# LOCAL DEVELOPMENT ONLY - DO NOT FUND: never use them outside a local chain.
|
|
501
|
+
# payments.x402.asset is a placeholder, not a real contract. Before running
|
|
502
|
+
# against the local chain, run \`npm run chain:deploy\` and set it to the
|
|
503
|
+
# MockUSDC address in \`.deploy/local.json\`. Before pointing this at a funded
|
|
504
|
+
# network, set \`payments.x402.payTo\` to your own settlement address.
|
|
505
|
+
#
|
|
506
|
+
# server.host is loopback (127.0.0.1): binding every interface should be a
|
|
507
|
+
# deliberate edit.
|
|
508
|
+
#
|
|
509
|
+
# server.adminToken and server.allowedOrigins are left unset, so no secret
|
|
510
|
+
# lands in a file you might commit. Without a token the operator routes
|
|
511
|
+
# (/api/receipts, /api/events) return 404. They serve the merchant's commerce
|
|
512
|
+
# ledger: payer addresses, amounts and settlement hashes. To enable them, add
|
|
513
|
+
# these keys to the server block below:
|
|
514
|
+
# server:
|
|
515
|
+
# adminToken: \${ADMIN_TOKEN}
|
|
516
|
+
# allowedOrigins:
|
|
517
|
+
# - http://localhost:5173
|
|
518
|
+
# See SECURITY.md before exposing this gateway beyond localhost.
|
|
519
|
+
`;
|
|
520
|
+
function renderInitConfigYaml(answers) {
|
|
521
|
+
const body = stringify(buildInitConfigObject(answers), { indent: 2 });
|
|
522
|
+
return `${HEADER}
|
|
523
|
+
${body}`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/cli/lib/mask.ts
|
|
527
|
+
function maskMiddle(value, keep = 6) {
|
|
528
|
+
if (value.length <= keep * 2) return value.length === 0 ? value : "\u2026";
|
|
529
|
+
return `${value.slice(0, keep)}\u2026${value.slice(-4)}`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/cli/commands/doctor/payments.ts
|
|
533
|
+
function describePaymentFlow(rail, defaultFlow) {
|
|
534
|
+
const overrides = Object.keys(rail.resourcePaymentFlows ?? {}).length;
|
|
535
|
+
const overrideCount = overrides > 0 ? ` (${overrides} resource ${overrides === 1 ? "override" : "overrides"})` : "";
|
|
536
|
+
return `paymentFlow=${rail.paymentFlow ?? defaultFlow}${overrideCount}`;
|
|
537
|
+
}
|
|
538
|
+
function readOffer(rec, rail) {
|
|
539
|
+
const { asset, network, assetDecimals, assetTransferMethod, status } = rec;
|
|
540
|
+
const payTo = rec[rail === "mpp" ? "recipient" : "payTo"];
|
|
541
|
+
if (typeof asset !== "string" || typeof network !== "string" || typeof payTo !== "string") {
|
|
542
|
+
return void 0;
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
network,
|
|
546
|
+
asset,
|
|
547
|
+
payTo,
|
|
548
|
+
...typeof assetDecimals === "number" ? { assetDecimals } : {},
|
|
549
|
+
...typeof assetTransferMethod === "string" ? { assetTransferMethod } : {},
|
|
550
|
+
...typeof status === "string" ? { status } : {}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function readOffers(value, rail) {
|
|
554
|
+
if (!Array.isArray(value)) return void 0;
|
|
555
|
+
const read = value.map((entry) => isRecord(entry) ? readOffer(entry, rail) : void 0);
|
|
556
|
+
return read.some((offer) => offer === void 0) ? void 0 : read;
|
|
557
|
+
}
|
|
558
|
+
function extractLiveRail(body, rail = "x402") {
|
|
559
|
+
const block = isRecord(body?.["payments"]) ? body["payments"][rail] : void 0;
|
|
560
|
+
if (!isRecord(block)) return void 0;
|
|
561
|
+
if (block["enabled"] === false) return "disabled";
|
|
562
|
+
if (block["enabled"] !== true) return void 0;
|
|
563
|
+
const first = readOffer(block, rail);
|
|
564
|
+
if (first === void 0) return void 0;
|
|
565
|
+
let offers = [first];
|
|
566
|
+
if (block["options"] !== void 0) {
|
|
567
|
+
const listed = readOffers(block["options"], rail);
|
|
568
|
+
if (listed === void 0) return void 0;
|
|
569
|
+
offers = listed;
|
|
570
|
+
}
|
|
571
|
+
let resourceOffers;
|
|
572
|
+
const ownLists = block["resourceOptions"];
|
|
573
|
+
if (ownLists !== void 0) {
|
|
574
|
+
if (!isRecord(ownLists)) return void 0;
|
|
575
|
+
resourceOffers = {};
|
|
576
|
+
for (const [id, list] of Object.entries(ownLists)) {
|
|
577
|
+
const read = readOffers(list, rail);
|
|
578
|
+
if (read === void 0) return void 0;
|
|
579
|
+
resourceOffers[id] = read;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const { paymentFlow, resourcePaymentFlows } = block;
|
|
583
|
+
return {
|
|
584
|
+
offers,
|
|
585
|
+
...resourceOffers !== void 0 ? { resourceOffers } : {},
|
|
586
|
+
...typeof paymentFlow === "string" ? { paymentFlow } : {},
|
|
587
|
+
...isRecord(resourcePaymentFlows) ? {
|
|
588
|
+
resourcePaymentFlows: Object.fromEntries(
|
|
589
|
+
Object.entries(resourcePaymentFlows).map(([id, flow]) => [id, String(flow)])
|
|
590
|
+
)
|
|
591
|
+
} : {}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function describeOverrides(flows) {
|
|
595
|
+
const entries = Object.entries(flows ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
596
|
+
return entries.length > 0 ? entries.map(([id, flow]) => `${id}=${flow}`).join(", ") : "none";
|
|
597
|
+
}
|
|
598
|
+
function findRailMismatch(configured, live, payToField = "payTo", customNetworks) {
|
|
599
|
+
const diffs = [];
|
|
600
|
+
const differ = (field, liveValue, localValue) => diffs.push(
|
|
601
|
+
`${field}: gateway is using ${liveValue} but local config resolves to ${localValue}`
|
|
602
|
+
);
|
|
603
|
+
const compareOffers = (label, localOffers, liveOffers) => {
|
|
604
|
+
if (liveOffers.length !== localOffers.length) {
|
|
605
|
+
differ(label, `${liveOffers.length} offer(s)`, localOffers.length);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
localOffers.forEach((local, index) => {
|
|
609
|
+
const remote = liveOffers[index];
|
|
610
|
+
const prefix = label !== "options" ? `${label}[${index}] ` : localOffers.length > 1 ? `option ${index} ` : "";
|
|
611
|
+
const profile = findX402Network(local.network, customNetworks);
|
|
612
|
+
const same = (a, b) => profile ? sameAddress(profile, a, b) : a === b;
|
|
613
|
+
if (local.network !== remote.network) {
|
|
614
|
+
differ(`${prefix}network`, `"${remote.network}"`, `"${local.network}"`);
|
|
615
|
+
}
|
|
616
|
+
if (!same(local.asset, remote.asset)) differ(`${prefix}asset`, remote.asset, local.asset);
|
|
617
|
+
if (!same(local.payTo, remote.payTo)) {
|
|
618
|
+
differ(`${prefix}${payToField}`, remote.payTo, local.payTo);
|
|
619
|
+
}
|
|
620
|
+
if (local.assetDecimals !== void 0 && remote.assetDecimals !== void 0 && local.assetDecimals !== remote.assetDecimals) {
|
|
621
|
+
differ(`${prefix}assetDecimals`, remote.assetDecimals, local.assetDecimals);
|
|
622
|
+
}
|
|
623
|
+
if (local.assetTransferMethod !== remote.assetTransferMethod) {
|
|
624
|
+
differ(
|
|
625
|
+
`${prefix}assetTransferMethod`,
|
|
626
|
+
remote.assetTransferMethod ?? "eip3009",
|
|
627
|
+
local.assetTransferMethod ?? "eip3009"
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
compareOffers("options", configured.offers, live.offers);
|
|
633
|
+
const localLists = configured.resourceOffers ?? {};
|
|
634
|
+
const liveLists = live.resourceOffers ?? {};
|
|
635
|
+
for (const id of [.../* @__PURE__ */ new Set([...Object.keys(localLists), ...Object.keys(liveLists)])].sort()) {
|
|
636
|
+
compareOffers(`resources.${id}.x402.accepts`, localLists[id] ?? [], liveLists[id] ?? []);
|
|
637
|
+
}
|
|
638
|
+
if (live.paymentFlow !== void 0) {
|
|
639
|
+
if (live.paymentFlow !== configured.paymentFlow) {
|
|
640
|
+
differ("paymentFlow", live.paymentFlow, configured.paymentFlow);
|
|
641
|
+
}
|
|
642
|
+
const liveOverrides = describeOverrides(live.resourcePaymentFlows);
|
|
643
|
+
const localOverrides = describeOverrides(configured.resourcePaymentFlows);
|
|
644
|
+
if (liveOverrides !== localOverrides)
|
|
645
|
+
differ("resourcePaymentFlows", liveOverrides, localOverrides);
|
|
646
|
+
}
|
|
647
|
+
return diffs.length === 0 ? void 0 : diffs.join("; ");
|
|
648
|
+
}
|
|
649
|
+
function describeSettlement(network, facilitator, customNetworks) {
|
|
650
|
+
const profile = findX402Network(network, customNetworks);
|
|
651
|
+
const mode = profile ? resolveDeploymentMode(profile) : void 0;
|
|
652
|
+
const where = profile === void 0 || mode === void 0 ? `unknown network ${network}` : mode === "local" ? `LOCAL dev chain (${network})` : `${describeDeploymentMode(mode)} on ${profile.displayName} (${network})`;
|
|
653
|
+
const via = facilitator.mode === "local" ? "local (in-process)" : `remote (auth=${facilitator.auth.type})`;
|
|
654
|
+
return { mode, where, via };
|
|
655
|
+
}
|
|
656
|
+
function mainnetSafetyCheck(name, payToField, facilitator, networks) {
|
|
657
|
+
const unauthenticated = facilitator.mode === "remote" && facilitator.auth.type === "none";
|
|
658
|
+
const where = `${describeDeploymentMode("mainnet")} on ${networks.join(", ")}`;
|
|
659
|
+
return {
|
|
660
|
+
name,
|
|
661
|
+
status: unauthenticated ? "WARN" : "INFO",
|
|
662
|
+
detail: unauthenticated ? `${where}: no credential is sent to the remote facilitator (accepted via allowUnauthenticatedFacilitator); its anonymous-access limits apply. Config load also requires allowMainnet, HTTPS, a non-development ${payToField} and the canonical asset. Payments fail closed.` : `${where}. Enforced at config load: explicit allowMainnet opt-in, remote facilitator over HTTPS with a credential, non-development ${payToField}, and the canonical asset of each network. Payments fail closed.`
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function railOffer(option) {
|
|
666
|
+
const { network, asset, payTo, assetDecimals, assetTransferMethod } = option;
|
|
667
|
+
return {
|
|
668
|
+
network,
|
|
669
|
+
asset,
|
|
670
|
+
payTo,
|
|
671
|
+
assetDecimals,
|
|
672
|
+
...assetTransferMethod !== void 0 ? { assetTransferMethod } : {}
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function offerHealthCheck(live, named) {
|
|
676
|
+
const all = [...live.offers, ...Object.values(live.resourceOffers ?? {}).flat()];
|
|
677
|
+
const offers = [
|
|
678
|
+
...new Map(
|
|
679
|
+
all.map((offer) => [`${offer.network} ${offer.asset} ${offer.payTo}`, offer])
|
|
680
|
+
).values()
|
|
681
|
+
];
|
|
682
|
+
const reported = offers.filter((offer) => offer.status !== void 0);
|
|
683
|
+
if (reported.length === 0) return void 0;
|
|
684
|
+
const failing = reported.filter((offer) => offer.status === "fail");
|
|
685
|
+
const degraded = reported.filter((offer) => offer.status === "warn");
|
|
686
|
+
const describe2 = (list) => list.map(
|
|
687
|
+
(offer) => `${named(offer.network)} ${maskMiddle(offer.asset)} to ${maskMiddle(offer.payTo)}`
|
|
688
|
+
).join("; ");
|
|
689
|
+
if (failing.length === 0 && degraded.length === 0) {
|
|
690
|
+
return {
|
|
691
|
+
name: "Payments (offers)",
|
|
692
|
+
status: "PASS",
|
|
693
|
+
detail: `${reported.length} offer(s) pass their health checks`
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const problems = [
|
|
697
|
+
...failing.length > 0 ? [`failing: ${describe2(failing)}`] : [],
|
|
698
|
+
...degraded.length > 0 ? [`degraded: ${describe2(degraded)}`] : []
|
|
699
|
+
];
|
|
700
|
+
return {
|
|
701
|
+
name: "Payments (offers)",
|
|
702
|
+
status: failing.length > 0 ? "FAIL" : "WARN",
|
|
703
|
+
detail: `${problems.join("; ")}. The gateway logs each reason at debug level`
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function paymentChecks(config, wellKnown, gatewayUrl, chainDeploy = false) {
|
|
707
|
+
const checks = [];
|
|
708
|
+
const stale = `; the gateway may be running against an older deployment - restart it${chainDeploy ? " or re-run chain:deploy" : ""}`;
|
|
3464
709
|
const x402 = config?.payments.x402;
|
|
3465
710
|
if (x402 === void 0 || !x402.enabled) {
|
|
3466
711
|
checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
|
|
3467
712
|
} else {
|
|
3468
|
-
const
|
|
3469
|
-
const
|
|
3470
|
-
const
|
|
3471
|
-
const
|
|
3472
|
-
|
|
713
|
+
const options = x402.options ?? [x402];
|
|
714
|
+
const resourceOptions = x402.resourceOptions ?? {};
|
|
715
|
+
const everyOption = [...options, ...Object.values(resourceOptions).flat()];
|
|
716
|
+
const { mode, where, via } = describeSettlement(x402.network, x402.facilitator, x402.networks);
|
|
717
|
+
const flow = describePaymentFlow(x402, X402_DEFAULT_PAYMENT_FLOW);
|
|
718
|
+
const named = (network) => `${findX402Network(network, x402.networks)?.displayName ?? "unknown network"} (${network})`;
|
|
719
|
+
const terms = options.length === 1 ? `${where}, destination=${maskMiddle(x402.payTo)}` : `${mode !== void 0 ? describeDeploymentMode(mode) : "unknown mode"}, ${options.length} options: ` + options.map((option) => `${named(option.network)} to ${maskMiddle(option.payTo)}`).join("; ");
|
|
720
|
+
const ownCount = Object.keys(resourceOptions).length;
|
|
721
|
+
const own = ownCount > 0 ? `, ${ownCount} resource${ownCount === 1 ? "" : "s"} with ${ownCount === 1 ? "its" : "their"} own offers` : "";
|
|
722
|
+
const summary = `x402 v2 (scheme=exact) enabled - ${terms}${own}, facilitator=${via}, ${flow}`;
|
|
723
|
+
const live = wellKnown?.ok ? extractLiveRail(wellKnown.body) : void 0;
|
|
724
|
+
const placeholder = PLACEHOLDER_ASSET_ADDRESS.toLowerCase();
|
|
725
|
+
if (everyOption.some((option) => option.asset.toLowerCase() === placeholder)) {
|
|
3473
726
|
checks.push({
|
|
3474
727
|
name: "Payments",
|
|
3475
728
|
status: "WARN",
|
|
3476
|
-
detail: `${summary}, but
|
|
729
|
+
detail: `${summary}, but at least one offer uses the init placeholder ${maskMiddle(PLACEHOLDER_ASSET_ADDRESS)}. Set that offer's asset to its token contract address${chainDeploy ? ' (${X402_ASSET} is filled from .deploy/local.json, which "npm run chain:deploy" writes)' : ""}`
|
|
3477
730
|
});
|
|
3478
731
|
} else if (live === "disabled") {
|
|
3479
732
|
checks.push({
|
|
@@ -3488,13 +741,29 @@ async function runDoctor(options, deps = {}) {
|
|
|
3488
741
|
detail: `${summary} (gateway's live settlement config could not be verified)`
|
|
3489
742
|
});
|
|
3490
743
|
} else {
|
|
3491
|
-
const
|
|
744
|
+
const configured = {
|
|
745
|
+
offers: options.map(railOffer),
|
|
746
|
+
...ownCount > 0 ? {
|
|
747
|
+
resourceOffers: Object.fromEntries(
|
|
748
|
+
Object.entries(resourceOptions).map(([id, list]) => [id, list.map(railOffer)])
|
|
749
|
+
)
|
|
750
|
+
} : {},
|
|
751
|
+
paymentFlow: x402.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
|
|
752
|
+
...x402.resourcePaymentFlows !== void 0 ? { resourcePaymentFlows: x402.resourcePaymentFlows } : {}
|
|
753
|
+
};
|
|
754
|
+
const mismatch = findRailMismatch(configured, live, "payTo", x402.networks);
|
|
3492
755
|
checks.push(
|
|
3493
|
-
mismatch === void 0 ? { name: "Payments", status: "PASS", detail: summary } : { name: "Payments", status: "FAIL", detail: mismatch }
|
|
756
|
+
mismatch === void 0 ? { name: "Payments", status: "PASS", detail: summary } : { name: "Payments", status: "FAIL", detail: `${mismatch}${stale}` }
|
|
3494
757
|
);
|
|
758
|
+
const offerHealth = offerHealthCheck(live, named);
|
|
759
|
+
if (offerHealth !== void 0) checks.push(offerHealth);
|
|
3495
760
|
}
|
|
3496
761
|
if (mode === "mainnet") {
|
|
3497
|
-
checks.push(
|
|
762
|
+
checks.push(
|
|
763
|
+
mainnetSafetyCheck("Mainnet safety", "payTo", x402.facilitator, [
|
|
764
|
+
...new Set(everyOption.map((option) => named(option.network)))
|
|
765
|
+
])
|
|
766
|
+
);
|
|
3498
767
|
}
|
|
3499
768
|
}
|
|
3500
769
|
const mpp = config?.payments.mpp;
|
|
@@ -3502,99 +771,141 @@ async function runDoctor(options, deps = {}) {
|
|
|
3502
771
|
checks.push({ name: "Payments (MPP)", status: "INFO", detail: "MPP not configured" });
|
|
3503
772
|
} else {
|
|
3504
773
|
const { mode, where, via } = describeSettlement(mpp.network, mpp.facilitator);
|
|
3505
|
-
const summary = `MPP ${MPP_PROFILE.intent}/${MPP_PROFILE.method}/${MPP_PROFILE.credentialType} enabled - ${where}, asset=${MPP_PROFILE.assetSymbol} ${maskMiddle(mpp.asset)}, recipient=${maskMiddle(mpp.recipient)}, facilitator=${via}, ${describePaymentFlow(mpp)}, spec ${MPP_SPEC_DRAFTS.core}@${MPP_SPEC_COMMIT.slice(0, 7)}, mppx ${MPPX_VERSION}`;
|
|
3506
|
-
const configured = {
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
} else {
|
|
3521
|
-
const mismatch = findX402Mismatch(configured, live, "recipient");
|
|
3522
|
-
checks.push(
|
|
3523
|
-
mismatch === void 0 ? { name: "Payments (MPP)", status: "PASS", detail: summary } : { name: "Payments (MPP)", status: "FAIL", detail: mismatch }
|
|
3524
|
-
);
|
|
3525
|
-
}
|
|
3526
|
-
if (mode === "mainnet") {
|
|
3527
|
-
checks.push(mainnetSafetyCheck("Mainnet safety (MPP)", "recipient", mpp.facilitator));
|
|
3528
|
-
}
|
|
3529
|
-
}
|
|
3530
|
-
if (config === void 0) {
|
|
3531
|
-
checks.push({ name: "Storage", status: "WARN", detail: "skipped: config invalid" });
|
|
3532
|
-
} else if (
|
|
3533
|
-
// Diagnose without creating: createSqliteReceiptStore creates the file,
|
|
3534
|
-
// so opening a wrong path (a typo, or a container path read from the
|
|
3535
|
-
// host) would leave an empty database and report it healthy, hiding the
|
|
3536
|
-
// real one. `:memory:` never exists on disk, so it takes the open branch
|
|
3537
|
-
// below.
|
|
3538
|
-
config.storage.receipts.path !== ":memory:" && !existsSync(config.storage.receipts.path)
|
|
3539
|
-
) {
|
|
3540
|
-
checks.push({
|
|
3541
|
-
name: "Storage",
|
|
3542
|
-
status: "WARN",
|
|
3543
|
-
detail: `${missingStoreDetail(config.storage.receipts.path)}. If the gateway is already running, this path does not match the one it uses.`
|
|
3544
|
-
});
|
|
3545
|
-
} else {
|
|
3546
|
-
let store;
|
|
3547
|
-
try {
|
|
3548
|
-
store = createStore(config.storage.receipts.path);
|
|
3549
|
-
await store.init();
|
|
3550
|
-
const storeHealth = await store.health();
|
|
3551
|
-
let receiptCount;
|
|
3552
|
-
try {
|
|
3553
|
-
receiptCount = await store.countReceipts();
|
|
3554
|
-
} catch {
|
|
3555
|
-
receiptCount = void 0;
|
|
3556
|
-
}
|
|
3557
|
-
let undeliveredCount;
|
|
3558
|
-
try {
|
|
3559
|
-
undeliveredCount = await store.countUndeliveredReceipts();
|
|
3560
|
-
} catch {
|
|
3561
|
-
undeliveredCount = void 0;
|
|
3562
|
-
}
|
|
3563
|
-
const status = storeHealth.status === "pass" ? "PASS" : storeHealth.status === "warn" ? "WARN" : "FAIL";
|
|
3564
|
-
const receiptsSuffix = receiptCount !== void 0 ? `; receipts=${receiptCount}${undeliveredCount !== void 0 && undeliveredCount > 0 ? ` (${undeliveredCount} undelivered)` : ""}` : "";
|
|
774
|
+
const summary = `MPP ${MPP_PROFILE.intent}/${MPP_PROFILE.method}/${MPP_PROFILE.credentialType} enabled - ${where}, asset=${MPP_PROFILE.assetSymbol} ${maskMiddle(mpp.asset)}, recipient=${maskMiddle(mpp.recipient)}, facilitator=${via}, ${describePaymentFlow(mpp, MPP_DEFAULT_PAYMENT_FLOW)}, spec ${MPP_SPEC_DRAFTS.core}@${MPP_SPEC_COMMIT.slice(0, 7)}, mppx ${MPPX_VERSION}`;
|
|
775
|
+
const configured = {
|
|
776
|
+
offers: [
|
|
777
|
+
{
|
|
778
|
+
network: mpp.network,
|
|
779
|
+
asset: mpp.asset,
|
|
780
|
+
payTo: mpp.recipient,
|
|
781
|
+
assetDecimals: MPP_PROFILE.assetDecimals
|
|
782
|
+
}
|
|
783
|
+
],
|
|
784
|
+
paymentFlow: mpp.paymentFlow ?? MPP_DEFAULT_PAYMENT_FLOW,
|
|
785
|
+
...mpp.resourcePaymentFlows !== void 0 ? { resourcePaymentFlows: mpp.resourcePaymentFlows } : {}
|
|
786
|
+
};
|
|
787
|
+
const live = wellKnown?.ok ? extractLiveRail(wellKnown.body, "mpp") : void 0;
|
|
788
|
+
if (live === "disabled") {
|
|
3565
789
|
checks.push({
|
|
3566
|
-
name: "
|
|
3567
|
-
status,
|
|
3568
|
-
detail:
|
|
790
|
+
name: "Payments (MPP)",
|
|
791
|
+
status: "FAIL",
|
|
792
|
+
detail: `Local config enables MPP, but ${gatewayUrl} reports it disabled`
|
|
3569
793
|
});
|
|
3570
|
-
}
|
|
794
|
+
} else if (live === void 0) {
|
|
3571
795
|
checks.push({
|
|
3572
|
-
name: "
|
|
3573
|
-
status: "
|
|
3574
|
-
detail:
|
|
796
|
+
name: "Payments (MPP)",
|
|
797
|
+
status: "INFO",
|
|
798
|
+
detail: `${summary} (live MPP settlement config unavailable for comparison)`
|
|
3575
799
|
});
|
|
3576
|
-
}
|
|
3577
|
-
|
|
800
|
+
} else {
|
|
801
|
+
const mismatch = findRailMismatch(configured, live, "recipient");
|
|
802
|
+
checks.push(
|
|
803
|
+
mismatch === void 0 ? { name: "Payments (MPP)", status: "PASS", detail: summary } : { name: "Payments (MPP)", status: "FAIL", detail: `${mismatch}${stale}` }
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
if (mode === "mainnet") {
|
|
807
|
+
checks.push(
|
|
808
|
+
mainnetSafetyCheck("Mainnet safety (MPP)", "recipient", mpp.facilitator, [
|
|
809
|
+
`${findX402Network(mpp.network)?.displayName ?? "unknown network"} (${mpp.network})`
|
|
810
|
+
])
|
|
811
|
+
);
|
|
3578
812
|
}
|
|
3579
813
|
}
|
|
3580
|
-
|
|
814
|
+
return checks;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/cli/commands/doctor/protocols.ts
|
|
818
|
+
function acpMappingCheck(config, operations) {
|
|
819
|
+
const byId = new Map(config.resources.map((resource) => [resource.id, resource]));
|
|
820
|
+
const problems = [];
|
|
821
|
+
for (const [operation, resourceId] of Object.entries(operations)) {
|
|
822
|
+
const resource = byId.get(resourceId);
|
|
823
|
+
if (resource === void 0) {
|
|
824
|
+
problems.push(`${operation} -> "${resourceId}" does not exist`);
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (!resource.exposedVia.includes("acp")) {
|
|
828
|
+
problems.push(`${operation} -> "${resourceId}" is not exposed via acp`);
|
|
829
|
+
}
|
|
830
|
+
if (resource.pricing.type !== "free" || resource.paymentMethods.length > 0) {
|
|
831
|
+
problems.push(`${operation} -> "${resourceId}" is not free to invoke`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (problems.length > 0) {
|
|
835
|
+
return { name: "ACP checkout mapping", status: "FAIL", detail: problems.join("; ") };
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
name: "ACP checkout mapping",
|
|
839
|
+
status: "PASS",
|
|
840
|
+
detail: `5 operations mapped to free acp-exposed resources (${Object.values(operations).join(", ")})`
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function protocolChecks(config, wellKnown) {
|
|
844
|
+
const checks = [];
|
|
845
|
+
if (config === void 0) {
|
|
846
|
+
checks.push({ name: "Protocols", status: "WARN", detail: "skipped: config invalid" });
|
|
847
|
+
} else if (wellKnown === void 0 || !wellKnown.ok) {
|
|
848
|
+
checks.push({ name: "Protocols", status: "FAIL", detail: "well-known document unreachable" });
|
|
849
|
+
} else {
|
|
850
|
+
const mcpMountPath = config.protocols.mcp.enabled ? config.protocols.mcp.mountPath : void 0;
|
|
851
|
+
const a2aMountPath = config.protocols.a2a.enabled ? config.protocols.a2a.mountPath : void 0;
|
|
3581
852
|
checks.push({
|
|
3582
|
-
name: "
|
|
853
|
+
name: "Protocols",
|
|
3583
854
|
status: "PASS",
|
|
3584
|
-
detail: "
|
|
855
|
+
detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"} a2a=${config.protocols.a2a.enabled ? `on (${a2aMountPath})` : "off"} acp=${config.protocols.acp.enabled ? `on (${config.protocols.acp.mountPath})` : "off"}`
|
|
3585
856
|
});
|
|
857
|
+
}
|
|
858
|
+
if (config === void 0) {
|
|
859
|
+
checks.push({ name: "A2A", status: "WARN", detail: "skipped: config invalid" });
|
|
860
|
+
} else if (!config.protocols.a2a.enabled) {
|
|
861
|
+
checks.push({ name: "A2A", status: "INFO", detail: "disabled" });
|
|
3586
862
|
} else {
|
|
3587
|
-
const local = readVersionReport();
|
|
3588
863
|
checks.push({
|
|
3589
|
-
name: "
|
|
864
|
+
name: "A2A",
|
|
865
|
+
status: "PASS",
|
|
866
|
+
detail: `experimental \xB7 spec ${A2A_SPEC_VERSION} \xB7 protocol ${A2A_PROTOCOL_VERSION} \xB7 binding ${A2A_PROTOCOL_BINDING} \xB7 mount ${config.protocols.a2a.mountPath} \xB7 card ${A2A_AGENT_CARD_PATH}`
|
|
867
|
+
});
|
|
868
|
+
checks.push({
|
|
869
|
+
name: "A2A unsupported",
|
|
3590
870
|
status: "INFO",
|
|
3591
|
-
detail:
|
|
871
|
+
detail: A2A_UNSUPPORTED.join(", ")
|
|
3592
872
|
});
|
|
3593
873
|
}
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
874
|
+
if (config === void 0) {
|
|
875
|
+
checks.push({ name: "ACP", status: "WARN", detail: "skipped: config invalid" });
|
|
876
|
+
} else if (!config.protocols.acp.enabled) {
|
|
877
|
+
checks.push({ name: "ACP", status: "INFO", detail: "disabled" });
|
|
878
|
+
} else {
|
|
879
|
+
const acp = config.protocols.acp;
|
|
880
|
+
checks.push({
|
|
881
|
+
name: "ACP",
|
|
882
|
+
status: "PASS",
|
|
883
|
+
detail: `experimental \xB7 spec ${ACP_SPEC_VERSION} \xB7 API-Version ${ACP_API_VERSION} \xB7 service checkout \xB7 mount ${acp.mountPath} \xB7 discovery ${ACP_WELL_KNOWN_PATH}`
|
|
884
|
+
});
|
|
885
|
+
checks.push({
|
|
886
|
+
name: "ACP auth",
|
|
887
|
+
status: "PASS",
|
|
888
|
+
detail: `bearer token configured (${acp.auth.token.length} characters, not shown)`
|
|
889
|
+
});
|
|
890
|
+
checks.push(
|
|
891
|
+
storeFileCheck(
|
|
892
|
+
"ACP idempotency",
|
|
893
|
+
acp.idempotency.path,
|
|
894
|
+
{
|
|
895
|
+
inMemory: "replay protection is lost on every restart",
|
|
896
|
+
unwritable: "the adapter cannot claim idempotency keys"
|
|
897
|
+
},
|
|
898
|
+
` (retention ${acp.idempotency.retentionHours}h)`
|
|
899
|
+
)
|
|
900
|
+
);
|
|
901
|
+
checks.push(acpMappingCheck(config, acp.checkout.operations));
|
|
902
|
+
checks.push({
|
|
903
|
+
name: "ACP unsupported",
|
|
904
|
+
status: "INFO",
|
|
905
|
+
detail: ACP_UNSUPPORTED.join(", ")
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return checks;
|
|
3598
909
|
}
|
|
3599
910
|
function statusColor(status, text2) {
|
|
3600
911
|
switch (status) {
|
|
@@ -3621,17 +932,122 @@ function printDoctorReport(report, io, json) {
|
|
|
3621
932
|
io.stdout(`Score: ${report.score.passed}/${report.score.total} checks passed`);
|
|
3622
933
|
}
|
|
3623
934
|
|
|
3624
|
-
// src/cli/commands/
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
935
|
+
// src/cli/commands/doctor/index.ts
|
|
936
|
+
var CHECK_TIMEOUT_MS = 1500;
|
|
937
|
+
function deriveGatewayUrl(explicit, config) {
|
|
938
|
+
if (explicit !== void 0) return explicit.replace(/\/$/, "");
|
|
939
|
+
if (config !== void 0) {
|
|
940
|
+
const raw = config.server.host;
|
|
941
|
+
const host = raw === "0.0.0.0" ? "127.0.0.1" : raw === "::" ? "::1" : raw;
|
|
942
|
+
const authority = host.includes(":") ? `[${host}]` : host;
|
|
943
|
+
return `http://${authority}:${config.server.port}`;
|
|
944
|
+
}
|
|
945
|
+
return "http://localhost:8080";
|
|
946
|
+
}
|
|
947
|
+
function substitutePathParams(url) {
|
|
948
|
+
let filled = url;
|
|
949
|
+
for (const name of extractPathParameterNames(url)) {
|
|
950
|
+
filled = filled.replaceAll(`{${name}}`, "demo-check");
|
|
951
|
+
}
|
|
952
|
+
return filled;
|
|
953
|
+
}
|
|
954
|
+
async function runDoctor(options, deps = {}) {
|
|
955
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
956
|
+
const loadConfig = deps.loadConfig ?? loadConfigDynamic;
|
|
957
|
+
const createStore = deps.createStore ?? ((path) => createSqliteReceiptStore({ path }));
|
|
958
|
+
const fillEnvFromManifest = deps.fillEnvFromManifest ?? fillEnvFromLocalChainManifest;
|
|
959
|
+
const chainDeploy = (deps.hasChainDeployScript ?? hasChainDeployScript)();
|
|
960
|
+
const checks = [];
|
|
961
|
+
let config;
|
|
962
|
+
const { env, filled, manifestFound } = fillEnvFromManifest(process.env);
|
|
963
|
+
const filledSuffix = filled.length > 0 ? ` (using local chain manifest ${LOCAL_CHAIN_MANIFEST_PATH} for ${filled.join(", ")})` : "";
|
|
964
|
+
try {
|
|
965
|
+
config = await loadConfig({
|
|
966
|
+
...options.configPath !== void 0 ? { path: options.configPath } : {},
|
|
967
|
+
env
|
|
968
|
+
});
|
|
969
|
+
checks.push({
|
|
970
|
+
name: "Config",
|
|
971
|
+
status: "PASS",
|
|
972
|
+
detail: `valid: ${config.resources.length} resource(s), merchant "${config.merchant.name}"${filledSuffix}`
|
|
973
|
+
});
|
|
974
|
+
} catch (err) {
|
|
975
|
+
const variable = isCommerceError(err) ? err.details?.["variable"] : void 0;
|
|
976
|
+
const hint = typeof variable === "string" ? `. Hint: ${unresolvedVariableHint(variable, manifestFound, chainDeploy)}` : "";
|
|
977
|
+
checks.push({
|
|
978
|
+
name: "Config",
|
|
979
|
+
status: "FAIL",
|
|
980
|
+
detail: `${err instanceof Error ? err.message : String(err)}${hint}`
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
const gatewayUrl = deriveGatewayUrl(options.gatewayUrl, config);
|
|
984
|
+
const health = await fetchJson(fetchImpl, `${gatewayUrl}/health`, CHECK_TIMEOUT_MS);
|
|
985
|
+
let gatewayUp = false;
|
|
986
|
+
if (!health.ok) {
|
|
987
|
+
checks.push({
|
|
988
|
+
name: "Gateway",
|
|
989
|
+
status: "FAIL",
|
|
990
|
+
detail: `unreachable at ${gatewayUrl} (${health.error ?? `HTTP ${health.status}`})`
|
|
991
|
+
});
|
|
992
|
+
} else {
|
|
993
|
+
gatewayUp = true;
|
|
994
|
+
const ready = await fetchJson(fetchImpl, `${gatewayUrl}/ready`, CHECK_TIMEOUT_MS);
|
|
995
|
+
checks.push(
|
|
996
|
+
ready.ok ? { name: "Gateway", status: "PASS", detail: `healthy and ready at ${gatewayUrl}` } : {
|
|
997
|
+
name: "Gateway",
|
|
998
|
+
status: "WARN",
|
|
999
|
+
detail: `healthy but not ready at ${gatewayUrl} (${ready.error ?? `HTTP ${ready.status}`})`
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
if (config === void 0) {
|
|
1004
|
+
checks.push({ name: "Backend", status: "WARN", detail: "skipped: config invalid" });
|
|
1005
|
+
} else if (config.resources.length === 0) {
|
|
1006
|
+
checks.push({ name: "Backend", status: "INFO", detail: "no resources configured" });
|
|
1007
|
+
} else {
|
|
1008
|
+
const resources = config.resources;
|
|
1009
|
+
const urls = [...new Set(resources.map((r) => substitutePathParams(r.handler.url)))];
|
|
1010
|
+
const results = await Promise.all(
|
|
1011
|
+
urls.map((url) => fetchJson(fetchImpl, url, CHECK_TIMEOUT_MS))
|
|
1012
|
+
);
|
|
1013
|
+
const reachable = results.filter((r) => r.status !== 0).length;
|
|
1014
|
+
const status = reachable === urls.length ? "PASS" : reachable === 0 ? "FAIL" : "WARN";
|
|
1015
|
+
checks.push({
|
|
1016
|
+
name: "Backend",
|
|
1017
|
+
status,
|
|
1018
|
+
detail: `${reachable}/${urls.length} backend host(s) reachable`
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
const wellKnown = gatewayUp ? await fetchJson(
|
|
1022
|
+
fetchImpl,
|
|
1023
|
+
`${gatewayUrl}/.well-known/agent-commerce`,
|
|
1024
|
+
CHECK_TIMEOUT_MS
|
|
1025
|
+
) : void 0;
|
|
1026
|
+
checks.push(...protocolChecks(config, wellKnown));
|
|
1027
|
+
checks.push(...ap2Checks(config));
|
|
1028
|
+
checks.push(...paymentChecks(config, wellKnown, gatewayUrl, chainDeploy));
|
|
1029
|
+
checks.push(...await storageChecks(config, createStore));
|
|
1030
|
+
if (wellKnown?.ok) {
|
|
1031
|
+
checks.push({
|
|
1032
|
+
name: "Protocol versions",
|
|
1033
|
+
status: "PASS",
|
|
1034
|
+
detail: "reported by gateway /.well-known/agent-commerce"
|
|
1035
|
+
});
|
|
1036
|
+
} else {
|
|
1037
|
+
const local = readVersionReport();
|
|
1038
|
+
checks.push({
|
|
1039
|
+
name: "Protocol versions",
|
|
1040
|
+
status: "INFO",
|
|
1041
|
+
detail: `gateway unreachable; local pins: ${local.pinned.map((p) => `${p.name}@${p.version}`).join(", ")}`
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
const scored = checks.filter((c) => c.status !== "INFO");
|
|
1045
|
+
const passed = scored.filter((c) => c.status === "PASS").length;
|
|
1046
|
+
const exitCode = checks.some((c) => c.status === "FAIL") ? 1 : 0;
|
|
1047
|
+
return { checks, score: { passed, total: scored.length }, exitCode };
|
|
1048
|
+
}
|
|
3632
1049
|
|
|
3633
1050
|
// src/openapi/refs.ts
|
|
3634
|
-
init_errors();
|
|
3635
1051
|
var MAX_REF_DEPTH = 100;
|
|
3636
1052
|
function isRefNode(value) {
|
|
3637
1053
|
return typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
@@ -3731,14 +1147,14 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3731
1147
|
}
|
|
3732
1148
|
const rootServers = document["servers"];
|
|
3733
1149
|
const rootSecurity = document["security"];
|
|
3734
|
-
for (const [
|
|
3735
|
-
if (
|
|
3736
|
-
if (!
|
|
1150
|
+
for (const [path, rawPathItem] of Object.entries(paths)) {
|
|
1151
|
+
if (path.startsWith("x-")) continue;
|
|
1152
|
+
if (!path.startsWith("/")) {
|
|
3737
1153
|
diagnostics.push({
|
|
3738
1154
|
severity: "warning",
|
|
3739
1155
|
code: "invalid-path-key",
|
|
3740
|
-
operation:
|
|
3741
|
-
message: `Skipped "${
|
|
1156
|
+
operation: path,
|
|
1157
|
+
message: `Skipped "${path}": a Paths key must start with "/"`
|
|
3742
1158
|
});
|
|
3743
1159
|
continue;
|
|
3744
1160
|
}
|
|
@@ -3748,8 +1164,8 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3748
1164
|
diagnostics.push({
|
|
3749
1165
|
severity: "warning",
|
|
3750
1166
|
code: "unsupported-method",
|
|
3751
|
-
operation: `${name.toUpperCase()} ${
|
|
3752
|
-
message: `Skipped ${name.toUpperCase()} ${
|
|
1167
|
+
operation: `${name.toUpperCase()} ${path}`,
|
|
1168
|
+
message: `Skipped ${name.toUpperCase()} ${path}: only GET, POST, PUT, PATCH and DELETE are supported`
|
|
3753
1169
|
});
|
|
3754
1170
|
};
|
|
3755
1171
|
for (const [key, rawOperation] of Object.entries(pathItem)) {
|
|
@@ -3766,9 +1182,9 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3766
1182
|
}
|
|
3767
1183
|
const operation = dereference(document, rawOperation).value;
|
|
3768
1184
|
if (!isRecord(operation)) continue;
|
|
3769
|
-
const where = `${method} ${
|
|
1185
|
+
const where = `${method} ${path}`;
|
|
3770
1186
|
const operationId = typeof operation["operationId"] === "string" ? operation["operationId"] : void 0;
|
|
3771
|
-
const resourceId = toResourceId(operationId, method,
|
|
1187
|
+
const resourceId = toResourceId(operationId, method, path);
|
|
3772
1188
|
const previous = claimed.get(resourceId);
|
|
3773
1189
|
if (previous !== void 0) {
|
|
3774
1190
|
throw new CommerceError(
|
|
@@ -3786,7 +1202,7 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3786
1202
|
diagnostics
|
|
3787
1203
|
);
|
|
3788
1204
|
if (server === void 0) continue;
|
|
3789
|
-
const backendUrl = `${server.replace(/\/+$/, "")}${
|
|
1205
|
+
const backendUrl = `${server.replace(/\/+$/, "")}${path}`;
|
|
3790
1206
|
const stray = findUnparsedBraceToken(backendUrl);
|
|
3791
1207
|
if (stray !== void 0) {
|
|
3792
1208
|
diagnostics.push({
|
|
@@ -3803,7 +1219,7 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3803
1219
|
operations.push({
|
|
3804
1220
|
resourceId,
|
|
3805
1221
|
method,
|
|
3806
|
-
path
|
|
1222
|
+
path,
|
|
3807
1223
|
backendUrl,
|
|
3808
1224
|
...operationId !== void 0 ? { operationId } : {},
|
|
3809
1225
|
tags: toArray(operation["tags"]).filter((tag) => typeof tag === "string"),
|
|
@@ -3823,10 +1239,10 @@ function discoverOperations(loaded, options = {}) {
|
|
|
3823
1239
|
}
|
|
3824
1240
|
return { operations, diagnostics };
|
|
3825
1241
|
}
|
|
3826
|
-
function toResourceId(operationId, method,
|
|
1242
|
+
function toResourceId(operationId, method, path) {
|
|
3827
1243
|
const fromOperationId = operationId === void 0 ? "" : normalizeId(operationId);
|
|
3828
1244
|
if (fromOperationId !== "") return fromOperationId;
|
|
3829
|
-
return normalizeId(`${method.toLowerCase()}_${
|
|
1245
|
+
return normalizeId(`${method.toLowerCase()}_${path}`);
|
|
3830
1246
|
}
|
|
3831
1247
|
function normalizeId(value) {
|
|
3832
1248
|
return value.replace(ID_DISALLOWED, "_").replace(/_+/g, "_").replace(/^[_.-]+|[_.-]+$/g, "").slice(0, MCP_TOOL_NAME_MAX_LENGTH);
|
|
@@ -3930,17 +1346,6 @@ function assertAbsoluteHttpUrl(value, label) {
|
|
|
3930
1346
|
function toArray(value) {
|
|
3931
1347
|
return Array.isArray(value) ? value : [];
|
|
3932
1348
|
}
|
|
3933
|
-
|
|
3934
|
-
// src/openapi/draft.ts
|
|
3935
|
-
init_is_record();
|
|
3936
|
-
|
|
3937
|
-
// src/openapi/request.ts
|
|
3938
|
-
init_execution();
|
|
3939
|
-
init_is_record();
|
|
3940
|
-
|
|
3941
|
-
// src/openapi/schema.ts
|
|
3942
|
-
init_errors();
|
|
3943
|
-
init_is_record();
|
|
3944
1349
|
var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
3945
1350
|
"object",
|
|
3946
1351
|
"string",
|
|
@@ -3963,7 +1368,7 @@ var ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
3963
1368
|
"definitions"
|
|
3964
1369
|
]);
|
|
3965
1370
|
function isPrimitiveSchema(schema) {
|
|
3966
|
-
const types =
|
|
1371
|
+
const types = typeList(schema["type"]);
|
|
3967
1372
|
if (types === void 0) return false;
|
|
3968
1373
|
return types.every((type) => type !== "object" && type !== "array" && SUPPORTED_TYPES.has(type));
|
|
3969
1374
|
}
|
|
@@ -4047,7 +1452,7 @@ function isCarriedKeyword(keyword) {
|
|
|
4047
1452
|
return keyword === "type" || keyword === "properties" || keyword === "required" || keyword === "additionalProperties" || keyword === "enum" || keyword === "items" || keyword === "nullable" || keyword === "$ref" || METADATA_KEYWORDS.includes(keyword);
|
|
4048
1453
|
}
|
|
4049
1454
|
function resolveTypes(schema) {
|
|
4050
|
-
const declared =
|
|
1455
|
+
const declared = typeList(schema["type"]);
|
|
4051
1456
|
if (declared === void 0) return void 0;
|
|
4052
1457
|
const known = declared.filter((type) => SUPPORTED_TYPES.has(type));
|
|
4053
1458
|
if (known.length === 0) {
|
|
@@ -4056,7 +1461,7 @@ function resolveTypes(schema) {
|
|
|
4056
1461
|
if (schema["nullable"] === true && !known.includes("null")) known.push("null");
|
|
4057
1462
|
return known;
|
|
4058
1463
|
}
|
|
4059
|
-
function
|
|
1464
|
+
function typeList(raw) {
|
|
4060
1465
|
if (typeof raw === "string") return [raw];
|
|
4061
1466
|
if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
|
|
4062
1467
|
return raw;
|
|
@@ -4074,7 +1479,7 @@ function mergeAllOf(document, schema, stack, dropped) {
|
|
|
4074
1479
|
const parts = [...branches, ...Object.keys(siblings).length > 0 ? [siblings] : []];
|
|
4075
1480
|
for (const branch of parts) {
|
|
4076
1481
|
const converted = convertNode(document, branch, stack, dropped);
|
|
4077
|
-
const types =
|
|
1482
|
+
const types = typeList(converted["type"]);
|
|
4078
1483
|
if (types !== void 0 && !types.includes("object")) {
|
|
4079
1484
|
throw new UnsupportedSchema("allOf mixes object and non-object schemas");
|
|
4080
1485
|
}
|
|
@@ -4507,13 +1912,10 @@ function renderResourcesYaml(result) {
|
|
|
4507
1912
|
});
|
|
4508
1913
|
return doc.toString({ lineWidth: 0 });
|
|
4509
1914
|
}
|
|
4510
|
-
|
|
4511
|
-
// src/openapi/load.ts
|
|
4512
|
-
init_errors();
|
|
4513
1915
|
var MAX_SOURCE_BYTES = 10 * 1024 * 1024;
|
|
4514
1916
|
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
|
|
4515
1917
|
var SUPPORTED_VERSIONS = /* @__PURE__ */ new Set(["3.0", "3.1", "3.2"]);
|
|
4516
|
-
function
|
|
1918
|
+
function invalid(message, details, cause) {
|
|
4517
1919
|
return new CommerceError("CONFIG_INVALID", message, {
|
|
4518
1920
|
details,
|
|
4519
1921
|
...cause !== void 0 ? { cause } : {}
|
|
@@ -4534,10 +1936,10 @@ async function readCapped(sourcePath, details) {
|
|
|
4534
1936
|
await handle.close();
|
|
4535
1937
|
}
|
|
4536
1938
|
} catch (error) {
|
|
4537
|
-
throw
|
|
1939
|
+
throw invalid(`OpenAPI document "${sourcePath}" could not be read`, details, error);
|
|
4538
1940
|
}
|
|
4539
1941
|
if (length > MAX_SOURCE_BYTES) {
|
|
4540
|
-
throw
|
|
1942
|
+
throw invalid(`OpenAPI document "${sourcePath}" is over the ${MAX_SOURCE_BYTES}-byte limit`, {
|
|
4541
1943
|
...details,
|
|
4542
1944
|
maxBytes: MAX_SOURCE_BYTES
|
|
4543
1945
|
});
|
|
@@ -4550,40 +1952,40 @@ async function loadOpenApiDocument(sourcePath) {
|
|
|
4550
1952
|
try {
|
|
4551
1953
|
stats = await stat(sourcePath);
|
|
4552
1954
|
} catch (error) {
|
|
4553
|
-
throw
|
|
1955
|
+
throw invalid(`OpenAPI document "${sourcePath}" could not be read`, details, error);
|
|
4554
1956
|
}
|
|
4555
1957
|
if (stats.isDirectory()) {
|
|
4556
|
-
throw
|
|
1958
|
+
throw invalid(`"${sourcePath}" is a directory, not an OpenAPI document`, details);
|
|
4557
1959
|
}
|
|
4558
1960
|
if (stats.size > MAX_SOURCE_BYTES) {
|
|
4559
|
-
throw
|
|
1961
|
+
throw invalid(
|
|
4560
1962
|
`OpenAPI document "${sourcePath}" is ${stats.size} bytes, over the ${MAX_SOURCE_BYTES}-byte limit`,
|
|
4561
1963
|
{ ...details, size: stats.size, maxBytes: MAX_SOURCE_BYTES }
|
|
4562
1964
|
);
|
|
4563
1965
|
}
|
|
4564
1966
|
const extension = extname(sourcePath).toLowerCase();
|
|
4565
1967
|
if (!SUPPORTED_EXTENSIONS.has(extension)) {
|
|
4566
|
-
throw
|
|
1968
|
+
throw invalid(
|
|
4567
1969
|
`OpenAPI document "${sourcePath}" has an unsupported extension "${extension || "(none)"}". Supported: .yaml, .yml, .json`,
|
|
4568
1970
|
details
|
|
4569
1971
|
);
|
|
4570
1972
|
}
|
|
4571
1973
|
const source = await readCapped(sourcePath, details);
|
|
4572
1974
|
if (source.trim() === "") {
|
|
4573
|
-
throw
|
|
1975
|
+
throw invalid(`OpenAPI document "${sourcePath}" is empty`, details);
|
|
4574
1976
|
}
|
|
4575
1977
|
let parsed;
|
|
4576
1978
|
try {
|
|
4577
1979
|
parsed = extension === ".json" ? JSON.parse(source) : parse(source);
|
|
4578
1980
|
} catch (error) {
|
|
4579
|
-
throw
|
|
1981
|
+
throw invalid(
|
|
4580
1982
|
`OpenAPI document "${sourcePath}" is not valid ${extension === ".json" ? "JSON" : "YAML"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
4581
1983
|
details,
|
|
4582
1984
|
error
|
|
4583
1985
|
);
|
|
4584
1986
|
}
|
|
4585
1987
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4586
|
-
throw
|
|
1988
|
+
throw invalid(`OpenAPI document "${sourcePath}" is not an object`, details);
|
|
4587
1989
|
}
|
|
4588
1990
|
const document = parsed;
|
|
4589
1991
|
const version = readVersion(document, sourcePath);
|
|
@@ -4591,7 +1993,7 @@ async function loadOpenApiDocument(sourcePath) {
|
|
|
4591
1993
|
const result = await validate(document);
|
|
4592
1994
|
if (!result.valid) {
|
|
4593
1995
|
const first = result.errors?.[0];
|
|
4594
|
-
throw
|
|
1996
|
+
throw invalid(
|
|
4595
1997
|
`OpenAPI document "${sourcePath}" is not a valid OpenAPI ${version} description: ${first ? `${first.message}${first.path ? ` (at ${first.path})` : ""}` : "unknown validation error"}`,
|
|
4596
1998
|
{ ...details, errors: result.errors?.slice(0, 10) ?? [] }
|
|
4597
1999
|
);
|
|
@@ -4601,18 +2003,18 @@ async function loadOpenApiDocument(sourcePath) {
|
|
|
4601
2003
|
function readVersion(document, sourcePath) {
|
|
4602
2004
|
const details = { sourcePath };
|
|
4603
2005
|
if (typeof document["swagger"] === "string") {
|
|
4604
|
-
throw
|
|
2006
|
+
throw invalid(
|
|
4605
2007
|
`"${sourcePath}" is a Swagger ${document["swagger"]} document. Only OpenAPI 3.0, 3.1 and 3.2 are supported - convert it first`,
|
|
4606
2008
|
details
|
|
4607
2009
|
);
|
|
4608
2010
|
}
|
|
4609
2011
|
const declared = document["openapi"];
|
|
4610
2012
|
if (typeof declared !== "string") {
|
|
4611
|
-
throw
|
|
2013
|
+
throw invalid(`"${sourcePath}" has no "openapi" version field`, details);
|
|
4612
2014
|
}
|
|
4613
2015
|
const featureVersion = /^(\d+\.\d+)(?:\.|$)/.exec(declared)?.[1];
|
|
4614
2016
|
if (featureVersion === void 0 || !SUPPORTED_VERSIONS.has(featureVersion)) {
|
|
4615
|
-
throw
|
|
2017
|
+
throw invalid(
|
|
4616
2018
|
`OpenAPI version "${declared}" is not supported. Supported: 3.0.x, 3.1.x, 3.2.x`,
|
|
4617
2019
|
{ ...details, version: declared }
|
|
4618
2020
|
);
|
|
@@ -4634,13 +2036,13 @@ function rejectUnsupportedReferences(document, sourcePath) {
|
|
|
4634
2036
|
for (const [key, value] of Object.entries(node)) {
|
|
4635
2037
|
if (key === "$ref" && typeof value === "string") {
|
|
4636
2038
|
if (!value.startsWith("#")) {
|
|
4637
|
-
throw
|
|
2039
|
+
throw invalid(
|
|
4638
2040
|
`OpenAPI document "${sourcePath}" contains an external reference "${value}". Only internal references (#/...) are supported; the importer performs no network or filesystem lookups`,
|
|
4639
2041
|
{ sourcePath, ref: value }
|
|
4640
2042
|
);
|
|
4641
2043
|
}
|
|
4642
2044
|
if (!isPercentDecodable(value)) {
|
|
4643
|
-
throw
|
|
2045
|
+
throw invalid(
|
|
4644
2046
|
`OpenAPI document "${sourcePath}" contains a reference "${value}" with a malformed percent-escape`,
|
|
4645
2047
|
{ sourcePath, ref: value }
|
|
4646
2048
|
);
|
|
@@ -4811,22 +2213,16 @@ function describe(error) {
|
|
|
4811
2213
|
return error instanceof Error ? error.message : String(error);
|
|
4812
2214
|
}
|
|
4813
2215
|
|
|
4814
|
-
// src/cli/commands/init.ts
|
|
4815
|
-
init_filename();
|
|
4816
|
-
|
|
4817
2216
|
// src/cli/commands/validate.ts
|
|
4818
|
-
|
|
4819
|
-
function formatConfigError(err, manifestFound = false) {
|
|
2217
|
+
function formatConfigError(err, manifestFound = false, chainDeploy = hasChainDeployScript()) {
|
|
4820
2218
|
if (isCommerceError(err)) {
|
|
4821
2219
|
const lines = [`FAIL ${err.code}: ${err.message}`];
|
|
4822
2220
|
if (err.details !== void 0) {
|
|
4823
2221
|
lines.push(` details: ${JSON.stringify(err.details)}`);
|
|
4824
2222
|
}
|
|
4825
2223
|
const variable = err.details?.["variable"];
|
|
4826
|
-
if (
|
|
4827
|
-
lines.push(
|
|
4828
|
-
` hint: "npm run chain:deploy" writes ${LOCAL_CHAIN_MANIFEST_PATH}, which validate/doctor read automatically to fill local X402_*/MERCHANT_WALLET placeholders.`
|
|
4829
|
-
);
|
|
2224
|
+
if (typeof variable === "string") {
|
|
2225
|
+
lines.push(` hint: ${unresolvedVariableHint(variable, manifestFound, chainDeploy)}`);
|
|
4830
2226
|
}
|
|
4831
2227
|
return lines.join("\n");
|
|
4832
2228
|
}
|
|
@@ -4836,14 +2232,14 @@ function formatConfigError(err, manifestFound = false) {
|
|
|
4836
2232
|
return `FAIL ${String(err)}`;
|
|
4837
2233
|
}
|
|
4838
2234
|
async function runValidate(options, io, deps = {}) {
|
|
4839
|
-
const
|
|
2235
|
+
const loadConfig = deps.loadConfig ?? loadConfigDynamic;
|
|
4840
2236
|
const fillEnvFromManifest = deps.fillEnvFromManifest ?? fillEnvFromLocalChainManifest;
|
|
4841
2237
|
const { env, filled, manifestFound } = fillEnvFromManifest(process.env);
|
|
4842
2238
|
if (filled.length > 0) {
|
|
4843
2239
|
io.stdout(`using local chain manifest ${LOCAL_CHAIN_MANIFEST_PATH} for ${filled.join(", ")}`);
|
|
4844
2240
|
}
|
|
4845
2241
|
try {
|
|
4846
|
-
const config = await
|
|
2242
|
+
const config = await loadConfig({
|
|
4847
2243
|
...options.configPath !== void 0 ? { path: options.configPath } : {},
|
|
4848
2244
|
env
|
|
4849
2245
|
});
|
|
@@ -4860,7 +2256,9 @@ async function runValidate(options, io, deps = {}) {
|
|
|
4860
2256
|
io.stdout(` authorization: ap2=${onOff(config.authorization?.ap2.enabled)}`);
|
|
4861
2257
|
return 0;
|
|
4862
2258
|
} catch (err) {
|
|
4863
|
-
io.stderr(
|
|
2259
|
+
io.stderr(
|
|
2260
|
+
formatConfigError(err, manifestFound, (deps.hasChainDeployScript ?? hasChainDeployScript)())
|
|
2261
|
+
);
|
|
4864
2262
|
return 1;
|
|
4865
2263
|
}
|
|
4866
2264
|
}
|
|
@@ -4928,7 +2326,7 @@ async function collectAnswersInteractive() {
|
|
|
4928
2326
|
validate: (value) => {
|
|
4929
2327
|
const trimmed = (value ?? "").trim();
|
|
4930
2328
|
if (trimmed.length === 0) return "Required";
|
|
4931
|
-
return
|
|
2329
|
+
return isEvmAddress(trimmed) ? void 0 : "Must be a 0x-prefixed 20-byte EVM address (42 characters)";
|
|
4932
2330
|
}
|
|
4933
2331
|
});
|
|
4934
2332
|
if (clack.isCancel(answer)) {
|
|
@@ -4942,11 +2340,11 @@ async function collectAnswersInteractive() {
|
|
|
4942
2340
|
}
|
|
4943
2341
|
async function runInit(options, io, deps = {}) {
|
|
4944
2342
|
const outputPath = options.outputPath ?? DEFAULT_CONFIG_FILENAME;
|
|
4945
|
-
const
|
|
2343
|
+
const loadConfig = deps.loadConfig ?? loadConfigDynamic;
|
|
4946
2344
|
const parseConfigInMemory = deps.parseConfig ?? parseConfigDynamic;
|
|
4947
2345
|
const fileExists = deps.fileExists ?? existsSync;
|
|
4948
|
-
const writeFileImpl = deps.writeFile ?? (async (
|
|
4949
|
-
await writeFile(
|
|
2346
|
+
const writeFileImpl = deps.writeFile ?? (async (path, content) => {
|
|
2347
|
+
await writeFile(path, content, "utf8");
|
|
4950
2348
|
});
|
|
4951
2349
|
if (fileExists(outputPath) && options.force !== true) {
|
|
4952
2350
|
io.stderr(`FAIL ${outputPath} already exists. Re-run with --force to overwrite.`);
|
|
@@ -4971,7 +2369,7 @@ async function runInit(options, io, deps = {}) {
|
|
|
4971
2369
|
io.stdout(`PASS Wrote ${outputPath}`);
|
|
4972
2370
|
io.stdout(`PASS ${outputPath} is valid: ${config.resources.length} resource(s)`);
|
|
4973
2371
|
try {
|
|
4974
|
-
await
|
|
2372
|
+
await loadConfig({ path: outputPath });
|
|
4975
2373
|
} catch (err) {
|
|
4976
2374
|
io.stderr(formatConfigError(err));
|
|
4977
2375
|
io.stderr(
|
|
@@ -4979,11 +2377,17 @@ async function runInit(options, io, deps = {}) {
|
|
|
4979
2377
|
);
|
|
4980
2378
|
return 1;
|
|
4981
2379
|
}
|
|
2380
|
+
const peers = [
|
|
2381
|
+
...config.payments.x402?.enabled === true ? ["x402"] : [],
|
|
2382
|
+
...config.protocols.mcp.enabled ? ["MCP"] : []
|
|
2383
|
+
];
|
|
2384
|
+
const startNote = peers.length > 0 ? `needs the ${peers.join(" and ")} peers` : "needs no optional peers";
|
|
4982
2385
|
io.stdout("");
|
|
4983
2386
|
io.stdout("Next steps:");
|
|
4984
2387
|
io.stdout(` agent-commerce validate --config ${outputPath}`);
|
|
4985
|
-
io.stdout(
|
|
4986
|
-
io.stdout(` agent-commerce doctor --config ${outputPath}`);
|
|
2388
|
+
io.stdout(` agent-commerce start --config ${outputPath} # ${startNote}`);
|
|
2389
|
+
io.stdout(` agent-commerce doctor --config ${outputPath} # from a second terminal`);
|
|
2390
|
+
io.stdout(" docker compose up # or the whole local demo stack, from a repo clone");
|
|
4987
2391
|
return 0;
|
|
4988
2392
|
}
|
|
4989
2393
|
|
|
@@ -5015,23 +2419,45 @@ var processIo = {
|
|
|
5015
2419
|
};
|
|
5016
2420
|
|
|
5017
2421
|
// src/cli/program.ts
|
|
2422
|
+
var ENV_FILE_HELP = "load environment variables from this file; variables already set win";
|
|
5018
2423
|
function buildProgram(io = processIo) {
|
|
5019
2424
|
const program = new Command();
|
|
5020
|
-
program.name("agent-commerce").description("CLI for the Agent Commerce Gateway: init, import, validate, doctor, demo.").version(readVersionReport().cliVersion, "-v, --version", "Print the CLI version.").exitOverride().configureOutput({
|
|
2425
|
+
program.name("agent-commerce").description("CLI for the Agent Commerce Gateway: init, import, validate, start, doctor, demo.").version(readVersionReport().cliVersion, "-v, --version", "Print the CLI version.").exitOverride().configureOutput({
|
|
5021
2426
|
writeOut: (str) => io.stdout(str.replace(/\n+$/, "")),
|
|
5022
2427
|
writeErr: (str) => io.stderr(str.replace(/\n+$/, ""))
|
|
2428
|
+
}).hook("preAction", (_program, command) => {
|
|
2429
|
+
const { envFile } = command.opts();
|
|
2430
|
+
if (envFile === void 0) return;
|
|
2431
|
+
try {
|
|
2432
|
+
process.loadEnvFile(envFile);
|
|
2433
|
+
} catch (error) {
|
|
2434
|
+
command.error(
|
|
2435
|
+
`error: cannot read --env-file "${envFile}": ${error instanceof Error ? error.message : String(error)}`
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
5023
2438
|
});
|
|
5024
2439
|
program.command("version").description("Print the CLI version and pinned protocol/SDK versions.").action(() => {
|
|
5025
2440
|
process.exitCode = runVersion(io);
|
|
5026
2441
|
});
|
|
5027
|
-
program.command("validate").description(`Validate ${DEFAULT_CONFIG_FILENAME} and exit non-zero if it is invalid.`).option("--config <path>", `path to ${DEFAULT_CONFIG_FILENAME}`).action(async (opts) => {
|
|
2442
|
+
program.command("validate").description(`Validate ${DEFAULT_CONFIG_FILENAME} and exit non-zero if it is invalid.`).option("--config <path>", `path to ${DEFAULT_CONFIG_FILENAME}`).option("--env-file <path>", ENV_FILE_HELP).action(async (opts) => {
|
|
5028
2443
|
const code = await runValidate(
|
|
5029
2444
|
opts.config !== void 0 ? { configPath: opts.config } : {},
|
|
5030
2445
|
io
|
|
5031
2446
|
);
|
|
5032
2447
|
process.exitCode = code;
|
|
5033
2448
|
});
|
|
5034
|
-
program.command("
|
|
2449
|
+
program.command("start").description(
|
|
2450
|
+
`Start the gateway from ${DEFAULT_CONFIG_FILENAME} and serve until SIGTERM or SIGINT.`
|
|
2451
|
+
).option("--config <path>", `path to ${DEFAULT_CONFIG_FILENAME}`).option("--env-file <path>", ENV_FILE_HELP).action(async (opts) => {
|
|
2452
|
+
try {
|
|
2453
|
+
const { runGateway } = await import('./run-5MB2D45X.js');
|
|
2454
|
+
await runGateway(opts.config !== void 0 ? { configPath: opts.config } : {});
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
io.stderr(formatConfigError(error, false, false));
|
|
2457
|
+
process.exit(1);
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
program.command("doctor").description("Diagnose config, gateway, backend, protocol and payment health.").option("--config <path>", `path to ${DEFAULT_CONFIG_FILENAME}`).option("--env-file <path>", ENV_FILE_HELP).option(
|
|
5035
2461
|
"--gateway <url>",
|
|
5036
2462
|
"gateway base URL (default: derived from config, else http://localhost:8080)"
|
|
5037
2463
|
).option("--json", "emit machine-readable JSON instead of a formatted report", false).action(async (opts) => {
|
|
@@ -5086,14 +2512,18 @@ function collect(value, previous) {
|
|
|
5086
2512
|
}
|
|
5087
2513
|
|
|
5088
2514
|
// src/cli/index.ts
|
|
2515
|
+
var serving = false;
|
|
5089
2516
|
for (const stream of [process.stdout, process.stderr]) {
|
|
5090
2517
|
stream.on("error", (err) => {
|
|
5091
|
-
if (err.code
|
|
5092
|
-
|
|
2518
|
+
if (err.code !== "EPIPE") throw err;
|
|
2519
|
+
if (!serving) process.exit(0);
|
|
5093
2520
|
});
|
|
5094
2521
|
}
|
|
5095
2522
|
async function main() {
|
|
5096
2523
|
const program = buildProgram();
|
|
2524
|
+
program.hook("preAction", (_program, command) => {
|
|
2525
|
+
serving = command.name() === "start";
|
|
2526
|
+
});
|
|
5097
2527
|
try {
|
|
5098
2528
|
await program.parseAsync(process.argv);
|
|
5099
2529
|
} catch (err) {
|