@agenzo/token-cli 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1649 -0
- package/dist/index.js.map +1 -0
- package/package.json +24 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1649 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// ../../packages/cli-core/dist/index.js
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, chmod as chmod3 } from "fs/promises";
|
|
11
|
+
import { join as join4 } from "path";
|
|
12
|
+
import { homedir as homedir3 } from "os";
|
|
13
|
+
import { input, password, select } from "@inquirer/prompts";
|
|
14
|
+
var CODE_NUM = {
|
|
15
|
+
AUTH_SESSION_EXPIRED: 1001,
|
|
16
|
+
AUTH_MAGIC_LINK_EXPIRED: 1002,
|
|
17
|
+
AUTH_INVITE_CODE_REQUIRED: 1003,
|
|
18
|
+
AUTH_INVITE_CODE_INVALID: 1004,
|
|
19
|
+
KEY_INVALID: 1101,
|
|
20
|
+
KEY_SCOPE_DENIED: 1102,
|
|
21
|
+
RESOURCE_NOT_FOUND: 2001,
|
|
22
|
+
RESOURCE_CONFLICT: 2002,
|
|
23
|
+
RESOURCE_STATE_INVALID: 2003,
|
|
24
|
+
PARAM_INVALID: 2101,
|
|
25
|
+
PARAM_IDEMPOTENCY_KEY_REQUIRED: 2102,
|
|
26
|
+
PARAM_IDEMPOTENCY_KEY_CONFLICT: 2103,
|
|
27
|
+
BILLING_MODE_MISMATCH: 3001,
|
|
28
|
+
ACCOUNT_NOT_FOUND: 3101,
|
|
29
|
+
ACCOUNT_SUSPENDED: 3102,
|
|
30
|
+
ACCOUNT_INSUFFICIENT_BALANCE: 3103,
|
|
31
|
+
PAYMENT_ORDER_NOT_FOUND: 3201,
|
|
32
|
+
PAYMENT_ORDER_NOT_PAID: 3202,
|
|
33
|
+
PAYMENT_ORDER_MISMATCH: 3203,
|
|
34
|
+
PAYMENT_ORDER_ALREADY_CONSUMED: 3204,
|
|
35
|
+
TOKEN_FEATURE_DISABLED: 4001,
|
|
36
|
+
SERVICE_NOT_FOUND: 4101,
|
|
37
|
+
VEHICLE_UNAVAILABLE: 4201,
|
|
38
|
+
QUOTE_EXPIRED: 4202,
|
|
39
|
+
BOOKING_FAILED: 4203,
|
|
40
|
+
CANCELLATION_NOT_ALLOWED: 4204,
|
|
41
|
+
RATE_LIMITED: 5001,
|
|
42
|
+
UPSTREAM_ERROR: 5101,
|
|
43
|
+
INTERNAL_ERROR: 5201,
|
|
44
|
+
NOT_IMPLEMENTED: 5300,
|
|
45
|
+
CLIENT_NOT_SIGNED_IN: 9001,
|
|
46
|
+
CLIENT_LOGIN_TIMEOUT: 9002,
|
|
47
|
+
CLIENT_ORG_NOT_LOCAL: 9003,
|
|
48
|
+
CLIENT_ORG_WRONG_ENV: 9004,
|
|
49
|
+
CLIENT_NO_PAYMENT_METHOD: 9005,
|
|
50
|
+
CLIENT_CARD_NOT_MATCHED: 9006,
|
|
51
|
+
CLIENT_ABORTED: 9007,
|
|
52
|
+
UPGRADE_REQUIRED: 9008
|
|
53
|
+
};
|
|
54
|
+
function codeNum(code) {
|
|
55
|
+
return CODE_NUM[code];
|
|
56
|
+
}
|
|
57
|
+
var CliError = class _CliError extends Error {
|
|
58
|
+
constructor(code, message, statusCode, requestId, upstream) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.code = code;
|
|
61
|
+
this.statusCode = statusCode;
|
|
62
|
+
this.requestId = requestId;
|
|
63
|
+
this.upstream = upstream;
|
|
64
|
+
this.name = "CliError";
|
|
65
|
+
}
|
|
66
|
+
code;
|
|
67
|
+
statusCode;
|
|
68
|
+
requestId;
|
|
69
|
+
upstream;
|
|
70
|
+
/**
|
|
71
|
+
* Map a backend ApiError to a CLI-facing code (§8.5 transitional mapping).
|
|
72
|
+
* Backend numeric codes / HTTP status are translated to §8.4 string codes;
|
|
73
|
+
* the raw backend message is NOT passed through (§8.1 principle 4) — a
|
|
74
|
+
* stable, fixed message per code is used instead.
|
|
75
|
+
*
|
|
76
|
+
* The optional `opts.auth` selects the auth semantics for the 401 mapping:
|
|
77
|
+
* - `'bearer'` (default, admin-cli Bearer Token): 401 → `AUTH_SESSION_EXPIRED`.
|
|
78
|
+
* - `'api-key'` (token-cli `X-Api-Key`): 401 → `KEY_INVALID`.
|
|
79
|
+
* 403 maps to `KEY_SCOPE_DENIED` under both auth modes. Omitting `opts`
|
|
80
|
+
* preserves the original Bearer behavior exactly (§8.5 / requirement 6.4).
|
|
81
|
+
*
|
|
82
|
+
* Domain string-code routing (D3 / requirement 5.4): when the backend
|
|
83
|
+
* supplies a string `error.code` in the v3 envelope that is a known §8 code
|
|
84
|
+
* (present in {@link CODE_NUM}), it is used verbatim as the CLI-facing code
|
|
85
|
+
* so ride/merchant-specific codes (`QUOTE_EXPIRED`, `VEHICLE_UNAVAILABLE`,
|
|
86
|
+
* `BILLING_MODE_MISMATCH`, `PAYMENT_ORDER_*`, …) survive intact. Otherwise —
|
|
87
|
+
* including when no string code is provided, as is the case for admin/token
|
|
88
|
+
* today — mapping falls back to the HTTP-status table below, leaving the
|
|
89
|
+
* existing Bearer/api-key behavior unchanged.
|
|
90
|
+
*/
|
|
91
|
+
static fromApi(error, opts) {
|
|
92
|
+
if (error.code && isKnownErrorCode(error.code)) {
|
|
93
|
+
return new _CliError(
|
|
94
|
+
error.code,
|
|
95
|
+
STABLE_MESSAGE[error.code] ?? "Request failed. Please check your input and retry.",
|
|
96
|
+
error.statusCode,
|
|
97
|
+
error.requestId,
|
|
98
|
+
error.upstream
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const status = error.statusCode;
|
|
102
|
+
let code;
|
|
103
|
+
if (status === 401) code = opts?.auth === "api-key" ? "KEY_INVALID" : "AUTH_SESSION_EXPIRED";
|
|
104
|
+
else if (status === 403) code = "KEY_SCOPE_DENIED";
|
|
105
|
+
else if (status === 404) code = "RESOURCE_NOT_FOUND";
|
|
106
|
+
else if (status === 409) code = "RESOURCE_CONFLICT";
|
|
107
|
+
else if (status === 429) code = "RATE_LIMITED";
|
|
108
|
+
else if (status >= 500) code = "INTERNAL_ERROR";
|
|
109
|
+
else code = "PARAM_INVALID";
|
|
110
|
+
return new _CliError(
|
|
111
|
+
code,
|
|
112
|
+
STABLE_MESSAGE[code] ?? "Request failed. Please check your input and retry.",
|
|
113
|
+
status,
|
|
114
|
+
error.requestId,
|
|
115
|
+
error.upstream
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var NetworkError = class extends CliError {
|
|
120
|
+
constructor(_url, timeout, _cause) {
|
|
121
|
+
super(
|
|
122
|
+
"UPSTREAM_ERROR",
|
|
123
|
+
timeout ? "The request timed out. The service may be temporarily unavailable \u2014 please retry." : "Connection failed. The service may be temporarily unavailable \u2014 please retry."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var ConfigError = class extends CliError {
|
|
128
|
+
constructor(message, filePath) {
|
|
129
|
+
super("INTERNAL_ERROR", message || "Unknown error");
|
|
130
|
+
this.filePath = filePath;
|
|
131
|
+
}
|
|
132
|
+
filePath;
|
|
133
|
+
};
|
|
134
|
+
var ValidationError = class extends CliError {
|
|
135
|
+
constructor(message) {
|
|
136
|
+
super("PARAM_INVALID", message || "Unknown error");
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
var IdempotencyKeyRequiredError = class extends CliError {
|
|
140
|
+
constructor(commandPath) {
|
|
141
|
+
super(
|
|
142
|
+
"PARAM_IDEMPOTENCY_KEY_REQUIRED",
|
|
143
|
+
`\`${commandPath}\` requires --idempotency-key <key>. Supply a unique key so the write can be safely retried.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var UpgradeRequiredError = class extends CliError {
|
|
148
|
+
constructor(currentVersion, minVersion, upgradeCommand) {
|
|
149
|
+
super(
|
|
150
|
+
"UPGRADE_REQUIRED",
|
|
151
|
+
`agenzo-admin-cli ${currentVersion} is out of date \u2014 the server requires ${minVersion} or newer. To upgrade, run: ${upgradeCommand}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var UserCancelError = class extends CliError {
|
|
156
|
+
constructor(message = "Operation cancelled by user") {
|
|
157
|
+
super("CLIENT_ABORTED", message || "Unknown error");
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function isKnownErrorCode(value) {
|
|
161
|
+
return Object.prototype.hasOwnProperty.call(CODE_NUM, value);
|
|
162
|
+
}
|
|
163
|
+
var STABLE_MESSAGE = {
|
|
164
|
+
AUTH_SESSION_EXPIRED: "Your session has expired. Please run `agenzo-admin-cli auth login` again.",
|
|
165
|
+
AUTH_MAGIC_LINK_EXPIRED: "Verification link is invalid or has expired. Please request a new one.",
|
|
166
|
+
AUTH_INVITE_CODE_REQUIRED: "An invitation code is required to register.",
|
|
167
|
+
AUTH_INVITE_CODE_INVALID: "The invitation code is invalid.",
|
|
168
|
+
KEY_INVALID: "The API key is invalid or has been revoked. Please check your --api-key and retry.",
|
|
169
|
+
KEY_SCOPE_DENIED: "This API key does not have the required scope for this command.",
|
|
170
|
+
RESOURCE_NOT_FOUND: "The resource was not found or does not belong to the current organization.",
|
|
171
|
+
RESOURCE_CONFLICT: "A resource with the same unique value already exists.",
|
|
172
|
+
RESOURCE_STATE_INVALID: "The resource is in a state that does not permit this operation.",
|
|
173
|
+
RATE_LIMITED: "Too many requests. Please back off and retry.",
|
|
174
|
+
UPSTREAM_ERROR: "A third-party service is temporarily unavailable. Please try again later.",
|
|
175
|
+
INTERNAL_ERROR: "Something went wrong on the server. Please retry and note the request_id.",
|
|
176
|
+
PARAM_INVALID: "One or more parameters are invalid. Please check your input and retry.",
|
|
177
|
+
PARAM_IDEMPOTENCY_KEY_CONFLICT: "A request with this idempotency key was already processed with different parameters.",
|
|
178
|
+
TOKEN_FEATURE_DISABLED: "This token feature is currently unavailable.",
|
|
179
|
+
BILLING_MODE_MISMATCH: "The billing mode does not match this operation. Please verify your account billing configuration.",
|
|
180
|
+
ACCOUNT_NOT_FOUND: "The settlement account was not found.",
|
|
181
|
+
ACCOUNT_SUSPENDED: "The settlement account is suspended. Please contact support.",
|
|
182
|
+
ACCOUNT_INSUFFICIENT_BALANCE: "The settlement account has insufficient balance for this operation.",
|
|
183
|
+
PAYMENT_ORDER_NOT_FOUND: "The payment order was not found.",
|
|
184
|
+
PAYMENT_ORDER_NOT_PAID: "The payment order has not been paid. Please complete payment and retry.",
|
|
185
|
+
PAYMENT_ORDER_MISMATCH: "The payment order does not match this order. Please check the --payment-order-id.",
|
|
186
|
+
PAYMENT_ORDER_ALREADY_CONSUMED: "The payment order has already been consumed by another booking.",
|
|
187
|
+
SERVICE_NOT_FOUND: "The requested service was not found.",
|
|
188
|
+
VEHICLE_UNAVAILABLE: "No vehicle is available for the requested trip. Please try a different time or class.",
|
|
189
|
+
QUOTE_EXPIRED: "The quote has expired. Please request a new quote and retry.",
|
|
190
|
+
BOOKING_FAILED: "The booking could not be completed. Please retry.",
|
|
191
|
+
CANCELLATION_NOT_ALLOWED: "This order cannot be cancelled in its current state.",
|
|
192
|
+
NOT_IMPLEMENTED: "This operation is not implemented yet."
|
|
193
|
+
};
|
|
194
|
+
function toErrorEnvelope(error) {
|
|
195
|
+
if (error instanceof CliError) {
|
|
196
|
+
return {
|
|
197
|
+
error: {
|
|
198
|
+
code: error.code,
|
|
199
|
+
code_num: codeNum(error.code),
|
|
200
|
+
message: error.message || "Unknown error",
|
|
201
|
+
...error.requestId ? { request_id: error.requestId } : {},
|
|
202
|
+
...error.upstream ? { upstream: error.upstream } : {}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unexpected error";
|
|
207
|
+
return {
|
|
208
|
+
error: {
|
|
209
|
+
code: "INTERNAL_ERROR",
|
|
210
|
+
code_num: codeNum("INTERNAL_ERROR"),
|
|
211
|
+
message: message || "Unknown error"
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
var UPGRADE_COMMAND = "npm install -g agenzo-admin-cli@latest";
|
|
216
|
+
var FALLBACK_VERSION = "0.1.1";
|
|
217
|
+
var cachedVersion = null;
|
|
218
|
+
function getCurrentVersion() {
|
|
219
|
+
if (cachedVersion !== null) return cachedVersion;
|
|
220
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
221
|
+
const candidates = [
|
|
222
|
+
join(here, "..", "package.json"),
|
|
223
|
+
// dist/ → project root
|
|
224
|
+
join(here, "..", "..", "package.json")
|
|
225
|
+
// src/utils/ → project root
|
|
226
|
+
];
|
|
227
|
+
for (const p of candidates) {
|
|
228
|
+
try {
|
|
229
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
230
|
+
if (typeof pkg.version === "string") {
|
|
231
|
+
cachedVersion = pkg.version;
|
|
232
|
+
return cachedVersion;
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
cachedVersion = FALLBACK_VERSION;
|
|
238
|
+
console.warn(
|
|
239
|
+
`[agenzo-admin-cli] package.json not found; using fallback version ${FALLBACK_VERSION}`
|
|
240
|
+
);
|
|
241
|
+
return cachedVersion;
|
|
242
|
+
}
|
|
243
|
+
function compareVersions(a, b) {
|
|
244
|
+
const parse = (v) => {
|
|
245
|
+
const clean = v.trim().replace(/^v/, "").split(/[-+]/)[0];
|
|
246
|
+
const parts = clean.split(".");
|
|
247
|
+
return [
|
|
248
|
+
parseInt(parts[0] ?? "0", 10) || 0,
|
|
249
|
+
parseInt(parts[1] ?? "0", 10) || 0,
|
|
250
|
+
parseInt(parts[2] ?? "0", 10) || 0
|
|
251
|
+
];
|
|
252
|
+
};
|
|
253
|
+
const [a1, a2, a3] = parse(a);
|
|
254
|
+
const [b1, b2, b3] = parse(b);
|
|
255
|
+
if (a1 !== b1) return a1 - b1;
|
|
256
|
+
if (a2 !== b2) return a2 - b2;
|
|
257
|
+
return a3 - b3;
|
|
258
|
+
}
|
|
259
|
+
function isBelow(current, minimum) {
|
|
260
|
+
return compareVersions(current, minimum) < 0;
|
|
261
|
+
}
|
|
262
|
+
var ApiClient = class {
|
|
263
|
+
baseUrl;
|
|
264
|
+
timeout;
|
|
265
|
+
constructor(config) {
|
|
266
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
267
|
+
this.timeout = config.timeout ?? 6e4;
|
|
268
|
+
}
|
|
269
|
+
buildHeaders(auth) {
|
|
270
|
+
const headers = {
|
|
271
|
+
"User-Agent": `agenzo-admin-cli/${getCurrentVersion()}`
|
|
272
|
+
};
|
|
273
|
+
if (auth.type === "bearer") {
|
|
274
|
+
headers["Authorization"] = `Bearer ${auth.token}`;
|
|
275
|
+
} else if (auth.type === "api-key") {
|
|
276
|
+
headers["X-Api-Key"] = auth.key;
|
|
277
|
+
}
|
|
278
|
+
return headers;
|
|
279
|
+
}
|
|
280
|
+
async get(path, auth, params) {
|
|
281
|
+
let url = `${this.baseUrl}${path}`;
|
|
282
|
+
if (params && Object.keys(params).length > 0) {
|
|
283
|
+
const searchParams = new URLSearchParams(params);
|
|
284
|
+
url += `?${searchParams.toString()}`;
|
|
285
|
+
}
|
|
286
|
+
return this.request(url, {
|
|
287
|
+
method: "GET",
|
|
288
|
+
headers: this.buildHeaders(auth)
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async post(path, auth, body, extraHeaders) {
|
|
292
|
+
const url = `${this.baseUrl}${path}`;
|
|
293
|
+
const headers = this.buildHeaders(auth);
|
|
294
|
+
headers["Content-Type"] = "application/json";
|
|
295
|
+
if (extraHeaders) {
|
|
296
|
+
Object.assign(headers, extraHeaders);
|
|
297
|
+
}
|
|
298
|
+
return this.request(url, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers,
|
|
301
|
+
body: body ? JSON.stringify(body) : void 0
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async request(url, init) {
|
|
305
|
+
const controller = new AbortController();
|
|
306
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
307
|
+
try {
|
|
308
|
+
const response = await fetch(url, {
|
|
309
|
+
...init,
|
|
310
|
+
signal: controller.signal
|
|
311
|
+
});
|
|
312
|
+
this.enforceMinVersion(response.headers.get("x-cli-min-version"));
|
|
313
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
314
|
+
let responseBody;
|
|
315
|
+
if (contentType.includes("application/json")) {
|
|
316
|
+
responseBody = await response.json();
|
|
317
|
+
} else {
|
|
318
|
+
const text = await response.text();
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
const statusMsg = this.friendlyStatusMessage(response.status);
|
|
321
|
+
return {
|
|
322
|
+
success: false,
|
|
323
|
+
errorCode: 0,
|
|
324
|
+
errorMessage: statusMsg,
|
|
325
|
+
statusCode: response.status
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
responseBody = JSON.parse(text);
|
|
330
|
+
} catch {
|
|
331
|
+
return {
|
|
332
|
+
success: false,
|
|
333
|
+
errorCode: 0,
|
|
334
|
+
errorMessage: `Unexpected response from server (${response.status})`,
|
|
335
|
+
statusCode: response.status
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const code = responseBody.code;
|
|
340
|
+
const message = responseBody.message;
|
|
341
|
+
const data = responseBody.data;
|
|
342
|
+
if (response.ok && (!code || code === "0000")) {
|
|
343
|
+
const payload = data ?? responseBody;
|
|
344
|
+
return { success: true, data: payload, message };
|
|
345
|
+
}
|
|
346
|
+
const errorCode = code ? parseInt(code, 10) : 0;
|
|
347
|
+
let errorMsg = message ?? response.statusText;
|
|
348
|
+
if (!errorMsg || errorMsg === response.statusText) {
|
|
349
|
+
const detail = responseBody.detail;
|
|
350
|
+
if (Array.isArray(detail)) {
|
|
351
|
+
errorMsg = detail.map((d) => {
|
|
352
|
+
const loc = d.loc?.slice(1).join(".") ?? "";
|
|
353
|
+
return loc ? `${loc}: ${d.msg}` : String(d.msg);
|
|
354
|
+
}).join("; ");
|
|
355
|
+
} else if (typeof detail === "string") {
|
|
356
|
+
errorMsg = detail;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const errorCodeStr = responseBody.error_code;
|
|
360
|
+
const rawData = responseBody.data;
|
|
361
|
+
const upstream = rawData?.upstream && typeof rawData.upstream === "object" ? rawData.upstream : void 0;
|
|
362
|
+
return {
|
|
363
|
+
success: false,
|
|
364
|
+
errorCode: isNaN(errorCode) ? 0 : errorCode,
|
|
365
|
+
errorMessage: errorMsg,
|
|
366
|
+
statusCode: response.status,
|
|
367
|
+
...errorCodeStr ? { code: errorCodeStr } : typeof code === "string" && code ? { code } : {},
|
|
368
|
+
requestId: responseBody.request_id ?? responseBody.requestId,
|
|
369
|
+
...upstream ? { upstream } : {}
|
|
370
|
+
};
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (error instanceof UpgradeRequiredError) {
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
376
|
+
throw new NetworkError(url, this.timeout);
|
|
377
|
+
}
|
|
378
|
+
throw new NetworkError(url, void 0, error instanceof Error ? error : void 0);
|
|
379
|
+
} finally {
|
|
380
|
+
clearTimeout(timeoutId);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Validate that the running CLI meets the server-advertised minimum.
|
|
385
|
+
* Missing or empty header → skip (backward compat with servers that don't
|
|
386
|
+
* advertise yet, and clients hitting legacy proxies).
|
|
387
|
+
*/
|
|
388
|
+
enforceMinVersion(headerValue) {
|
|
389
|
+
const minVersion = headerValue?.trim();
|
|
390
|
+
if (!minVersion) return;
|
|
391
|
+
const current = getCurrentVersion();
|
|
392
|
+
if (isBelow(current, minVersion)) {
|
|
393
|
+
throw new UpgradeRequiredError(current, minVersion, UPGRADE_COMMAND);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
friendlyStatusMessage(status) {
|
|
397
|
+
const messages = {
|
|
398
|
+
400: "Invalid request. Please check your input.",
|
|
399
|
+
401: "Authentication failed. Please check your API key or login again.",
|
|
400
|
+
403: "Access denied. You do not have permission for this operation.",
|
|
401
|
+
404: "Resource not found. Please check the ID.",
|
|
402
|
+
409: "Conflict. This resource may already exist.",
|
|
403
|
+
422: "Invalid input. Please check the request parameters.",
|
|
404
|
+
429: "Too many requests. Please wait and try again.",
|
|
405
|
+
500: "Something went wrong on the server. Please try again later.",
|
|
406
|
+
502: "Service is temporarily unavailable. Please try again in a moment.",
|
|
407
|
+
503: "Service is temporarily unavailable. Please try again in a moment.",
|
|
408
|
+
504: "The request took too long. Please try again."
|
|
409
|
+
};
|
|
410
|
+
return messages[status] ?? `Something went wrong (${status}). Please try again later.`;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
var DEFAULT_CONFIG = {
|
|
414
|
+
active_org: null,
|
|
415
|
+
active_developer_id: null,
|
|
416
|
+
api_host: "https://agent.everonet.com",
|
|
417
|
+
api_path: "/api/v3/agent-pay"
|
|
418
|
+
};
|
|
419
|
+
var BUILTIN_PROFILES = {
|
|
420
|
+
production: "https://agent.everonet.com",
|
|
421
|
+
testing: "https://agent-test.everonet.com"
|
|
422
|
+
};
|
|
423
|
+
function resolveApiHost(host) {
|
|
424
|
+
if (BUILTIN_PROFILES[host]) {
|
|
425
|
+
return BUILTIN_PROFILES[host];
|
|
426
|
+
}
|
|
427
|
+
let url;
|
|
428
|
+
try {
|
|
429
|
+
url = new URL(host);
|
|
430
|
+
} catch {
|
|
431
|
+
throw new ValidationError(`Unknown profile or invalid URL: ${host}`);
|
|
432
|
+
}
|
|
433
|
+
if (url.protocol === "https:") {
|
|
434
|
+
return host;
|
|
435
|
+
}
|
|
436
|
+
if (url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1")) {
|
|
437
|
+
return host;
|
|
438
|
+
}
|
|
439
|
+
throw new ValidationError(
|
|
440
|
+
`Insecure API host is not allowed: ${host}. Use HTTPS, except for localhost or 127.0.0.1.`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
var ConfigManager = class {
|
|
444
|
+
basePath;
|
|
445
|
+
configPath;
|
|
446
|
+
constructor(basePath) {
|
|
447
|
+
this.basePath = basePath ?? join4(homedir3(), ".agenzo-admin-cli");
|
|
448
|
+
this.configPath = join4(this.basePath, "config.json");
|
|
449
|
+
}
|
|
450
|
+
async ensureDirectories() {
|
|
451
|
+
await mkdir3(this.basePath, { recursive: true, mode: 448 });
|
|
452
|
+
await chmod3(this.basePath, 448);
|
|
453
|
+
const credDir = join4(this.basePath, "credentials");
|
|
454
|
+
await mkdir3(credDir, { recursive: true, mode: 448 });
|
|
455
|
+
await chmod3(credDir, 448);
|
|
456
|
+
}
|
|
457
|
+
async load() {
|
|
458
|
+
try {
|
|
459
|
+
const content = await readFile3(this.configPath, "utf-8");
|
|
460
|
+
try {
|
|
461
|
+
const raw = JSON.parse(content);
|
|
462
|
+
if (raw.api_base_url && !raw.api_host) {
|
|
463
|
+
const url = String(raw.api_base_url);
|
|
464
|
+
const pathIndex = url.indexOf("/api/");
|
|
465
|
+
raw.api_host = pathIndex > 0 ? url.slice(0, pathIndex) : url;
|
|
466
|
+
raw.api_path = pathIndex > 0 ? url.slice(pathIndex) : DEFAULT_CONFIG.api_path;
|
|
467
|
+
delete raw.api_base_url;
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
active_org: raw.active_org ?? null,
|
|
471
|
+
active_developer_id: raw.active_developer_id ?? null,
|
|
472
|
+
api_host: raw.api_host ?? DEFAULT_CONFIG.api_host,
|
|
473
|
+
api_path: raw.api_path ?? DEFAULT_CONFIG.api_path
|
|
474
|
+
};
|
|
475
|
+
} catch {
|
|
476
|
+
throw new ConfigError(
|
|
477
|
+
`Invalid config file: ${this.configPath}`,
|
|
478
|
+
this.configPath
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof ConfigError) throw error;
|
|
483
|
+
if (error.code === "ENOENT") {
|
|
484
|
+
return { ...DEFAULT_CONFIG };
|
|
485
|
+
}
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async save(config) {
|
|
490
|
+
await this.ensureDirectories();
|
|
491
|
+
await writeFile3(this.configPath, JSON.stringify(config, null, 2), {
|
|
492
|
+
encoding: "utf-8",
|
|
493
|
+
mode: 384
|
|
494
|
+
});
|
|
495
|
+
await chmod3(this.configPath, 384);
|
|
496
|
+
}
|
|
497
|
+
async getActiveOrg() {
|
|
498
|
+
const config = await this.load();
|
|
499
|
+
return config.active_org;
|
|
500
|
+
}
|
|
501
|
+
async setActiveOrg(orgId) {
|
|
502
|
+
const config = await this.load();
|
|
503
|
+
config.active_org = orgId;
|
|
504
|
+
await this.save(config);
|
|
505
|
+
}
|
|
506
|
+
async getApiBaseUrl() {
|
|
507
|
+
const config = await this.load();
|
|
508
|
+
const host = resolveApiHost(config.api_host).replace(/\/+$/, "");
|
|
509
|
+
const path = config.api_path.startsWith("/") ? config.api_path : `/${config.api_path}`;
|
|
510
|
+
return `${host}${path}`;
|
|
511
|
+
}
|
|
512
|
+
async setApiHost(host) {
|
|
513
|
+
const config = await this.load();
|
|
514
|
+
config.api_host = resolveApiHost(host);
|
|
515
|
+
await this.save(config);
|
|
516
|
+
}
|
|
517
|
+
async getApiHost() {
|
|
518
|
+
const config = await this.load();
|
|
519
|
+
return resolveApiHost(config.api_host);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
function exitCodeFor(error) {
|
|
523
|
+
if (!(error instanceof CliError)) {
|
|
524
|
+
return 1;
|
|
525
|
+
}
|
|
526
|
+
const code = error.code;
|
|
527
|
+
if (code === "UPGRADE_REQUIRED") {
|
|
528
|
+
return 2;
|
|
529
|
+
}
|
|
530
|
+
if (code === "CLIENT_ABORTED") {
|
|
531
|
+
return 5;
|
|
532
|
+
}
|
|
533
|
+
if (code.startsWith("AUTH_") || code.startsWith("KEY_")) {
|
|
534
|
+
return 3;
|
|
535
|
+
}
|
|
536
|
+
if (code === "UPSTREAM_ERROR" || code === "INTERNAL_ERROR" || code === "RATE_LIMITED") {
|
|
537
|
+
return 4;
|
|
538
|
+
}
|
|
539
|
+
return 1;
|
|
540
|
+
}
|
|
541
|
+
var STATUS_ICONS = {
|
|
542
|
+
success: "\u2713",
|
|
543
|
+
error: "\u2717",
|
|
544
|
+
info: "\u2139",
|
|
545
|
+
warning: "\u26A0",
|
|
546
|
+
loading: "\u280B"
|
|
547
|
+
};
|
|
548
|
+
var Formatter = class _Formatter {
|
|
549
|
+
/** Get display width of a string (CJK characters count as 2) */
|
|
550
|
+
static displayWidth(str) {
|
|
551
|
+
let width = 0;
|
|
552
|
+
for (const char of str) {
|
|
553
|
+
const code = char.codePointAt(0);
|
|
554
|
+
if (code >= 19968 && code <= 40959 || // CJK Unified
|
|
555
|
+
code >= 12288 && code <= 12351 || // CJK Punctuation
|
|
556
|
+
code >= 13312 && code <= 19903 || // CJK Extension A
|
|
557
|
+
code >= 65280 && code <= 65519 || // Fullwidth Forms
|
|
558
|
+
code >= 63744 && code <= 64255 || // CJK Compatibility
|
|
559
|
+
code >= 131072 && code <= 173791) {
|
|
560
|
+
width += 2;
|
|
561
|
+
} else {
|
|
562
|
+
width += 1;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return width;
|
|
566
|
+
}
|
|
567
|
+
/** Pad string to target display width (right-pad with spaces) */
|
|
568
|
+
static padEndDisplay(str, targetWidth) {
|
|
569
|
+
const currentWidth = _Formatter.displayWidth(str);
|
|
570
|
+
const padding = Math.max(0, targetWidth - currentWidth);
|
|
571
|
+
return str + " ".repeat(padding);
|
|
572
|
+
}
|
|
573
|
+
/** Pad string to target display width (left-pad with spaces) */
|
|
574
|
+
static padStartDisplay(str, targetWidth) {
|
|
575
|
+
const currentWidth = _Formatter.displayWidth(str);
|
|
576
|
+
const padding = Math.max(0, targetWidth - currentWidth);
|
|
577
|
+
return " ".repeat(padding) + str;
|
|
578
|
+
}
|
|
579
|
+
/** Table output for list commands — columns aligned by max width */
|
|
580
|
+
static table(headers, rows) {
|
|
581
|
+
const colWidths = headers.map((h, i) => {
|
|
582
|
+
const cellWidths = rows.map((r) => _Formatter.displayWidth(r[i] ?? ""));
|
|
583
|
+
return Math.max(_Formatter.displayWidth(h), ...cellWidths);
|
|
584
|
+
});
|
|
585
|
+
const sep = " ";
|
|
586
|
+
const headerLine = headers.map((h, i) => _Formatter.padEndDisplay(h, colWidths[i])).join(sep);
|
|
587
|
+
const divider = colWidths.map((w) => "-".repeat(w)).join(sep);
|
|
588
|
+
const dataLines = rows.map(
|
|
589
|
+
(row) => row.map((cell, i) => _Formatter.padEndDisplay(cell ?? "", colWidths[i])).join(sep)
|
|
590
|
+
);
|
|
591
|
+
return [headerLine, divider, ...dataLines].join("\n");
|
|
592
|
+
}
|
|
593
|
+
/** Key-value output for detail commands — keys left-aligned */
|
|
594
|
+
static keyValue(entries) {
|
|
595
|
+
const maxKeyWidth = Math.max(...entries.map(([k]) => _Formatter.displayWidth(k)));
|
|
596
|
+
return entries.map(([key, value]) => `${_Formatter.padEndDisplay(key, maxKeyWidth)} ${value}`).join("\n");
|
|
597
|
+
}
|
|
598
|
+
/** Status-prefixed message */
|
|
599
|
+
static status(type, message) {
|
|
600
|
+
return `${STATUS_ICONS[type]} ${message}`;
|
|
601
|
+
}
|
|
602
|
+
/** Mask sensitive info, keeping only the prefix */
|
|
603
|
+
static maskKey(key, prefixLength = 8) {
|
|
604
|
+
if (key.length <= prefixLength) {
|
|
605
|
+
return key;
|
|
606
|
+
}
|
|
607
|
+
return key.slice(0, prefixLength) + "*".repeat(key.length - prefixLength);
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Format ISO 8601 timestamp to local timezone display.
|
|
611
|
+
* Automatically uses the user's system timezone.
|
|
612
|
+
* e.g. "2026-05-07T03:24:53+00:00" → "2026-05-07 11:24:53" (in CST)
|
|
613
|
+
* "2026-05-07T03:24:53+00:00" → "2026-05-06 20:24:53" (in PDT)
|
|
614
|
+
* Returns the original string if parsing fails or input is empty.
|
|
615
|
+
*/
|
|
616
|
+
static formatTime(iso) {
|
|
617
|
+
if (!iso) return "";
|
|
618
|
+
const date = new Date(iso);
|
|
619
|
+
if (isNaN(date.getTime())) return iso;
|
|
620
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
621
|
+
const y = date.getFullYear();
|
|
622
|
+
const m = pad(date.getMonth() + 1);
|
|
623
|
+
const d = pad(date.getDate());
|
|
624
|
+
const h = pad(date.getHours());
|
|
625
|
+
const min = pad(date.getMinutes());
|
|
626
|
+
const s = pad(date.getSeconds());
|
|
627
|
+
const offsetMin = -date.getTimezoneOffset();
|
|
628
|
+
const sign = offsetMin >= 0 ? "+" : "-";
|
|
629
|
+
const absOffset = Math.abs(offsetMin);
|
|
630
|
+
const tzH = pad(Math.floor(absOffset / 60));
|
|
631
|
+
const tzM = pad(absOffset % 60);
|
|
632
|
+
return `${y}-${m}-${d} ${h}:${min}:${s} (UTC${sign}${tzH}:${tzM})`;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
var DEFAULT_FORMAT = "table";
|
|
636
|
+
function isOutputFormat(value) {
|
|
637
|
+
return value === "json" || value === "table";
|
|
638
|
+
}
|
|
639
|
+
function resolveFormat(flag, env = process.env.AGENZO_FORMAT) {
|
|
640
|
+
if (flag !== void 0) {
|
|
641
|
+
return isOutputFormat(flag) ? flag : DEFAULT_FORMAT;
|
|
642
|
+
}
|
|
643
|
+
if (env !== void 0 && isOutputFormat(env)) {
|
|
644
|
+
return env;
|
|
645
|
+
}
|
|
646
|
+
return DEFAULT_FORMAT;
|
|
647
|
+
}
|
|
648
|
+
function render(result, opts) {
|
|
649
|
+
if (opts.format === "json") {
|
|
650
|
+
process.stdout.write(`${JSON.stringify(result.data, null, 2)}
|
|
651
|
+
`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
process.stdout.write(`${result.text()}
|
|
655
|
+
`);
|
|
656
|
+
}
|
|
657
|
+
function notify(format, type, message) {
|
|
658
|
+
if (format === "json") {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
console.error(Formatter.status(type, message));
|
|
662
|
+
}
|
|
663
|
+
async function renderWithContext(result, opts, configManager) {
|
|
664
|
+
if (opts.format !== "json") {
|
|
665
|
+
render(result, opts);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const config = await configManager.load();
|
|
669
|
+
const host = config.api_host.replace(/\/+$/, "");
|
|
670
|
+
const profile = Object.entries(BUILTIN_PROFILES).find(([, v]) => v === host)?.[0] ?? "custom";
|
|
671
|
+
const dataObj = result.data && typeof result.data === "object" ? result.data : { value: result.data };
|
|
672
|
+
const payload = {
|
|
673
|
+
profile,
|
|
674
|
+
endpoint: host,
|
|
675
|
+
...dataObj
|
|
676
|
+
};
|
|
677
|
+
render({ ...result, data: payload }, opts);
|
|
678
|
+
}
|
|
679
|
+
var PromptEngine = class {
|
|
680
|
+
/** Return flagValue directly if provided, otherwise prompt interactively */
|
|
681
|
+
static async resolveInput(flagValue, config) {
|
|
682
|
+
if (flagValue !== void 0) {
|
|
683
|
+
return flagValue;
|
|
684
|
+
}
|
|
685
|
+
if (config.type === "password") {
|
|
686
|
+
return password({ message: config.message, mask: "*" });
|
|
687
|
+
}
|
|
688
|
+
if (config.type === "select" && config.choices) {
|
|
689
|
+
return select({
|
|
690
|
+
message: config.message,
|
|
691
|
+
choices: config.choices
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
return input({
|
|
695
|
+
message: config.message,
|
|
696
|
+
validate: config.validate
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// src/payment-methods/prompts.ts
|
|
702
|
+
import { password as password2 } from "@inquirer/prompts";
|
|
703
|
+
async function collectCvv() {
|
|
704
|
+
return password2({
|
|
705
|
+
message: "CVV:",
|
|
706
|
+
mask: "*"
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
async function collectPaymentMethodParams(type, flags) {
|
|
710
|
+
const params = { type };
|
|
711
|
+
const email = await PromptEngine.resolveInput(flags.cardEmail ?? flags.email, {
|
|
712
|
+
message: "Email (for 3DS verification):"
|
|
713
|
+
});
|
|
714
|
+
params.email = email;
|
|
715
|
+
if (type === "card") {
|
|
716
|
+
params.card_number = await PromptEngine.resolveInput(flags.cardNumber, {
|
|
717
|
+
message: "Card number:"
|
|
718
|
+
});
|
|
719
|
+
params.expiry_date = await PromptEngine.resolveInput(flags.expiry, {
|
|
720
|
+
message: "Expiry (MMYY):"
|
|
721
|
+
});
|
|
722
|
+
if (flags.cvv) {
|
|
723
|
+
params.cvv = flags.cvv;
|
|
724
|
+
} else {
|
|
725
|
+
params.cvv = await collectCvv();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return params;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/payment-methods/add.ts
|
|
732
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
733
|
+
var POLL_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
734
|
+
function registerAddCommand(parent, deps) {
|
|
735
|
+
const cmd = parent.command("add").description("Add a payment method (card binding + 3DS)").option("--api-key <key>", "API Key for authentication").option("--type <type>", "Payment method type (default: card)", "card").option("--email <email>", "Email for 3DS verification").option("--card-number <number>", "Card number").option("--expiry <mmyy>", "Expiry date (MMYY format)").option("--cvv <cvv>", "Card CVV").option(
|
|
736
|
+
"--idempotency-key <key>",
|
|
737
|
+
"Idempotency key forwarded verbatim as the Idempotency-Key header"
|
|
738
|
+
);
|
|
739
|
+
cmd.action(async () => {
|
|
740
|
+
const opts = cmd.optsWithGlobals();
|
|
741
|
+
const format = resolveFormat(opts.format);
|
|
742
|
+
const isYes = Boolean(opts.yes);
|
|
743
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
744
|
+
message: "API Key:",
|
|
745
|
+
type: "password"
|
|
746
|
+
});
|
|
747
|
+
const type = opts.type || "card";
|
|
748
|
+
const flags = {
|
|
749
|
+
email: opts.email,
|
|
750
|
+
cardNumber: opts.cardNumber,
|
|
751
|
+
expiry: opts.expiry,
|
|
752
|
+
cvv: opts.cvv
|
|
753
|
+
};
|
|
754
|
+
const params = await collectPaymentMethodParams(type, flags);
|
|
755
|
+
let idempotencyKey = opts.idempotencyKey;
|
|
756
|
+
if (!idempotencyKey) {
|
|
757
|
+
if (isYes) {
|
|
758
|
+
throw new IdempotencyKeyRequiredError("payment-methods add");
|
|
759
|
+
}
|
|
760
|
+
idempotencyKey = await PromptEngine.resolveInput(void 0, {
|
|
761
|
+
message: "Idempotency key (unique per write, for safe retry):",
|
|
762
|
+
validate: (v) => v.trim().length > 0 || "Idempotency key is required"
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
const extraHeaders = {
|
|
766
|
+
"Idempotency-Key": idempotencyKey
|
|
767
|
+
};
|
|
768
|
+
const result = await deps.apiClient.post(
|
|
769
|
+
"/payment-methods/create",
|
|
770
|
+
{ type: "api-key", key: apiKey },
|
|
771
|
+
params,
|
|
772
|
+
extraHeaders
|
|
773
|
+
);
|
|
774
|
+
if (!result.success) {
|
|
775
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
776
|
+
}
|
|
777
|
+
const pm = result.data;
|
|
778
|
+
notify(format, "success", "Payment method created");
|
|
779
|
+
const createdResult = {
|
|
780
|
+
data: pm,
|
|
781
|
+
text: () => {
|
|
782
|
+
const lines = [
|
|
783
|
+
["ID", pm.id],
|
|
784
|
+
["Type", pm.type],
|
|
785
|
+
["Status", pm.status]
|
|
786
|
+
];
|
|
787
|
+
if (pm.brand) lines.push(["Brand", pm.brand]);
|
|
788
|
+
if (pm.first6) lines.push(["First 6", pm.first6]);
|
|
789
|
+
if (pm.last4) lines.push(["Last 4", pm.last4]);
|
|
790
|
+
return Formatter.keyValue(lines);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
const configManager = new ConfigManager();
|
|
794
|
+
await renderWithContext(createdResult, { format }, configManager);
|
|
795
|
+
notify(format, "info", "Complete 3DS verification via email to activate");
|
|
796
|
+
if (type === "card" && pm.status === "PENDING") {
|
|
797
|
+
const finalStatus = await poll3dsVerification(
|
|
798
|
+
deps.apiClient,
|
|
799
|
+
apiKey,
|
|
800
|
+
pm.id,
|
|
801
|
+
format
|
|
802
|
+
);
|
|
803
|
+
if (finalStatus === "ACTIVE") {
|
|
804
|
+
const getResult = await deps.apiClient.get(
|
|
805
|
+
`/payment-methods/${pm.id}`,
|
|
806
|
+
{ type: "api-key", key: apiKey }
|
|
807
|
+
);
|
|
808
|
+
if (getResult.success) {
|
|
809
|
+
const activatedPm = getResult.data;
|
|
810
|
+
notify(format, "success", "Payment method activated");
|
|
811
|
+
const activatedResult = {
|
|
812
|
+
data: activatedPm,
|
|
813
|
+
text: () => {
|
|
814
|
+
const lines = [
|
|
815
|
+
["ID", activatedPm.id],
|
|
816
|
+
["Type", activatedPm.type],
|
|
817
|
+
["Status", activatedPm.status]
|
|
818
|
+
];
|
|
819
|
+
if (activatedPm.brand) lines.push(["Brand", activatedPm.brand]);
|
|
820
|
+
if (activatedPm.first6) lines.push(["First 6", activatedPm.first6]);
|
|
821
|
+
if (activatedPm.last4) lines.push(["Last 4", activatedPm.last4]);
|
|
822
|
+
return Formatter.keyValue(lines);
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
await renderWithContext(activatedResult, { format }, configManager);
|
|
826
|
+
} else {
|
|
827
|
+
notify(format, "success", "Payment method activated");
|
|
828
|
+
const degraded = { ...pm, status: "ACTIVE" };
|
|
829
|
+
const degradedResult = {
|
|
830
|
+
data: degraded,
|
|
831
|
+
text: () => {
|
|
832
|
+
const lines = [
|
|
833
|
+
["ID", degraded.id],
|
|
834
|
+
["Type", degraded.type],
|
|
835
|
+
["Status", degraded.status]
|
|
836
|
+
];
|
|
837
|
+
if (degraded.brand) lines.push(["Brand", degraded.brand]);
|
|
838
|
+
if (degraded.first6) lines.push(["First 6", degraded.first6]);
|
|
839
|
+
if (degraded.last4) lines.push(["Last 4", degraded.last4]);
|
|
840
|
+
return Formatter.keyValue(lines);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
await renderWithContext(degradedResult, { format }, configManager);
|
|
844
|
+
}
|
|
845
|
+
} else if (finalStatus === "FAILED") {
|
|
846
|
+
notify(format, "error", "3DS verification failed");
|
|
847
|
+
} else if (finalStatus === "TIMEOUT") {
|
|
848
|
+
notify(
|
|
849
|
+
format,
|
|
850
|
+
"info",
|
|
851
|
+
`Verification timed out (15 min). Check status with: agenzo-token-cli payment-methods get ${pm.id} --api-key <your_key>`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
async function poll3dsVerification(apiClient, apiKey, paymentMethodId, format) {
|
|
858
|
+
const startTime = Date.now();
|
|
859
|
+
notify(format, "info", "Waiting for 3DS verification...");
|
|
860
|
+
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
|
861
|
+
await sleep(POLL_INTERVAL_MS);
|
|
862
|
+
const result = await apiClient.get(
|
|
863
|
+
"/payment-methods/verification/status",
|
|
864
|
+
{ type: "api-key", key: apiKey },
|
|
865
|
+
{ payment_method_id: paymentMethodId }
|
|
866
|
+
);
|
|
867
|
+
if (result.success) {
|
|
868
|
+
const status = result.data.status;
|
|
869
|
+
if (status === "ACTIVE") {
|
|
870
|
+
return "ACTIVE";
|
|
871
|
+
}
|
|
872
|
+
if (status === "FAILED") {
|
|
873
|
+
return "FAILED";
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return "TIMEOUT";
|
|
878
|
+
}
|
|
879
|
+
function sleep(ms) {
|
|
880
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/payment-methods/list.ts
|
|
884
|
+
function registerListCommand(parent, deps) {
|
|
885
|
+
const cmd = parent.command("list").description("List payment methods").option("--api-key <key>", "API Key for authentication").option("--member <member_id>", "Filter by member ID");
|
|
886
|
+
cmd.action(async () => {
|
|
887
|
+
const opts = cmd.optsWithGlobals();
|
|
888
|
+
const format = resolveFormat(opts.format);
|
|
889
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
890
|
+
message: "API Key:",
|
|
891
|
+
type: "password"
|
|
892
|
+
});
|
|
893
|
+
const params = {};
|
|
894
|
+
if (opts.member) {
|
|
895
|
+
params.member_id = opts.member;
|
|
896
|
+
}
|
|
897
|
+
const result = await deps.apiClient.get(
|
|
898
|
+
"/payment-methods",
|
|
899
|
+
{ type: "api-key", key: apiKey },
|
|
900
|
+
Object.keys(params).length > 0 ? params : void 0
|
|
901
|
+
);
|
|
902
|
+
if (!result.success) {
|
|
903
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
904
|
+
}
|
|
905
|
+
const methods = result.data;
|
|
906
|
+
const configManager = new ConfigManager();
|
|
907
|
+
const commandResult = {
|
|
908
|
+
data: { payment_methods: methods },
|
|
909
|
+
text: () => {
|
|
910
|
+
if (methods.length === 0) {
|
|
911
|
+
return Formatter.status("info", "No payment methods found");
|
|
912
|
+
}
|
|
913
|
+
const headers = ["ID", "Type", "Brand", "First 6", "Last 4", "Status"];
|
|
914
|
+
const rows = methods.map((m) => [
|
|
915
|
+
m.id,
|
|
916
|
+
m.type,
|
|
917
|
+
m.brand || "-",
|
|
918
|
+
m.first6 || "-",
|
|
919
|
+
m.last4 || "-",
|
|
920
|
+
m.status
|
|
921
|
+
]);
|
|
922
|
+
return Formatter.table(headers, rows);
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/payment-methods/get.ts
|
|
930
|
+
function registerGetCommand(parent, deps) {
|
|
931
|
+
const cmd = parent.command("get <pm_id>").description("Get a payment method by ID").option("--api-key <key>", "API key for authentication");
|
|
932
|
+
cmd.action(async (pmId) => {
|
|
933
|
+
const opts = cmd.optsWithGlobals();
|
|
934
|
+
const format = resolveFormat(opts.format);
|
|
935
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
936
|
+
message: "API Key:",
|
|
937
|
+
type: "password"
|
|
938
|
+
});
|
|
939
|
+
const result = await deps.apiClient.get(
|
|
940
|
+
`/payment-methods/${pmId}`,
|
|
941
|
+
{ type: "api-key", key: apiKey }
|
|
942
|
+
);
|
|
943
|
+
if (!result.success) {
|
|
944
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
945
|
+
}
|
|
946
|
+
const pm = result.data;
|
|
947
|
+
const entries = [
|
|
948
|
+
["ID", pm.id],
|
|
949
|
+
["Type", pm.type]
|
|
950
|
+
];
|
|
951
|
+
if (pm.brand) {
|
|
952
|
+
entries.push(["Brand", pm.brand]);
|
|
953
|
+
}
|
|
954
|
+
if (pm.first6) {
|
|
955
|
+
entries.push(["First 6", pm.first6]);
|
|
956
|
+
}
|
|
957
|
+
if (pm.last4) {
|
|
958
|
+
entries.push(["Last 4", pm.last4]);
|
|
959
|
+
}
|
|
960
|
+
entries.push(["Status", pm.status]);
|
|
961
|
+
entries.push(["Created", Formatter.formatTime(pm.created_at)]);
|
|
962
|
+
const configManager = new ConfigManager();
|
|
963
|
+
const cmdResult = {
|
|
964
|
+
data: pm,
|
|
965
|
+
text: () => Formatter.keyValue(entries)
|
|
966
|
+
};
|
|
967
|
+
await renderWithContext(cmdResult, { format }, configManager);
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/payment-methods/disable.ts
|
|
972
|
+
function registerDisableCommand(parent, deps) {
|
|
973
|
+
const cmd = parent.command("disable <pm_id>").description("Disable a payment method").option("--api-key <key>", "API key for authentication").option(
|
|
974
|
+
"--idempotency-key <key>",
|
|
975
|
+
"Idempotency key forwarded verbatim as the Idempotency-Key header"
|
|
976
|
+
);
|
|
977
|
+
cmd.action(async (pmId) => {
|
|
978
|
+
const opts = cmd.optsWithGlobals();
|
|
979
|
+
const format = resolveFormat(opts.format);
|
|
980
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
981
|
+
message: "API Key:",
|
|
982
|
+
type: "password"
|
|
983
|
+
});
|
|
984
|
+
let idempotencyKey = opts.idempotencyKey;
|
|
985
|
+
if (!idempotencyKey) {
|
|
986
|
+
if (opts.yes) {
|
|
987
|
+
throw new IdempotencyKeyRequiredError("payment-methods disable");
|
|
988
|
+
}
|
|
989
|
+
idempotencyKey = await PromptEngine.resolveInput(void 0, {
|
|
990
|
+
message: "Idempotency key (unique per write, for safe retry):",
|
|
991
|
+
validate: (v) => v.trim().length > 0 || "Idempotency key is required"
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
const extraHeaders = {
|
|
995
|
+
"Idempotency-Key": idempotencyKey
|
|
996
|
+
};
|
|
997
|
+
const result = await deps.apiClient.post(
|
|
998
|
+
`/payment-methods/${pmId}/disable`,
|
|
999
|
+
{ type: "api-key", key: apiKey },
|
|
1000
|
+
void 0,
|
|
1001
|
+
extraHeaders
|
|
1002
|
+
);
|
|
1003
|
+
if (!result.success) {
|
|
1004
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
1005
|
+
}
|
|
1006
|
+
const data = result.data;
|
|
1007
|
+
notify(format, "success", `Payment method ${pmId} disabled`);
|
|
1008
|
+
const cmdResult = {
|
|
1009
|
+
data,
|
|
1010
|
+
text: () => Formatter.keyValue([
|
|
1011
|
+
["Status", data.status],
|
|
1012
|
+
["Revoked tokens", String(data.revoked_tokens_count ?? 0)]
|
|
1013
|
+
])
|
|
1014
|
+
};
|
|
1015
|
+
const configManager = new ConfigManager();
|
|
1016
|
+
await renderWithContext(cmdResult, { format }, configManager);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/payment-tokens/create.ts
|
|
1021
|
+
import { confirm, select as select2 } from "@inquirer/prompts";
|
|
1022
|
+
var DEFAULT_NT_FEE_CENTS = 500;
|
|
1023
|
+
var USDC_UNIT = 1e6;
|
|
1024
|
+
function mapTokenType(cliType) {
|
|
1025
|
+
if (cliType === "network-token") return "network_token";
|
|
1026
|
+
return cliType;
|
|
1027
|
+
}
|
|
1028
|
+
function usdToCents(amountStr) {
|
|
1029
|
+
const trimmed = amountStr.trim();
|
|
1030
|
+
if (!/^\d*\.?\d{0,2}$/.test(trimmed) || trimmed === "" || trimmed === ".") {
|
|
1031
|
+
throw new CliError("PARAM_INVALID", `Invalid amount format: "${amountStr}". Expected a decimal like "12.50".`);
|
|
1032
|
+
}
|
|
1033
|
+
const parts = trimmed.split(".");
|
|
1034
|
+
const integerPart = parts[0] || "0";
|
|
1035
|
+
let fractionalPart = parts[1] || "0";
|
|
1036
|
+
fractionalPart = fractionalPart.padEnd(2, "0");
|
|
1037
|
+
const integerCents = parseInt(integerPart, 10) * 100;
|
|
1038
|
+
const fractionalCents = parseInt(fractionalPart, 10);
|
|
1039
|
+
return integerCents + fractionalCents;
|
|
1040
|
+
}
|
|
1041
|
+
async function resolvePaymentMethod(apiClient, apiKey, opts) {
|
|
1042
|
+
if (opts.paymentMethodId) {
|
|
1043
|
+
return opts.paymentMethodId;
|
|
1044
|
+
}
|
|
1045
|
+
const result = await apiClient.get(
|
|
1046
|
+
"/payment-methods",
|
|
1047
|
+
{ type: "api-key", key: apiKey }
|
|
1048
|
+
);
|
|
1049
|
+
if (!result.success) {
|
|
1050
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
1051
|
+
}
|
|
1052
|
+
const activeCards = result.data.filter((m) => m.status === "ACTIVE");
|
|
1053
|
+
if (activeCards.length === 0) {
|
|
1054
|
+
throw new CliError(
|
|
1055
|
+
"CLIENT_NO_PAYMENT_METHOD",
|
|
1056
|
+
"No active payment methods found. Add one with: agenzo-token-cli payment-methods add --api-key <your_key>"
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
if (opts.card) {
|
|
1060
|
+
const matched = activeCards.find((m) => m.last4 === opts.card);
|
|
1061
|
+
if (!matched) {
|
|
1062
|
+
throw new CliError(
|
|
1063
|
+
"CLIENT_CARD_NOT_MATCHED",
|
|
1064
|
+
`No active card ending in ${opts.card}. Available: ${activeCards.map((m) => m.last4 || "????").join(", ")}`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
return matched.id;
|
|
1068
|
+
}
|
|
1069
|
+
if (activeCards.length === 1) {
|
|
1070
|
+
return activeCards[0].id;
|
|
1071
|
+
}
|
|
1072
|
+
if (opts.yes) {
|
|
1073
|
+
throw new CliError(
|
|
1074
|
+
"PARAM_INVALID",
|
|
1075
|
+
"Multiple active payment methods found. Specify --payment-method-id or --card to disambiguate."
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
const selected = await select2({
|
|
1079
|
+
message: "Select a payment method:",
|
|
1080
|
+
choices: activeCards.map((m) => ({
|
|
1081
|
+
name: `${m.id} (${m.brand || m.type} ****${m.last4 || "????"})`,
|
|
1082
|
+
value: m.id
|
|
1083
|
+
}))
|
|
1084
|
+
});
|
|
1085
|
+
return selected;
|
|
1086
|
+
}
|
|
1087
|
+
function formatPaymentToken(data) {
|
|
1088
|
+
const type = data.type;
|
|
1089
|
+
const lines = [];
|
|
1090
|
+
if (type === "vcn") {
|
|
1091
|
+
const vcn = data.vcn ?? data;
|
|
1092
|
+
lines.push(["Payment Token ID", String(data.id || vcn.id || "")]);
|
|
1093
|
+
lines.push(["Type", "VCN"]);
|
|
1094
|
+
lines.push(["Card Number", String(vcn.card_number || "")]);
|
|
1095
|
+
lines.push(["Expiry", String(vcn.expiry || "")]);
|
|
1096
|
+
lines.push(["CVC", String(vcn.cvc || "")]);
|
|
1097
|
+
lines.push(["Limit", `$${formatCentsToUsd(vcn.amount_limit)}`]);
|
|
1098
|
+
lines.push(["Currency", String(vcn.currency || "USD")]);
|
|
1099
|
+
lines.push(["Status", String(vcn.status || data.status || "")]);
|
|
1100
|
+
} else if (type === "network_token") {
|
|
1101
|
+
const nt = data.network_token ?? data;
|
|
1102
|
+
lines.push(["Payment Token ID", String(data.id || nt.id || "")]);
|
|
1103
|
+
lines.push(["Type", "Network Token"]);
|
|
1104
|
+
lines.push(["Brand", String(nt.payment_brand || nt.brand || "")]);
|
|
1105
|
+
lines.push(["ECI", String(nt.eci || "")]);
|
|
1106
|
+
lines.push(["Cryptogram", String(nt.token_cryptogram || nt.cryptogram || "")]);
|
|
1107
|
+
lines.push(["Expiry", String(nt.expiry_date || nt.expiry || "")]);
|
|
1108
|
+
lines.push(["Value", String(nt.value || "")]);
|
|
1109
|
+
} else if (type === "x402") {
|
|
1110
|
+
const x402 = data.x402 ?? data;
|
|
1111
|
+
lines.push(["Payment Token ID", String(data.id || x402.id || "")]);
|
|
1112
|
+
lines.push(["Type", "X402"]);
|
|
1113
|
+
lines.push(["Signature Value", String(x402.signature_value || "")]);
|
|
1114
|
+
lines.push(["Status", String(x402.status || data.status || "")]);
|
|
1115
|
+
} else {
|
|
1116
|
+
lines.push(["Payment Token ID", String(data.id || "")]);
|
|
1117
|
+
lines.push(["Type", String(type || "unknown")]);
|
|
1118
|
+
lines.push(["Status", String(data.status || "")]);
|
|
1119
|
+
}
|
|
1120
|
+
return Formatter.keyValue(lines);
|
|
1121
|
+
}
|
|
1122
|
+
function formatCentsToUsd(cents) {
|
|
1123
|
+
if (cents === void 0 || cents === null) return "0.00";
|
|
1124
|
+
const dollars = Math.floor(cents / 100);
|
|
1125
|
+
const remainder = cents % 100;
|
|
1126
|
+
return `${dollars}.${String(remainder).padStart(2, "0")}`;
|
|
1127
|
+
}
|
|
1128
|
+
function registerCreateCommand(parent, deps) {
|
|
1129
|
+
const cmd = parent.command("create").description("Create a payment token (VCN / Network Token / X402)").option("--api-key <key>", "API Key for authentication").option("--type <type>", "Token type: vcn | network-token | x402").option("--payment-method-id <id>", "Payment method ID to use").option("--card <last4>", "Match payment method by last 4 digits").option("--member <member_id>", "Member ID").option("--amount <amount>", "Amount in USD (VCN / X402)").option("--currency <currency>", "Currency (default: USD)").option("--pay-to <address>", "Pay-to address (X402)").option("--nonce <nonce>", "Nonce (X402)").option("--network <network>", "Network (X402)").option("--deadline <deadline>", "Deadline (X402)").option("--external-tx-id <id>", "External transaction ID").option(
|
|
1130
|
+
"--idempotency-key <key>",
|
|
1131
|
+
"Idempotency key forwarded verbatim as the Idempotency-Key header"
|
|
1132
|
+
);
|
|
1133
|
+
cmd.action(async () => {
|
|
1134
|
+
const opts = cmd.optsWithGlobals();
|
|
1135
|
+
const format = resolveFormat(opts.format);
|
|
1136
|
+
const isYes = Boolean(opts.yes);
|
|
1137
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
1138
|
+
message: "API Key:",
|
|
1139
|
+
type: "password"
|
|
1140
|
+
});
|
|
1141
|
+
const cliType = await PromptEngine.resolveInput(opts.type, {
|
|
1142
|
+
message: "Token type:",
|
|
1143
|
+
type: "select",
|
|
1144
|
+
choices: [
|
|
1145
|
+
{ name: "VCN (Virtual Card Number)", value: "vcn" },
|
|
1146
|
+
{ name: "Network Token", value: "network-token" },
|
|
1147
|
+
{ name: "X402 (USDC on-chain)", value: "x402" }
|
|
1148
|
+
]
|
|
1149
|
+
});
|
|
1150
|
+
const serverType = mapTokenType(cliType);
|
|
1151
|
+
const paymentMethodId = await resolvePaymentMethod(deps.apiClient, apiKey, {
|
|
1152
|
+
paymentMethodId: opts.paymentMethodId,
|
|
1153
|
+
card: opts.card,
|
|
1154
|
+
yes: isYes
|
|
1155
|
+
});
|
|
1156
|
+
let member = opts.member;
|
|
1157
|
+
if (!member && !isYes) {
|
|
1158
|
+
const memberInput = await PromptEngine.resolveInput(void 0, {
|
|
1159
|
+
message: "Member ID (optional, press Enter to skip):",
|
|
1160
|
+
validate: () => true
|
|
1161
|
+
// always valid (optional)
|
|
1162
|
+
});
|
|
1163
|
+
if (memberInput.trim()) {
|
|
1164
|
+
member = memberInput.trim();
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
let freezeAmountCents;
|
|
1168
|
+
let feeCents;
|
|
1169
|
+
let feeDisplay;
|
|
1170
|
+
let freezeDisplay;
|
|
1171
|
+
const typeBody = {};
|
|
1172
|
+
if (serverType === "vcn") {
|
|
1173
|
+
await checkVcnFeatureEnabled(deps.apiClient, apiKey);
|
|
1174
|
+
const amountStr = await PromptEngine.resolveInput(opts.amount, {
|
|
1175
|
+
message: "Amount (USD, e.g. 25.00):",
|
|
1176
|
+
validate: (v) => {
|
|
1177
|
+
try {
|
|
1178
|
+
const cents2 = usdToCents(v);
|
|
1179
|
+
if (cents2 < 1 || cents2 > 5e4) {
|
|
1180
|
+
return "Amount must be between $0.01 and $500.00";
|
|
1181
|
+
}
|
|
1182
|
+
return true;
|
|
1183
|
+
} catch {
|
|
1184
|
+
return 'Invalid amount format. Use a decimal like "25.00"';
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
const cents = usdToCents(amountStr);
|
|
1189
|
+
if (cents < 1 || cents > 5e4) {
|
|
1190
|
+
throw new CliError(
|
|
1191
|
+
"PARAM_INVALID",
|
|
1192
|
+
"Amount must be between $0.01 and $500.00"
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
feeCents = Math.max(1, Math.round(cents * 0.05));
|
|
1196
|
+
freezeAmountCents = cents + feeCents;
|
|
1197
|
+
feeDisplay = `$${formatCentsToUsd(feeCents)}`;
|
|
1198
|
+
freezeDisplay = `$${formatCentsToUsd(freezeAmountCents)}`;
|
|
1199
|
+
typeBody.amount = cents;
|
|
1200
|
+
if (opts.currency) {
|
|
1201
|
+
typeBody.currency = opts.currency;
|
|
1202
|
+
}
|
|
1203
|
+
} else if (serverType === "network_token") {
|
|
1204
|
+
feeCents = await fetchNetworkTokenFee(deps.apiClient, apiKey);
|
|
1205
|
+
freezeAmountCents = feeCents;
|
|
1206
|
+
feeDisplay = `$${formatCentsToUsd(feeCents)}`;
|
|
1207
|
+
freezeDisplay = feeDisplay;
|
|
1208
|
+
} else if (serverType === "x402") {
|
|
1209
|
+
const amountStr = await PromptEngine.resolveInput(opts.amount, {
|
|
1210
|
+
message: "Amount (USDC):",
|
|
1211
|
+
validate: (v) => {
|
|
1212
|
+
const n = parseFloat(v);
|
|
1213
|
+
if (isNaN(n) || n <= 0) return "Amount must be a positive number";
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
const amountUsdc = parseFloat(amountStr);
|
|
1218
|
+
const amountUnits = Math.round(amountUsdc * USDC_UNIT);
|
|
1219
|
+
const minFeeUnits = Math.round(0.01 * USDC_UNIT);
|
|
1220
|
+
const percentFeeUnits = Math.round(amountUnits * 0.05);
|
|
1221
|
+
const feeUnits = Math.max(minFeeUnits, percentFeeUnits);
|
|
1222
|
+
const freezeUnits = amountUnits + feeUnits;
|
|
1223
|
+
feeDisplay = `${(feeUnits / USDC_UNIT).toFixed(6)} USDC`;
|
|
1224
|
+
freezeDisplay = `${(freezeUnits / USDC_UNIT).toFixed(6)} USDC`;
|
|
1225
|
+
typeBody.amount = amountUnits;
|
|
1226
|
+
typeBody.pay_to = await PromptEngine.resolveInput(opts.payTo, {
|
|
1227
|
+
message: "Pay-to address:"
|
|
1228
|
+
});
|
|
1229
|
+
typeBody.nonce = await PromptEngine.resolveInput(opts.nonce, {
|
|
1230
|
+
message: "Nonce:"
|
|
1231
|
+
});
|
|
1232
|
+
typeBody.network = await PromptEngine.resolveInput(opts.network, {
|
|
1233
|
+
message: "Network:"
|
|
1234
|
+
});
|
|
1235
|
+
const deadlineStr = await PromptEngine.resolveInput(opts.deadline, {
|
|
1236
|
+
message: "Deadline (Unix timestamp):",
|
|
1237
|
+
validate: (v) => /^\d+$/.test(v.trim()) || "Deadline must be a Unix timestamp (integer seconds)"
|
|
1238
|
+
});
|
|
1239
|
+
const deadlineNum = Number(deadlineStr.trim());
|
|
1240
|
+
if (!Number.isInteger(deadlineNum) || deadlineNum <= 0) {
|
|
1241
|
+
throw new CliError(
|
|
1242
|
+
"PARAM_INVALID",
|
|
1243
|
+
`Invalid deadline: "${deadlineStr}". Expected a Unix timestamp (integer seconds).`
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
typeBody.deadline = deadlineNum;
|
|
1247
|
+
}
|
|
1248
|
+
if (!isYes) {
|
|
1249
|
+
const warningLines = [`Freeze: ${freezeDisplay}`, `Fee: ${feeDisplay}`];
|
|
1250
|
+
notify(format, "warning", warningLines.join(" | "));
|
|
1251
|
+
const confirmed = await confirm({
|
|
1252
|
+
message: "Proceed with token creation?",
|
|
1253
|
+
default: true
|
|
1254
|
+
});
|
|
1255
|
+
if (!confirmed) {
|
|
1256
|
+
throw new CliError("CLIENT_ABORTED", "Token creation cancelled by user");
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
let idempotencyKey = opts.idempotencyKey;
|
|
1260
|
+
if (!idempotencyKey) {
|
|
1261
|
+
if (isYes) {
|
|
1262
|
+
throw new IdempotencyKeyRequiredError("payment-tokens create");
|
|
1263
|
+
}
|
|
1264
|
+
idempotencyKey = await PromptEngine.resolveInput(void 0, {
|
|
1265
|
+
message: "Idempotency key (unique per write, for safe retry):",
|
|
1266
|
+
validate: (v) => v.trim().length > 0 || "Idempotency key is required"
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
const body = {
|
|
1270
|
+
type: serverType,
|
|
1271
|
+
payment_method_id: paymentMethodId,
|
|
1272
|
+
...typeBody
|
|
1273
|
+
};
|
|
1274
|
+
if (member) {
|
|
1275
|
+
body.member_id = member;
|
|
1276
|
+
}
|
|
1277
|
+
if (opts.externalTxId) {
|
|
1278
|
+
body.external_tx_id = opts.externalTxId;
|
|
1279
|
+
}
|
|
1280
|
+
const extraHeaders = {
|
|
1281
|
+
"Idempotency-Key": idempotencyKey
|
|
1282
|
+
};
|
|
1283
|
+
const result = await deps.apiClient.post(
|
|
1284
|
+
"/payment-tokens/create",
|
|
1285
|
+
{ type: "api-key", key: apiKey },
|
|
1286
|
+
body,
|
|
1287
|
+
extraHeaders
|
|
1288
|
+
);
|
|
1289
|
+
if (!result.success) {
|
|
1290
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
1291
|
+
}
|
|
1292
|
+
const tokenData = result.data;
|
|
1293
|
+
if (!tokenData.type) {
|
|
1294
|
+
tokenData.type = serverType;
|
|
1295
|
+
}
|
|
1296
|
+
notify(format, "success", "Payment token created");
|
|
1297
|
+
const configManager = new ConfigManager();
|
|
1298
|
+
const commandResult = {
|
|
1299
|
+
data: tokenData,
|
|
1300
|
+
text: () => {
|
|
1301
|
+
let output = formatPaymentToken(tokenData);
|
|
1302
|
+
if (serverType === "x402") {
|
|
1303
|
+
output += "\n" + Formatter.status("info", "Use the Signature Value in the X-PAYMENT request header");
|
|
1304
|
+
}
|
|
1305
|
+
return output;
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
async function checkVcnFeatureEnabled(apiClient, apiKey) {
|
|
1312
|
+
const result = await apiClient.get(
|
|
1313
|
+
"/features/vcn",
|
|
1314
|
+
{ type: "api-key", key: apiKey }
|
|
1315
|
+
);
|
|
1316
|
+
if (!result.success) {
|
|
1317
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
1318
|
+
}
|
|
1319
|
+
if (!result.data.enabled) {
|
|
1320
|
+
throw new CliError(
|
|
1321
|
+
"TOKEN_FEATURE_DISABLED",
|
|
1322
|
+
"VCN feature is not enabled for your account. Contact support to activate."
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
async function fetchNetworkTokenFee(apiClient, apiKey) {
|
|
1327
|
+
try {
|
|
1328
|
+
const result = await apiClient.get(
|
|
1329
|
+
"/config/network-token-fee",
|
|
1330
|
+
{ type: "api-key", key: apiKey }
|
|
1331
|
+
);
|
|
1332
|
+
if (result.success && typeof result.data.fee_cents === "number") {
|
|
1333
|
+
return result.data.fee_cents;
|
|
1334
|
+
}
|
|
1335
|
+
return DEFAULT_NT_FEE_CENTS;
|
|
1336
|
+
} catch {
|
|
1337
|
+
return DEFAULT_NT_FEE_CENTS;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/payment-tokens/list.ts
|
|
1342
|
+
function getSummary(token) {
|
|
1343
|
+
const type = token.type;
|
|
1344
|
+
if (type === "vcn") {
|
|
1345
|
+
const first6 = String(token.first6 || token.first_six || "");
|
|
1346
|
+
const last4 = String(token.last4 || token.last_four || "");
|
|
1347
|
+
const limit = token.amount_limit ?? token.limit;
|
|
1348
|
+
const limitDisplay = limit !== void 0 && limit !== null ? `$${(limit / 100).toFixed(2)}` : "";
|
|
1349
|
+
return `${first6}****${last4} ${limitDisplay}`.trim();
|
|
1350
|
+
}
|
|
1351
|
+
if (type === "network_token") {
|
|
1352
|
+
const nt = token.network_token;
|
|
1353
|
+
return String(nt?.payment_brand || token.brand || "");
|
|
1354
|
+
}
|
|
1355
|
+
if (type === "x402") {
|
|
1356
|
+
const amount = String(token.amount ?? "");
|
|
1357
|
+
const network = String(token.network || "");
|
|
1358
|
+
return `${amount} ${network}`.trim();
|
|
1359
|
+
}
|
|
1360
|
+
return "";
|
|
1361
|
+
}
|
|
1362
|
+
function registerListCommand2(parent, deps) {
|
|
1363
|
+
const cmd = parent.command("list").description("List payment tokens").option("--api-key <key>", "API Key for authentication").option("--type <type>", "Filter by token type").option("--member <member_id>", "Filter by member ID");
|
|
1364
|
+
cmd.action(async () => {
|
|
1365
|
+
const opts = cmd.optsWithGlobals();
|
|
1366
|
+
const format = resolveFormat(opts.format);
|
|
1367
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
1368
|
+
message: "API Key:",
|
|
1369
|
+
type: "password"
|
|
1370
|
+
});
|
|
1371
|
+
const params = {};
|
|
1372
|
+
if (opts.type) {
|
|
1373
|
+
params.type = opts.type;
|
|
1374
|
+
}
|
|
1375
|
+
if (opts.member) {
|
|
1376
|
+
params.member_id = opts.member;
|
|
1377
|
+
}
|
|
1378
|
+
const result = await deps.apiClient.get(
|
|
1379
|
+
"/payment-tokens",
|
|
1380
|
+
{ type: "api-key", key: apiKey },
|
|
1381
|
+
Object.keys(params).length > 0 ? params : void 0
|
|
1382
|
+
);
|
|
1383
|
+
if (!result.success) {
|
|
1384
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
1385
|
+
}
|
|
1386
|
+
const tokens = result.data;
|
|
1387
|
+
const configManager = new ConfigManager();
|
|
1388
|
+
const commandResult = {
|
|
1389
|
+
data: { payment_tokens: tokens },
|
|
1390
|
+
text: () => {
|
|
1391
|
+
if (tokens.length === 0) {
|
|
1392
|
+
return Formatter.status("info", "No payment tokens found");
|
|
1393
|
+
}
|
|
1394
|
+
const headers = ["Token ID", "Type", "Status", "Summary"];
|
|
1395
|
+
const rows = tokens.map((t) => [
|
|
1396
|
+
String(t.id || ""),
|
|
1397
|
+
String(t.type || ""),
|
|
1398
|
+
String(t.status || ""),
|
|
1399
|
+
getSummary(t)
|
|
1400
|
+
]);
|
|
1401
|
+
return Formatter.table(headers, rows);
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/payment-tokens/get.ts
|
|
1409
|
+
function asString(value) {
|
|
1410
|
+
return typeof value === "string" ? value : "";
|
|
1411
|
+
}
|
|
1412
|
+
function maskPan(value, lastFour) {
|
|
1413
|
+
const suffix = lastFour || value.slice(-4);
|
|
1414
|
+
if (!suffix) return "";
|
|
1415
|
+
const maskedLength = Math.max(value.length - suffix.length, 4);
|
|
1416
|
+
return `${"*".repeat(maskedLength)}${suffix}`;
|
|
1417
|
+
}
|
|
1418
|
+
function maskCvc(value) {
|
|
1419
|
+
return value ? "***" : "";
|
|
1420
|
+
}
|
|
1421
|
+
function maskVcnFields(vcn) {
|
|
1422
|
+
const masked = { ...vcn };
|
|
1423
|
+
const lastFour = asString(masked.last_four ?? masked.last4);
|
|
1424
|
+
if (typeof masked.card_number === "string") {
|
|
1425
|
+
masked.card_number = maskPan(masked.card_number, lastFour);
|
|
1426
|
+
}
|
|
1427
|
+
if (typeof masked.pan === "string") {
|
|
1428
|
+
masked.pan = maskPan(masked.pan, lastFour);
|
|
1429
|
+
}
|
|
1430
|
+
if (typeof masked.cvc === "string") {
|
|
1431
|
+
masked.cvc = maskCvc(masked.cvc);
|
|
1432
|
+
}
|
|
1433
|
+
if (typeof masked.cvv === "string") {
|
|
1434
|
+
masked.cvv = maskCvc(masked.cvv);
|
|
1435
|
+
}
|
|
1436
|
+
return masked;
|
|
1437
|
+
}
|
|
1438
|
+
function buildPaymentTokenGetPayload(data, opts = {}) {
|
|
1439
|
+
if (opts.revealSensitive || data.type !== "vcn") {
|
|
1440
|
+
return data;
|
|
1441
|
+
}
|
|
1442
|
+
const payload = { ...data };
|
|
1443
|
+
if (payload.vcn && typeof payload.vcn === "object") {
|
|
1444
|
+
payload.vcn = maskVcnFields(payload.vcn);
|
|
1445
|
+
return payload;
|
|
1446
|
+
}
|
|
1447
|
+
return maskVcnFields(payload);
|
|
1448
|
+
}
|
|
1449
|
+
function formatPaymentTokenGet(data, opts = {}) {
|
|
1450
|
+
const type = data.type;
|
|
1451
|
+
const lines = [];
|
|
1452
|
+
if (type === "vcn") {
|
|
1453
|
+
const vcn = data.vcn ?? data;
|
|
1454
|
+
const lastFour = asString(vcn.last_four ?? vcn.last4);
|
|
1455
|
+
const cardNumber = asString(vcn.card_number ?? vcn.pan);
|
|
1456
|
+
const cvc = asString(vcn.cvc ?? vcn.cvv);
|
|
1457
|
+
lines.push(["Token ID", String(data.id || vcn.id || "")]);
|
|
1458
|
+
lines.push(["Type", "VCN"]);
|
|
1459
|
+
lines.push([
|
|
1460
|
+
"Card Number",
|
|
1461
|
+
opts.revealSensitive ? cardNumber : maskPan(cardNumber, lastFour)
|
|
1462
|
+
]);
|
|
1463
|
+
lines.push(["Last 4", lastFour]);
|
|
1464
|
+
lines.push(["Expiry", String(vcn.expiry || "")]);
|
|
1465
|
+
lines.push(["CVC", opts.revealSensitive ? cvc : maskCvc(cvc)]);
|
|
1466
|
+
lines.push(["Limit", formatCentsPlain(vcn.amount_limit)]);
|
|
1467
|
+
lines.push(["Balance", formatCentsPlain(vcn.balance)]);
|
|
1468
|
+
lines.push(["Currency", String(vcn.currency || "USD")]);
|
|
1469
|
+
lines.push(["Status", String(vcn.status || data.status || "")]);
|
|
1470
|
+
} else if (type === "network_token") {
|
|
1471
|
+
const nt = data.network_token ?? data;
|
|
1472
|
+
lines.push(["Token ID", String(data.id || nt.id || "")]);
|
|
1473
|
+
lines.push(["Type", "Network Token"]);
|
|
1474
|
+
lines.push(["Brand", String(nt.payment_brand || nt.brand || "")]);
|
|
1475
|
+
lines.push(["ECI", String(nt.eci || "")]);
|
|
1476
|
+
lines.push(["Cryptogram", String(nt.token_cryptogram || nt.cryptogram || "")]);
|
|
1477
|
+
lines.push(["Expiry", String(nt.expiry_date || nt.expiry || "")]);
|
|
1478
|
+
lines.push(["Value", String(nt.value || "")]);
|
|
1479
|
+
} else if (type === "x402") {
|
|
1480
|
+
const x402 = data.x402 ?? data;
|
|
1481
|
+
lines.push(["Token ID", String(data.id || x402.id || "")]);
|
|
1482
|
+
lines.push(["Type", "X402"]);
|
|
1483
|
+
lines.push(["Signature Value", String(x402.signature_value || "")]);
|
|
1484
|
+
lines.push(["Status", String(x402.status || data.status || "")]);
|
|
1485
|
+
} else {
|
|
1486
|
+
lines.push(["Token ID", String(data.id || "")]);
|
|
1487
|
+
lines.push(["Type", String(type || "unknown")]);
|
|
1488
|
+
lines.push(["Status", String(data.status || "")]);
|
|
1489
|
+
}
|
|
1490
|
+
return Formatter.keyValue(lines);
|
|
1491
|
+
}
|
|
1492
|
+
function formatCentsPlain(cents) {
|
|
1493
|
+
if (cents === void 0 || cents === null) return "0.00";
|
|
1494
|
+
const dollars = Math.floor(cents / 100);
|
|
1495
|
+
const remainder = cents % 100;
|
|
1496
|
+
return `${dollars}.${String(remainder).padStart(2, "0")}`;
|
|
1497
|
+
}
|
|
1498
|
+
function registerGetCommand2(parent, deps) {
|
|
1499
|
+
const cmd = parent.command("get <payment_token_id>").description("Get a payment token by ID").option("--api-key <key>", "API key for authentication").option("--reveal", "Reveal full VCN card number and CVC in the output");
|
|
1500
|
+
cmd.action(async (paymentTokenId) => {
|
|
1501
|
+
const opts = cmd.optsWithGlobals();
|
|
1502
|
+
const format = resolveFormat(opts.format);
|
|
1503
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
1504
|
+
message: "API Key:",
|
|
1505
|
+
type: "password"
|
|
1506
|
+
});
|
|
1507
|
+
const result = await deps.apiClient.get(
|
|
1508
|
+
`/payment-tokens/${paymentTokenId}`,
|
|
1509
|
+
{ type: "api-key", key: apiKey }
|
|
1510
|
+
);
|
|
1511
|
+
if (!result.success) {
|
|
1512
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
1513
|
+
}
|
|
1514
|
+
const revealSensitive = Boolean(opts.reveal);
|
|
1515
|
+
const tokenData = buildPaymentTokenGetPayload(result.data, { revealSensitive });
|
|
1516
|
+
const configManager = new ConfigManager();
|
|
1517
|
+
const commandResult = {
|
|
1518
|
+
data: tokenData,
|
|
1519
|
+
text: () => formatPaymentTokenGet(result.data, { revealSensitive })
|
|
1520
|
+
};
|
|
1521
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/payment-tokens/revoke.ts
|
|
1526
|
+
function registerRevokeCommand(parent, deps) {
|
|
1527
|
+
const cmd = parent.command("revoke <payment_token_id>").description("Revoke a payment token").option("--api-key <key>", "API key for authentication").option(
|
|
1528
|
+
"--idempotency-key <key>",
|
|
1529
|
+
"Idempotency key forwarded verbatim as the Idempotency-Key header"
|
|
1530
|
+
);
|
|
1531
|
+
cmd.action(async (paymentTokenId) => {
|
|
1532
|
+
const opts = cmd.optsWithGlobals();
|
|
1533
|
+
const format = resolveFormat(opts.format);
|
|
1534
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
1535
|
+
message: "API Key:",
|
|
1536
|
+
type: "password"
|
|
1537
|
+
});
|
|
1538
|
+
let idempotencyKey = opts.idempotencyKey;
|
|
1539
|
+
if (!idempotencyKey) {
|
|
1540
|
+
if (opts.yes) {
|
|
1541
|
+
throw new IdempotencyKeyRequiredError("payment-tokens revoke");
|
|
1542
|
+
}
|
|
1543
|
+
idempotencyKey = await PromptEngine.resolveInput(void 0, {
|
|
1544
|
+
message: "Idempotency key (unique per write, for safe retry):",
|
|
1545
|
+
validate: (v) => v.trim().length > 0 || "Idempotency key is required"
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
const extraHeaders = {
|
|
1549
|
+
"Idempotency-Key": idempotencyKey
|
|
1550
|
+
};
|
|
1551
|
+
const result = await deps.apiClient.post(
|
|
1552
|
+
`/payment-tokens/${paymentTokenId}/revoke`,
|
|
1553
|
+
{ type: "api-key", key: apiKey },
|
|
1554
|
+
void 0,
|
|
1555
|
+
extraHeaders
|
|
1556
|
+
);
|
|
1557
|
+
if (!result.success) {
|
|
1558
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
1559
|
+
}
|
|
1560
|
+
const data = result.data;
|
|
1561
|
+
const isDelayedRevoke = data.status === "ACTIVE" && data.expires_at != null;
|
|
1562
|
+
if (isDelayedRevoke) {
|
|
1563
|
+
notify(format, "success", "Revoke scheduled (cryptogram will auto-expire)");
|
|
1564
|
+
const kvPairs = [
|
|
1565
|
+
["Token ID", data.id],
|
|
1566
|
+
["Status", data.status],
|
|
1567
|
+
["Expires At", Formatter.formatTime(data.expires_at)]
|
|
1568
|
+
];
|
|
1569
|
+
if (data.message) {
|
|
1570
|
+
kvPairs.push(["Message", data.message]);
|
|
1571
|
+
}
|
|
1572
|
+
const cmdResult = {
|
|
1573
|
+
data,
|
|
1574
|
+
text: () => Formatter.keyValue(kvPairs)
|
|
1575
|
+
};
|
|
1576
|
+
const configManager = new ConfigManager();
|
|
1577
|
+
await renderWithContext(cmdResult, { format }, configManager);
|
|
1578
|
+
} else {
|
|
1579
|
+
notify(format, "success", "Payment token revoked");
|
|
1580
|
+
const cmdResult = {
|
|
1581
|
+
data,
|
|
1582
|
+
text: () => Formatter.keyValue([
|
|
1583
|
+
["Token ID", data.id],
|
|
1584
|
+
["Status", data.status],
|
|
1585
|
+
["Revoked At", Formatter.formatTime(data.revoked_at)]
|
|
1586
|
+
])
|
|
1587
|
+
};
|
|
1588
|
+
const configManager = new ConfigManager();
|
|
1589
|
+
await renderWithContext(cmdResult, { format }, configManager);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/index.ts
|
|
1595
|
+
var programRef;
|
|
1596
|
+
async function main() {
|
|
1597
|
+
const configManager = new ConfigManager();
|
|
1598
|
+
await configManager.ensureDirectories();
|
|
1599
|
+
const apiBaseUrl = await configManager.getApiBaseUrl();
|
|
1600
|
+
const apiClient = new ApiClient({ baseUrl: apiBaseUrl });
|
|
1601
|
+
const deps = { apiClient };
|
|
1602
|
+
const program = new Command();
|
|
1603
|
+
programRef = program;
|
|
1604
|
+
program.name("agenzo-token-cli").version(getCurrentVersion()).description(
|
|
1605
|
+
"Agenzo token plane: payment methods (card binding + 3DS) and payment tokens (VCN / Network Token / X402)"
|
|
1606
|
+
).option("--verbose", "Show verbose logs").option("--yes", "Skip confirmation prompts (for automation/AI Agents)").option(
|
|
1607
|
+
"--format <format>",
|
|
1608
|
+
"Output format: json | table (default: table; or set AGENZO_FORMAT)"
|
|
1609
|
+
);
|
|
1610
|
+
program.hook("preAction", (thisCommand) => {
|
|
1611
|
+
const flag = thisCommand.opts().format;
|
|
1612
|
+
process.env.AGENZO_FORMAT = resolveFormat(flag);
|
|
1613
|
+
});
|
|
1614
|
+
const pmCmd = program.command("payment-methods").description("Payment method management");
|
|
1615
|
+
registerAddCommand(pmCmd, deps);
|
|
1616
|
+
registerListCommand(pmCmd, deps);
|
|
1617
|
+
registerGetCommand(pmCmd, deps);
|
|
1618
|
+
registerDisableCommand(pmCmd, deps);
|
|
1619
|
+
const ptCmd = program.command("payment-tokens").description("Payment token management");
|
|
1620
|
+
registerCreateCommand(ptCmd, deps);
|
|
1621
|
+
registerListCommand2(ptCmd, deps);
|
|
1622
|
+
registerGetCommand2(ptCmd, deps);
|
|
1623
|
+
registerRevokeCommand(ptCmd, deps);
|
|
1624
|
+
await program.parseAsync(process.argv);
|
|
1625
|
+
}
|
|
1626
|
+
function resolveActiveFormat() {
|
|
1627
|
+
const flag = programRef?.opts().format;
|
|
1628
|
+
return resolveFormat(flag);
|
|
1629
|
+
}
|
|
1630
|
+
function reportError(error) {
|
|
1631
|
+
const envelope = toErrorEnvelope(error);
|
|
1632
|
+
const format = resolveActiveFormat();
|
|
1633
|
+
if (format === "json") {
|
|
1634
|
+
console.error(JSON.stringify(envelope));
|
|
1635
|
+
} else {
|
|
1636
|
+
console.error(
|
|
1637
|
+
Formatter.status("error", `[${envelope.error.code_num}] ${envelope.error.message}`)
|
|
1638
|
+
);
|
|
1639
|
+
if (!(error instanceof CliError) && process.argv.includes("--verbose")) {
|
|
1640
|
+
console.error(error);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
process.exit(exitCodeFor(error));
|
|
1644
|
+
}
|
|
1645
|
+
process.on("SIGINT", () => {
|
|
1646
|
+
reportError(new UserCancelError());
|
|
1647
|
+
});
|
|
1648
|
+
main().catch(reportError);
|
|
1649
|
+
//# sourceMappingURL=index.js.map
|