@devlab.group/agent-commerce 0.1.0-alpha.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/LICENSE +17 -0
- package/README.md +324 -0
- package/dist/chunk-CZ6ZDAYD.js +192 -0
- package/dist/chunk-CZ6ZDAYD.js.map +1 -0
- package/dist/cli/index.js +2396 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +412 -0
- package/dist/index.js +2930 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +68 -0
- package/dist/mcp.js +400 -0
- package/dist/mcp.js.map +1 -0
- package/dist/protocol-adapter-C0goMgLK.d.ts +223 -0
- package/dist/runtime-LCk2TzGN.d.ts +277 -0
- package/dist/x402.d.ts +77 -0
- package/dist/x402.js +650 -0
- package/dist/x402.js.map +1 -0
- package/package.json +136 -0
|
@@ -0,0 +1,2396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { ZodError, z } from 'zod';
|
|
3
|
+
import { existsSync, mkdirSync, accessSync, constants, readFileSync, chmodSync, promises } from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { resolve, dirname, join } from 'path';
|
|
6
|
+
import { stringify, parse, YAMLError } from 'yaml';
|
|
7
|
+
import { CommanderError, Command } from 'commander';
|
|
8
|
+
import picocolors from 'picocolors';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import Database from 'better-sqlite3';
|
|
11
|
+
import { createRequire } from 'module';
|
|
12
|
+
import { writeFile } from 'fs/promises';
|
|
13
|
+
import * as clack from '@clack/prompts';
|
|
14
|
+
|
|
15
|
+
var __defProp = Object.defineProperty;
|
|
16
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
17
|
+
var __esm = (fn, res) => function __init() {
|
|
18
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/config/filename.ts
|
|
26
|
+
var DEFAULT_CONFIG_FILENAME;
|
|
27
|
+
var init_filename = __esm({
|
|
28
|
+
"src/config/filename.ts"() {
|
|
29
|
+
DEFAULT_CONFIG_FILENAME = "config.yaml";
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// src/core/domain/resource.ts
|
|
34
|
+
var init_resource = __esm({
|
|
35
|
+
"src/core/domain/resource.ts"() {
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// src/core/errors/codes.ts
|
|
40
|
+
var COMMERCE_ERROR_HTTP_STATUS, RETRYABLE_ERROR_CODES;
|
|
41
|
+
var init_codes = __esm({
|
|
42
|
+
"src/core/errors/codes.ts"() {
|
|
43
|
+
COMMERCE_ERROR_HTTP_STATUS = {
|
|
44
|
+
CONFIG_INVALID: 500,
|
|
45
|
+
RESOURCE_NOT_FOUND: 404,
|
|
46
|
+
INPUT_INVALID: 400,
|
|
47
|
+
PAYMENT_REQUIRED: 402,
|
|
48
|
+
PAYMENT_INVALID: 402,
|
|
49
|
+
PAYMENT_REPLAYED: 409,
|
|
50
|
+
PAYMENT_PROVIDER_UNAVAILABLE: 503,
|
|
51
|
+
PAYMENT_SETTLEMENT_FAILED: 502,
|
|
52
|
+
BACKEND_TIMEOUT: 504,
|
|
53
|
+
BACKEND_ERROR: 502,
|
|
54
|
+
PROTOCOL_UNSUPPORTED: 501,
|
|
55
|
+
GATEWAY_BUSY: 503,
|
|
56
|
+
STORAGE_ERROR: 500,
|
|
57
|
+
INTERNAL_ERROR: 500
|
|
58
|
+
};
|
|
59
|
+
RETRYABLE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
60
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
61
|
+
"BACKEND_TIMEOUT",
|
|
62
|
+
// Load shedding is transient by definition: the caller should back off and
|
|
63
|
+
// try again. Before this code existed the MCP adapter's queue-full path threw
|
|
64
|
+
// PROTOCOL_UNSUPPORTED — HTTP 501, retryable:false — so an agent framework
|
|
65
|
+
// keyed on the machine-readable flag (which exists precisely so clients need
|
|
66
|
+
// not parse prose) would read a momentary throttle as a permanent capability
|
|
67
|
+
// gap and stop calling the tool.
|
|
68
|
+
"GATEWAY_BUSY"
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// src/core/errors/commerce-error.ts
|
|
74
|
+
function isCommerceError(value) {
|
|
75
|
+
return value instanceof CommerceError;
|
|
76
|
+
}
|
|
77
|
+
function toCommerceError(value, fallbackCode = "INTERNAL_ERROR", fallbackMessage = "Unexpected internal error") {
|
|
78
|
+
if (isCommerceError(value)) return value;
|
|
79
|
+
return new CommerceError(fallbackCode, fallbackMessage, { cause: value });
|
|
80
|
+
}
|
|
81
|
+
var CommerceError;
|
|
82
|
+
var init_commerce_error = __esm({
|
|
83
|
+
"src/core/errors/commerce-error.ts"() {
|
|
84
|
+
init_codes();
|
|
85
|
+
CommerceError = class _CommerceError extends Error {
|
|
86
|
+
code;
|
|
87
|
+
httpStatus;
|
|
88
|
+
retryable;
|
|
89
|
+
details;
|
|
90
|
+
requestId;
|
|
91
|
+
resourceId;
|
|
92
|
+
constructor(code, message, options = {}) {
|
|
93
|
+
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
94
|
+
this.name = "CommerceError";
|
|
95
|
+
this.code = code;
|
|
96
|
+
this.httpStatus = options.httpStatus ?? COMMERCE_ERROR_HTTP_STATUS[code];
|
|
97
|
+
this.retryable = RETRYABLE_ERROR_CODES.has(code);
|
|
98
|
+
if (options.details !== void 0) this.details = options.details;
|
|
99
|
+
if (options.requestId !== void 0) this.requestId = options.requestId;
|
|
100
|
+
if (options.resourceId !== void 0) this.resourceId = options.resourceId;
|
|
101
|
+
Error.captureStackTrace?.(this, _CommerceError);
|
|
102
|
+
}
|
|
103
|
+
/** Non-secret, client-safe representation. */
|
|
104
|
+
toInfo() {
|
|
105
|
+
return {
|
|
106
|
+
code: this.code,
|
|
107
|
+
message: this.message,
|
|
108
|
+
httpStatus: this.httpStatus,
|
|
109
|
+
retryable: this.retryable,
|
|
110
|
+
...this.requestId !== void 0 ? { requestId: this.requestId } : {},
|
|
111
|
+
...this.resourceId !== void 0 ? { resourceId: this.resourceId } : {},
|
|
112
|
+
...this.details !== void 0 ? { details: this.details } : {}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// src/core/errors/index.ts
|
|
120
|
+
var init_errors = __esm({
|
|
121
|
+
"src/core/errors/index.ts"() {
|
|
122
|
+
init_codes();
|
|
123
|
+
init_commerce_error();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// src/core/interfaces/logger.ts
|
|
128
|
+
var NOOP_LOGGER;
|
|
129
|
+
var init_logger = __esm({
|
|
130
|
+
"src/core/interfaces/logger.ts"() {
|
|
131
|
+
NOOP_LOGGER = {
|
|
132
|
+
debug: () => {
|
|
133
|
+
},
|
|
134
|
+
info: () => {
|
|
135
|
+
},
|
|
136
|
+
warn: () => {
|
|
137
|
+
},
|
|
138
|
+
error: () => {
|
|
139
|
+
},
|
|
140
|
+
child: () => NOOP_LOGGER
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// src/core/execution/backend-http.ts
|
|
146
|
+
function extractPathParameterNames(url) {
|
|
147
|
+
return [...url.matchAll(PATH_PARAM_PATTERN)].map((match) => match[1]);
|
|
148
|
+
}
|
|
149
|
+
function findUnparsedBraceToken(url) {
|
|
150
|
+
const residue = url.replace(PATH_PARAM_PATTERN, "");
|
|
151
|
+
const index = residue.search(/[{}]/);
|
|
152
|
+
if (index === -1) return void 0;
|
|
153
|
+
const token = /\{[^{}]*\}?|\}/.exec(residue.slice(index));
|
|
154
|
+
return token?.[0] ?? residue[index];
|
|
155
|
+
}
|
|
156
|
+
var PATH_PARAM_PATTERN;
|
|
157
|
+
var init_backend_http = __esm({
|
|
158
|
+
"src/core/execution/backend-http.ts"() {
|
|
159
|
+
init_resource();
|
|
160
|
+
init_errors();
|
|
161
|
+
init_logger();
|
|
162
|
+
PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// src/core/execution/event-bus.ts
|
|
167
|
+
var init_event_bus = __esm({
|
|
168
|
+
"src/core/execution/event-bus.ts"() {
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// src/core/domain/wire.ts
|
|
173
|
+
var PAYMENT_INPUT_FIELD;
|
|
174
|
+
var init_wire = __esm({
|
|
175
|
+
"src/core/domain/wire.ts"() {
|
|
176
|
+
PAYMENT_INPUT_FIELD = "_payment";
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// src/core/execution/validation.ts
|
|
181
|
+
function typeList(raw) {
|
|
182
|
+
if (typeof raw === "string") return [raw];
|
|
183
|
+
if (Array.isArray(raw) && raw.every((entry) => typeof entry === "string")) {
|
|
184
|
+
return raw;
|
|
185
|
+
}
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
function isObjectSchemaNode(schema) {
|
|
189
|
+
const types = typeList(schema["type"]);
|
|
190
|
+
if (types !== void 0) return types.includes("object");
|
|
191
|
+
return Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required");
|
|
192
|
+
}
|
|
193
|
+
var init_validation = __esm({
|
|
194
|
+
"src/core/execution/validation.ts"() {
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// src/core/execution/pipeline.ts
|
|
199
|
+
var init_pipeline = __esm({
|
|
200
|
+
"src/core/execution/pipeline.ts"() {
|
|
201
|
+
init_wire();
|
|
202
|
+
init_errors();
|
|
203
|
+
init_backend_http();
|
|
204
|
+
init_validation();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// src/core/execution/registry.ts
|
|
209
|
+
var init_registry = __esm({
|
|
210
|
+
"src/core/execution/registry.ts"() {
|
|
211
|
+
init_errors();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// src/core/execution/index.ts
|
|
216
|
+
var init_execution = __esm({
|
|
217
|
+
"src/core/execution/index.ts"() {
|
|
218
|
+
init_backend_http();
|
|
219
|
+
init_event_bus();
|
|
220
|
+
init_pipeline();
|
|
221
|
+
init_registry();
|
|
222
|
+
init_validation();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// src/core/domain/event.ts
|
|
227
|
+
var init_event = __esm({
|
|
228
|
+
"src/core/domain/event.ts"() {
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// src/core/interfaces/protocol-adapter.ts
|
|
233
|
+
var init_protocol_adapter = __esm({
|
|
234
|
+
"src/core/interfaces/protocol-adapter.ts"() {
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// src/core/interfaces/runtime.ts
|
|
239
|
+
var systemClock;
|
|
240
|
+
var init_runtime = __esm({
|
|
241
|
+
"src/core/interfaces/runtime.ts"() {
|
|
242
|
+
systemClock = {
|
|
243
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
244
|
+
nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
245
|
+
monotonicMs: () => performance.now()
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// src/core/public-types.ts
|
|
251
|
+
var init_public_types = __esm({
|
|
252
|
+
"src/core/public-types.ts"() {
|
|
253
|
+
init_event();
|
|
254
|
+
init_resource();
|
|
255
|
+
init_wire();
|
|
256
|
+
init_errors();
|
|
257
|
+
init_logger();
|
|
258
|
+
init_protocol_adapter();
|
|
259
|
+
init_runtime();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// src/core/index.ts
|
|
264
|
+
var init_core = __esm({
|
|
265
|
+
"src/core/index.ts"() {
|
|
266
|
+
init_public_types();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// src/config/env.ts
|
|
271
|
+
function substituteEnv(value, env, path2 = "$") {
|
|
272
|
+
if (typeof value === "string") {
|
|
273
|
+
return substituteString(value, env, path2);
|
|
274
|
+
}
|
|
275
|
+
if (Array.isArray(value)) {
|
|
276
|
+
return value.map((entry, index) => substituteEnv(entry, env, `${path2}[${index}]`));
|
|
277
|
+
}
|
|
278
|
+
if (isPlainObject(value)) {
|
|
279
|
+
const result = {};
|
|
280
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
281
|
+
result[key] = substituteEnv(entry, env, `${path2}.${key}`);
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
return value;
|
|
286
|
+
}
|
|
287
|
+
function substituteString(value, env, path2) {
|
|
288
|
+
const substituted = substituteOnce(value, env, path2);
|
|
289
|
+
PLACEHOLDER_PATTERN.lastIndex = 0;
|
|
290
|
+
if (PLACEHOLDER_PATTERN.test(substituted)) {
|
|
291
|
+
PLACEHOLDER_PATTERN.lastIndex = 0;
|
|
292
|
+
const residual = PLACEHOLDER_PATTERN.exec(substituted)?.[0] ?? "${\u2026}";
|
|
293
|
+
PLACEHOLDER_PATTERN.lastIndex = 0;
|
|
294
|
+
throw new CommerceError(
|
|
295
|
+
"CONFIG_INVALID",
|
|
296
|
+
`Configuration value at "${path2}" still contains the unresolved placeholder "${residual}" after substitution \u2014 nested placeholders (e.g. "\${A:-\${B}}") are not supported. Use a plain default, or set the variable.`,
|
|
297
|
+
{ details: { path: path2, residual } }
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return substituted;
|
|
301
|
+
}
|
|
302
|
+
function substituteOnce(value, env, path2) {
|
|
303
|
+
return value.replace(
|
|
304
|
+
PLACEHOLDER_PATTERN,
|
|
305
|
+
(_match, name, hasDefault, def) => {
|
|
306
|
+
const envValue = env[name];
|
|
307
|
+
if (hasDefault !== void 0) {
|
|
308
|
+
if (envValue === void 0 || envValue === "") return def ?? "";
|
|
309
|
+
return envValue;
|
|
310
|
+
}
|
|
311
|
+
if (envValue === void 0) {
|
|
312
|
+
throw new CommerceError(
|
|
313
|
+
"CONFIG_INVALID",
|
|
314
|
+
`Unresolved environment variable "\${${name}}" referenced at config path "${path2}"`,
|
|
315
|
+
{ details: { variable: name, path: path2 } }
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return envValue;
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
function isPlainObject(value) {
|
|
323
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
324
|
+
}
|
|
325
|
+
var PLACEHOLDER_PATTERN;
|
|
326
|
+
var init_env = __esm({
|
|
327
|
+
"src/config/env.ts"() {
|
|
328
|
+
init_core();
|
|
329
|
+
PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
function parseConfig(raw, env) {
|
|
333
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
334
|
+
throw new CommerceError(
|
|
335
|
+
"CONFIG_INVALID",
|
|
336
|
+
'Configuration root must be a mapping (object) at "$"',
|
|
337
|
+
{
|
|
338
|
+
details: { path: "$" }
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
checkVersion(raw);
|
|
343
|
+
const substituted = substituteEnv(raw, env);
|
|
344
|
+
const parsed = parseWithZod(RawConfigSchema, substituted);
|
|
345
|
+
return normalise(parsed);
|
|
346
|
+
}
|
|
347
|
+
function checkVersion(raw) {
|
|
348
|
+
if (!("version" in raw)) {
|
|
349
|
+
throw new CommerceError("CONFIG_INVALID", 'Configuration is missing required field "version"', {
|
|
350
|
+
details: { path: "version" }
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const rawVersion = raw["version"];
|
|
354
|
+
const version = typeof rawVersion === "string" ? Number(rawVersion) : rawVersion;
|
|
355
|
+
if (version !== SUPPORTED_CONFIG_VERSION) {
|
|
356
|
+
throw new CommerceError(
|
|
357
|
+
"CONFIG_INVALID",
|
|
358
|
+
`Unsupported config version "${String(rawVersion)}": this gateway only supports version ${SUPPORTED_CONFIG_VERSION}`,
|
|
359
|
+
{ details: { path: "version", supported: SUPPORTED_CONFIG_VERSION } }
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function parseWithZod(schema, value) {
|
|
364
|
+
const result = schema.safeParse(value);
|
|
365
|
+
if (!result.success) {
|
|
366
|
+
throw zodErrorToConfigError(result.error);
|
|
367
|
+
}
|
|
368
|
+
return result.data;
|
|
369
|
+
}
|
|
370
|
+
function zodErrorToConfigError(error) {
|
|
371
|
+
const issues = error.issues.map(describeIssue);
|
|
372
|
+
const first = issues[0];
|
|
373
|
+
const message = first ? `Configuration invalid at "${first.path}": ${first.message}` : "Configuration invalid";
|
|
374
|
+
return new CommerceError("CONFIG_INVALID", message, { details: { issues } });
|
|
375
|
+
}
|
|
376
|
+
function describeIssue(issue) {
|
|
377
|
+
const path2 = issue.path.length > 0 ? issue.path.join(".") : "$";
|
|
378
|
+
return { path: path2, message: issue.message, code: issue.code };
|
|
379
|
+
}
|
|
380
|
+
function toNumber(value, path2, bounds = {}) {
|
|
381
|
+
if (typeof value === "string" && !/^\s*\d+\s*$/.test(value)) {
|
|
382
|
+
throw new CommerceError(
|
|
383
|
+
"CONFIG_INVALID",
|
|
384
|
+
`Configuration value at "${path2}" must be an integer written in decimal digits (got ${value === "" ? "an empty string" : `"${value}"`})`,
|
|
385
|
+
{ details: { path: path2 } }
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
389
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
390
|
+
throw new CommerceError(
|
|
391
|
+
"CONFIG_INVALID",
|
|
392
|
+
`Configuration value at "${path2}" must be an integer`,
|
|
393
|
+
{
|
|
394
|
+
details: { path: path2 }
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
if (bounds.min !== void 0 && n < bounds.min) {
|
|
399
|
+
throw new CommerceError(
|
|
400
|
+
"CONFIG_INVALID",
|
|
401
|
+
`Configuration value at "${path2}" must be >= ${bounds.min}`,
|
|
402
|
+
{
|
|
403
|
+
details: { path: path2 }
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (bounds.max !== void 0 && n > bounds.max) {
|
|
408
|
+
throw new CommerceError(
|
|
409
|
+
"CONFIG_INVALID",
|
|
410
|
+
`Configuration value at "${path2}" must be <= ${bounds.max}`,
|
|
411
|
+
{
|
|
412
|
+
details: { path: path2 }
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return n;
|
|
417
|
+
}
|
|
418
|
+
function toBoolean(value, path2) {
|
|
419
|
+
if (typeof value === "boolean") return value;
|
|
420
|
+
const normalized = value.trim().toLowerCase();
|
|
421
|
+
if (normalized === "true") return true;
|
|
422
|
+
if (normalized === "false") return false;
|
|
423
|
+
throw new CommerceError(
|
|
424
|
+
"CONFIG_INVALID",
|
|
425
|
+
`Configuration value at "${path2}" must be a boolean ("true" or "false")`,
|
|
426
|
+
{ details: { path: path2 } }
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
function normalise(raw) {
|
|
430
|
+
const protocols = {
|
|
431
|
+
http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
|
|
432
|
+
mcp: {
|
|
433
|
+
enabled: toBoolean(raw.protocols.mcp.enabled, "protocols.mcp.enabled"),
|
|
434
|
+
mountPath: raw.protocols.mcp.mountPath
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
const x402Raw = raw.payments.x402;
|
|
438
|
+
if (x402Raw?.facilitator.mode === "remote") {
|
|
439
|
+
throw new CommerceError(
|
|
440
|
+
"CONFIG_INVALID",
|
|
441
|
+
'payments.x402.facilitator.mode "remote" is not supported in v0.1.0-alpha; use "local" (the deterministic dev-chain facilitator)',
|
|
442
|
+
{ details: { path: "payments.x402.facilitator.mode" } }
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const x402 = x402Raw ? {
|
|
446
|
+
enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
|
|
447
|
+
network: x402Raw.network,
|
|
448
|
+
rpcUrl: x402Raw.rpcUrl,
|
|
449
|
+
asset: x402Raw.asset,
|
|
450
|
+
assetName: x402Raw.assetName,
|
|
451
|
+
assetVersion: x402Raw.assetVersion,
|
|
452
|
+
assetDecimals: toNumber(x402Raw.assetDecimals, "payments.x402.assetDecimals", {
|
|
453
|
+
min: 0,
|
|
454
|
+
max: 36
|
|
455
|
+
}),
|
|
456
|
+
payTo: x402Raw.payTo,
|
|
457
|
+
maxTimeoutSeconds: toNumber(x402Raw.maxTimeoutSeconds, "payments.x402.maxTimeoutSeconds", {
|
|
458
|
+
min: 1
|
|
459
|
+
}),
|
|
460
|
+
// Only "local" can reach here — "remote" was rejected above.
|
|
461
|
+
facilitator: {
|
|
462
|
+
mode: "local",
|
|
463
|
+
signerPrivateKey: x402Raw.facilitator.signerPrivateKey
|
|
464
|
+
}
|
|
465
|
+
} : void 0;
|
|
466
|
+
if (x402) {
|
|
467
|
+
validateAddress("payments.x402.payTo", x402.payTo);
|
|
468
|
+
validateAddress("payments.x402.asset", x402.asset);
|
|
469
|
+
}
|
|
470
|
+
const resources = Object.entries(raw.resources).map(
|
|
471
|
+
([id, entry]) => normaliseResource(id, entry, protocols, x402)
|
|
472
|
+
);
|
|
473
|
+
return {
|
|
474
|
+
version: SUPPORTED_CONFIG_VERSION,
|
|
475
|
+
merchant: {
|
|
476
|
+
id: raw.merchant.id,
|
|
477
|
+
name: raw.merchant.name,
|
|
478
|
+
publicBaseUrl: raw.merchant.publicBaseUrl
|
|
479
|
+
},
|
|
480
|
+
// min 0, not 1: port 0 is the standard "let the OS pick a free port"
|
|
481
|
+
// convention, useful for tests and ephemeral dev/demo instances.
|
|
482
|
+
server: {
|
|
483
|
+
port: toNumber(raw.server.port, "server.port", { min: 0, max: 65535 }),
|
|
484
|
+
host: raw.server.host,
|
|
485
|
+
...raw.server.adminToken !== void 0 ? { adminToken: raw.server.adminToken } : {},
|
|
486
|
+
allowedOrigins: raw.server.allowedOrigins ?? []
|
|
487
|
+
},
|
|
488
|
+
storage: { receipts: { driver: "sqlite", path: raw.storage.receipts.path } },
|
|
489
|
+
protocols,
|
|
490
|
+
resources,
|
|
491
|
+
payments: {
|
|
492
|
+
...x402 !== void 0 ? { x402 } : {}
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function normaliseResource(id, entry, protocols, x402) {
|
|
497
|
+
if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
|
|
498
|
+
const inputProperties = entry.input?.["properties"];
|
|
499
|
+
if (inputProperties && typeof inputProperties === "object" && PAYMENT_INPUT_FIELD in inputProperties) {
|
|
500
|
+
throw new CommerceError(
|
|
501
|
+
"CONFIG_INVALID",
|
|
502
|
+
`Resource "${id}" declares an input property "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
|
|
503
|
+
{
|
|
504
|
+
details: {
|
|
505
|
+
path: `resources.${id}.input.properties.${PAYMENT_INPUT_FIELD}`,
|
|
506
|
+
resourceId: id
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
for (const protocol of entry.expose) {
|
|
512
|
+
if (!SUPPORTED_PROTOCOLS.has(protocol)) {
|
|
513
|
+
const hint = protocol === "ucp" ? " (UCP is planned, not supported in v0.1.0-alpha)" : "";
|
|
514
|
+
throw new CommerceError(
|
|
515
|
+
"CONFIG_INVALID",
|
|
516
|
+
`Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp.`,
|
|
517
|
+
{ details: { path: `resources.${id}.expose`, resourceId: id, protocol } }
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (entry.expose.includes("mcp")) {
|
|
522
|
+
if (!protocols.mcp.enabled) {
|
|
523
|
+
throw new CommerceError(
|
|
524
|
+
"CONFIG_INVALID",
|
|
525
|
+
`Resource "${id}" is exposed via "mcp" but protocols.mcp.enabled is false`,
|
|
526
|
+
{ details: { path: `resources.${id}.expose`, resourceId: id } }
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (!MCP_TOOL_NAME_PATTERN.test(id)) {
|
|
530
|
+
throw new CommerceError(
|
|
531
|
+
"CONFIG_INVALID",
|
|
532
|
+
`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)`,
|
|
533
|
+
{ details: { path: `resources.${id}`, resourceId: id } }
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (entry.expose.includes("http") && !protocols.http.enabled) {
|
|
538
|
+
throw new CommerceError(
|
|
539
|
+
"CONFIG_INVALID",
|
|
540
|
+
`Resource "${id}" is exposed via "http" but protocols.http.enabled is false`,
|
|
541
|
+
{ details: { path: `resources.${id}.expose`, resourceId: id } }
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
validateBackendUrl(id, entry.backend.url);
|
|
545
|
+
validatePathParametersDeclared(id, entry.backend.url, entry.input);
|
|
546
|
+
if (entry.pricing.type === "dynamic") {
|
|
547
|
+
throw new CommerceError(
|
|
548
|
+
"CONFIG_INVALID",
|
|
549
|
+
`Resource "${id}" uses pricing.type "dynamic", which is not supported in v0.1.0-alpha`,
|
|
550
|
+
{ details: { path: `resources.${id}.pricing.type`, resourceId: id } }
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
if (entry.pricing.type === "fixed") {
|
|
554
|
+
validatePricingAmount(id, entry.pricing.amount, x402);
|
|
555
|
+
}
|
|
556
|
+
const paymentMethods = entry.payments ?? [];
|
|
557
|
+
if (entry.pricing.type === "fixed") {
|
|
558
|
+
if (paymentMethods.length === 0) {
|
|
559
|
+
throw new CommerceError(
|
|
560
|
+
"CONFIG_INVALID",
|
|
561
|
+
`Resource "${id}" has fixed pricing but declares no "payments" (a paid resource must name at least one payment method)`,
|
|
562
|
+
{ details: { path: `resources.${id}.payments`, resourceId: id } }
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
for (const method of paymentMethods) {
|
|
566
|
+
if (!SUPPORTED_PAYMENT_METHODS.has(method)) {
|
|
567
|
+
throw new CommerceError(
|
|
568
|
+
"CONFIG_INVALID",
|
|
569
|
+
`Resource "${id}" names unsupported payment method "${method}". Supported: x402.`,
|
|
570
|
+
{ details: { path: `resources.${id}.payments`, resourceId: id, method } }
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (method === "x402" && (!x402 || !x402.enabled)) {
|
|
574
|
+
throw new CommerceError(
|
|
575
|
+
"CONFIG_INVALID",
|
|
576
|
+
`Resource "${id}" names payment method "x402" which is not configured or not enabled under payments.x402`,
|
|
577
|
+
{ details: { path: `resources.${id}.payments`, resourceId: id, method } }
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
|
|
583
|
+
return {
|
|
584
|
+
id,
|
|
585
|
+
name: entry.name,
|
|
586
|
+
...entry.description !== void 0 ? { description: entry.description } : {},
|
|
587
|
+
inputSchema: entry.input !== void 0 ? defaultClosedObjectSchema(entry.input) : EMPTY_CLOSED_OBJECT_SCHEMA,
|
|
588
|
+
...entry.output !== void 0 ? { outputSchema: entry.output } : {},
|
|
589
|
+
handler: {
|
|
590
|
+
type: "http",
|
|
591
|
+
method: entry.backend.method,
|
|
592
|
+
url: entry.backend.url,
|
|
593
|
+
...entry.backend.headers !== void 0 ? { headers: entry.backend.headers } : {},
|
|
594
|
+
...entry.backend.timeoutMs !== void 0 ? {
|
|
595
|
+
timeoutMs: toNumber(entry.backend.timeoutMs, `resources.${id}.backend.timeoutMs`, {
|
|
596
|
+
min: 1
|
|
597
|
+
})
|
|
598
|
+
} : {}
|
|
599
|
+
},
|
|
600
|
+
pricing,
|
|
601
|
+
exposedVia: entry.expose,
|
|
602
|
+
paymentMethods
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function defaultClosedObjectSchema(schema) {
|
|
606
|
+
const isObjectSchema = isObjectSchemaNode(schema);
|
|
607
|
+
const result = { ...schema };
|
|
608
|
+
if (isObjectSchema) {
|
|
609
|
+
const properties = schema["properties"];
|
|
610
|
+
if (isPlainObject2(properties)) {
|
|
611
|
+
result["properties"] = Object.fromEntries(
|
|
612
|
+
Object.entries(properties).map(([key, sub]) => [
|
|
613
|
+
key,
|
|
614
|
+
isPlainObject2(sub) ? defaultClosedObjectSchema(sub) : sub
|
|
615
|
+
])
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
if (!Object.hasOwn(schema, "additionalProperties")) {
|
|
619
|
+
result["additionalProperties"] = false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const items = schema["items"];
|
|
623
|
+
if (isPlainObject2(items)) {
|
|
624
|
+
result["items"] = defaultClosedObjectSchema(items);
|
|
625
|
+
}
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
function isPlainObject2(value) {
|
|
629
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
630
|
+
}
|
|
631
|
+
function excludesObjectType(schema) {
|
|
632
|
+
const type = schema["type"];
|
|
633
|
+
if (type === void 0 || type === "object") return false;
|
|
634
|
+
if (Array.isArray(type)) return !type.includes("object");
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
function validateResourceSchemaKeywords(id, path2, schema) {
|
|
638
|
+
for (const keyword of UNSUPPORTED_SCHEMA_KEYWORDS) {
|
|
639
|
+
if (Object.hasOwn(schema, keyword)) {
|
|
640
|
+
console.warn(
|
|
641
|
+
`[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.`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if ((Object.hasOwn(schema, "properties") || Object.hasOwn(schema, "required")) && excludesObjectType(schema)) {
|
|
646
|
+
throw new CommerceError(
|
|
647
|
+
"CONFIG_INVALID",
|
|
648
|
+
`Resource "${id}" ${path2} declares "properties" and/or "required" but its "type" (${JSON.stringify(schema["type"])}) does not include "object" \u2014 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".`,
|
|
649
|
+
{ details: { path: `resources.${id}.${path2}`, resourceId: id } }
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const requiredRaw = schema["required"];
|
|
653
|
+
const closed = !Object.hasOwn(schema, "additionalProperties") ? true : schema["additionalProperties"] === false;
|
|
654
|
+
if (Array.isArray(requiredRaw) && closed && isObjectSchemaNode(schema)) {
|
|
655
|
+
const declared = isPlainObject2(schema["properties"]) ? schema["properties"] : {};
|
|
656
|
+
const undeclared = requiredRaw.filter(
|
|
657
|
+
(name) => typeof name === "string" && !Object.hasOwn(declared, name)
|
|
658
|
+
);
|
|
659
|
+
if (undeclared.length > 0) {
|
|
660
|
+
throw new CommerceError(
|
|
661
|
+
"CONFIG_INVALID",
|
|
662
|
+
`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".`,
|
|
663
|
+
{ details: { path: `resources.${id}.${path2}`, resourceId: id, undeclared } }
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const properties = schema["properties"];
|
|
668
|
+
if (isPlainObject2(properties)) {
|
|
669
|
+
for (const [key, sub] of Object.entries(properties)) {
|
|
670
|
+
if (isPlainObject2(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const items = schema["items"];
|
|
674
|
+
if (isPlainObject2(items)) {
|
|
675
|
+
validateResourceSchemaKeywords(id, `${path2}.items`, items);
|
|
676
|
+
} else if (Array.isArray(items)) {
|
|
677
|
+
console.warn(
|
|
678
|
+
`[agent-commerce] resource "${id}" ${path2}.items is a tuple (an array of schemas), which this gateway does not enforce \u2014 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.`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function validatePricingAmount(id, amount, x402) {
|
|
683
|
+
const path2 = `resources.${id}.pricing.amount`;
|
|
684
|
+
if (!PRICING_AMOUNT_PATTERN.test(amount)) {
|
|
685
|
+
throw new CommerceError(
|
|
686
|
+
"CONFIG_INVALID",
|
|
687
|
+
`Resource "${id}" has pricing.amount "${amount}", which is not a plain positive decimal (no currency symbol, no thousands separator, no exponent \u2014 e.g. "0.01")`,
|
|
688
|
+
{ details: { path: path2, resourceId: id } }
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (ZERO_AMOUNT_PATTERN.test(amount)) {
|
|
692
|
+
throw new CommerceError(
|
|
693
|
+
"CONFIG_INVALID",
|
|
694
|
+
`Resource "${id}" has pricing.amount "0" \u2014 a paid resource cannot cost zero; use "pricing: { type: free }" instead`,
|
|
695
|
+
{ details: { path: path2, resourceId: id } }
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
if (x402 !== void 0) {
|
|
699
|
+
const fractionalDigits = amount.includes(".") ? amount.split(".")[1]?.length ?? 0 : 0;
|
|
700
|
+
if (fractionalDigits > x402.assetDecimals) {
|
|
701
|
+
throw new CommerceError(
|
|
702
|
+
"CONFIG_INVALID",
|
|
703
|
+
`Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${x402.assetDecimals} decimals)`,
|
|
704
|
+
{ details: { path: path2, resourceId: id } }
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function validatePathParametersDeclared(id, url, input) {
|
|
710
|
+
const stray = findUnparsedBraceToken(url);
|
|
711
|
+
if (stray !== void 0) {
|
|
712
|
+
throw new CommerceError(
|
|
713
|
+
"CONFIG_INVALID",
|
|
714
|
+
`Resource "${id}" has backend.url containing "${stray}", which is not a valid path parameter (allowed characters: A-Z a-z 0-9 _ . -). It would be sent to the backend literally, so a paid resource would settle payment and then never reach the backend`,
|
|
715
|
+
{ details: { path: `resources.${id}.backend.url`, resourceId: id, token: stray } }
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
const params = extractPathParameterNames(url);
|
|
719
|
+
if (params.length === 0) return;
|
|
720
|
+
const propertiesRaw = input?.["properties"];
|
|
721
|
+
const properties = isPlainObject2(propertiesRaw) ? propertiesRaw : {};
|
|
722
|
+
const requiredRaw = input?.["required"];
|
|
723
|
+
const required = new Set(
|
|
724
|
+
Array.isArray(requiredRaw) ? requiredRaw.filter((entry) => typeof entry === "string") : []
|
|
725
|
+
);
|
|
726
|
+
for (const param of params) {
|
|
727
|
+
const path2 = `resources.${id}.backend.url`;
|
|
728
|
+
if (!Object.hasOwn(properties, param)) {
|
|
729
|
+
throw new CommerceError(
|
|
730
|
+
"CONFIG_INVALID",
|
|
731
|
+
`Resource "${id}" has backend.url path parameter "{${param}}" which is not declared in its input schema \u2014 the caller has no way to supply it, so every call would settle payment (if priced) and then fail to reach the backend`,
|
|
732
|
+
{ details: { path: path2, resourceId: id, param } }
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (!required.has(param)) {
|
|
736
|
+
throw new CommerceError(
|
|
737
|
+
"CONFIG_INVALID",
|
|
738
|
+
`Resource "${id}" has backend.url path parameter "{${param}}" declared in its input schema but not listed in "required" \u2014 a caller that omits it hits the same unservable-request problem as an undeclared parameter`,
|
|
739
|
+
{ details: { path: path2, resourceId: id, param } }
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function validateBackendUrl(id, url) {
|
|
745
|
+
let parsed;
|
|
746
|
+
try {
|
|
747
|
+
parsed = new URL(url);
|
|
748
|
+
} catch {
|
|
749
|
+
throw new CommerceError(
|
|
750
|
+
"CONFIG_INVALID",
|
|
751
|
+
`Resource "${id}" has an invalid backend.url "${url}": must be an absolute http:// or https:// URL`,
|
|
752
|
+
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
756
|
+
throw new CommerceError(
|
|
757
|
+
"CONFIG_INVALID",
|
|
758
|
+
`Resource "${id}" has a backend.url with scheme "${parsed.protocol}": must be http:// or https://`,
|
|
759
|
+
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function validateAddress(path2, value) {
|
|
764
|
+
if (!ADDRESS_PATTERN.test(value)) {
|
|
765
|
+
throw new CommerceError(
|
|
766
|
+
"CONFIG_INVALID",
|
|
767
|
+
`Configuration value at "${path2}" is not a plausible address (expected "0x" followed by 40 hex characters)`,
|
|
768
|
+
{ details: { path: path2 } }
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if (ZERO_ADDRESS_PATTERN.test(value)) {
|
|
772
|
+
throw new CommerceError(
|
|
773
|
+
"CONFIG_INVALID",
|
|
774
|
+
`Configuration value at "${path2}" must not be the zero address`,
|
|
775
|
+
{
|
|
776
|
+
details: { path: path2 }
|
|
777
|
+
}
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
var SUPPORTED_CONFIG_VERSION, ADDRESS_PATTERN, ZERO_ADDRESS_PATTERN, MCP_TOOL_NAME_PATTERN, NumberOrString, BooleanOrString, MerchantSchema, ServerSchema, StorageSchema, ProtocolsSchema, BackendMethodSchema, BackendHandlerSchema, PricingSchema, JsonSchemaValueSchema, ResourceEntrySchema, ResourcesMapSchema, FacilitatorSchema, X402Schema, PaymentsSchema, RawConfigSchema, SUPPORTED_PROTOCOLS, SUPPORTED_PAYMENT_METHODS, EMPTY_CLOSED_OBJECT_SCHEMA, UNSUPPORTED_SCHEMA_KEYWORDS, PRICING_AMOUNT_PATTERN, ZERO_AMOUNT_PATTERN;
|
|
782
|
+
var init_schema = __esm({
|
|
783
|
+
"src/config/schema.ts"() {
|
|
784
|
+
init_execution();
|
|
785
|
+
init_core();
|
|
786
|
+
init_env();
|
|
787
|
+
SUPPORTED_CONFIG_VERSION = 1;
|
|
788
|
+
ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
|
|
789
|
+
ZERO_ADDRESS_PATTERN = /^0x0{40}$/i;
|
|
790
|
+
MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
|
|
791
|
+
NumberOrString = z.union([z.number(), z.string()]);
|
|
792
|
+
BooleanOrString = z.union([z.boolean(), z.string()]);
|
|
793
|
+
MerchantSchema = z.object({
|
|
794
|
+
id: z.string().min(1),
|
|
795
|
+
name: z.string().min(1),
|
|
796
|
+
publicBaseUrl: z.string().min(1)
|
|
797
|
+
}).strict();
|
|
798
|
+
ServerSchema = z.object({
|
|
799
|
+
port: NumberOrString,
|
|
800
|
+
host: z.string().min(1),
|
|
801
|
+
/** Shared secret gating /api/receipts, /api/events, /api/events/stream. No token -> those routes 404. */
|
|
802
|
+
adminToken: z.string().min(1).optional(),
|
|
803
|
+
/** Browser origins allowed to read the dashboard-facing routes. Empty by default: closed. */
|
|
804
|
+
// Entries are matched literally against the browser's
|
|
805
|
+
// `Origin` header, so `"*"` and a trailing slash match nothing at all —
|
|
806
|
+
// fail-closed (a lockout, not a bypass), but silently, and a lockout with
|
|
807
|
+
// no explanation is the kind of thing an operator "fixes" by disabling the
|
|
808
|
+
// check. Reject those two shapes with a pointer instead.
|
|
809
|
+
allowedOrigins: z.array(
|
|
810
|
+
z.string().min(1).refine((origin) => origin !== "*", {
|
|
811
|
+
message: `wildcard "*" is not supported \u2014 allowedOrigins entries are matched literally against the browser's Origin header, so "*" would match nothing. List each scheme://host[:port] explicitly.`
|
|
812
|
+
}).refine((origin) => !origin.endsWith("/"), {
|
|
813
|
+
message: 'must not end with "/" \u2014 a browser Origin header never has a trailing slash, so this entry would match nothing. Use e.g. "http://localhost:5173".'
|
|
814
|
+
})
|
|
815
|
+
).optional()
|
|
816
|
+
}).strict();
|
|
817
|
+
StorageSchema = z.object({
|
|
818
|
+
receipts: z.object({
|
|
819
|
+
driver: z.literal("sqlite"),
|
|
820
|
+
path: z.string().min(1)
|
|
821
|
+
}).strict()
|
|
822
|
+
}).strict();
|
|
823
|
+
ProtocolsSchema = z.object({
|
|
824
|
+
http: z.object({ enabled: BooleanOrString }).strict(),
|
|
825
|
+
mcp: z.object({
|
|
826
|
+
enabled: BooleanOrString,
|
|
827
|
+
mountPath: z.string().min(1)
|
|
828
|
+
}).strict()
|
|
829
|
+
}).strict();
|
|
830
|
+
BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
831
|
+
BackendHandlerSchema = z.object({
|
|
832
|
+
type: z.literal("http"),
|
|
833
|
+
method: BackendMethodSchema,
|
|
834
|
+
url: z.string().min(1),
|
|
835
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
836
|
+
timeoutMs: NumberOrString.optional()
|
|
837
|
+
}).strict();
|
|
838
|
+
PricingSchema = z.discriminatedUnion("type", [
|
|
839
|
+
z.object({ type: z.literal("free") }).strict(),
|
|
840
|
+
z.object({
|
|
841
|
+
type: z.literal("fixed"),
|
|
842
|
+
amount: z.string().min(1),
|
|
843
|
+
currency: z.string().min(1)
|
|
844
|
+
}).strict(),
|
|
845
|
+
z.object({
|
|
846
|
+
type: z.literal("dynamic"),
|
|
847
|
+
resolver: z.string().min(1)
|
|
848
|
+
}).strict()
|
|
849
|
+
]);
|
|
850
|
+
JsonSchemaValueSchema = z.record(z.string(), z.unknown());
|
|
851
|
+
ResourceEntrySchema = z.object({
|
|
852
|
+
name: z.string().min(1),
|
|
853
|
+
description: z.string().optional(),
|
|
854
|
+
input: JsonSchemaValueSchema.optional(),
|
|
855
|
+
output: JsonSchemaValueSchema.optional(),
|
|
856
|
+
backend: BackendHandlerSchema,
|
|
857
|
+
pricing: PricingSchema,
|
|
858
|
+
expose: z.array(z.string().min(1)).min(1),
|
|
859
|
+
payments: z.array(z.string().min(1)).optional()
|
|
860
|
+
}).strict();
|
|
861
|
+
ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
|
|
862
|
+
FacilitatorSchema = z.discriminatedUnion("mode", [
|
|
863
|
+
z.object({
|
|
864
|
+
mode: z.literal("local"),
|
|
865
|
+
signerPrivateKey: z.string().min(1)
|
|
866
|
+
}).strict(),
|
|
867
|
+
z.object({
|
|
868
|
+
mode: z.literal("remote"),
|
|
869
|
+
url: z.string().min(1)
|
|
870
|
+
}).strict()
|
|
871
|
+
]);
|
|
872
|
+
X402Schema = z.object({
|
|
873
|
+
enabled: BooleanOrString,
|
|
874
|
+
network: z.string().min(1),
|
|
875
|
+
rpcUrl: z.string().min(1),
|
|
876
|
+
asset: z.string().min(1),
|
|
877
|
+
assetName: z.string().min(1),
|
|
878
|
+
assetVersion: z.string().min(1),
|
|
879
|
+
assetDecimals: NumberOrString,
|
|
880
|
+
payTo: z.string().min(1),
|
|
881
|
+
maxTimeoutSeconds: NumberOrString,
|
|
882
|
+
facilitator: FacilitatorSchema
|
|
883
|
+
}).strict();
|
|
884
|
+
PaymentsSchema = z.object({
|
|
885
|
+
x402: X402Schema.optional()
|
|
886
|
+
}).strict();
|
|
887
|
+
RawConfigSchema = z.object({
|
|
888
|
+
version: z.literal(SUPPORTED_CONFIG_VERSION),
|
|
889
|
+
merchant: MerchantSchema,
|
|
890
|
+
server: ServerSchema,
|
|
891
|
+
storage: StorageSchema,
|
|
892
|
+
protocols: ProtocolsSchema,
|
|
893
|
+
resources: ResourcesMapSchema,
|
|
894
|
+
payments: PaymentsSchema
|
|
895
|
+
}).strict();
|
|
896
|
+
SUPPORTED_PROTOCOLS = /* @__PURE__ */ new Set(["http", "mcp"]);
|
|
897
|
+
SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
|
|
898
|
+
EMPTY_CLOSED_OBJECT_SCHEMA = {
|
|
899
|
+
type: "object",
|
|
900
|
+
properties: {},
|
|
901
|
+
additionalProperties: false
|
|
902
|
+
};
|
|
903
|
+
UNSUPPORTED_SCHEMA_KEYWORDS = [
|
|
904
|
+
"pattern",
|
|
905
|
+
"minLength",
|
|
906
|
+
"maxLength",
|
|
907
|
+
"format",
|
|
908
|
+
"minimum",
|
|
909
|
+
"maximum",
|
|
910
|
+
"exclusiveMinimum",
|
|
911
|
+
"exclusiveMaximum",
|
|
912
|
+
"multipleOf",
|
|
913
|
+
"oneOf",
|
|
914
|
+
"anyOf",
|
|
915
|
+
"allOf",
|
|
916
|
+
"not",
|
|
917
|
+
"$ref",
|
|
918
|
+
"const",
|
|
919
|
+
"patternProperties",
|
|
920
|
+
"minItems",
|
|
921
|
+
"maxItems",
|
|
922
|
+
"uniqueItems"
|
|
923
|
+
];
|
|
924
|
+
PRICING_AMOUNT_PATTERN = /^\d+(?:\.\d+)?$/;
|
|
925
|
+
ZERO_AMOUNT_PATTERN = /^0(?:\.0+)?$/;
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
async function loadConfig(options = {}) {
|
|
929
|
+
const env = options.env ?? process.env;
|
|
930
|
+
const cwd = options.cwd ?? process.cwd();
|
|
931
|
+
const configPath = resolveConfigPath(options.path, env, cwd);
|
|
932
|
+
let text2;
|
|
933
|
+
try {
|
|
934
|
+
text2 = await promises.readFile(configPath, "utf8");
|
|
935
|
+
} catch (error) {
|
|
936
|
+
throw new CommerceError("CONFIG_INVALID", `Could not read configuration file "${configPath}"`, {
|
|
937
|
+
details: { path: configPath },
|
|
938
|
+
cause: error
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
let raw;
|
|
942
|
+
try {
|
|
943
|
+
raw = parse(text2);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
const { reason, details } = describeYamlError(error);
|
|
946
|
+
throw new CommerceError(
|
|
947
|
+
"CONFIG_INVALID",
|
|
948
|
+
`Configuration file "${configPath}" is not valid YAML: ${reason}`,
|
|
949
|
+
{
|
|
950
|
+
details: { path: configPath, ...details },
|
|
951
|
+
// Retained for programmatic callers. Nothing in the CLI prints a
|
|
952
|
+
// `cause`, and nothing should start: this is the object whose
|
|
953
|
+
// `.message` carries the source excerpt (see below).
|
|
954
|
+
cause: error
|
|
955
|
+
}
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
return parseConfig(raw, env);
|
|
959
|
+
}
|
|
960
|
+
function describeYamlError(error) {
|
|
961
|
+
if (!(error instanceof YAMLError)) {
|
|
962
|
+
return { reason: "unknown YAML parse error", details: {} };
|
|
963
|
+
}
|
|
964
|
+
const start = error.linePos?.[0];
|
|
965
|
+
const where = start === void 0 ? "" : ` at line ${String(start.line)}, column ${String(start.col)}`;
|
|
966
|
+
const explanation = YAML_ERROR_EXPLANATIONS[error.code];
|
|
967
|
+
const what = explanation === void 0 ? error.code : `${error.code} (${explanation})`;
|
|
968
|
+
return {
|
|
969
|
+
reason: `${what}${where}`,
|
|
970
|
+
details: {
|
|
971
|
+
yamlErrorCode: error.code,
|
|
972
|
+
...start === void 0 ? {} : { line: start.line, column: start.col }
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function resolveConfigPath(explicitPath, env, cwd) {
|
|
977
|
+
if (explicitPath) return path.resolve(cwd, explicitPath);
|
|
978
|
+
const fromEnv = env[CONFIG_PATH_ENV_VAR];
|
|
979
|
+
if (fromEnv) return path.resolve(cwd, fromEnv);
|
|
980
|
+
return path.resolve(cwd, DEFAULT_CONFIG_FILENAME);
|
|
981
|
+
}
|
|
982
|
+
var CONFIG_PATH_ENV_VAR, YAML_ERROR_EXPLANATIONS;
|
|
983
|
+
var init_load = __esm({
|
|
984
|
+
"src/config/load.ts"() {
|
|
985
|
+
init_core();
|
|
986
|
+
init_filename();
|
|
987
|
+
init_schema();
|
|
988
|
+
CONFIG_PATH_ENV_VAR = "AGENT_COMMERCE_CONFIG";
|
|
989
|
+
YAML_ERROR_EXPLANATIONS = {
|
|
990
|
+
DUPLICATE_KEY: "map keys must be unique",
|
|
991
|
+
BAD_INDENT: "bad indentation",
|
|
992
|
+
TAB_AS_INDENT: "tabs cannot be used for indentation",
|
|
993
|
+
MISSING_CHAR: "a required character is missing",
|
|
994
|
+
BLOCK_AS_IMPLICIT_KEY: "a block value cannot be used as a key (missing quotes, or a stray colon)",
|
|
995
|
+
MULTILINE_IMPLICIT_KEY: "a key spans multiple lines (missing quotes, or a stray colon)",
|
|
996
|
+
UNEXPECTED_TOKEN: "unexpected token"
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// src/config/index.ts
|
|
1002
|
+
var config_exports = {};
|
|
1003
|
+
__export(config_exports, {
|
|
1004
|
+
loadConfig: () => loadConfig,
|
|
1005
|
+
parseConfig: () => parseConfig,
|
|
1006
|
+
substituteEnv: () => substituteEnv
|
|
1007
|
+
});
|
|
1008
|
+
var init_config = __esm({
|
|
1009
|
+
"src/config/index.ts"() {
|
|
1010
|
+
init_env();
|
|
1011
|
+
init_load();
|
|
1012
|
+
init_schema();
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// src/cli/program.ts
|
|
1017
|
+
init_filename();
|
|
1018
|
+
|
|
1019
|
+
// src/cli/commands/demo.ts
|
|
1020
|
+
function defaultDemoSteps() {
|
|
1021
|
+
return [
|
|
1022
|
+
{
|
|
1023
|
+
name: "Start local chain + gateway + merchant (docker compose)",
|
|
1024
|
+
command: "docker",
|
|
1025
|
+
args: ["compose", "up", "--build", "-d"]
|
|
1026
|
+
},
|
|
1027
|
+
{ name: "Run the deterministic buyer agent", command: "npm", args: ["run", "demo:agent"] }
|
|
1028
|
+
];
|
|
1029
|
+
}
|
|
1030
|
+
async function runDemo(io, deps = {}) {
|
|
1031
|
+
const steps = deps.steps ?? defaultDemoSteps();
|
|
1032
|
+
const run = deps.run ?? execCommand;
|
|
1033
|
+
io.stdout("agent-commerce demo \u2014 running the documented quickstart steps:");
|
|
1034
|
+
for (const step of steps) {
|
|
1035
|
+
io.stdout("");
|
|
1036
|
+
io.stdout(`==> ${step.name}`);
|
|
1037
|
+
io.stdout(` $ ${step.command} ${step.args.join(" ")}`);
|
|
1038
|
+
const result = await run(step.command, step.args);
|
|
1039
|
+
if (result.code !== 0) {
|
|
1040
|
+
io.stderr(`FAIL "${step.name}" exited with code ${result.code}.`);
|
|
1041
|
+
if (result.stderrTail !== void 0 && result.stderrTail.length > 0) {
|
|
1042
|
+
io.stderr(result.stderrTail);
|
|
1043
|
+
}
|
|
1044
|
+
io.stderr("");
|
|
1045
|
+
io.stderr("Nothing further was run. To retry manually:");
|
|
1046
|
+
io.stderr(` ${step.command} ${step.args.join(" ")}`);
|
|
1047
|
+
return 1;
|
|
1048
|
+
}
|
|
1049
|
+
if (step.healthCheck !== void 0) {
|
|
1050
|
+
const healthy = await step.healthCheck();
|
|
1051
|
+
if (!healthy) {
|
|
1052
|
+
io.stderr(`FAIL "${step.name}" completed but its health check did not pass.`);
|
|
1053
|
+
return 1;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
io.stdout(`OK ${step.name}`);
|
|
1057
|
+
}
|
|
1058
|
+
io.stdout("");
|
|
1059
|
+
io.stdout("PASS Demo is up. Try: agent-commerce doctor --config config-demo.yaml");
|
|
1060
|
+
return 0;
|
|
1061
|
+
}
|
|
1062
|
+
async function execCommand(command, args) {
|
|
1063
|
+
const { spawn } = await import('child_process');
|
|
1064
|
+
return new Promise((resolvePromise) => {
|
|
1065
|
+
let stderr = "";
|
|
1066
|
+
const child = spawn(command, args, { stdio: ["ignore", "inherit", "pipe"] });
|
|
1067
|
+
child.stderr?.on("data", (chunk) => {
|
|
1068
|
+
stderr += chunk.toString("utf8");
|
|
1069
|
+
});
|
|
1070
|
+
child.on("error", (err) => {
|
|
1071
|
+
resolvePromise({ code: 1, stderrTail: err.message });
|
|
1072
|
+
});
|
|
1073
|
+
child.on("close", (code) => {
|
|
1074
|
+
const tail = stderr.trim().split("\n").slice(-10).join("\n");
|
|
1075
|
+
resolvePromise({ code: code ?? 1, ...tail.length > 0 ? { stderrTail: tail } : {} });
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/cli/commands/doctor.ts
|
|
1081
|
+
init_execution();
|
|
1082
|
+
init_core();
|
|
1083
|
+
|
|
1084
|
+
// src/storage/receipts/store.ts
|
|
1085
|
+
init_core();
|
|
1086
|
+
var PACKAGE_VERSION = "0.1.0-alpha.0" ;
|
|
1087
|
+
|
|
1088
|
+
// src/storage/receipts/redact.ts
|
|
1089
|
+
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;
|
|
1090
|
+
var REDACTED = "[REDACTED]";
|
|
1091
|
+
function isSecretKey(key) {
|
|
1092
|
+
return SECRET_KEY_PATTERN.test(key);
|
|
1093
|
+
}
|
|
1094
|
+
function redact(value) {
|
|
1095
|
+
return redactValue(value);
|
|
1096
|
+
}
|
|
1097
|
+
function redactValue(value) {
|
|
1098
|
+
if (Array.isArray(value)) {
|
|
1099
|
+
return value.map(redactValue);
|
|
1100
|
+
}
|
|
1101
|
+
if (value !== null && typeof value === "object") {
|
|
1102
|
+
const out = {};
|
|
1103
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
1104
|
+
out[key] = isSecretKey(key) ? REDACTED : redactValue(inner);
|
|
1105
|
+
}
|
|
1106
|
+
return out;
|
|
1107
|
+
}
|
|
1108
|
+
return value;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/storage/receipts/rows.ts
|
|
1112
|
+
function receiptToRow(receipt) {
|
|
1113
|
+
return {
|
|
1114
|
+
id: receipt.id,
|
|
1115
|
+
request_id: receipt.requestId,
|
|
1116
|
+
resource_id: receipt.resourceId,
|
|
1117
|
+
payment_json: receipt.payment !== void 0 ? JSON.stringify(redact(receipt.payment)) : null,
|
|
1118
|
+
delivered_at: receipt.deliveredAt,
|
|
1119
|
+
backend_status: receipt.backendStatus,
|
|
1120
|
+
duration_ms: receipt.durationMs !== void 0 ? receipt.durationMs : null,
|
|
1121
|
+
protocol: receipt.protocol !== void 0 ? receipt.protocol : null,
|
|
1122
|
+
metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function rowToReceipt(row) {
|
|
1126
|
+
const payment = row.payment_json !== null ? JSON.parse(row.payment_json) : void 0;
|
|
1127
|
+
const metadata = row.metadata_json !== null ? JSON.parse(row.metadata_json) : void 0;
|
|
1128
|
+
return {
|
|
1129
|
+
id: row.id,
|
|
1130
|
+
requestId: row.request_id,
|
|
1131
|
+
resourceId: row.resource_id,
|
|
1132
|
+
deliveredAt: row.delivered_at,
|
|
1133
|
+
backendStatus: row.backend_status,
|
|
1134
|
+
...payment !== void 0 ? { payment } : {},
|
|
1135
|
+
...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
|
|
1136
|
+
...row.protocol !== null ? { protocol: row.protocol } : {},
|
|
1137
|
+
...metadata !== void 0 ? { metadata } : {}
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function eventToRow(event) {
|
|
1141
|
+
return {
|
|
1142
|
+
id: event.id,
|
|
1143
|
+
type: event.type,
|
|
1144
|
+
request_id: event.requestId,
|
|
1145
|
+
resource_id: event.resourceId !== void 0 ? event.resourceId : null,
|
|
1146
|
+
at: event.at,
|
|
1147
|
+
adapter: event.adapter !== void 0 ? event.adapter : null,
|
|
1148
|
+
payment_provider: event.paymentProvider !== void 0 ? event.paymentProvider : null,
|
|
1149
|
+
duration_ms: event.durationMs !== void 0 ? event.durationMs : null,
|
|
1150
|
+
status: event.status !== void 0 ? event.status : null,
|
|
1151
|
+
data_json: event.data !== void 0 ? JSON.stringify(redact(event.data)) : null
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function rowToEvent(row) {
|
|
1155
|
+
const data = row.data_json !== null ? JSON.parse(row.data_json) : void 0;
|
|
1156
|
+
return {
|
|
1157
|
+
id: row.id,
|
|
1158
|
+
type: row.type,
|
|
1159
|
+
requestId: row.request_id,
|
|
1160
|
+
at: row.at,
|
|
1161
|
+
...row.resource_id !== null ? { resourceId: row.resource_id } : {},
|
|
1162
|
+
...row.adapter !== null ? { adapter: row.adapter } : {},
|
|
1163
|
+
...row.payment_provider !== null ? { paymentProvider: row.payment_provider } : {},
|
|
1164
|
+
...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
|
|
1165
|
+
...row.status !== null ? { status: row.status } : {},
|
|
1166
|
+
...data !== void 0 ? { data } : {}
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function rowToPaymentAttempt(row) {
|
|
1170
|
+
return {
|
|
1171
|
+
id: row.id,
|
|
1172
|
+
requestId: row.request_id,
|
|
1173
|
+
resourceId: row.resource_id,
|
|
1174
|
+
provider: row.provider,
|
|
1175
|
+
replayKey: row.replay_key,
|
|
1176
|
+
status: row.status,
|
|
1177
|
+
amount: row.amount,
|
|
1178
|
+
currency: row.currency,
|
|
1179
|
+
...row.payer !== null ? { payer: row.payer } : {},
|
|
1180
|
+
...row.payee !== null ? { payee: row.payee } : {},
|
|
1181
|
+
...row.external_reference !== null ? { externalReference: row.external_reference } : {},
|
|
1182
|
+
...row.rejection_reason !== null ? { rejectionReason: row.rejection_reason } : {},
|
|
1183
|
+
createdAt: row.created_at,
|
|
1184
|
+
updatedAt: row.updated_at
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/storage/receipts/schema.ts
|
|
1189
|
+
var MIGRATIONS = [
|
|
1190
|
+
{
|
|
1191
|
+
version: 1,
|
|
1192
|
+
up(db) {
|
|
1193
|
+
db.exec(`
|
|
1194
|
+
CREATE TABLE IF NOT EXISTS receipts (
|
|
1195
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1196
|
+
id TEXT NOT NULL UNIQUE,
|
|
1197
|
+
request_id TEXT NOT NULL,
|
|
1198
|
+
resource_id TEXT NOT NULL,
|
|
1199
|
+
payment_json TEXT,
|
|
1200
|
+
delivered_at TEXT NOT NULL,
|
|
1201
|
+
backend_status INTEGER NOT NULL,
|
|
1202
|
+
duration_ms INTEGER,
|
|
1203
|
+
protocol TEXT,
|
|
1204
|
+
metadata_json TEXT
|
|
1205
|
+
);
|
|
1206
|
+
CREATE INDEX IF NOT EXISTS idx_receipts_request_id ON receipts(request_id);
|
|
1207
|
+
CREATE INDEX IF NOT EXISTS idx_receipts_delivered_at ON receipts(delivered_at);
|
|
1208
|
+
|
|
1209
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
1210
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1211
|
+
id TEXT NOT NULL UNIQUE,
|
|
1212
|
+
type TEXT NOT NULL,
|
|
1213
|
+
request_id TEXT NOT NULL,
|
|
1214
|
+
resource_id TEXT,
|
|
1215
|
+
at TEXT NOT NULL,
|
|
1216
|
+
adapter TEXT,
|
|
1217
|
+
payment_provider TEXT,
|
|
1218
|
+
duration_ms INTEGER,
|
|
1219
|
+
status TEXT,
|
|
1220
|
+
data_json TEXT
|
|
1221
|
+
);
|
|
1222
|
+
CREATE INDEX IF NOT EXISTS idx_events_request_id ON events(request_id);
|
|
1223
|
+
CREATE INDEX IF NOT EXISTS idx_events_at ON events(at);
|
|
1224
|
+
|
|
1225
|
+
CREATE TABLE IF NOT EXISTS payment_attempts (
|
|
1226
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1227
|
+
id TEXT NOT NULL UNIQUE,
|
|
1228
|
+
request_id TEXT NOT NULL,
|
|
1229
|
+
resource_id TEXT NOT NULL,
|
|
1230
|
+
provider TEXT NOT NULL,
|
|
1231
|
+
replay_key TEXT NOT NULL UNIQUE,
|
|
1232
|
+
status TEXT NOT NULL,
|
|
1233
|
+
amount TEXT NOT NULL,
|
|
1234
|
+
currency TEXT NOT NULL,
|
|
1235
|
+
payer TEXT,
|
|
1236
|
+
payee TEXT,
|
|
1237
|
+
external_reference TEXT,
|
|
1238
|
+
rejection_reason TEXT,
|
|
1239
|
+
created_at TEXT NOT NULL,
|
|
1240
|
+
updated_at TEXT NOT NULL
|
|
1241
|
+
);
|
|
1242
|
+
CREATE INDEX IF NOT EXISTS idx_payment_attempts_request_id ON payment_attempts(request_id);
|
|
1243
|
+
CREATE INDEX IF NOT EXISTS idx_payment_attempts_created_at ON payment_attempts(created_at);
|
|
1244
|
+
`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
];
|
|
1248
|
+
var SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
1249
|
+
function migrate(db) {
|
|
1250
|
+
const current = db.pragma("user_version", { simple: true });
|
|
1251
|
+
for (const migration of MIGRATIONS) {
|
|
1252
|
+
if (migration.version <= current) continue;
|
|
1253
|
+
const apply = db.transaction(() => {
|
|
1254
|
+
migration.up(db);
|
|
1255
|
+
db.pragma(`user_version = ${migration.version}`);
|
|
1256
|
+
});
|
|
1257
|
+
apply();
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// src/storage/receipts/store.ts
|
|
1262
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
1263
|
+
var MAX_LIST_LIMIT = 500;
|
|
1264
|
+
function clampListLimit(limit) {
|
|
1265
|
+
const base = limit !== void 0 && Number.isFinite(limit) ? limit : DEFAULT_LIST_LIMIT;
|
|
1266
|
+
return Math.min(Math.max(Math.trunc(base), 1), MAX_LIST_LIMIT);
|
|
1267
|
+
}
|
|
1268
|
+
var defaultIds = {
|
|
1269
|
+
next: (prefix) => prefix !== void 0 ? `${prefix}_${randomUUID()}` : randomUUID()
|
|
1270
|
+
};
|
|
1271
|
+
function isUniqueConstraintOn(err, column) {
|
|
1272
|
+
return err instanceof Error && "code" in err && err.code === "SQLITE_CONSTRAINT_UNIQUE" && err.message.includes(column);
|
|
1273
|
+
}
|
|
1274
|
+
function restrictDatabasePermissions(path2, logger) {
|
|
1275
|
+
for (const file of [path2, `${path2}-wal`, `${path2}-shm`]) {
|
|
1276
|
+
try {
|
|
1277
|
+
chmodSync(file, 384);
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
const code = error.code;
|
|
1280
|
+
if (code === "ENOENT") continue;
|
|
1281
|
+
logger.warn(
|
|
1282
|
+
{ file, error: code ?? "unknown" },
|
|
1283
|
+
"could not restrict receipt database permissions to owner-only"
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
function createSqliteReceiptStore(options) {
|
|
1289
|
+
const logger = options.logger ?? NOOP_LOGGER;
|
|
1290
|
+
const clock = options.clock ?? systemClock;
|
|
1291
|
+
const ids = options.ids ?? defaultIds;
|
|
1292
|
+
const path2 = options.path;
|
|
1293
|
+
const isFileBacked = path2 !== ":memory:";
|
|
1294
|
+
if (isFileBacked) {
|
|
1295
|
+
const dir = dirname(path2);
|
|
1296
|
+
if (dir !== "" && dir !== "." && !existsSync(dir)) {
|
|
1297
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (isFileBacked && existsSync(path2)) {
|
|
1301
|
+
try {
|
|
1302
|
+
accessSync(path2, constants.W_OK);
|
|
1303
|
+
} catch {
|
|
1304
|
+
throw new CommerceError(
|
|
1305
|
+
"STORAGE_ERROR",
|
|
1306
|
+
`Receipt database "${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 \u2014 recreate it (docker compose down -v) or fix its ownership.`,
|
|
1307
|
+
{ details: { path: path2 } }
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const db = new Database(path2);
|
|
1312
|
+
db.pragma("journal_mode = WAL");
|
|
1313
|
+
db.pragma("foreign_keys = ON");
|
|
1314
|
+
migrate(db);
|
|
1315
|
+
if (isFileBacked) restrictDatabasePermissions(path2, logger);
|
|
1316
|
+
const insertReceiptStmt = db.prepare(
|
|
1317
|
+
`INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json)
|
|
1318
|
+
VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json)`
|
|
1319
|
+
);
|
|
1320
|
+
const getReceiptStmt = db.prepare("SELECT * FROM receipts WHERE id = ?");
|
|
1321
|
+
const listReceiptsStmt = db.prepare(
|
|
1322
|
+
"SELECT * FROM receipts ORDER BY delivered_at DESC, seq DESC LIMIT ?"
|
|
1323
|
+
);
|
|
1324
|
+
const listReceiptsByRequestStmt = db.prepare(
|
|
1325
|
+
"SELECT * FROM receipts WHERE request_id = ? ORDER BY delivered_at DESC, seq DESC LIMIT ?"
|
|
1326
|
+
);
|
|
1327
|
+
const countReceiptsStmt = db.prepare(
|
|
1328
|
+
"SELECT COUNT(*) AS count FROM receipts"
|
|
1329
|
+
);
|
|
1330
|
+
const countUndeliveredReceiptsStmt = db.prepare(
|
|
1331
|
+
"SELECT COUNT(*) AS count FROM receipts WHERE backend_status NOT BETWEEN 200 AND 299"
|
|
1332
|
+
);
|
|
1333
|
+
const insertEventStmt = db.prepare(
|
|
1334
|
+
`INSERT INTO events (id, type, request_id, resource_id, at, adapter, payment_provider, duration_ms, status, data_json)
|
|
1335
|
+
VALUES (@id, @type, @request_id, @resource_id, @at, @adapter, @payment_provider, @duration_ms, @status, @data_json)`
|
|
1336
|
+
);
|
|
1337
|
+
const listEventsStmt = db.prepare(
|
|
1338
|
+
"SELECT * FROM events ORDER BY at DESC, seq DESC LIMIT ?"
|
|
1339
|
+
);
|
|
1340
|
+
const listEventsByRequestStmt = db.prepare(
|
|
1341
|
+
"SELECT * FROM events WHERE request_id = ? ORDER BY at DESC, seq DESC LIMIT ?"
|
|
1342
|
+
);
|
|
1343
|
+
const insertPaymentAttemptStmt = db.prepare(
|
|
1344
|
+
`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)
|
|
1345
|
+
VALUES (@id, @request_id, @resource_id, @provider, @replay_key, @status, @amount, @currency, @payer, @payee, @external_reference, @rejection_reason, @created_at, @updated_at)`
|
|
1346
|
+
);
|
|
1347
|
+
const getPaymentAttemptByIdStmt = db.prepare(
|
|
1348
|
+
"SELECT * FROM payment_attempts WHERE id = ?"
|
|
1349
|
+
);
|
|
1350
|
+
const updatePaymentAttemptStmt = db.prepare(
|
|
1351
|
+
`UPDATE payment_attempts SET status = @status,
|
|
1352
|
+
external_reference = COALESCE(@external_reference, external_reference),
|
|
1353
|
+
rejection_reason = COALESCE(@rejection_reason, rejection_reason),
|
|
1354
|
+
updated_at = @updated_at
|
|
1355
|
+
WHERE replay_key = @replay_key`
|
|
1356
|
+
);
|
|
1357
|
+
const listPaymentAttemptsStmt = db.prepare(
|
|
1358
|
+
"SELECT * FROM payment_attempts ORDER BY created_at DESC, seq DESC LIMIT ?"
|
|
1359
|
+
);
|
|
1360
|
+
const listPaymentAttemptsByRequestStmt = db.prepare(
|
|
1361
|
+
"SELECT * FROM payment_attempts WHERE request_id = ? ORDER BY created_at DESC, seq DESC LIMIT ?"
|
|
1362
|
+
);
|
|
1363
|
+
let closed = false;
|
|
1364
|
+
const descriptor = {
|
|
1365
|
+
name: "sqlite-receipt-store",
|
|
1366
|
+
kind: "storage",
|
|
1367
|
+
implementationVersion: PACKAGE_VERSION,
|
|
1368
|
+
supportedSpec: `sqlite-schema-v${SCHEMA_VERSION}`,
|
|
1369
|
+
capabilities: ["receipts", "events", "payment-attempts"],
|
|
1370
|
+
status: "stable"
|
|
1371
|
+
};
|
|
1372
|
+
const store = {
|
|
1373
|
+
async init() {
|
|
1374
|
+
const version = db.pragma("user_version", { simple: true });
|
|
1375
|
+
if (version !== SCHEMA_VERSION) {
|
|
1376
|
+
throw new CommerceError(
|
|
1377
|
+
"STORAGE_ERROR",
|
|
1378
|
+
`Receipt store schema version mismatch (found ${version}, expected ${SCHEMA_VERSION})`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
async appendEvent(event) {
|
|
1383
|
+
try {
|
|
1384
|
+
insertEventStmt.run(eventToRow(event));
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
logger.warn(
|
|
1387
|
+
{
|
|
1388
|
+
err: err instanceof Error ? err.message : String(err),
|
|
1389
|
+
eventId: event.id,
|
|
1390
|
+
requestId: event.requestId
|
|
1391
|
+
},
|
|
1392
|
+
"receipt-store: failed to persist commerce event"
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1396
|
+
async reservePaymentAttempt(reservation) {
|
|
1397
|
+
const now = clock.nowIso();
|
|
1398
|
+
const id = ids.next("attempt");
|
|
1399
|
+
try {
|
|
1400
|
+
insertPaymentAttemptStmt.run({
|
|
1401
|
+
id,
|
|
1402
|
+
request_id: reservation.requestId,
|
|
1403
|
+
resource_id: reservation.resourceId,
|
|
1404
|
+
provider: reservation.provider,
|
|
1405
|
+
replay_key: reservation.replayKey,
|
|
1406
|
+
status: "reserved",
|
|
1407
|
+
amount: reservation.amount,
|
|
1408
|
+
currency: reservation.currency,
|
|
1409
|
+
payer: reservation.payer ?? null,
|
|
1410
|
+
payee: reservation.payee ?? null,
|
|
1411
|
+
external_reference: null,
|
|
1412
|
+
rejection_reason: null,
|
|
1413
|
+
created_at: now,
|
|
1414
|
+
updated_at: now
|
|
1415
|
+
});
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
|
|
1418
|
+
throw new CommerceError(
|
|
1419
|
+
"PAYMENT_REPLAYED",
|
|
1420
|
+
`Payment authorisation has already been used (replayKey=${reservation.replayKey})`,
|
|
1421
|
+
{ requestId: reservation.requestId, resourceId: reservation.resourceId }
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");
|
|
1425
|
+
}
|
|
1426
|
+
const row = getPaymentAttemptByIdStmt.get(id);
|
|
1427
|
+
if (row === void 0) {
|
|
1428
|
+
throw new CommerceError(
|
|
1429
|
+
"STORAGE_ERROR",
|
|
1430
|
+
"Payment attempt vanished immediately after insert"
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
return rowToPaymentAttempt(row);
|
|
1434
|
+
},
|
|
1435
|
+
async updatePaymentAttempt(update) {
|
|
1436
|
+
try {
|
|
1437
|
+
const result = updatePaymentAttemptStmt.run({
|
|
1438
|
+
replay_key: update.replayKey,
|
|
1439
|
+
status: update.status,
|
|
1440
|
+
external_reference: update.externalReference ?? null,
|
|
1441
|
+
rejection_reason: update.rejectionReason ?? null,
|
|
1442
|
+
updated_at: clock.nowIso()
|
|
1443
|
+
});
|
|
1444
|
+
if (result.changes === 0) {
|
|
1445
|
+
throw new CommerceError(
|
|
1446
|
+
"STORAGE_ERROR",
|
|
1447
|
+
`No payment attempt found for replayKey=${update.replayKey}`
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
throw toCommerceError(err, "STORAGE_ERROR", "Failed to update payment attempt");
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
async saveReceipt(receipt) {
|
|
1455
|
+
try {
|
|
1456
|
+
insertReceiptStmt.run(receiptToRow(receipt));
|
|
1457
|
+
} catch (err) {
|
|
1458
|
+
throw toCommerceError(err, "STORAGE_ERROR", "Failed to save receipt");
|
|
1459
|
+
}
|
|
1460
|
+
},
|
|
1461
|
+
async getReceipt(id) {
|
|
1462
|
+
const row = getReceiptStmt.get(id);
|
|
1463
|
+
return row !== void 0 ? rowToReceipt(row) : void 0;
|
|
1464
|
+
},
|
|
1465
|
+
async listReceipts(listOptions = {}) {
|
|
1466
|
+
const limit = clampListLimit(listOptions.limit);
|
|
1467
|
+
const rows = listOptions.requestId !== void 0 ? listReceiptsByRequestStmt.all(listOptions.requestId, limit) : listReceiptsStmt.all(limit);
|
|
1468
|
+
return rows.map(rowToReceipt);
|
|
1469
|
+
},
|
|
1470
|
+
/** Exact total, ignoring the listReceipts clamp — see the interface doc for why this exists. */
|
|
1471
|
+
async countReceipts() {
|
|
1472
|
+
return countReceiptsStmt.get()?.count ?? 0;
|
|
1473
|
+
},
|
|
1474
|
+
/** Exact count of non-2xx receipts — see the interface doc for why this exists. */
|
|
1475
|
+
async countUndeliveredReceipts() {
|
|
1476
|
+
return countUndeliveredReceiptsStmt.get()?.count ?? 0;
|
|
1477
|
+
},
|
|
1478
|
+
async listEvents(listOptions = {}) {
|
|
1479
|
+
const limit = clampListLimit(listOptions.limit);
|
|
1480
|
+
const rows = listOptions.requestId !== void 0 ? listEventsByRequestStmt.all(listOptions.requestId, limit) : listEventsStmt.all(limit);
|
|
1481
|
+
return rows.map(rowToEvent);
|
|
1482
|
+
},
|
|
1483
|
+
async listPaymentAttempts(listOptions = {}) {
|
|
1484
|
+
const limit = clampListLimit(listOptions.limit);
|
|
1485
|
+
const rows = listOptions.requestId !== void 0 ? listPaymentAttemptsByRequestStmt.all(listOptions.requestId, limit) : listPaymentAttemptsStmt.all(limit);
|
|
1486
|
+
return rows.map(rowToPaymentAttempt);
|
|
1487
|
+
},
|
|
1488
|
+
descriptor,
|
|
1489
|
+
async health() {
|
|
1490
|
+
const startedAt = clock.monotonicMs();
|
|
1491
|
+
if (closed) {
|
|
1492
|
+
return { status: "fail", detail: "store-unavailable", checkedAt: clock.nowIso() };
|
|
1493
|
+
}
|
|
1494
|
+
if (isFileBacked) {
|
|
1495
|
+
try {
|
|
1496
|
+
accessSync(path2, constants.W_OK);
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
logger.warn(
|
|
1499
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
1500
|
+
"receipt-store: health check failed (store not writable)"
|
|
1501
|
+
);
|
|
1502
|
+
return {
|
|
1503
|
+
status: "fail",
|
|
1504
|
+
detail: "store-unwritable",
|
|
1505
|
+
checkedAt: clock.nowIso(),
|
|
1506
|
+
durationMs: clock.monotonicMs() - startedAt
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
try {
|
|
1511
|
+
const version = db.pragma("user_version", { simple: true });
|
|
1512
|
+
if (version !== SCHEMA_VERSION) {
|
|
1513
|
+
logger.warn(
|
|
1514
|
+
{ found: version, expected: SCHEMA_VERSION },
|
|
1515
|
+
"receipt-store: health check failed (schema version mismatch)"
|
|
1516
|
+
);
|
|
1517
|
+
return {
|
|
1518
|
+
status: "fail",
|
|
1519
|
+
detail: "store-schema-mismatch",
|
|
1520
|
+
checkedAt: clock.nowIso(),
|
|
1521
|
+
durationMs: clock.monotonicMs() - startedAt
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
status: "pass",
|
|
1526
|
+
detail: `sqlite schema v${SCHEMA_VERSION} writable`,
|
|
1527
|
+
checkedAt: clock.nowIso(),
|
|
1528
|
+
durationMs: clock.monotonicMs() - startedAt
|
|
1529
|
+
};
|
|
1530
|
+
} catch (err) {
|
|
1531
|
+
logger.warn(
|
|
1532
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
1533
|
+
"receipt-store: health check failed (store unavailable)"
|
|
1534
|
+
);
|
|
1535
|
+
return {
|
|
1536
|
+
status: "fail",
|
|
1537
|
+
detail: "store-unavailable",
|
|
1538
|
+
checkedAt: clock.nowIso(),
|
|
1539
|
+
durationMs: clock.monotonicMs() - startedAt
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
async close() {
|
|
1544
|
+
if (!closed) {
|
|
1545
|
+
closed = true;
|
|
1546
|
+
db.close();
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
return store;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/cli/lib/config-client.ts
|
|
1554
|
+
async function loadConfigDynamic(options = {}) {
|
|
1555
|
+
let mod;
|
|
1556
|
+
try {
|
|
1557
|
+
mod = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
throw new Error(
|
|
1560
|
+
`the configuration module could not be loaded: ${err instanceof Error ? err.message : String(err)}`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
return mod.loadConfig(options);
|
|
1564
|
+
}
|
|
1565
|
+
async function parseConfigDynamic(raw, env = process.env) {
|
|
1566
|
+
let mod;
|
|
1567
|
+
try {
|
|
1568
|
+
mod = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
1569
|
+
} catch (err) {
|
|
1570
|
+
throw new Error(
|
|
1571
|
+
`the configuration module could not be loaded: ${err instanceof Error ? err.message : String(err)}`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
return mod.parseConfig(raw, env);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/cli/lib/http.ts
|
|
1578
|
+
async function fetchJson(fetchImpl, url, timeoutMs = 1500) {
|
|
1579
|
+
const controller = new AbortController();
|
|
1580
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1581
|
+
try {
|
|
1582
|
+
const res = await fetchImpl(url, { signal: controller.signal });
|
|
1583
|
+
let body;
|
|
1584
|
+
try {
|
|
1585
|
+
body = await res.json();
|
|
1586
|
+
} catch {
|
|
1587
|
+
body = void 0;
|
|
1588
|
+
}
|
|
1589
|
+
return { ok: res.ok, status: res.status, ...body !== void 0 ? { body } : {} };
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
return {
|
|
1592
|
+
ok: false,
|
|
1593
|
+
status: 0,
|
|
1594
|
+
error: err instanceof Error ? err.message : "unknown fetch error"
|
|
1595
|
+
};
|
|
1596
|
+
} finally {
|
|
1597
|
+
clearTimeout(timer);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// src/payments/x402/local-chain/accounts.ts
|
|
1602
|
+
var ANVIL_WELL_KNOWN_ACCOUNTS = [
|
|
1603
|
+
{
|
|
1604
|
+
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
|
1605
|
+
privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
|
|
1609
|
+
privateKey: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
|
|
1613
|
+
privateKey: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
|
|
1614
|
+
}
|
|
1615
|
+
];
|
|
1616
|
+
|
|
1617
|
+
// src/cli/lib/init-config.ts
|
|
1618
|
+
var LOCAL_DEV_MERCHANT_ADDRESS = ANVIL_WELL_KNOWN_ACCOUNTS[1].address;
|
|
1619
|
+
var LOCAL_DEV_FACILITATOR_PRIVATE_KEY = ANVIL_WELL_KNOWN_ACCOUNTS[0].privateKey;
|
|
1620
|
+
var PLACEHOLDER_ASSET_ADDRESS = "0x000000000000000000000000000000000000dEaD";
|
|
1621
|
+
function defaultInitAnswers() {
|
|
1622
|
+
return {
|
|
1623
|
+
backendBaseUrl: "http://localhost:3000",
|
|
1624
|
+
resources: ["weather", "report"],
|
|
1625
|
+
protocols: ["http", "mcp"],
|
|
1626
|
+
x402Enabled: true,
|
|
1627
|
+
merchantPayTo: LOCAL_DEV_MERCHANT_ADDRESS
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
function buildResourcesMap(answers) {
|
|
1631
|
+
const resources = {};
|
|
1632
|
+
const expose = [...answers.protocols];
|
|
1633
|
+
if (answers.resources.includes("weather")) {
|
|
1634
|
+
resources.weather = {
|
|
1635
|
+
name: "Get weather",
|
|
1636
|
+
description: "Deterministic demo weather lookup (free resource).",
|
|
1637
|
+
input: {
|
|
1638
|
+
type: "object",
|
|
1639
|
+
properties: { city: { type: "string" } },
|
|
1640
|
+
required: ["city"],
|
|
1641
|
+
additionalProperties: false
|
|
1642
|
+
},
|
|
1643
|
+
backend: {
|
|
1644
|
+
type: "http",
|
|
1645
|
+
method: "GET",
|
|
1646
|
+
url: `${answers.backendBaseUrl}/api/weather/{city}`
|
|
1647
|
+
},
|
|
1648
|
+
pricing: { type: "free" },
|
|
1649
|
+
expose
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
if (answers.resources.includes("report")) {
|
|
1653
|
+
const paid = answers.x402Enabled;
|
|
1654
|
+
resources.report = {
|
|
1655
|
+
name: "Get market report",
|
|
1656
|
+
description: "Deterministic demo premium market report.",
|
|
1657
|
+
// No caller-supplied fields for this resource: an explicit empty,
|
|
1658
|
+
// closed schema (rather than omitting `input`) so a missing schema can
|
|
1659
|
+
// never be read as "anything goes".
|
|
1660
|
+
input: {
|
|
1661
|
+
type: "object",
|
|
1662
|
+
properties: {},
|
|
1663
|
+
additionalProperties: false
|
|
1664
|
+
},
|
|
1665
|
+
backend: {
|
|
1666
|
+
type: "http",
|
|
1667
|
+
method: "GET",
|
|
1668
|
+
url: `${answers.backendBaseUrl}/api/report`
|
|
1669
|
+
},
|
|
1670
|
+
pricing: paid ? { type: "fixed", amount: "0.01", currency: "USDC" } : { type: "free" },
|
|
1671
|
+
expose,
|
|
1672
|
+
...paid ? { payments: ["x402"] } : {}
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
return resources;
|
|
1676
|
+
}
|
|
1677
|
+
function buildInitConfigObject(answers) {
|
|
1678
|
+
return {
|
|
1679
|
+
version: 1,
|
|
1680
|
+
merchant: {
|
|
1681
|
+
id: "demo-merchant",
|
|
1682
|
+
name: "Demo Merchant",
|
|
1683
|
+
publicBaseUrl: "http://localhost:8080"
|
|
1684
|
+
},
|
|
1685
|
+
server: { port: 8080, host: "127.0.0.1" },
|
|
1686
|
+
storage: { receipts: { driver: "sqlite", path: "./data/receipts.db" } },
|
|
1687
|
+
protocols: {
|
|
1688
|
+
http: { enabled: answers.protocols.includes("http") },
|
|
1689
|
+
mcp: { enabled: answers.protocols.includes("mcp"), mountPath: "/mcp" }
|
|
1690
|
+
},
|
|
1691
|
+
resources: buildResourcesMap(answers),
|
|
1692
|
+
payments: answers.x402Enabled ? {
|
|
1693
|
+
x402: {
|
|
1694
|
+
enabled: true,
|
|
1695
|
+
network: "base-sepolia",
|
|
1696
|
+
rpcUrl: "http://127.0.0.1:8545",
|
|
1697
|
+
asset: PLACEHOLDER_ASSET_ADDRESS,
|
|
1698
|
+
assetName: "MockUSDC",
|
|
1699
|
+
assetVersion: "2",
|
|
1700
|
+
assetDecimals: 6,
|
|
1701
|
+
payTo: answers.merchantPayTo,
|
|
1702
|
+
maxTimeoutSeconds: 60,
|
|
1703
|
+
facilitator: { mode: "local", signerPrivateKey: LOCAL_DEV_FACILITATOR_PRIVATE_KEY }
|
|
1704
|
+
}
|
|
1705
|
+
} : {}
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
var HEADER = `# Generated by \`agent-commerce init\`.
|
|
1709
|
+
#
|
|
1710
|
+
# Any address/key below labelled "LOCAL DEVELOPMENT ONLY" or "placeholder" is
|
|
1711
|
+
# either one of Anvil's well-known, public dev accounts (never fund them,
|
|
1712
|
+
# never use them outside a local chain) or a
|
|
1713
|
+
# not-yet-real address. Before running against the local chain, run
|
|
1714
|
+
# \`npm run chain:deploy\` and replace \`payments.x402.asset\` with the deployed
|
|
1715
|
+
# MockUSDC address from \`.deploy/local.json\`. Replace \`payments.x402.payTo\`
|
|
1716
|
+
# with your real merchant-controlled settlement address before ever pointing
|
|
1717
|
+
# this at a funded network.
|
|
1718
|
+
#
|
|
1719
|
+
# server.host is loopback (127.0.0.1) on purpose: binding every interface
|
|
1720
|
+
# should be a deliberate edit, not something you inherit from a generator.
|
|
1721
|
+
#
|
|
1722
|
+
# server.adminToken / server.allowedOrigins are left unset here \u2014 a generated
|
|
1723
|
+
# secret in a file you might commit is its own problem, so this is left as a
|
|
1724
|
+
# placeholder for you to fill in rather than a real token. Without a token,
|
|
1725
|
+
# the operator routes (/api/receipts, /api/events, /api/events/stream) \u2014
|
|
1726
|
+
# the merchant's commerce ledger: payer addresses, amounts, settlement
|
|
1727
|
+
# hashes \u2014 return 404 rather than serving openly. To enable them, uncomment
|
|
1728
|
+
# and set:
|
|
1729
|
+
# server:
|
|
1730
|
+
# adminToken: \${ADMIN_TOKEN}
|
|
1731
|
+
# allowedOrigins:
|
|
1732
|
+
# - http://localhost:5173
|
|
1733
|
+
# See SECURITY.md before exposing this gateway beyond localhost.
|
|
1734
|
+
`;
|
|
1735
|
+
function renderInitConfigYaml(answers) {
|
|
1736
|
+
const body = stringify(buildInitConfigObject(answers), { indent: 2 });
|
|
1737
|
+
return `${HEADER}
|
|
1738
|
+
${body}`;
|
|
1739
|
+
}
|
|
1740
|
+
var LOCAL_CHAIN_MANIFEST_PATH = ".deploy/local.json";
|
|
1741
|
+
var REQUIRED_TOP_LEVEL_STRING_FIELDS = [
|
|
1742
|
+
"rpcUrl",
|
|
1743
|
+
"asset",
|
|
1744
|
+
"assetName",
|
|
1745
|
+
"assetVersion",
|
|
1746
|
+
"buyerInitialBalance"
|
|
1747
|
+
];
|
|
1748
|
+
function readLocalChainManifest(cwd = process.cwd()) {
|
|
1749
|
+
const path2 = join(cwd, LOCAL_CHAIN_MANIFEST_PATH);
|
|
1750
|
+
if (!existsSync(path2)) {
|
|
1751
|
+
throw new Error(
|
|
1752
|
+
`Local chain manifest not found at "${path2}". Run "npm run chain:start" then "npm run chain:deploy" first.`
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
let raw;
|
|
1756
|
+
try {
|
|
1757
|
+
raw = readFileSync(path2, "utf8");
|
|
1758
|
+
} catch (cause) {
|
|
1759
|
+
throw new Error(`Could not read local chain manifest at "${path2}".`, { cause });
|
|
1760
|
+
}
|
|
1761
|
+
let parsed;
|
|
1762
|
+
try {
|
|
1763
|
+
parsed = JSON.parse(raw);
|
|
1764
|
+
} catch (cause) {
|
|
1765
|
+
throw new Error(
|
|
1766
|
+
`Local chain manifest at "${path2}" is not valid JSON. Re-run "npm run chain:deploy" to regenerate it.`,
|
|
1767
|
+
{ cause }
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
return validateManifestShape(parsed, path2);
|
|
1771
|
+
}
|
|
1772
|
+
function validateManifestShape(value, path2) {
|
|
1773
|
+
const fail = (detail) => {
|
|
1774
|
+
throw new Error(
|
|
1775
|
+
`Local chain manifest at "${path2}" is invalid: ${detail}. Re-run "npm run chain:deploy".`
|
|
1776
|
+
);
|
|
1777
|
+
};
|
|
1778
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1779
|
+
return fail("expected a JSON object");
|
|
1780
|
+
}
|
|
1781
|
+
const record = value;
|
|
1782
|
+
if (typeof record.chainId !== "number") {
|
|
1783
|
+
return fail('"chainId" must be a number');
|
|
1784
|
+
}
|
|
1785
|
+
if (typeof record.assetDecimals !== "number") {
|
|
1786
|
+
return fail('"assetDecimals" must be a number');
|
|
1787
|
+
}
|
|
1788
|
+
for (const field of REQUIRED_TOP_LEVEL_STRING_FIELDS) {
|
|
1789
|
+
if (typeof record[field] !== "string" || record[field] === "") {
|
|
1790
|
+
return fail(`"${field}" must be a non-empty string`);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
if ("hostRpcUrl" in record && record.hostRpcUrl !== void 0) {
|
|
1794
|
+
if (typeof record.hostRpcUrl !== "string" || record.hostRpcUrl === "") {
|
|
1795
|
+
return fail('"hostRpcUrl" must be a non-empty string when present');
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
const merchant = record.merchant;
|
|
1799
|
+
if (typeof merchant !== "object" || merchant === null || typeof merchant.address !== "string" || typeof merchant.privateKeyLabel !== "string") {
|
|
1800
|
+
return fail('"merchant" must be { address: string, privateKeyLabel: string }');
|
|
1801
|
+
}
|
|
1802
|
+
for (const key of ["buyer", "facilitator"]) {
|
|
1803
|
+
const account = record[key];
|
|
1804
|
+
if (typeof account !== "object" || account === null || typeof account.address !== "string" || typeof account.privateKey !== "string" || typeof account.note !== "string") {
|
|
1805
|
+
return fail(`"${key}" must be { address: string, privateKey: string, note: string }`);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return record;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/cli/lib/manifest-env.ts
|
|
1812
|
+
var MANIFEST_ENV_VARS = [
|
|
1813
|
+
["X402_ASSET", (m) => m.asset],
|
|
1814
|
+
["X402_ASSET_NAME", (m) => m.assetName],
|
|
1815
|
+
["X402_ASSET_VERSION", (m) => m.assetVersion],
|
|
1816
|
+
["X402_ASSET_DECIMALS", (m) => String(m.assetDecimals)],
|
|
1817
|
+
["MERCHANT_WALLET", (m) => m.merchant.address],
|
|
1818
|
+
["X402_FACILITATOR_PRIVATE_KEY", (m) => m.facilitator.privateKey]
|
|
1819
|
+
];
|
|
1820
|
+
var MANIFEST_FILLABLE_ENV_VAR_NAMES = new Set(
|
|
1821
|
+
MANIFEST_ENV_VARS.map(([name]) => name)
|
|
1822
|
+
);
|
|
1823
|
+
function fillEnvFromLocalChainManifest(baseEnv, cwd = process.cwd()) {
|
|
1824
|
+
let manifest;
|
|
1825
|
+
try {
|
|
1826
|
+
manifest = readLocalChainManifest(cwd);
|
|
1827
|
+
} catch {
|
|
1828
|
+
return { env: baseEnv, filled: [], manifestFound: false };
|
|
1829
|
+
}
|
|
1830
|
+
const filled = [];
|
|
1831
|
+
const env = { ...baseEnv };
|
|
1832
|
+
for (const [name, read] of MANIFEST_ENV_VARS) {
|
|
1833
|
+
if (env[name] === void 0) {
|
|
1834
|
+
env[name] = read(manifest);
|
|
1835
|
+
filled.push(name);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return { env, filled, manifestFound: true };
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/cli/lib/mask.ts
|
|
1842
|
+
function maskMiddle(value, keep = 6) {
|
|
1843
|
+
if (value.length <= keep * 2) return value.length === 0 ? value : "\u2026";
|
|
1844
|
+
return `${value.slice(0, keep)}\u2026${value.slice(-4)}`;
|
|
1845
|
+
}
|
|
1846
|
+
createRequire(import.meta.url);
|
|
1847
|
+
function readVersionReport() {
|
|
1848
|
+
const injected = '{"cliVersion":"0.1.0-alpha.0","pinned":[{"name":"@modelcontextprotocol/sdk","version":"1.30.0","via":"@devlab.group/agent-commerce"},{"name":"x402","version":"1.2.0","via":"@devlab.group/agent-commerce"},{"name":"viem","version":"2.55.18","via":"@devlab.group/agent-commerce"},{"name":"better-sqlite3","version":"13.0.3","via":"@devlab.group/agent-commerce"},{"name":"fastify","version":"5.12.0","via":"@devlab.group/agent-commerce"},{"name":"pino","version":"10.3.1","via":"@devlab.group/agent-commerce"},{"name":"zod","version":"3.25.76","via":"@devlab.group/agent-commerce"}]}' ;
|
|
1849
|
+
{
|
|
1850
|
+
return JSON.parse(injected);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/cli/commands/doctor.ts
|
|
1855
|
+
var CHECK_TIMEOUT_MS = 1500;
|
|
1856
|
+
function deriveGatewayUrl(explicit, config) {
|
|
1857
|
+
if (explicit !== void 0) return explicit.replace(/\/$/, "");
|
|
1858
|
+
if (config !== void 0) {
|
|
1859
|
+
const raw = config.server.host;
|
|
1860
|
+
const host = raw === "0.0.0.0" ? "127.0.0.1" : raw === "::" ? "::1" : raw;
|
|
1861
|
+
const authority = host.includes(":") ? `[${host}]` : host;
|
|
1862
|
+
return `http://${authority}:${config.server.port}`;
|
|
1863
|
+
}
|
|
1864
|
+
return "http://localhost:8080";
|
|
1865
|
+
}
|
|
1866
|
+
function substitutePathParams(url) {
|
|
1867
|
+
let filled = url;
|
|
1868
|
+
for (const name of extractPathParameterNames(url)) {
|
|
1869
|
+
filled = filled.split(`{${name}}`).join("demo-check");
|
|
1870
|
+
}
|
|
1871
|
+
return filled;
|
|
1872
|
+
}
|
|
1873
|
+
function extractWellKnownX402(body) {
|
|
1874
|
+
const payments = body?.["payments"];
|
|
1875
|
+
if (typeof payments !== "object" || payments === null) return void 0;
|
|
1876
|
+
const x402 = payments["x402"];
|
|
1877
|
+
if (typeof x402 !== "object" || x402 === null) return void 0;
|
|
1878
|
+
const rec = x402;
|
|
1879
|
+
if (rec["enabled"] === false) return "disabled";
|
|
1880
|
+
if (rec["enabled"] !== true) return void 0;
|
|
1881
|
+
const { asset, network, payTo } = rec;
|
|
1882
|
+
if (typeof asset !== "string" || typeof network !== "string" || typeof payTo !== "string") {
|
|
1883
|
+
return void 0;
|
|
1884
|
+
}
|
|
1885
|
+
return { asset, network, payTo };
|
|
1886
|
+
}
|
|
1887
|
+
function sameAddress(a, b) {
|
|
1888
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
1889
|
+
}
|
|
1890
|
+
function findX402Mismatch(configured, live) {
|
|
1891
|
+
const diffs = [];
|
|
1892
|
+
if (!sameAddress(configured.asset, live.asset)) {
|
|
1893
|
+
diffs.push(
|
|
1894
|
+
`asset: gateway is using ${live.asset} but local config resolves to ${configured.asset}`
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
if (configured.network !== live.network) {
|
|
1898
|
+
diffs.push(
|
|
1899
|
+
`network: gateway is using "${live.network}" but local config resolves to "${configured.network}"`
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
if (!sameAddress(configured.payTo, live.payTo)) {
|
|
1903
|
+
diffs.push(
|
|
1904
|
+
`payTo: gateway is using ${live.payTo} but local config resolves to ${configured.payTo}`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
if (diffs.length === 0) return void 0;
|
|
1908
|
+
return `${diffs.join("; ")} \u2014 the gateway may be running against an older deployment; restart it or re-run chain:deploy`;
|
|
1909
|
+
}
|
|
1910
|
+
async function runDoctor(options, deps = {}) {
|
|
1911
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
1912
|
+
const loadConfig2 = deps.loadConfig ?? loadConfigDynamic;
|
|
1913
|
+
const createStore = deps.createStore ?? ((path2) => createSqliteReceiptStore({ path: path2 }));
|
|
1914
|
+
const fillEnvFromManifest = deps.fillEnvFromManifest ?? fillEnvFromLocalChainManifest;
|
|
1915
|
+
const checks = [];
|
|
1916
|
+
let config;
|
|
1917
|
+
const { env, filled, manifestFound } = fillEnvFromManifest(process.env);
|
|
1918
|
+
const filledSuffix = filled.length > 0 ? ` (using local chain manifest ${LOCAL_CHAIN_MANIFEST_PATH} for ${filled.join(", ")})` : "";
|
|
1919
|
+
try {
|
|
1920
|
+
config = await loadConfig2({
|
|
1921
|
+
...options.configPath !== void 0 ? { path: options.configPath } : {},
|
|
1922
|
+
env
|
|
1923
|
+
});
|
|
1924
|
+
checks.push({
|
|
1925
|
+
name: "Config",
|
|
1926
|
+
status: "PASS",
|
|
1927
|
+
detail: `valid \u2014 ${config.resources.length} resource(s), merchant "${config.merchant.name}"${filledSuffix}`
|
|
1928
|
+
});
|
|
1929
|
+
} catch (err) {
|
|
1930
|
+
const variable = isCommerceError(err) ? err.details?.["variable"] : void 0;
|
|
1931
|
+
const hint = !manifestFound && typeof variable === "string" && MANIFEST_FILLABLE_ENV_VAR_NAMES.has(variable) ? ` \u2014 run "npm run chain:deploy" (writes ${LOCAL_CHAIN_MANIFEST_PATH}, read automatically for local X402_*/MERCHANT_WALLET placeholders)` : "";
|
|
1932
|
+
checks.push({
|
|
1933
|
+
name: "Config",
|
|
1934
|
+
status: "FAIL",
|
|
1935
|
+
detail: `${err instanceof Error ? err.message : String(err)}${hint}`
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
const gatewayUrl = deriveGatewayUrl(options.gatewayUrl, config);
|
|
1939
|
+
const health = await fetchJson(fetchImpl, `${gatewayUrl}/health`, CHECK_TIMEOUT_MS);
|
|
1940
|
+
let gatewayUp = false;
|
|
1941
|
+
if (!health.ok) {
|
|
1942
|
+
checks.push({
|
|
1943
|
+
name: "Gateway",
|
|
1944
|
+
status: "FAIL",
|
|
1945
|
+
detail: `unreachable at ${gatewayUrl} (${health.error ?? `HTTP ${health.status}`})`
|
|
1946
|
+
});
|
|
1947
|
+
} else {
|
|
1948
|
+
gatewayUp = true;
|
|
1949
|
+
const ready = await fetchJson(fetchImpl, `${gatewayUrl}/ready`, CHECK_TIMEOUT_MS);
|
|
1950
|
+
checks.push(
|
|
1951
|
+
ready.ok ? { name: "Gateway", status: "PASS", detail: `healthy and ready at ${gatewayUrl}` } : {
|
|
1952
|
+
name: "Gateway",
|
|
1953
|
+
status: "WARN",
|
|
1954
|
+
detail: `healthy but not ready at ${gatewayUrl} (${ready.error ?? `HTTP ${ready.status}`})`
|
|
1955
|
+
}
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
if (config === void 0) {
|
|
1959
|
+
checks.push({ name: "Backend", status: "WARN", detail: "skipped \u2014 config invalid" });
|
|
1960
|
+
} else if (config.resources.length === 0) {
|
|
1961
|
+
checks.push({ name: "Backend", status: "INFO", detail: "no resources configured" });
|
|
1962
|
+
} else {
|
|
1963
|
+
const resources = config.resources;
|
|
1964
|
+
const urls = [...new Set(resources.map((r) => substitutePathParams(r.handler.url)))];
|
|
1965
|
+
const results = await Promise.all(
|
|
1966
|
+
urls.map((url) => fetchJson(fetchImpl, url, CHECK_TIMEOUT_MS))
|
|
1967
|
+
);
|
|
1968
|
+
const reachable = results.filter((r) => r.status !== 0).length;
|
|
1969
|
+
const status = reachable === urls.length ? "PASS" : reachable === 0 ? "FAIL" : "WARN";
|
|
1970
|
+
checks.push({
|
|
1971
|
+
name: "Backend",
|
|
1972
|
+
status,
|
|
1973
|
+
detail: `${reachable}/${urls.length} backend host(s) reachable`
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
const wellKnown = gatewayUp ? await fetchJson(
|
|
1977
|
+
fetchImpl,
|
|
1978
|
+
`${gatewayUrl}/.well-known/agent-commerce`,
|
|
1979
|
+
CHECK_TIMEOUT_MS
|
|
1980
|
+
) : void 0;
|
|
1981
|
+
if (config === void 0) {
|
|
1982
|
+
checks.push({ name: "Protocols", status: "WARN", detail: "skipped \u2014 config invalid" });
|
|
1983
|
+
} else if (wellKnown === void 0 || !wellKnown.ok) {
|
|
1984
|
+
checks.push({ name: "Protocols", status: "FAIL", detail: "well-known document unreachable" });
|
|
1985
|
+
} else {
|
|
1986
|
+
const mcpMountPath = config.protocols.mcp.enabled ? config.protocols.mcp.mountPath : void 0;
|
|
1987
|
+
checks.push({
|
|
1988
|
+
name: "Protocols",
|
|
1989
|
+
status: "PASS",
|
|
1990
|
+
detail: `http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? `on (${mcpMountPath})` : "off"}`
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
const x402 = config?.payments.x402;
|
|
1994
|
+
if (x402 === void 0 || !x402.enabled) {
|
|
1995
|
+
checks.push({ name: "Payments", status: "INFO", detail: "x402 not configured" });
|
|
1996
|
+
} else {
|
|
1997
|
+
const summary = `x402 enabled \u2014 network=${x402.network}, destination=${maskMiddle(x402.payTo)}, facilitator=${x402.facilitator.mode}`;
|
|
1998
|
+
const live = wellKnown?.ok ? extractWellKnownX402(wellKnown.body) : void 0;
|
|
1999
|
+
if (sameAddress(x402.asset, PLACEHOLDER_ASSET_ADDRESS)) {
|
|
2000
|
+
checks.push({
|
|
2001
|
+
name: "Payments",
|
|
2002
|
+
status: "WARN",
|
|
2003
|
+
detail: `${summary}, but the asset is still the init placeholder ${maskMiddle(PLACEHOLDER_ASSET_ADDRESS)} \u2014 no token contract exists there, so every paid call will fail. Set payments.x402.asset (\${X402_ASSET} is filled from .deploy/local.json by "npm run chain:deploy")`
|
|
2004
|
+
});
|
|
2005
|
+
} else if (live === "disabled") {
|
|
2006
|
+
checks.push({
|
|
2007
|
+
name: "Payments",
|
|
2008
|
+
status: "FAIL",
|
|
2009
|
+
detail: `${summary}, but the gateway at ${gatewayUrl} reports x402 disabled \u2014 it is running a different configuration`
|
|
2010
|
+
});
|
|
2011
|
+
} else if (live === void 0) {
|
|
2012
|
+
checks.push({
|
|
2013
|
+
name: "Payments",
|
|
2014
|
+
status: "INFO",
|
|
2015
|
+
detail: `${summary} (gateway's live settlement config could not be verified)`
|
|
2016
|
+
});
|
|
2017
|
+
} else {
|
|
2018
|
+
const mismatch = findX402Mismatch(x402, live);
|
|
2019
|
+
checks.push(
|
|
2020
|
+
mismatch === void 0 ? { name: "Payments", status: "PASS", detail: summary } : { name: "Payments", status: "FAIL", detail: mismatch }
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
checks.push({
|
|
2025
|
+
name: "Payments (MPP)",
|
|
2026
|
+
status: "INFO",
|
|
2027
|
+
detail: "planned \u2014 not implemented in v0.1"
|
|
2028
|
+
});
|
|
2029
|
+
if (config === void 0) {
|
|
2030
|
+
checks.push({ name: "Storage", status: "WARN", detail: "skipped \u2014 config invalid" });
|
|
2031
|
+
} else if (
|
|
2032
|
+
// Diagnose without creating: createSqliteReceiptStore mkdir+creates the
|
|
2033
|
+
// file, so calling it on a wrong path (a typo, or a container path read
|
|
2034
|
+
// from the host) silently produces a fresh empty database and a false
|
|
2035
|
+
// "healthy, receipts=0" PASS — a read-only command must not have this
|
|
2036
|
+
// side effect, and it shadows the real store's real receipts entirely.
|
|
2037
|
+
//. `:memory:` never exists on disk by
|
|
2038
|
+
// definition, so it always falls through to the real open/health/count
|
|
2039
|
+
// branch below, same as today.
|
|
2040
|
+
config.storage.receipts.path !== ":memory:" && !existsSync(config.storage.receipts.path)
|
|
2041
|
+
) {
|
|
2042
|
+
checks.push({
|
|
2043
|
+
name: "Storage",
|
|
2044
|
+
status: "WARN",
|
|
2045
|
+
detail: `no store exists yet at "${config.storage.receipts.path}" \u2014 it is created automatically the first time the gateway starts. If the gateway is already running, this path does not match the one it actually uses.`
|
|
2046
|
+
});
|
|
2047
|
+
} else {
|
|
2048
|
+
let store;
|
|
2049
|
+
try {
|
|
2050
|
+
store = createStore(config.storage.receipts.path);
|
|
2051
|
+
await store.init();
|
|
2052
|
+
const storeHealth = await store.health();
|
|
2053
|
+
let receiptCount;
|
|
2054
|
+
try {
|
|
2055
|
+
receiptCount = await store.countReceipts();
|
|
2056
|
+
} catch {
|
|
2057
|
+
receiptCount = void 0;
|
|
2058
|
+
}
|
|
2059
|
+
let undeliveredCount;
|
|
2060
|
+
try {
|
|
2061
|
+
undeliveredCount = await store.countUndeliveredReceipts();
|
|
2062
|
+
} catch {
|
|
2063
|
+
undeliveredCount = void 0;
|
|
2064
|
+
}
|
|
2065
|
+
const status = storeHealth.status === "pass" ? "PASS" : storeHealth.status === "warn" ? "WARN" : "FAIL";
|
|
2066
|
+
const receiptsSuffix = receiptCount !== void 0 ? `; receipts=${receiptCount}${undeliveredCount !== void 0 && undeliveredCount > 0 ? ` (${undeliveredCount} undelivered)` : ""}` : "";
|
|
2067
|
+
checks.push({
|
|
2068
|
+
name: "Storage",
|
|
2069
|
+
status,
|
|
2070
|
+
detail: `${storeHealth.detail ?? "sqlite"}${receiptsSuffix}`
|
|
2071
|
+
});
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
checks.push({
|
|
2074
|
+
name: "Storage",
|
|
2075
|
+
status: "FAIL",
|
|
2076
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
2077
|
+
});
|
|
2078
|
+
} finally {
|
|
2079
|
+
await store?.close();
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
if (wellKnown !== void 0 && wellKnown.ok) {
|
|
2083
|
+
checks.push({
|
|
2084
|
+
name: "Protocol versions",
|
|
2085
|
+
status: "PASS",
|
|
2086
|
+
detail: "reported by gateway /.well-known/agent-commerce"
|
|
2087
|
+
});
|
|
2088
|
+
} else {
|
|
2089
|
+
const local = readVersionReport();
|
|
2090
|
+
checks.push({
|
|
2091
|
+
name: "Protocol versions",
|
|
2092
|
+
status: "INFO",
|
|
2093
|
+
detail: `gateway unreachable \u2014 local pins: ${local.pinned.map((p) => `${p.name}@${p.version}`).join(", ")}`
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
const scored = checks.filter((c) => c.status !== "INFO");
|
|
2097
|
+
const passed = scored.filter((c) => c.status === "PASS").length;
|
|
2098
|
+
const exitCode = checks.some((c) => c.status === "FAIL") ? 1 : 0;
|
|
2099
|
+
return { checks, score: { passed, total: scored.length }, exitCode };
|
|
2100
|
+
}
|
|
2101
|
+
function statusColor(status, text2) {
|
|
2102
|
+
switch (status) {
|
|
2103
|
+
case "PASS":
|
|
2104
|
+
return picocolors.green(text2);
|
|
2105
|
+
case "WARN":
|
|
2106
|
+
return picocolors.yellow(text2);
|
|
2107
|
+
case "FAIL":
|
|
2108
|
+
return picocolors.red(text2);
|
|
2109
|
+
case "INFO":
|
|
2110
|
+
return picocolors.cyan(text2);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
function printDoctorReport(report, io, json) {
|
|
2114
|
+
if (json) {
|
|
2115
|
+
io.stdout(JSON.stringify(report, null, 2));
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
for (const check of report.checks) {
|
|
2119
|
+
const label = statusColor(check.status, check.status.padEnd(5));
|
|
2120
|
+
io.stdout(`${label} ${check.name.padEnd(20)} ${check.detail}`);
|
|
2121
|
+
}
|
|
2122
|
+
io.stdout("");
|
|
2123
|
+
io.stdout(`Score: ${report.score.passed}/${report.score.total} checks passed`);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// src/cli/commands/init.ts
|
|
2127
|
+
init_filename();
|
|
2128
|
+
|
|
2129
|
+
// src/cli/commands/validate.ts
|
|
2130
|
+
init_core();
|
|
2131
|
+
function formatConfigError(err, manifestFound = false) {
|
|
2132
|
+
if (isCommerceError(err)) {
|
|
2133
|
+
const lines = [`FAIL ${err.code}: ${err.message}`];
|
|
2134
|
+
if (err.details !== void 0) {
|
|
2135
|
+
lines.push(` details: ${JSON.stringify(err.details)}`);
|
|
2136
|
+
}
|
|
2137
|
+
const variable = err.details?.["variable"];
|
|
2138
|
+
if (!manifestFound && typeof variable === "string" && MANIFEST_FILLABLE_ENV_VAR_NAMES.has(variable)) {
|
|
2139
|
+
lines.push(
|
|
2140
|
+
` hint: "npm run chain:deploy" writes ${LOCAL_CHAIN_MANIFEST_PATH}, which validate/doctor read automatically to fill local X402_*/MERCHANT_WALLET placeholders.`
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
return lines.join("\n");
|
|
2144
|
+
}
|
|
2145
|
+
if (err instanceof ZodError) {
|
|
2146
|
+
const lines = ["FAIL Configuration schema errors:"];
|
|
2147
|
+
for (const issue of err.issues) {
|
|
2148
|
+
const path2 = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
2149
|
+
lines.push(` - ${path2}: ${issue.message}`);
|
|
2150
|
+
}
|
|
2151
|
+
return lines.join("\n");
|
|
2152
|
+
}
|
|
2153
|
+
if (err instanceof Error) {
|
|
2154
|
+
return `FAIL ${err.message}`;
|
|
2155
|
+
}
|
|
2156
|
+
return `FAIL ${String(err)}`;
|
|
2157
|
+
}
|
|
2158
|
+
async function runValidate(options, io, deps = {}) {
|
|
2159
|
+
const loadConfig2 = deps.loadConfig ?? loadConfigDynamic;
|
|
2160
|
+
const fillEnvFromManifest = deps.fillEnvFromManifest ?? fillEnvFromLocalChainManifest;
|
|
2161
|
+
const { env, filled, manifestFound } = fillEnvFromManifest(process.env);
|
|
2162
|
+
if (filled.length > 0) {
|
|
2163
|
+
io.stdout(`using local chain manifest ${LOCAL_CHAIN_MANIFEST_PATH} for ${filled.join(", ")}`);
|
|
2164
|
+
}
|
|
2165
|
+
try {
|
|
2166
|
+
const config = await loadConfig2({
|
|
2167
|
+
...options.configPath !== void 0 ? { path: options.configPath } : {},
|
|
2168
|
+
env
|
|
2169
|
+
});
|
|
2170
|
+
io.stdout("PASS Configuration is valid");
|
|
2171
|
+
io.stdout(` merchant: ${config.merchant.name} (${config.merchant.id})`);
|
|
2172
|
+
io.stdout(` resources: ${config.resources.length}`);
|
|
2173
|
+
io.stdout(
|
|
2174
|
+
` protocols: http=${config.protocols.http.enabled ? "on" : "off"} mcp=${config.protocols.mcp.enabled ? "on" : "off"}`
|
|
2175
|
+
);
|
|
2176
|
+
io.stdout(` payments: x402=${config.payments.x402?.enabled === true ? "on" : "off"}`);
|
|
2177
|
+
return 0;
|
|
2178
|
+
} catch (err) {
|
|
2179
|
+
io.stderr(formatConfigError(err, manifestFound));
|
|
2180
|
+
return 1;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// src/cli/commands/init.ts
|
|
2185
|
+
async function collectAnswersInteractive() {
|
|
2186
|
+
clack.intro("agent-commerce init");
|
|
2187
|
+
const backendBaseUrl = await clack.text({
|
|
2188
|
+
message: "Backend base URL (your existing merchant API)",
|
|
2189
|
+
initialValue: "http://localhost:3000",
|
|
2190
|
+
// shape-checked here, where the user can retype it,
|
|
2191
|
+
// rather than surfacing as a config error after the fact.
|
|
2192
|
+
validate: (value) => {
|
|
2193
|
+
const trimmed = (value ?? "").trim();
|
|
2194
|
+
if (trimmed.length === 0) return "Required";
|
|
2195
|
+
let parsed;
|
|
2196
|
+
try {
|
|
2197
|
+
parsed = new URL(trimmed);
|
|
2198
|
+
} catch {
|
|
2199
|
+
return "Must be an absolute URL, e.g. http://localhost:3000";
|
|
2200
|
+
}
|
|
2201
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? void 0 : "Must use http:// or https://";
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
if (clack.isCancel(backendBaseUrl)) {
|
|
2205
|
+
clack.cancel("Aborted.");
|
|
2206
|
+
return void 0;
|
|
2207
|
+
}
|
|
2208
|
+
const resources = await clack.multiselect({
|
|
2209
|
+
message: "Which example resource(s) do you want to expose?",
|
|
2210
|
+
options: [
|
|
2211
|
+
{ value: "weather", label: "Weather (free)" },
|
|
2212
|
+
{ value: "report", label: "Market report (paid)" }
|
|
2213
|
+
],
|
|
2214
|
+
initialValues: ["weather", "report"],
|
|
2215
|
+
required: false
|
|
2216
|
+
});
|
|
2217
|
+
if (clack.isCancel(resources)) {
|
|
2218
|
+
clack.cancel("Aborted.");
|
|
2219
|
+
return void 0;
|
|
2220
|
+
}
|
|
2221
|
+
const protocols = await clack.multiselect({
|
|
2222
|
+
message: "Which protocols should be enabled?",
|
|
2223
|
+
options: [
|
|
2224
|
+
{ value: "http", label: "HTTP" },
|
|
2225
|
+
{ value: "mcp", label: "MCP" }
|
|
2226
|
+
],
|
|
2227
|
+
initialValues: ["http", "mcp"],
|
|
2228
|
+
// `required: false` permits an empty selection, which renders
|
|
2229
|
+
// `expose: []` on every resource and fails the schema's min(1). clack's
|
|
2230
|
+
// multiselect has no `validate` hook, so this is caught by the in-memory
|
|
2231
|
+
// validation in `runInit` — which now reports the real schema error and
|
|
2232
|
+
// writes nothing, instead of leaving an invalid file behind.
|
|
2233
|
+
required: false
|
|
2234
|
+
});
|
|
2235
|
+
if (clack.isCancel(protocols)) {
|
|
2236
|
+
clack.cancel("Aborted.");
|
|
2237
|
+
return void 0;
|
|
2238
|
+
}
|
|
2239
|
+
const x402Enabled = await clack.confirm({ message: "Enable x402 payments?", initialValue: true });
|
|
2240
|
+
if (clack.isCancel(x402Enabled)) {
|
|
2241
|
+
clack.cancel("Aborted.");
|
|
2242
|
+
return void 0;
|
|
2243
|
+
}
|
|
2244
|
+
let merchantPayTo = defaultInitAnswers().merchantPayTo;
|
|
2245
|
+
if (x402Enabled) {
|
|
2246
|
+
const answer = await clack.text({
|
|
2247
|
+
message: "Merchant settlement destination address (merchant-controlled \u2014 never a gateway-owned wallet)",
|
|
2248
|
+
initialValue: merchantPayTo,
|
|
2249
|
+
validate: (value) => {
|
|
2250
|
+
const trimmed = (value ?? "").trim();
|
|
2251
|
+
if (trimmed.length === 0) return "Required";
|
|
2252
|
+
return /^0x[0-9a-fA-F]{40}$/.test(trimmed) ? void 0 : "Must be a 0x-prefixed 20-byte EVM address (42 characters)";
|
|
2253
|
+
}
|
|
2254
|
+
});
|
|
2255
|
+
if (clack.isCancel(answer)) {
|
|
2256
|
+
clack.cancel("Aborted.");
|
|
2257
|
+
return void 0;
|
|
2258
|
+
}
|
|
2259
|
+
merchantPayTo = answer;
|
|
2260
|
+
}
|
|
2261
|
+
clack.outro("Configuration answers collected.");
|
|
2262
|
+
return { backendBaseUrl, resources, protocols, x402Enabled, merchantPayTo };
|
|
2263
|
+
}
|
|
2264
|
+
async function runInit(options, io, deps = {}) {
|
|
2265
|
+
const outputPath = options.outputPath ?? DEFAULT_CONFIG_FILENAME;
|
|
2266
|
+
const loadConfig2 = deps.loadConfig ?? loadConfigDynamic;
|
|
2267
|
+
const parseConfigInMemory = deps.parseConfig ?? parseConfigDynamic;
|
|
2268
|
+
const fileExists = deps.fileExists ?? existsSync;
|
|
2269
|
+
const writeFileImpl = deps.writeFile ?? (async (path2, content) => {
|
|
2270
|
+
await writeFile(path2, content, "utf8");
|
|
2271
|
+
});
|
|
2272
|
+
if (fileExists(outputPath) && options.force !== true) {
|
|
2273
|
+
io.stderr(`FAIL ${outputPath} already exists. Re-run with --force to overwrite.`);
|
|
2274
|
+
return 1;
|
|
2275
|
+
}
|
|
2276
|
+
const answers = options.yes === true ? defaultInitAnswers() : await (deps.collectAnswers ?? collectAnswersInteractive)();
|
|
2277
|
+
if (answers === void 0) {
|
|
2278
|
+
io.stderr("FAIL init aborted, nothing was written.");
|
|
2279
|
+
return 1;
|
|
2280
|
+
}
|
|
2281
|
+
let config;
|
|
2282
|
+
try {
|
|
2283
|
+
config = await parseConfigInMemory(buildInitConfigObject(answers));
|
|
2284
|
+
} catch (err) {
|
|
2285
|
+
io.stderr(formatConfigError(err));
|
|
2286
|
+
io.stderr(`FAIL Nothing was written to ${outputPath}.`);
|
|
2287
|
+
return 1;
|
|
2288
|
+
}
|
|
2289
|
+
const yamlContent = renderInitConfigYaml(answers);
|
|
2290
|
+
const resolvedPath = resolve(outputPath);
|
|
2291
|
+
const dir = dirname(resolvedPath);
|
|
2292
|
+
if (dir !== "" && !existsSync(dir)) {
|
|
2293
|
+
mkdirSync(dir, { recursive: true });
|
|
2294
|
+
}
|
|
2295
|
+
await writeFileImpl(outputPath, yamlContent);
|
|
2296
|
+
io.stdout(`PASS Wrote ${outputPath}`);
|
|
2297
|
+
io.stdout(`PASS ${outputPath} is valid \u2014 ${config.resources.length} resource(s)`);
|
|
2298
|
+
try {
|
|
2299
|
+
await loadConfig2({ path: outputPath });
|
|
2300
|
+
} catch (err) {
|
|
2301
|
+
io.stderr(formatConfigError(err));
|
|
2302
|
+
io.stderr(
|
|
2303
|
+
`FAIL ${outputPath} was written but does not parse, although the same answers validated in memory \u2014 this is a bug in the config renderer, please report it.`
|
|
2304
|
+
);
|
|
2305
|
+
return 1;
|
|
2306
|
+
}
|
|
2307
|
+
io.stdout("");
|
|
2308
|
+
io.stdout("Next steps:");
|
|
2309
|
+
io.stdout(` agent-commerce validate --config ${outputPath}`);
|
|
2310
|
+
io.stdout(" docker compose up # start the local demo stack (from a repo clone)");
|
|
2311
|
+
io.stdout(` agent-commerce doctor --config ${outputPath}`);
|
|
2312
|
+
return 0;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// src/cli/commands/version.ts
|
|
2316
|
+
function runVersion(io, deps = {}) {
|
|
2317
|
+
const report = (deps.readVersionReport ?? readVersionReport)();
|
|
2318
|
+
io.stdout(`agent-commerce v${report.cliVersion}`);
|
|
2319
|
+
if (report.pinned.length > 0) {
|
|
2320
|
+
io.stdout("");
|
|
2321
|
+
io.stdout("Pinned protocol / SDK versions:");
|
|
2322
|
+
for (const pin of report.pinned) {
|
|
2323
|
+
io.stdout(` ${pin.name.padEnd(24)} ${pin.version.padEnd(12)} (via ${pin.via})`);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
return 0;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// src/cli/lib/io.ts
|
|
2330
|
+
var processIo = {
|
|
2331
|
+
stdout: (line) => {
|
|
2332
|
+
process.stdout.write(`${line}
|
|
2333
|
+
`);
|
|
2334
|
+
},
|
|
2335
|
+
stderr: (line) => {
|
|
2336
|
+
process.stderr.write(`${line}
|
|
2337
|
+
`);
|
|
2338
|
+
}
|
|
2339
|
+
};
|
|
2340
|
+
|
|
2341
|
+
// src/cli/program.ts
|
|
2342
|
+
function buildProgram(io = processIo) {
|
|
2343
|
+
const program = new Command();
|
|
2344
|
+
program.name("agent-commerce").description("CLI for the Agent Commerce Gateway: init, validate, doctor, demo.").version(readVersionReport().cliVersion, "-v, --version", "Print the CLI version.").exitOverride().configureOutput({
|
|
2345
|
+
writeOut: (str) => io.stdout(str.replace(/\n+$/, "")),
|
|
2346
|
+
writeErr: (str) => io.stderr(str.replace(/\n+$/, ""))
|
|
2347
|
+
});
|
|
2348
|
+
program.command("version").description("Print the CLI version and pinned protocol/SDK versions.").action(() => {
|
|
2349
|
+
process.exitCode = runVersion(io);
|
|
2350
|
+
});
|
|
2351
|
+
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) => {
|
|
2352
|
+
const code = await runValidate(
|
|
2353
|
+
opts.config !== void 0 ? { configPath: opts.config } : {},
|
|
2354
|
+
io
|
|
2355
|
+
);
|
|
2356
|
+
process.exitCode = code;
|
|
2357
|
+
});
|
|
2358
|
+
program.command("doctor").description("Diagnose config, gateway, backend, protocol and payment health.").option("--config <path>", `path to ${DEFAULT_CONFIG_FILENAME}`).option(
|
|
2359
|
+
"--gateway <url>",
|
|
2360
|
+
"gateway base URL (default: derived from config, else http://localhost:8080)"
|
|
2361
|
+
).option("--json", "emit machine-readable JSON instead of a formatted report", false).action(async (opts) => {
|
|
2362
|
+
const report = await runDoctor({
|
|
2363
|
+
...opts.config !== void 0 ? { configPath: opts.config } : {},
|
|
2364
|
+
...opts.gateway !== void 0 ? { gatewayUrl: opts.gateway } : {}
|
|
2365
|
+
});
|
|
2366
|
+
printDoctorReport(report, io, opts.json === true);
|
|
2367
|
+
process.exitCode = report.exitCode;
|
|
2368
|
+
});
|
|
2369
|
+
program.command("init").description(`Interactively generate ${DEFAULT_CONFIG_FILENAME}.`).option("--output <path>", "output path", DEFAULT_CONFIG_FILENAME).option("--force", "overwrite an existing file", false).option("--yes", "use non-interactive defaults (required for scripting/tests)", false).action(async (opts) => {
|
|
2370
|
+
const code = await runInit({ outputPath: opts.output, force: opts.force, yes: opts.yes }, io);
|
|
2371
|
+
process.exitCode = code;
|
|
2372
|
+
});
|
|
2373
|
+
program.command("demo").description(
|
|
2374
|
+
"Run the documented docker compose + demo agent quickstart, verifying health between steps."
|
|
2375
|
+
).action(async () => {
|
|
2376
|
+
process.exitCode = await runDemo(io);
|
|
2377
|
+
});
|
|
2378
|
+
return program;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// src/cli/index.ts
|
|
2382
|
+
async function main() {
|
|
2383
|
+
const program = buildProgram();
|
|
2384
|
+
try {
|
|
2385
|
+
await program.parseAsync(process.argv);
|
|
2386
|
+
} catch (err) {
|
|
2387
|
+
if (err instanceof CommanderError) {
|
|
2388
|
+
process.exitCode = err.exitCode;
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
throw err;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
void main();
|
|
2395
|
+
//# sourceMappingURL=index.js.map
|
|
2396
|
+
//# sourceMappingURL=index.js.map
|