@itpay/cli 0.1.10 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/itp +53 -4563
- package/docs/agent/buyer/cart-checkout.json +20 -11
- package/docs/agent/buyer/catalog-search.json +23 -16
- package/docs/agent/buyer/human-claim-ui.json +2 -0
- package/docs/agent/buyer/payment-qr.json +1 -0
- package/docs/agent/buyer/payment-wait.json +5 -1
- package/docs/agent/buyer/qr-refresh.json +2 -0
- package/docs/agent/buyer/quickstart.json +5 -3
- package/docs/agent/buyer/recovery.json +3 -0
- package/docs/agent/buyer/secure-delivery.json +2 -0
- package/docs/agent/buyer/vault-agent-read.json +4 -0
- package/install.ps1 +15 -3
- package/install.sh +16 -3
- package/lib/buyer.js +1675 -0
- package/lib/docs.js +200 -0
- package/lib/env.js +713 -0
- package/lib/http.js +151 -0
- package/lib/ops.js +135 -0
- package/lib/render-human.js +463 -0
- package/lib/runtime.js +1532 -0
- package/package.json +2 -1
- package/skills/itpay-buyer/SKILL.md +8 -4
package/bin/itp
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const CONFIG_DIR = path.join(os.homedir(), ".itp");
|
|
13
|
-
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
14
|
-
const STATE_PATH = path.join(CONFIG_DIR, "state.json");
|
|
15
|
-
const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
|
|
16
|
-
const RUNS_DIR = path.join(CONFIG_DIR, "runs");
|
|
17
|
-
const LOCK_PATH = path.join(CONFIG_DIR, "state.lock");
|
|
18
|
-
const CLI_FILE = fileURLToPath(import.meta.url);
|
|
19
|
-
const CLI_DIR = path.dirname(CLI_FILE);
|
|
20
|
-
const PACKAGE_ROOT = path.dirname(CLI_DIR);
|
|
21
|
-
const VERSION = packageVersion();
|
|
3
|
+
import { output, outputError, parseFlags, readState, VERSION } from "../lib/env.js";
|
|
4
|
+
import { docs, skill } from "../lib/docs.js";
|
|
5
|
+
import { ops, admin } from "../lib/ops.js";
|
|
6
|
+
import { buyer, buyerBuy } from "../lib/buyer.js";
|
|
7
|
+
import {
|
|
8
|
+
accountLoginLink, accountSetPassword, accountShow, agentStatus, authDevice, authLogin, authRegister, authStatus, balance,
|
|
9
|
+
checkoutCreate, checkoutList, checkoutOpen, checkoutQR, checkoutRecover, doctor, grantsInstall, grantsList, grantsRevoke,
|
|
10
|
+
grantsShow, installRuntime, keys, paymentWait, plansList, plansShow, resume, runs, setup, sync, token, usage
|
|
11
|
+
} from "../lib/runtime.js";
|
|
22
12
|
|
|
23
13
|
main().catch((error) => {
|
|
24
14
|
outputError(error);
|
|
@@ -35,160 +25,70 @@ async function main() {
|
|
|
35
25
|
const [group, command, ...rest] = args;
|
|
36
26
|
const flags = parseFlags(rest);
|
|
37
27
|
|
|
38
|
-
if (group === "usage")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (group === "balance") {
|
|
43
|
-
await balance(parseFlags(args.slice(1)));
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
if (group === "plans" && (!command || String(command).startsWith("--"))) {
|
|
47
|
-
await plansList(parseFlags(args.slice(1)));
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (group === "setup") {
|
|
51
|
-
await setup(parseFlags(args.slice(1)));
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
28
|
+
if (group === "usage") return usage(parseFlags(args.slice(1)));
|
|
29
|
+
if (group === "balance") return balance(parseFlags(args.slice(1)));
|
|
30
|
+
if (group === "plans" && (!command || String(command).startsWith("--"))) return plansList(parseFlags(args.slice(1)));
|
|
31
|
+
if (group === "setup") return setup(parseFlags(args.slice(1)));
|
|
54
32
|
if (group === "buy") {
|
|
55
33
|
const buyFlags = parseFlags(command && String(command).startsWith("--") ? args.slice(1) : rest);
|
|
56
34
|
if (command && !String(command).startsWith("--")) buyFlags.selection = command;
|
|
57
|
-
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (group === "buyer") {
|
|
61
|
-
await buyer(command, rest, parseFlags(rest));
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (group === "ops") {
|
|
65
|
-
await ops(command, rest, parseFlags(rest));
|
|
66
|
-
return;
|
|
35
|
+
return buyerBuy(buyFlags);
|
|
67
36
|
}
|
|
37
|
+
if (group === "buyer") return buyer(command, rest, parseFlags(rest));
|
|
38
|
+
if (group === "ops") return ops(command, rest, parseFlags(rest));
|
|
68
39
|
if (group === "docs") {
|
|
69
40
|
const docsCommand = command && !String(command).startsWith("--") ? command : "list";
|
|
70
41
|
const docsFlags = parseFlags(command && String(command).startsWith("--") ? args.slice(1) : rest);
|
|
71
|
-
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (group === "status") {
|
|
75
|
-
await agentStatus(parseFlags(args.slice(1)));
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (group === "resume") {
|
|
79
|
-
await resume(parseFlags(args.slice(1)));
|
|
80
|
-
return;
|
|
42
|
+
return docs(docsCommand, command && String(command).startsWith("--") ? [] : rest, docsFlags);
|
|
81
43
|
}
|
|
44
|
+
if (group === "status") return agentStatus(parseFlags(args.slice(1)));
|
|
45
|
+
if (group === "resume") return resume(parseFlags(args.slice(1)));
|
|
82
46
|
if (group === "runs") {
|
|
83
47
|
const runCommand = command && !String(command).startsWith("--") ? command : "current";
|
|
84
48
|
const runFlags = parseFlags(command && String(command).startsWith("--") ? args.slice(1) : rest);
|
|
85
|
-
if (rest[0] && !String(rest[0]).startsWith("--"))
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
await runs(runCommand, runFlags);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (group === "doctor") {
|
|
92
|
-
await doctor(parseFlags(args.slice(1)));
|
|
93
|
-
return;
|
|
49
|
+
if (rest[0] && !String(rest[0]).startsWith("--")) runFlags.run_id = rest[0];
|
|
50
|
+
return runs(runCommand, runFlags);
|
|
94
51
|
}
|
|
52
|
+
if (group === "doctor") return doctor(parseFlags(args.slice(1)));
|
|
95
53
|
if (group === "skill") {
|
|
96
54
|
const skillCommand = command && !String(command).startsWith("--") ? command : "show";
|
|
97
55
|
const skillFlags = parseFlags(command && String(command).startsWith("--") ? args.slice(1) : rest);
|
|
98
|
-
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (group === "keys") {
|
|
102
|
-
await keys(command, parseFlags(rest));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (group === "token") {
|
|
106
|
-
await token(command, parseFlags(rest));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (group === "sync") {
|
|
110
|
-
await sync(parseFlags(args.slice(1)));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (group === "admin") {
|
|
114
|
-
await admin(command, rest, parseFlags(rest));
|
|
115
|
-
return;
|
|
56
|
+
return skill(skillCommand, skillFlags);
|
|
116
57
|
}
|
|
58
|
+
if (group === "keys") return keys(command, parseFlags(rest));
|
|
59
|
+
if (group === "token") return token(command, parseFlags(rest));
|
|
60
|
+
if (group === "sync") return sync(parseFlags(args.slice(1)));
|
|
61
|
+
if (group === "admin") return admin(command, rest, parseFlags(rest));
|
|
117
62
|
if (group === "auth" && command === "device") {
|
|
118
63
|
const deviceFlags = parseFlags(rest.slice(1));
|
|
119
|
-
if (rest[0] === "poll" && rest[1] && !String(rest[1]).startsWith("--"))
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
case "
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
case "
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
case "
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
case "
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
case "
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
case "
|
|
143
|
-
await accountSetPassword(flags);
|
|
144
|
-
return;
|
|
145
|
-
case "plans list":
|
|
146
|
-
await plansList(flags);
|
|
147
|
-
return;
|
|
148
|
-
case "plans show":
|
|
149
|
-
await plansShow(rest[0], flags);
|
|
150
|
-
return;
|
|
151
|
-
case "checkout create":
|
|
152
|
-
await checkoutCreate(flags);
|
|
153
|
-
return;
|
|
154
|
-
case "checkout recover":
|
|
155
|
-
await checkoutRecover(rest[0] || readState().last_checkout_id, flags);
|
|
156
|
-
return;
|
|
157
|
-
case "checkout open":
|
|
158
|
-
await checkoutOpen(flags);
|
|
159
|
-
return;
|
|
160
|
-
case "checkout qr":
|
|
161
|
-
await checkoutQR(rest[0] || readState().last_checkout_id, flags);
|
|
162
|
-
return;
|
|
163
|
-
case "checkout list":
|
|
164
|
-
await checkoutList(flags);
|
|
165
|
-
return;
|
|
166
|
-
case "payment wait":
|
|
167
|
-
await paymentWait(rest[0] || readState().last_checkout_id, flags);
|
|
168
|
-
return;
|
|
169
|
-
case "balance":
|
|
170
|
-
await balance(flags);
|
|
171
|
-
return;
|
|
172
|
-
case "grants list":
|
|
173
|
-
await grantsList(flags);
|
|
174
|
-
return;
|
|
175
|
-
case "grants show":
|
|
176
|
-
await grantsShow(rest[0], flags);
|
|
177
|
-
return;
|
|
178
|
-
case "grants install":
|
|
179
|
-
await grantsInstall(rest[0], flags);
|
|
180
|
-
return;
|
|
181
|
-
case "grants revoke":
|
|
182
|
-
await grantsRevoke(rest[0], flags);
|
|
183
|
-
return;
|
|
64
|
+
if (rest[0] === "poll" && rest[1] && !String(rest[1]).startsWith("--")) deviceFlags.auth_id = rest[1];
|
|
65
|
+
return authDevice(rest[0], deviceFlags);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
switch ([group || "", command || ""].join(" ").trim()) {
|
|
69
|
+
case "auth register": return authRegister(flags);
|
|
70
|
+
case "auth login": return authLogin(flags);
|
|
71
|
+
case "auth status": return authStatus(flags);
|
|
72
|
+
case "account show": return accountShow(flags);
|
|
73
|
+
case "account login-link": return accountLoginLink(flags);
|
|
74
|
+
case "account set-password": return accountSetPassword(flags);
|
|
75
|
+
case "plans list": return plansList(flags);
|
|
76
|
+
case "plans show": return plansShow(rest[0], flags);
|
|
77
|
+
case "checkout create": return checkoutCreate(flags);
|
|
78
|
+
case "checkout recover": return checkoutRecover(rest[0] || readState().last_checkout_id, flags);
|
|
79
|
+
case "checkout open": return checkoutOpen(flags);
|
|
80
|
+
case "checkout qr": return checkoutQR(rest[0] || readState().last_checkout_id, flags);
|
|
81
|
+
case "checkout list": return checkoutList(flags);
|
|
82
|
+
case "payment wait": return paymentWait(rest[0] || readState().last_checkout_id, flags);
|
|
83
|
+
case "balance": return balance(flags);
|
|
84
|
+
case "grants list": return grantsList(flags);
|
|
85
|
+
case "grants show": return grantsShow(rest[0], flags);
|
|
86
|
+
case "grants install": return grantsInstall(rest[0], flags);
|
|
87
|
+
case "grants revoke": return grantsRevoke(rest[0], flags);
|
|
184
88
|
case "install claude-code":
|
|
185
89
|
case "install codex":
|
|
186
|
-
case "install openclaw":
|
|
187
|
-
|
|
188
|
-
return;
|
|
189
|
-
case "doctor":
|
|
190
|
-
await doctor(flags);
|
|
191
|
-
return;
|
|
90
|
+
case "install openclaw": return installRuntime(command, flags);
|
|
91
|
+
case "doctor": return doctor(flags);
|
|
192
92
|
default:
|
|
193
93
|
output({
|
|
194
94
|
version: VERSION,
|
|
@@ -244,4413 +144,3 @@ async function main() {
|
|
|
244
144
|
});
|
|
245
145
|
}
|
|
246
146
|
}
|
|
247
|
-
|
|
248
|
-
async function authRegister(flags) {
|
|
249
|
-
const response = await completeDeviceAuth(flags);
|
|
250
|
-
output(response);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function setup(flags) {
|
|
254
|
-
return await withStateLock(async () => {
|
|
255
|
-
const target = flags.target || flags.runtime || "generic";
|
|
256
|
-
const purchase = normalizePurchaseFlags(flags, true);
|
|
257
|
-
const plan = purchase.plan || null;
|
|
258
|
-
const credits = purchase.credits || null;
|
|
259
|
-
const method = flags.method || "alipay";
|
|
260
|
-
validateLivePaymentFlags(method, flags);
|
|
261
|
-
const shouldInstallRuntime = Boolean(flags.install_runtime || flags.install_config || flags.write_runtime_config) && !flags.no_runtime_install && !flags.no_install;
|
|
262
|
-
if (shouldInstallRuntime && target === "generic") {
|
|
263
|
-
throw new Error("--install-runtime requires --target codex, --target claude-code, or --target openclaw");
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
let run = prepareSetupRun(flags, { target, plan, credits, method, install_runtime: shouldInstallRuntime });
|
|
267
|
-
const setupFlags = { ...flags, runtime: target, run_id: run.run_id };
|
|
268
|
-
if (shouldReturnAfterAgentTextQR(setupFlags)) {
|
|
269
|
-
setupFlags.no_wait_auth = true;
|
|
270
|
-
setupFlags.no_wait_payment = true;
|
|
271
|
-
}
|
|
272
|
-
run = updateRun(run, { phase: "checking_auth", status: "running", api_base: apiBase(setupFlags) });
|
|
273
|
-
|
|
274
|
-
const auth = await ensureAuthenticated(setupFlags);
|
|
275
|
-
if (!auth.authenticated) {
|
|
276
|
-
run = mergeRun(run, {
|
|
277
|
-
phase: "waiting_human_auth",
|
|
278
|
-
status: "waiting_human_auth",
|
|
279
|
-
auth: {
|
|
280
|
-
auth_id: auth.auth_id,
|
|
281
|
-
status: "pending",
|
|
282
|
-
expires_at: auth.expires_at
|
|
283
|
-
},
|
|
284
|
-
human_action: auth.human_action || null,
|
|
285
|
-
safe_summary: "Waiting for Alipay authentication scan."
|
|
286
|
-
});
|
|
287
|
-
writeRun(run);
|
|
288
|
-
output(agentRunResponse(run, {
|
|
289
|
-
status: "waiting_human_auth",
|
|
290
|
-
action: "scan_alipay_auth",
|
|
291
|
-
auth_id: auth.auth_id,
|
|
292
|
-
user_code: auth.user_code,
|
|
293
|
-
verification_uri: auth.verification_uri,
|
|
294
|
-
verification_uri_complete: auth.verification_uri_complete,
|
|
295
|
-
alipay_authorization_url: auth.alipay_authorization_url,
|
|
296
|
-
expires_at: auth.expires_at,
|
|
297
|
-
interval: auth.interval,
|
|
298
|
-
human_action: auth.human_action,
|
|
299
|
-
next_action: {
|
|
300
|
-
type: "show_qr_and_wait",
|
|
301
|
-
command: resumeCommand(run, setupFlags, { no_wait_payment: Boolean(setupFlags.no_wait_payment) }),
|
|
302
|
-
retry_after_ms: Number(auth.interval || 2) * 1000
|
|
303
|
-
}
|
|
304
|
-
}));
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
run = mergeRun(run, {
|
|
309
|
-
phase: "authenticated",
|
|
310
|
-
status: "running",
|
|
311
|
-
account: {
|
|
312
|
-
authenticated: true,
|
|
313
|
-
account_id: auth.account_id,
|
|
314
|
-
device_id: auth.device_id,
|
|
315
|
-
newapi_user_id: auth.newapi_user_id || null,
|
|
316
|
-
session_reused: Boolean(auth.session_reused)
|
|
317
|
-
},
|
|
318
|
-
auth: {
|
|
319
|
-
...(run.auth || {}),
|
|
320
|
-
status: "consumed"
|
|
321
|
-
},
|
|
322
|
-
human_action: null,
|
|
323
|
-
safe_summary: "Agent device authenticated."
|
|
324
|
-
});
|
|
325
|
-
writeRun(run);
|
|
326
|
-
|
|
327
|
-
let checkout = run.checkout?.checkout_id
|
|
328
|
-
? await api(`/api/itp/checkout/${encodeURIComponent(run.checkout.checkout_id)}`, { method: "GET" }, setupFlags)
|
|
329
|
-
: null;
|
|
330
|
-
if (!checkout || isTerminalCheckoutFailure(checkout.status)) {
|
|
331
|
-
checkout = await createCheckoutResult({
|
|
332
|
-
...setupFlags,
|
|
333
|
-
plan,
|
|
334
|
-
credits,
|
|
335
|
-
method,
|
|
336
|
-
idempotency_key: flags.idempotency_key || run.idempotency_key
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
run = mergeRun(run, {
|
|
341
|
-
phase: checkout.grant_id ? "grant_ready" : "waiting_human_payment",
|
|
342
|
-
status: checkout.grant_id ? "grant_ready" : "waiting_human_payment",
|
|
343
|
-
plan_id: checkout.plan_id || plan,
|
|
344
|
-
credits: checkout.credits || credits,
|
|
345
|
-
purchase_kind: checkout.purchase?.kind || purchase.kind,
|
|
346
|
-
checkout: {
|
|
347
|
-
checkout_id: checkout.checkout_id,
|
|
348
|
-
order_id: checkout.order_id,
|
|
349
|
-
status: checkout.status,
|
|
350
|
-
expires_at: checkout.expires_at,
|
|
351
|
-
purchase: checkout.purchase || null
|
|
352
|
-
},
|
|
353
|
-
payment: {
|
|
354
|
-
provider: method,
|
|
355
|
-
status: checkout.status
|
|
356
|
-
},
|
|
357
|
-
grant: {
|
|
358
|
-
...(run.grant || {}),
|
|
359
|
-
grant_id: checkout.grant_id || run.grant?.grant_id || null
|
|
360
|
-
},
|
|
361
|
-
human_action: checkout.human_action || null,
|
|
362
|
-
safe_summary: checkout.grant_id ? "Payment verified and grant is ready." : "Waiting for Alipay payment scan."
|
|
363
|
-
});
|
|
364
|
-
writeRun(run);
|
|
365
|
-
|
|
366
|
-
if (checkout.human_action) {
|
|
367
|
-
await renderHumanAction(checkout.human_action, setupFlags);
|
|
368
|
-
} else if (checkout.payment?.cashier_url) {
|
|
369
|
-
process.stderr.write(`Open Alipay payment URL: ${checkout.payment.cashier_url}\n`);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (!checkout.grant_id && (setupFlags.no_wait_payment || setupFlags.no_wait)) {
|
|
373
|
-
output(agentRunResponse(run, {
|
|
374
|
-
status: "waiting_human_payment",
|
|
375
|
-
action: "scan_alipay_payment",
|
|
376
|
-
account_id: auth.account_id,
|
|
377
|
-
device_id: auth.device_id,
|
|
378
|
-
checkout_id: checkout.checkout_id,
|
|
379
|
-
order_id: checkout.order_id,
|
|
380
|
-
plan_id: checkout.plan_id || plan,
|
|
381
|
-
credits: checkout.credits || credits,
|
|
382
|
-
purchase: checkout.purchase || null,
|
|
383
|
-
expires_at: checkout.expires_at,
|
|
384
|
-
payment: checkout.payment,
|
|
385
|
-
human_action: checkout.human_action,
|
|
386
|
-
next_action: checkout.next_action || {
|
|
387
|
-
type: "show_qr_and_wait",
|
|
388
|
-
command: resumeCommand(run, setupFlags),
|
|
389
|
-
retry_after_ms: 2000
|
|
390
|
-
}
|
|
391
|
-
}));
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const payment = checkout.grant_id
|
|
396
|
-
? { status: checkout.status, checkout_id: checkout.checkout_id, order_id: checkout.order_id, grant_id: checkout.grant_id }
|
|
397
|
-
: await paymentWaitResult(checkout.checkout_id, setupFlags);
|
|
398
|
-
run = mergeRun(readRun(run.run_id) || run, {
|
|
399
|
-
phase: "grant_ready",
|
|
400
|
-
checkout: {
|
|
401
|
-
...(run.checkout || {}),
|
|
402
|
-
checkout_id: payment.checkout_id,
|
|
403
|
-
order_id: payment.order_id,
|
|
404
|
-
status: payment.status
|
|
405
|
-
},
|
|
406
|
-
payment: {
|
|
407
|
-
provider: method,
|
|
408
|
-
status: payment.status
|
|
409
|
-
},
|
|
410
|
-
grant: {
|
|
411
|
-
grant_id: payment.grant_id,
|
|
412
|
-
installed: false
|
|
413
|
-
},
|
|
414
|
-
human_action: null,
|
|
415
|
-
safe_summary: "Payment verified and grant is ready."
|
|
416
|
-
});
|
|
417
|
-
writeRun(run);
|
|
418
|
-
|
|
419
|
-
const grant = await grantsInstallResult(payment.grant_id, { ...setupFlags, target });
|
|
420
|
-
const runtimeInstall = shouldInstallRuntime
|
|
421
|
-
? await installRuntimeResult(target, {
|
|
422
|
-
...setupFlags,
|
|
423
|
-
grant: payment.grant_id,
|
|
424
|
-
no_test: flags.test ? false : true
|
|
425
|
-
})
|
|
426
|
-
: {
|
|
427
|
-
status: "skipped",
|
|
428
|
-
reason: "runtime_config_install_is_opt_in",
|
|
429
|
-
command: target === "generic"
|
|
430
|
-
? `${cliCommand("install")} <target> --grant ${shellQuote(payment.grant_id)} --json`
|
|
431
|
-
: cliCommand("install", target, "--grant", payment.grant_id, "--json")
|
|
432
|
-
};
|
|
433
|
-
const tokenCommand = cliCommand("token", "issue", "--grant", payment.grant_id, "--stdout");
|
|
434
|
-
run = mergeRun(readRun(run.run_id) || run, {
|
|
435
|
-
phase: shouldInstallRuntime ? "done" : "grant_ready",
|
|
436
|
-
status: shouldInstallRuntime ? "installed" : "grant_ready",
|
|
437
|
-
grant: {
|
|
438
|
-
grant_id: payment.grant_id,
|
|
439
|
-
installed: true,
|
|
440
|
-
credential_store: grant.credential?.credential_store || null
|
|
441
|
-
},
|
|
442
|
-
result: {
|
|
443
|
-
base_url: grant.base_url,
|
|
444
|
-
openai_base_url: grant.openai_base_url,
|
|
445
|
-
anthropic_base_url: grant.anthropic_base_url,
|
|
446
|
-
gemini_base_url: grant.gemini_base_url
|
|
447
|
-
},
|
|
448
|
-
safe_summary: shouldInstallRuntime ? "Runtime configured." : "Grant credential stored."
|
|
449
|
-
});
|
|
450
|
-
writeRun(run);
|
|
451
|
-
|
|
452
|
-
output(agentRunResponse(run, {
|
|
453
|
-
status: shouldInstallRuntime ? "installed" : "grant_ready",
|
|
454
|
-
account_id: auth.account_id,
|
|
455
|
-
device_id: auth.device_id,
|
|
456
|
-
checkout_id: checkout.checkout_id,
|
|
457
|
-
order_id: checkout.order_id,
|
|
458
|
-
plan_id: checkout.plan_id || plan,
|
|
459
|
-
credits: checkout.credits || credits,
|
|
460
|
-
purchase: checkout.purchase || null,
|
|
461
|
-
grant_id: payment.grant_id,
|
|
462
|
-
target,
|
|
463
|
-
base_url: grant.base_url,
|
|
464
|
-
openai_base_url: grant.openai_base_url,
|
|
465
|
-
anthropic_base_url: grant.anthropic_base_url,
|
|
466
|
-
gemini_base_url: grant.gemini_base_url,
|
|
467
|
-
credential: {
|
|
468
|
-
stored: true,
|
|
469
|
-
credential_store: grant.credential?.credential_store,
|
|
470
|
-
warning: grant.credential?.warning,
|
|
471
|
-
token_command: tokenCommand,
|
|
472
|
-
stdout_required_for_raw_token: true
|
|
473
|
-
},
|
|
474
|
-
auth,
|
|
475
|
-
checkout,
|
|
476
|
-
payment,
|
|
477
|
-
grant_install: grant,
|
|
478
|
-
runtime_install: runtimeInstall,
|
|
479
|
-
next_action: shouldInstallRuntime
|
|
480
|
-
? null
|
|
481
|
-
: {
|
|
482
|
-
type: "configure_agent_optional",
|
|
483
|
-
token_command: tokenCommand,
|
|
484
|
-
runtime_install_command: runtimeInstall.command
|
|
485
|
-
}
|
|
486
|
-
}));
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
async function buyerBuy(flags) {
|
|
491
|
-
const selectionID = flags.selection || flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id;
|
|
492
|
-
if (!selectionID) throw new Error("catalog variant id is required, for example: itp buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --json");
|
|
493
|
-
const selection = await resolveBuyerCatalogSelection(selectionID, flags);
|
|
494
|
-
const cart = await createBuyerCart(selection, flags);
|
|
495
|
-
let checkout = await createBuyerCheckoutFromCart(cart, selection, flags);
|
|
496
|
-
if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
|
|
497
|
-
await renderHumanAction(checkout.human_action, flags);
|
|
498
|
-
if (flags.no_wait || flags.no_wait_auth) {
|
|
499
|
-
output(buyerRunOutput({
|
|
500
|
-
status: "waiting_human_auth",
|
|
501
|
-
selection,
|
|
502
|
-
cart,
|
|
503
|
-
checkout,
|
|
504
|
-
human_action: checkout.human_action,
|
|
505
|
-
agent_next_actions: checkout.agent_next_actions || ["wait_human_auth", "poll_checkout"],
|
|
506
|
-
next: {
|
|
507
|
-
command: cliCommand("buyer", "checkout", "resume", checkout.checkout_id, "--json"),
|
|
508
|
-
safe_for_agent: true,
|
|
509
|
-
instruction: "After presenting the first-purchase auth-to-payment QR, keep this resume command running/waiting. Do not stop at QR display unless the human explicitly asks you to pause."
|
|
510
|
-
}
|
|
511
|
-
}));
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
checkout = await waitBuyerCheckoutAuth(checkout, flags);
|
|
515
|
-
}
|
|
516
|
-
const intent = checkout.payment_intent_id
|
|
517
|
-
? await getBuyerPaymentIntent(checkout.payment_intent_id, flags)
|
|
518
|
-
: await createBuyerPaymentIntent(checkout.checkout_id, flags);
|
|
519
|
-
await renderItPayPaymentAction(intent, flags);
|
|
520
|
-
|
|
521
|
-
if (flags.no_wait || flags.no_wait_payment) {
|
|
522
|
-
output(buyerRunOutput({
|
|
523
|
-
status: "waiting_user_payment",
|
|
524
|
-
selection,
|
|
525
|
-
cart,
|
|
526
|
-
checkout,
|
|
527
|
-
payment_intent: intent,
|
|
528
|
-
agent_next_actions: intent.agent_next_actions || ["wait_payment"],
|
|
529
|
-
next: { command: cliCommand("buyer", "payment", "wait", intent.payment_intent_id, "--json") }
|
|
530
|
-
}));
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const event = await waitBuyerPayment(intent, flags);
|
|
535
|
-
const delivery = await waitBuyerDelivery(checkout.checkout_id, flags);
|
|
536
|
-
const finalCheckout = delivery.checkout || checkout;
|
|
537
|
-
const delivered = isBuyerDeliveryComplete(finalCheckout);
|
|
538
|
-
output(buyerRunOutput({
|
|
539
|
-
status: delivered ? "delivery_claimable" : event.event_type === "payment_intent.verified" ? "payment_verified" : "waiting_user_payment",
|
|
540
|
-
selection,
|
|
541
|
-
cart,
|
|
542
|
-
checkout: finalCheckout,
|
|
543
|
-
payment_intent: intent,
|
|
544
|
-
payment_event: event,
|
|
545
|
-
delivery: delivery.delivery || finalCheckout.delivery || null,
|
|
546
|
-
agent_next_actions: delivered ? deliveryAwareAgentNextActions(finalCheckout) : (finalCheckout.agent_next_actions || event.agent_next_actions || intent.agent_next_actions || ["poll_checkout"]),
|
|
547
|
-
optional_agent_read_grant: optionalAgentReadGrantHint(finalCheckout.checkout_id || checkout.checkout_id, finalCheckout),
|
|
548
|
-
next: delivered
|
|
549
|
-
? { type: "human_check_email", safe_for_agent: true }
|
|
550
|
-
: { command: cliCommand("buyer", "checkout", "status", checkout.checkout_id, "--json"), safe_for_agent: true }
|
|
551
|
-
}));
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
async function buyer(command, rest, flags) {
|
|
555
|
-
const subcommand = rest[0] && !String(rest[0]).startsWith("--") ? rest[0] : "";
|
|
556
|
-
if (command === "catalog") {
|
|
557
|
-
if (subcommand === "search") {
|
|
558
|
-
const query = flags.query || flags.q || "";
|
|
559
|
-
const body = {
|
|
560
|
-
query,
|
|
561
|
-
filters: buyerCatalogSearchFilters(flags),
|
|
562
|
-
context: {},
|
|
563
|
-
pagination: {}
|
|
564
|
-
};
|
|
565
|
-
if (flags.currency) body.context.currency = String(flags.currency);
|
|
566
|
-
if (flags.page_size || flags.limit) body.pagination.limit = Number(flags.page_size || flags.limit);
|
|
567
|
-
if (flags.cursor) body.pagination.cursor = String(flags.cursor);
|
|
568
|
-
const catalog = await coreApi("/v1/catalog/search", { method: "POST", body }, flags);
|
|
569
|
-
output(buyerRunOutput({
|
|
570
|
-
status: "catalog_search_results",
|
|
571
|
-
catalog,
|
|
572
|
-
products: catalog.products || [],
|
|
573
|
-
agent_next_actions: ["choose_variant"]
|
|
574
|
-
}));
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
if (subcommand === "get") {
|
|
578
|
-
const selectionID = flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id || rest[1];
|
|
579
|
-
const detail = await getBuyerUCPProduct(selectionID, flags);
|
|
580
|
-
const selection = selectionFromUCPProduct(detail, selectionID, flags);
|
|
581
|
-
output(buyerRunOutput({
|
|
582
|
-
status: "catalog_product",
|
|
583
|
-
product: detail.product,
|
|
584
|
-
messages: detail.messages || [],
|
|
585
|
-
selection,
|
|
586
|
-
agent_next_actions: ["create_cart"]
|
|
587
|
-
}));
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (command === "cart") {
|
|
592
|
-
if (subcommand === "create") {
|
|
593
|
-
const selectionIDs = buyerCartSelectionIDs(rest, flags);
|
|
594
|
-
const selections = await resolveBuyerCatalogSelections(selectionIDs, flags);
|
|
595
|
-
const cart = await createBuyerCartFromSelections(selections, flags);
|
|
596
|
-
output(buyerRunOutput({
|
|
597
|
-
status: "cart_created",
|
|
598
|
-
selection: selections.length === 1 ? selections[0] : undefined,
|
|
599
|
-
selections,
|
|
600
|
-
cart,
|
|
601
|
-
cart_id: cart.cart_id || cart.id,
|
|
602
|
-
agent_next_actions: cart.agent_next_actions || ["create_checkout_from_cart"]
|
|
603
|
-
}));
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
if (subcommand === "add") {
|
|
607
|
-
const cartID = flags.cart || flags.cart_id || positional(rest, 1) || readState().last_core_cart_id;
|
|
608
|
-
if (!cartID) throw new Error("cart_id is required");
|
|
609
|
-
const selectionIDs = buyerCartSelectionIDs(["add"], flags);
|
|
610
|
-
const selections = await resolveBuyerCatalogSelections(selectionIDs, flags);
|
|
611
|
-
if (selections.length !== 1) throw new Error("buyer cart add requires exactly one --variant");
|
|
612
|
-
const cart = await addBuyerCartLineItem(cartID, selections[0], flags);
|
|
613
|
-
output(buyerRunOutput({
|
|
614
|
-
status: "cart_updated",
|
|
615
|
-
selection: selections[0],
|
|
616
|
-
cart,
|
|
617
|
-
cart_id: cart.cart_id || cart.id,
|
|
618
|
-
agent_next_actions: cart.agent_next_actions || ["view_cart", "create_checkout_from_cart"]
|
|
619
|
-
}));
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
if (subcommand === "remove" || subcommand === "delete") {
|
|
623
|
-
const cartID = flags.cart || flags.cart_id || positional(rest, 1) || readState().last_core_cart_id;
|
|
624
|
-
const lineID = flags.line || flags.line_id || flags.cart_line_item_id || positional(rest, 2);
|
|
625
|
-
if (!cartID) throw new Error("cart_id is required");
|
|
626
|
-
if (!lineID) throw new Error("cart_line_item_id is required; run buyer cart show <cart_id> --json first");
|
|
627
|
-
const cart = await removeBuyerCartLineItem(cartID, lineID, flags);
|
|
628
|
-
output(buyerRunOutput({
|
|
629
|
-
status: "cart_updated",
|
|
630
|
-
cart,
|
|
631
|
-
cart_id: cart.cart_id || cart.id,
|
|
632
|
-
removed_line_item_id: lineID,
|
|
633
|
-
agent_next_actions: cart.agent_next_actions || ["view_cart", "create_checkout_from_cart"]
|
|
634
|
-
}));
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
if (subcommand === "show" || subcommand === "status") {
|
|
638
|
-
const cartID = flags.cart || flags.cart_id || positional(rest, 1) || readState().last_core_cart_id;
|
|
639
|
-
if (!cartID) throw new Error("cart_id is required");
|
|
640
|
-
const cart = await getBuyerCart(cartID, flags);
|
|
641
|
-
output(buyerRunOutput({
|
|
642
|
-
status: cart.status || "cart_ready",
|
|
643
|
-
cart,
|
|
644
|
-
cart_id: cart.cart_id || cart.id,
|
|
645
|
-
agent_next_actions: cart.agent_next_actions || ["create_checkout_from_cart"]
|
|
646
|
-
}));
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
if (command === "shelf") {
|
|
651
|
-
if (subcommand === "manifest") {
|
|
652
|
-
output(await coreApi("/v1/catalog/manifests/current", { method: "GET" }, flags));
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
if (subcommand === "snapshot") {
|
|
656
|
-
const version = flags.version || positional(rest, 1);
|
|
657
|
-
if (!version) throw new Error("snapshot version is required");
|
|
658
|
-
output(await coreApi(`/v1/catalog/snapshots/${encodeURIComponent(version)}`, { method: "GET" }, flags));
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
if (subcommand === "delta") {
|
|
662
|
-
const since = flags.since || positional(rest, 1);
|
|
663
|
-
if (!since) throw new Error("delta --since version is required");
|
|
664
|
-
const params = new URLSearchParams({ since });
|
|
665
|
-
output(await coreApi(`/v1/catalog/delta?${params.toString()}`, { method: "GET" }, flags));
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
if (command === "checkout") {
|
|
670
|
-
if (subcommand === "create") {
|
|
671
|
-
const cartID = flags.cart || flags.cart_id;
|
|
672
|
-
if (cartID) {
|
|
673
|
-
const cart = await getBuyerCart(cartID, flags);
|
|
674
|
-
const checkout = await createBuyerCheckoutFromCart(cart, null, flags);
|
|
675
|
-
if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
|
|
676
|
-
await renderHumanAction(checkout.human_action, flags);
|
|
677
|
-
}
|
|
678
|
-
output(buyerRunOutput({
|
|
679
|
-
status: "checkout_created",
|
|
680
|
-
cart,
|
|
681
|
-
checkout,
|
|
682
|
-
agent_next_actions: checkout.agent_next_actions || ["create_payment_intent"],
|
|
683
|
-
next: checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth"
|
|
684
|
-
? {
|
|
685
|
-
command: cliCommand("buyer", "checkout", "resume", checkout.checkout_id, "--json"),
|
|
686
|
-
safe_for_agent: true,
|
|
687
|
-
instruction: "After presenting the first-purchase auth-to-payment QR, keep this resume command running/waiting. Do not stop at QR display unless the human explicitly asks you to pause."
|
|
688
|
-
}
|
|
689
|
-
: undefined
|
|
690
|
-
}));
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
const selectionID = flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id;
|
|
694
|
-
const selection = await resolveBuyerCatalogSelection(selectionID, flags);
|
|
695
|
-
const cart = await createBuyerCart(selection, flags);
|
|
696
|
-
const checkout = await createBuyerCheckoutFromCart(cart, selection, flags);
|
|
697
|
-
if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
|
|
698
|
-
await renderHumanAction(checkout.human_action, flags);
|
|
699
|
-
}
|
|
700
|
-
output(buyerRunOutput({
|
|
701
|
-
status: "checkout_created",
|
|
702
|
-
selection,
|
|
703
|
-
cart,
|
|
704
|
-
checkout,
|
|
705
|
-
agent_next_actions: checkout.agent_next_actions || ["create_payment_intent"],
|
|
706
|
-
next: checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth"
|
|
707
|
-
? {
|
|
708
|
-
command: cliCommand("buyer", "checkout", "resume", checkout.checkout_id, "--json"),
|
|
709
|
-
safe_for_agent: true,
|
|
710
|
-
instruction: "After presenting the first-purchase auth-to-payment QR, keep this resume command running/waiting. Do not stop at QR display unless the human explicitly asks you to pause."
|
|
711
|
-
}
|
|
712
|
-
: undefined
|
|
713
|
-
}));
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
if (subcommand === "status") {
|
|
717
|
-
const checkoutID = flags.checkout || flags.checkout_id || positional(rest, 1) || readState().last_core_checkout_id;
|
|
718
|
-
if (!checkoutID) throw new Error("checkout_id is required");
|
|
719
|
-
const checkout = await getBuyerCheckout(checkoutID, flags);
|
|
720
|
-
const claimedSession = await maybeClaimBuyerSessionForCheckout(checkout, flags);
|
|
721
|
-
output(buyerRunOutput({
|
|
722
|
-
status: checkout.delivery_status || checkout.status,
|
|
723
|
-
checkout,
|
|
724
|
-
delivery: checkout.delivery,
|
|
725
|
-
buyer_session: buyerSessionClaimStatus(claimedSession),
|
|
726
|
-
agent_next_actions: deliveryAwareAgentNextActions(checkout),
|
|
727
|
-
optional_agent_read_grant: optionalAgentReadGrantHint(checkout.checkout_id, checkout)
|
|
728
|
-
}));
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
if (subcommand === "resume") {
|
|
732
|
-
const checkoutID = flags.checkout || flags.checkout_id || positional(rest, 1) || readState().last_core_checkout_id;
|
|
733
|
-
if (!checkoutID) throw new Error("checkout_id is required");
|
|
734
|
-
let checkout = await getBuyerCheckout(checkoutID, flags);
|
|
735
|
-
if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
|
|
736
|
-
await renderHumanAction(checkout.human_action, flags);
|
|
737
|
-
if (flags.no_wait || flags.no_wait_auth) {
|
|
738
|
-
output(buyerRunOutput({
|
|
739
|
-
status: "waiting_human_auth",
|
|
740
|
-
checkout,
|
|
741
|
-
human_action: checkout.human_action,
|
|
742
|
-
agent_next_actions: checkout.agent_next_actions || ["wait_human_auth", "poll_checkout"],
|
|
743
|
-
next: {
|
|
744
|
-
command: cliCommand("buyer", "checkout", "resume", checkoutID, "--json"),
|
|
745
|
-
safe_for_agent: true,
|
|
746
|
-
instruction: "Run this resume command and keep it active; do not stop after showing the auth-to-payment QR unless the human explicitly asks you to pause."
|
|
747
|
-
}
|
|
748
|
-
}));
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
checkout = await waitBuyerCheckoutAuth(checkout, flags);
|
|
752
|
-
if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
|
|
753
|
-
output(buyerRunOutput({
|
|
754
|
-
status: "waiting_human_auth",
|
|
755
|
-
checkout,
|
|
756
|
-
human_action: checkout.human_action,
|
|
757
|
-
agent_next_actions: checkout.agent_next_actions || ["wait_human_auth", "poll_checkout"],
|
|
758
|
-
next: {
|
|
759
|
-
command: cliCommand("buyer", "checkout", "resume", checkoutID, "--json"),
|
|
760
|
-
safe_for_agent: true,
|
|
761
|
-
instruction: "Auth is still pending. Keep waiting/resuming the same checkout; do not create a new checkout."
|
|
762
|
-
}
|
|
763
|
-
}));
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
const claimedSession = await maybeClaimBuyerSessionForCheckout(checkout, flags);
|
|
768
|
-
if (checkout.payment_intent_id) {
|
|
769
|
-
const intent = await getBuyerPaymentIntent(checkout.payment_intent_id, flags);
|
|
770
|
-
await renderItPayPaymentAction(intent, flags);
|
|
771
|
-
output(buyerRunOutput({
|
|
772
|
-
status: intent.status === "verified" ? "payment_verified" : "waiting_user_payment",
|
|
773
|
-
checkout,
|
|
774
|
-
payment_intent: intent,
|
|
775
|
-
buyer_session: buyerSessionClaimStatus(claimedSession),
|
|
776
|
-
agent_next_actions: intent.agent_next_actions || checkout.agent_next_actions || ["wait_payment"]
|
|
777
|
-
}));
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
if (checkout.agent_next_actions?.includes("create_payment_intent") || checkout.next_required_action === "create_payment_intent") {
|
|
781
|
-
const intent = await createBuyerPaymentIntent(checkout.checkout_id, flags);
|
|
782
|
-
await renderItPayPaymentAction(intent, flags);
|
|
783
|
-
output(buyerRunOutput({ status: "waiting_user_payment", checkout, payment_intent: intent, agent_next_actions: intent.agent_next_actions || ["wait_payment"] }));
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
output(buyerRunOutput({
|
|
787
|
-
status: checkout.delivery_status || checkout.status,
|
|
788
|
-
checkout,
|
|
789
|
-
delivery: checkout.delivery,
|
|
790
|
-
buyer_session: buyerSessionClaimStatus(claimedSession),
|
|
791
|
-
agent_next_actions: deliveryAwareAgentNextActions(checkout),
|
|
792
|
-
optional_agent_read_grant: optionalAgentReadGrantHint(checkout.checkout_id, checkout)
|
|
793
|
-
}));
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
if (command === "payment" && subcommand === "wait") {
|
|
798
|
-
const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 1) || readState().last_core_payment_intent_id;
|
|
799
|
-
if (!paymentIntentID) throw new Error("payment_intent_id is required");
|
|
800
|
-
const intent = await getBuyerPaymentIntent(paymentIntentID, flags);
|
|
801
|
-
const event = intent.status === "verified"
|
|
802
|
-
? { event_type: "payment_intent.verified", payment_intent_id: paymentIntentID, agent_next_actions: intent.agent_next_actions || ["poll_checkout"] }
|
|
803
|
-
: await waitBuyerPayment(intent, flags);
|
|
804
|
-
output(buyerRunOutput({ status: event.event_type === "payment_intent.verified" ? "payment_verified" : "waiting_user_payment", payment_intent: intent, payment_event: event, agent_next_actions: event.agent_next_actions || intent.agent_next_actions }));
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
if (command === "payment" && subcommand === "refresh-qr") {
|
|
808
|
-
const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 1) || readState().last_core_payment_intent_id;
|
|
809
|
-
if (!paymentIntentID) throw new Error("payment_intent_id is required");
|
|
810
|
-
const refreshed = await refreshBuyerPaymentQR(paymentIntentID, flags);
|
|
811
|
-
await renderItPayPaymentAction(refreshed, flags);
|
|
812
|
-
output(buyerRunOutput({
|
|
813
|
-
status: refreshed.status === "verified" ? "payment_verified" : "waiting_user_payment",
|
|
814
|
-
payment_intent: refreshed,
|
|
815
|
-
agent_next_actions: refreshed.agent_next_actions || ["wait_payment"],
|
|
816
|
-
next: refreshed.status === "verified"
|
|
817
|
-
? { command: cliCommand("buyer", "checkout", "status", refreshed.checkout_id, "--json"), safe_for_agent: true }
|
|
818
|
-
: { command: cliCommand("buyer", "payment", "wait", refreshed.payment_intent_id, "--json"), safe_for_agent: true }
|
|
819
|
-
}));
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
if (command === "deliveries") {
|
|
823
|
-
if (subcommand === "list") {
|
|
824
|
-
const checkoutID = flags.checkout || flags.checkout_id || readState().last_core_checkout_id;
|
|
825
|
-
if (!checkoutID) throw new Error("--checkout is required");
|
|
826
|
-
const checkout = await getBuyerCheckout(checkoutID, flags);
|
|
827
|
-
output(buyerDeliveryListOutput(checkout));
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
if (subcommand === "show") {
|
|
831
|
-
const checkoutID = flags.checkout || flags.checkout_id || readState().last_core_checkout_id;
|
|
832
|
-
if (!checkoutID) throw new Error("--checkout is required for agent-safe delivery status");
|
|
833
|
-
const checkout = await getBuyerCheckout(checkoutID, flags);
|
|
834
|
-
output({
|
|
835
|
-
schema_version: "itp.buyer.v1",
|
|
836
|
-
status: checkout.delivery?.status || checkout.delivery_status,
|
|
837
|
-
delivery_id: positional(rest, 1) || flags.delivery || flags.delivery_id || null,
|
|
838
|
-
checkout_id: checkout.checkout_id,
|
|
839
|
-
delivery: checkout.delivery || null,
|
|
840
|
-
agent_next_actions: deliveryAwareAgentNextActions(checkout),
|
|
841
|
-
optional_agent_read_grant: optionalAgentReadGrantHint(checkout.checkout_id, checkout),
|
|
842
|
-
secrets: { raw_content_included: false, claim_token_included: false }
|
|
843
|
-
});
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
if (command === "refund") {
|
|
848
|
-
if (subcommand === "create") {
|
|
849
|
-
const orderID = flags.order || flags.order_id || positional(rest, 1);
|
|
850
|
-
if (!orderID) throw new Error("order_id is required");
|
|
851
|
-
const refund = await createBuyerRefund(orderID, flags);
|
|
852
|
-
if (refund.status === "policy_risk_confirmation_required") {
|
|
853
|
-
output(buyerRunOutput(refund));
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
output(buyerRunOutput({
|
|
857
|
-
status: refund.status || "refund_requested",
|
|
858
|
-
refund,
|
|
859
|
-
refund_eligibility: refund.refund_eligibility || null,
|
|
860
|
-
agent_next_actions: ["watch_refund_status", "explain_refund_policy"]
|
|
861
|
-
}));
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
if (subcommand === "list") {
|
|
865
|
-
const orderID = flags.order || flags.order_id || positional(rest, 1);
|
|
866
|
-
if (!orderID) throw new Error("order_id is required");
|
|
867
|
-
const refunds = await listBuyerRefunds(orderID, flags);
|
|
868
|
-
output(buyerRunOutput({
|
|
869
|
-
status: "refunds",
|
|
870
|
-
order_id: orderID,
|
|
871
|
-
...refunds,
|
|
872
|
-
agent_next_actions: ["show_refund_status"]
|
|
873
|
-
}));
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
if (subcommand === "show") {
|
|
877
|
-
const refundID = flags.refund || flags.refund_id || positional(rest, 1);
|
|
878
|
-
if (!refundID) throw new Error("refund_id is required");
|
|
879
|
-
const refund = await getBuyerRefund(refundID, flags);
|
|
880
|
-
output(buyerRunOutput({
|
|
881
|
-
status: refund.status || "refund",
|
|
882
|
-
refund,
|
|
883
|
-
agent_next_actions: ["show_refund_status"]
|
|
884
|
-
}));
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
if (subcommand === "cancel") {
|
|
888
|
-
const refundID = flags.refund || flags.refund_id || positional(rest, 1);
|
|
889
|
-
if (!refundID) throw new Error("refund_id is required");
|
|
890
|
-
const refund = await cancelBuyerRefund(refundID, flags);
|
|
891
|
-
output(buyerRunOutput({
|
|
892
|
-
status: refund.status || "refund_canceled",
|
|
893
|
-
refund,
|
|
894
|
-
agent_next_actions: ["show_refund_status", "claim_delivery_if_still_needed"]
|
|
895
|
-
}));
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
if (command === "vault") {
|
|
900
|
-
if (subcommand === "grants") {
|
|
901
|
-
const action = rest[1] && !String(rest[1]).startsWith("--") ? rest[1] : "list";
|
|
902
|
-
if (action === "list") {
|
|
903
|
-
const claimedSession = await ensureBuyerSessionForVaultAccess(flags);
|
|
904
|
-
const grants = await listBuyerAgentReadGrants(flags);
|
|
905
|
-
output(buyerRunOutput({
|
|
906
|
-
status: "agent_read_grants",
|
|
907
|
-
...grants,
|
|
908
|
-
buyer_session: buyerSessionClaimStatus(claimedSession),
|
|
909
|
-
agent_next_actions: grants.agent_readable_grants?.length ? ["read_agent_grant_view"] : ["wait_for_human_agent_read_grant"]
|
|
910
|
-
}));
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
if (action === "read" || action === "show") {
|
|
914
|
-
const grantID = flags.grant || flags.grant_id || flags.agent_read_grant_id || positional(rest, 2);
|
|
915
|
-
if (!grantID) throw new Error("agent_read_grant_id is required");
|
|
916
|
-
await ensureBuyerSessionForVaultAccess(flags);
|
|
917
|
-
const view = await readBuyerAgentReadGrant(grantID, flags);
|
|
918
|
-
output(buyerRunOutput({
|
|
919
|
-
status: "agent_read_grant_view",
|
|
920
|
-
grant: view,
|
|
921
|
-
agent_next_actions: ["use_human_approved_fields_only"]
|
|
922
|
-
}));
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
if (subcommand === "read") {
|
|
927
|
-
await ensureBuyerSessionForVaultAccess(flags);
|
|
928
|
-
const view = await readBuyerVaultArtifactGrant(flags);
|
|
929
|
-
output(buyerRunOutput({
|
|
930
|
-
status: "agent_read_grant_view",
|
|
931
|
-
grant: view,
|
|
932
|
-
agent_next_actions: ["use_human_approved_fields_only"]
|
|
933
|
-
}));
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
if (command === "account" && subcommand === "login-link") {
|
|
938
|
-
await accountLoginLink(flags);
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
if (command === "auth" && subcommand === "status") {
|
|
942
|
-
output(await buyerAuthStatusOutput(flags));
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
throw new Error(`unknown buyer command: ${[command, subcommand].filter(Boolean).join(" ") || ""}`);
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
function buyerCatalogSearchFilters(flags = {}) {
|
|
949
|
-
const filters = {};
|
|
950
|
-
const categories = csvValues(flags.category || flags.categories);
|
|
951
|
-
if (categories.length) filters.categories = categories;
|
|
952
|
-
|
|
953
|
-
const mappings = [
|
|
954
|
-
["service_type", "ai.itpay.service_type"],
|
|
955
|
-
["delivery_method", "ai.itpay.delivery_method"],
|
|
956
|
-
["provider", "ai.itpay.provider"],
|
|
957
|
-
["provider_product_id", "ai.itpay.provider_product_id"],
|
|
958
|
-
["provider_product", "ai.itpay.provider_product_id"],
|
|
959
|
-
["sensitivity_level", "ai.itpay.sensitivity_level"],
|
|
960
|
-
["sensitivity", "ai.itpay.sensitivity_level"],
|
|
961
|
-
["delivery_mode", "ai.itpay.delivery_mode"],
|
|
962
|
-
["settlement_group", "ai.itpay.settlement_group"]
|
|
963
|
-
];
|
|
964
|
-
for (const [flagName, filterName] of mappings) {
|
|
965
|
-
if (flags[flagName]) filters[filterName] = String(flags[flagName]);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const listMappings = [
|
|
969
|
-
["use_case", "ai.itpay.taxonomy.use_cases"],
|
|
970
|
-
["use_cases", "ai.itpay.taxonomy.use_cases"],
|
|
971
|
-
["input_facet", "ai.itpay.taxonomy.input_facets"],
|
|
972
|
-
["input_facets", "ai.itpay.taxonomy.input_facets"],
|
|
973
|
-
["output_facet", "ai.itpay.taxonomy.output_facets"],
|
|
974
|
-
["output_facets", "ai.itpay.taxonomy.output_facets"],
|
|
975
|
-
["required_profile_field", "ai.itpay.required_profile_fields"],
|
|
976
|
-
["required_profile_fields", "ai.itpay.required_profile_fields"],
|
|
977
|
-
["agent_runtime", "ai.itpay.agent_runtimes"],
|
|
978
|
-
["agent_runtimes", "ai.itpay.agent_runtimes"]
|
|
979
|
-
];
|
|
980
|
-
for (const [flagName, filterName] of listMappings) {
|
|
981
|
-
const values = csvValues(flags[flagName]);
|
|
982
|
-
if (values.length) filters[filterName] = values;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
const boolMappings = [
|
|
986
|
-
["payment_qr_mpm", "ai.itpay.payment.qr_mpm"],
|
|
987
|
-
["merchant_verified", "ai.itpay.merchant_verified"],
|
|
988
|
-
["requires_human_input", "ai.itpay.requires_human_input"],
|
|
989
|
-
["requires_webauthn_reveal", "ai.itpay.requires_webauthn_reveal"],
|
|
990
|
-
["agent_may_execute_query", "ai.itpay.agent_may_execute_query"],
|
|
991
|
-
["agent_may_view_raw_result", "ai.itpay.agent_may_view_raw_result"]
|
|
992
|
-
];
|
|
993
|
-
for (const [flagName, filterName] of boolMappings) {
|
|
994
|
-
if (flags[flagName] !== undefined) filters[filterName] = booleanFlag(flags[flagName]);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
const hasMin = flags.price_min !== undefined || flags.min_price !== undefined;
|
|
998
|
-
const hasMax = flags.price_max !== undefined || flags.max_price !== undefined;
|
|
999
|
-
const min = hasMin ? Number(flags.price_min ?? flags.min_price) : NaN;
|
|
1000
|
-
const max = hasMax ? Number(flags.price_max ?? flags.max_price) : NaN;
|
|
1001
|
-
if (Number.isFinite(min) || Number.isFinite(max)) {
|
|
1002
|
-
filters.price = {};
|
|
1003
|
-
if (Number.isFinite(min)) filters.price.min = min;
|
|
1004
|
-
if (Number.isFinite(max)) filters.price.max = max;
|
|
1005
|
-
}
|
|
1006
|
-
return filters;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
function csvValues(value) {
|
|
1010
|
-
if (value === undefined || value === null || value === false) return [];
|
|
1011
|
-
if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
|
|
1012
|
-
return String(value).split(",").map((item) => item.trim()).filter(Boolean);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
function booleanFlag(value) {
|
|
1016
|
-
if (value === true || value === false) return value;
|
|
1017
|
-
const normalized = String(value).trim().toLowerCase();
|
|
1018
|
-
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
1019
|
-
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
1020
|
-
throw new Error(`invalid boolean flag value: ${value}`);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
function intFlag(value, name) {
|
|
1024
|
-
const number = Number(value);
|
|
1025
|
-
if (!Number.isInteger(number)) throw new Error(`${name} must be an integer`);
|
|
1026
|
-
return number;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
async function ops(command, rest, flags) {
|
|
1030
|
-
if (command !== "sandbox") throw new Error(`unknown ops command: ${command || ""}`);
|
|
1031
|
-
const area = rest[0];
|
|
1032
|
-
const action = rest[1];
|
|
1033
|
-
if (area === "worker" && action === "run-once") {
|
|
1034
|
-
output(await coreApi("/v1/sandbox/workers/run-once", { method: "POST", ops: true }, flags));
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
if (area === "recover-alipay-once") {
|
|
1038
|
-
output(await coreApi("/v1/local/workers/recover-alipay-sandbox-once", { method: "POST", ops: true }, flags));
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
if (area === "payment" && action === "query") {
|
|
1042
|
-
const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 2);
|
|
1043
|
-
if (!paymentIntentID) throw new Error("payment_intent_id is required");
|
|
1044
|
-
output(await coreApi(`/v1/payment-intents/${encodeURIComponent(paymentIntentID)}/alipay-sandbox-query`, { method: "POST", ops: true }, flags));
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
if (area === "refund") {
|
|
1048
|
-
const refundID = flags.refund || flags.refund_id || positional(rest, 2);
|
|
1049
|
-
if (!refundID) throw new Error("refund_id is required");
|
|
1050
|
-
if (action === "show") {
|
|
1051
|
-
output(await coreApi(`/v1/sandbox/ops/refunds/${encodeURIComponent(refundID)}`, { method: "GET", ops: true }, flags));
|
|
1052
|
-
return;
|
|
1053
|
-
}
|
|
1054
|
-
if (action === "approve" || action === "reject") {
|
|
1055
|
-
output(await coreApi(`/v1/sandbox/ops/refunds/${encodeURIComponent(refundID)}/${action}`, {
|
|
1056
|
-
method: "POST",
|
|
1057
|
-
ops: true,
|
|
1058
|
-
idempotencyKey: flags.idempotency_key || `idem_cli_refund_${action}_${refundID}`,
|
|
1059
|
-
body: {
|
|
1060
|
-
reason_code: flags.reason || flags.reason_code || (action === "approve" ? "approved_by_ops" : "not_eligible"),
|
|
1061
|
-
note: flags.note || ""
|
|
1062
|
-
}
|
|
1063
|
-
}, flags));
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
if (action === "execute") {
|
|
1067
|
-
output(await coreApi(`/v1/sandbox/ops/refunds/${encodeURIComponent(refundID)}/execute`, {
|
|
1068
|
-
method: "POST",
|
|
1069
|
-
ops: true,
|
|
1070
|
-
idempotencyKey: flags.idempotency_key || `idem_cli_refund_execute_${refundID}`
|
|
1071
|
-
}, flags));
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
if (area === "ledger" && action === "entries") {
|
|
1076
|
-
const params = new URLSearchParams();
|
|
1077
|
-
if (flags.order || flags.order_id) params.set("order_id", String(flags.order || flags.order_id));
|
|
1078
|
-
if (flags.refund || flags.refund_id) params.set("refund_id", String(flags.refund || flags.refund_id));
|
|
1079
|
-
if (flags.payment_intent || flags.payment_intent_id) params.set("payment_intent_id", String(flags.payment_intent || flags.payment_intent_id));
|
|
1080
|
-
if ([...params.keys()].length === 0) {
|
|
1081
|
-
throw new Error("ledger filter is required: use --order, --refund, or --payment-intent");
|
|
1082
|
-
}
|
|
1083
|
-
output(await coreApi(`/v1/sandbox/ops/ledger/entries${queryString(params)}`, { method: "GET", ops: true }, flags));
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
if (area === "reconciliation") {
|
|
1087
|
-
if (action === "run") {
|
|
1088
|
-
output(await coreApi("/v1/sandbox/ops/reconciliation-runs", {
|
|
1089
|
-
method: "POST",
|
|
1090
|
-
ops: true,
|
|
1091
|
-
idempotencyKey: flags.idempotency_key || `idem_cli_reconciliation_${cryptoRandom()}`,
|
|
1092
|
-
body: {
|
|
1093
|
-
reconciliation_run_id: flags.reconciliation_run_id || flags.run_id || "",
|
|
1094
|
-
status: flags.status || "matched",
|
|
1095
|
-
expected_amount_minor: intFlag(flags.expected_amount_minor || flags.expected || 0, "expected_amount_minor"),
|
|
1096
|
-
observed_amount_minor: intFlag(flags.observed_amount_minor || flags.observed || 0, "observed_amount_minor"),
|
|
1097
|
-
currency: flags.currency || "CNY",
|
|
1098
|
-
raw_statement_ref: flags.raw_statement_ref || flags.statement_ref || ""
|
|
1099
|
-
}
|
|
1100
|
-
}, flags));
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
if (action === "show") {
|
|
1104
|
-
const runID = flags.reconciliation_run_id || flags.run_id || positional(rest, 2);
|
|
1105
|
-
if (!runID) throw new Error("reconciliation_run_id is required");
|
|
1106
|
-
output(await coreApi(`/v1/sandbox/ops/reconciliation-runs/${encodeURIComponent(runID)}`, { method: "GET", ops: true }, flags));
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
if (area === "settlement" && action === "show") {
|
|
1111
|
-
const settlementBatchID = flags.settlement_batch || flags.settlement_batch_id || positional(rest, 2);
|
|
1112
|
-
if (!settlementBatchID) throw new Error("settlement_batch_id is required");
|
|
1113
|
-
output(await coreApi(`/v1/sandbox/ops/settlement-batches/${encodeURIComponent(settlementBatchID)}`, { method: "GET", ops: true }, flags));
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
throw new Error(`unknown ops sandbox command: ${rest.join(" ")}`);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
async function resolveBuyerCatalogSelection(selectionID, flags = {}) {
|
|
1120
|
-
if (!selectionID) throw new Error("catalog selection id is required");
|
|
1121
|
-
const detail = await getBuyerUCPProduct(selectionID, flags);
|
|
1122
|
-
return selectionFromUCPProduct(detail, selectionID, flags);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
async function resolveBuyerCatalogSelections(selectionIDs, flags = {}) {
|
|
1126
|
-
const ids = selectionIDs.map((id) => String(id).trim()).filter(Boolean);
|
|
1127
|
-
if (!ids.length) throw new Error("at least one catalog variant id is required");
|
|
1128
|
-
const selections = [];
|
|
1129
|
-
for (const id of ids) {
|
|
1130
|
-
selections.push(await resolveBuyerCatalogSelection(id, flags));
|
|
1131
|
-
}
|
|
1132
|
-
return selections;
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
async function getBuyerUCPProduct(selectionID, flags = {}) {
|
|
1136
|
-
if (!selectionID) throw new Error("catalog selection id is required");
|
|
1137
|
-
const body = {
|
|
1138
|
-
id: String(selectionID),
|
|
1139
|
-
filters: {},
|
|
1140
|
-
context: {}
|
|
1141
|
-
};
|
|
1142
|
-
if (flags.currency) body.context.currency = String(flags.currency);
|
|
1143
|
-
return await coreApi("/v1/catalog/selections/resolve", { method: "POST", body }, flags);
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
function selectionFromUCPProduct(detail, selectionID, flags = {}) {
|
|
1147
|
-
const product = detail?.product;
|
|
1148
|
-
if (!product) throw new Error(`catalog selection not found: ${selectionID}`);
|
|
1149
|
-
const variants = Array.isArray(product.variants) ? product.variants : [];
|
|
1150
|
-
const selectedVariantID = product.selected?.variant_id || selectionID;
|
|
1151
|
-
const variant = variants.find((candidate) => candidate.id === selectionID) ||
|
|
1152
|
-
variants.find((candidate) => candidate.id === selectedVariantID) ||
|
|
1153
|
-
variants[0];
|
|
1154
|
-
if (!variant) throw new Error(`catalog product has no variants: ${selectionID}`);
|
|
1155
|
-
const metadata = {
|
|
1156
|
-
...(product.metadata || {}),
|
|
1157
|
-
...(variant.metadata || {})
|
|
1158
|
-
};
|
|
1159
|
-
const requiredFields = Array.isArray(metadata["ai.itpay.required_profile_fields"])
|
|
1160
|
-
? metadata["ai.itpay.required_profile_fields"].map((field) => String(field)).filter(Boolean)
|
|
1161
|
-
: [];
|
|
1162
|
-
return {
|
|
1163
|
-
catalog_item_id: product.id,
|
|
1164
|
-
catalog_variant_id: variant.id,
|
|
1165
|
-
ucp_variant_id: variant.id,
|
|
1166
|
-
offer_id: flags.offer || flags.offer_id || metadata["ai.itpay.offer_id"] || "",
|
|
1167
|
-
catalog_version: metadata["ai.itpay.catalog_version"] || product.selected?.catalog_version || "",
|
|
1168
|
-
expected_amount: Number(flags.expected_amount || variant.price?.amount || 0),
|
|
1169
|
-
currency: flags.currency || variant.price?.currency || metadata.currency || "",
|
|
1170
|
-
title: product.title,
|
|
1171
|
-
description: product.description || variant.description || "",
|
|
1172
|
-
variant_title: variant.title,
|
|
1173
|
-
required_contact_fields: requiredFields,
|
|
1174
|
-
product,
|
|
1175
|
-
variant,
|
|
1176
|
-
metadata,
|
|
1177
|
-
purchasable: variant.availability?.available !== false
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
async function createBuyerCart(selection, flags = {}) {
|
|
1182
|
-
return await createBuyerCartFromSelections([selection], flags);
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
async function createBuyerCartFromSelections(selections, flags = {}) {
|
|
1186
|
-
if (!Array.isArray(selections) || !selections.length) throw new Error("at least one catalog selection is required");
|
|
1187
|
-
const quantities = buyerCartQuantities(selections.length, flags);
|
|
1188
|
-
const lineItems = selections.map((selection, index) => {
|
|
1189
|
-
if (!selection?.purchasable) throw new Error(`catalog variant is not purchasable: ${selection?.catalog_variant_id || selection?.ucp_variant_id || index}`);
|
|
1190
|
-
return {
|
|
1191
|
-
item: { id: selection.catalog_variant_id || selection.ucp_variant_id },
|
|
1192
|
-
quantity: quantities[index],
|
|
1193
|
-
input: buyerLineInputForSelection(selection, flags, index)
|
|
1194
|
-
};
|
|
1195
|
-
});
|
|
1196
|
-
const currencies = new Set(selections.map((selection) => String(flags.currency || selection.currency || "")).filter(Boolean));
|
|
1197
|
-
if (currencies.size > 1) {
|
|
1198
|
-
throw new Error(`selected variants have different currencies: ${Array.from(currencies).join(", ")}`);
|
|
1199
|
-
}
|
|
1200
|
-
const currency = flags.currency || selections[0]?.currency || "";
|
|
1201
|
-
const body = {
|
|
1202
|
-
line_items: lineItems,
|
|
1203
|
-
context: {},
|
|
1204
|
-
client_reference_id: flags.cart_client_reference_id || flags.client_reference_id || `cli_cart_${Date.now()}`
|
|
1205
|
-
};
|
|
1206
|
-
if (currency) body.context.currency = String(currency);
|
|
1207
|
-
const cart = await coreApi("/v1/carts", {
|
|
1208
|
-
method: "POST",
|
|
1209
|
-
idempotencyKey: flags.cart_idempotency_key || `idem_cli_cart_${cryptoRandom()}`,
|
|
1210
|
-
body
|
|
1211
|
-
}, flags);
|
|
1212
|
-
writeState({ ...readState(), last_core_cart_id: cart.cart_id || cart.id });
|
|
1213
|
-
return cart;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
function buyerCartSelectionIDs(rest, flags = {}) {
|
|
1217
|
-
const raw = flags.variants || flags.variant_ids || flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id || positional(rest, 1);
|
|
1218
|
-
if (!raw) return [];
|
|
1219
|
-
if (Array.isArray(raw)) return raw.flatMap((value) => splitCSV(value));
|
|
1220
|
-
return splitCSV(raw);
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
function buyerCartQuantities(count, flags = {}) {
|
|
1224
|
-
const raw = flags.quantities || flags.quantity || flags.qty;
|
|
1225
|
-
const values = raw === undefined || raw === null || raw === true
|
|
1226
|
-
? []
|
|
1227
|
-
: splitCSV(raw).map((value) => Number(value));
|
|
1228
|
-
if (values.length && values.length !== count) {
|
|
1229
|
-
throw new Error(`--quantities must provide ${count} value(s), got ${values.length}`);
|
|
1230
|
-
}
|
|
1231
|
-
const quantities = values.length ? values : Array(count).fill(1);
|
|
1232
|
-
for (const quantity of quantities) {
|
|
1233
|
-
if (!Number.isInteger(quantity) || quantity <= 0) {
|
|
1234
|
-
throw new Error(`cart quantity must be a positive integer, got ${quantity}`);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
return quantities;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function buyerLineInputForSelection(selection, flags = {}, index = 0) {
|
|
1241
|
-
const input = parseBuyerInputs(flags, index);
|
|
1242
|
-
const providerProductID = String(selection?.metadata?.["ai.itpay.provider_product_id"] || selection?.variant?.metadata?.["ai.itpay.provider_product_id"] || "");
|
|
1243
|
-
if (providerProductID === "81api_company_fuzzy_search") {
|
|
1244
|
-
if (!input.company_name && flags.company_name) input.company_name = String(flags.company_name).trim();
|
|
1245
|
-
if (!input.company_name && flags.keyword) input.company_name = String(flags.keyword).trim();
|
|
1246
|
-
if (!input.PageNum && flags.page_num) input.PageNum = String(flags.page_num).trim();
|
|
1247
|
-
if (!input.PageNum) input.PageNum = "1";
|
|
1248
|
-
if (!input.company_name) {
|
|
1249
|
-
throw new Error("企业工商数据模糊查询 requires --input company_name=<关键词> or --company-name <关键词>. Ask the user for a company keyword/short name before checkout.");
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
if (providerProductID === "81api_company_base_info") {
|
|
1253
|
-
if (!input.company_name_or_credit_no && flags.company_name_or_credit_no) input.company_name_or_credit_no = String(flags.company_name_or_credit_no).trim();
|
|
1254
|
-
if (!input.company_name_or_credit_no && flags.company_name) input.company_name_or_credit_no = String(flags.company_name).trim();
|
|
1255
|
-
if (!input.isRaiseErrorCode && flags.is_raise_error_code !== undefined) input.isRaiseErrorCode = String(flags.is_raise_error_code).trim();
|
|
1256
|
-
if (!input.isRaiseErrorCode) input.isRaiseErrorCode = "0";
|
|
1257
|
-
if (!input.company_name_or_credit_no) {
|
|
1258
|
-
throw new Error("企业工商数据精准查询 requires --input company_name_or_credit_no=<完整企业名称或统一社会信用代码>. If the user only gave a brand/short name, run fuzzy search first or resolve the exact registered company name before checkout.");
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
return input;
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
function parseBuyerInputs(flags = {}, index = 0) {
|
|
1265
|
-
const input = {};
|
|
1266
|
-
const rawValues = [];
|
|
1267
|
-
for (const key of ["input", "inputs"]) {
|
|
1268
|
-
const raw = flags[key];
|
|
1269
|
-
if (Array.isArray(raw)) rawValues.push(...raw);
|
|
1270
|
-
else if (raw !== undefined && raw !== true) rawValues.push(raw);
|
|
1271
|
-
}
|
|
1272
|
-
for (const raw of rawValues) {
|
|
1273
|
-
for (const part of splitInputParts(raw)) {
|
|
1274
|
-
const eq = part.indexOf("=");
|
|
1275
|
-
if (eq <= 0) throw new Error(`invalid --input ${part}; expected key=value`);
|
|
1276
|
-
const key = part.slice(0, eq).trim();
|
|
1277
|
-
const value = part.slice(eq + 1).trim();
|
|
1278
|
-
if (key) input[key] = value;
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
const indexed = flags[`input_${index + 1}`] || flags[`inputs_${index + 1}`];
|
|
1282
|
-
if (indexed && indexed !== true) {
|
|
1283
|
-
for (const part of splitInputParts(indexed)) {
|
|
1284
|
-
const eq = part.indexOf("=");
|
|
1285
|
-
if (eq <= 0) throw new Error(`invalid indexed input ${part}; expected key=value`);
|
|
1286
|
-
input[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
return input;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
function splitInputParts(raw) {
|
|
1293
|
-
const text = String(raw || "").trim();
|
|
1294
|
-
if (!text) return [];
|
|
1295
|
-
if (text.startsWith("{")) {
|
|
1296
|
-
const parsed = JSON.parse(text);
|
|
1297
|
-
return Object.entries(parsed).map(([key, value]) => `${key}=${value}`);
|
|
1298
|
-
}
|
|
1299
|
-
return text.split(",").map((part) => part.trim()).filter(Boolean);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
function splitCSV(value) {
|
|
1303
|
-
return String(value || "")
|
|
1304
|
-
.split(",")
|
|
1305
|
-
.map((part) => part.trim())
|
|
1306
|
-
.filter(Boolean);
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
async function getBuyerCart(cartID, flags = {}) {
|
|
1310
|
-
if (!cartID) throw new Error("cart_id is required");
|
|
1311
|
-
return await coreApi(`/v1/carts/${encodeURIComponent(cartID)}`, { method: "GET" }, flags);
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
async function addBuyerCartLineItem(cartID, selection, flags = {}) {
|
|
1315
|
-
if (!cartID) throw new Error("cart_id is required");
|
|
1316
|
-
if (!selection?.purchasable) throw new Error(`catalog variant is not purchasable: ${selection?.catalog_variant_id || selection?.ucp_variant_id || ""}`);
|
|
1317
|
-
const quantity = buyerCartQuantities(1, flags)[0];
|
|
1318
|
-
const body = {
|
|
1319
|
-
item: { id: selection.catalog_variant_id || selection.ucp_variant_id },
|
|
1320
|
-
quantity,
|
|
1321
|
-
input: buyerLineInputForSelection(selection, flags, 0)
|
|
1322
|
-
};
|
|
1323
|
-
const cart = await coreApi(`/v1/carts/${encodeURIComponent(cartID)}/line-items`, {
|
|
1324
|
-
method: "POST",
|
|
1325
|
-
idempotencyKey: flags.cart_idempotency_key || `idem_cli_cart_add_${cryptoRandom()}`,
|
|
1326
|
-
body
|
|
1327
|
-
}, flags);
|
|
1328
|
-
writeState({ ...readState(), last_core_cart_id: cart.cart_id || cart.id });
|
|
1329
|
-
return cart;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
async function removeBuyerCartLineItem(cartID, lineID, flags = {}) {
|
|
1333
|
-
if (!cartID) throw new Error("cart_id is required");
|
|
1334
|
-
if (!lineID) throw new Error("cart_line_item_id is required");
|
|
1335
|
-
const cart = await coreApi(`/v1/carts/${encodeURIComponent(cartID)}/line-items/${encodeURIComponent(lineID)}`, {
|
|
1336
|
-
method: "DELETE"
|
|
1337
|
-
}, flags);
|
|
1338
|
-
writeState({ ...readState(), last_core_cart_id: cart.cart_id || cart.id });
|
|
1339
|
-
return cart;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
async function createBuyerCheckoutFromCart(cart, selection = null, flags = {}) {
|
|
1343
|
-
const cartID = typeof cart === "string" ? cart : (cart?.cart_id || cart?.id);
|
|
1344
|
-
if (!cartID) throw new Error("cart_id is required");
|
|
1345
|
-
const deliveryContact = {};
|
|
1346
|
-
if (flags.email) deliveryContact.email = flags.email;
|
|
1347
|
-
if (flags.phone) deliveryContact.phone = flags.phone;
|
|
1348
|
-
const missing = requiredDeliveryContactFields(selection).filter((field) => !deliveryContact[field]);
|
|
1349
|
-
if (missing.length) {
|
|
1350
|
-
throw new Error(`missing required delivery contact: ${missing.join(", ")}; provide ${missing.map((field) => `--${field} <value>`).join(" ")}`);
|
|
1351
|
-
}
|
|
1352
|
-
const request = {
|
|
1353
|
-
method: "POST",
|
|
1354
|
-
idempotencyKey: flags.checkout_idempotency_key || flags.idempotency_key || `idem_cli_checkout_${cartID}`,
|
|
1355
|
-
body: {
|
|
1356
|
-
cart_id: cartID,
|
|
1357
|
-
client_reference_id: flags.checkout_client_reference_id || flags.client_reference_id || `cli_checkout_${cartID}`,
|
|
1358
|
-
delivery_contact: deliveryContact
|
|
1359
|
-
}
|
|
1360
|
-
};
|
|
1361
|
-
let checkout;
|
|
1362
|
-
try {
|
|
1363
|
-
checkout = await coreApi("/v1/checkouts", request, flags);
|
|
1364
|
-
} catch (error) {
|
|
1365
|
-
if (error?.status === 401 && readSessionToken(readCredentials()) && !flags.access_token) {
|
|
1366
|
-
writeCredentials(deleteSessionCredential(readCredentials()));
|
|
1367
|
-
checkout = await coreApi("/v1/checkouts", request, flags);
|
|
1368
|
-
} else {
|
|
1369
|
-
throw error;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
writeState({ ...readState(), last_core_cart_id: cartID, last_core_checkout_id: checkout.checkout_id });
|
|
1373
|
-
rememberCoreAuthAction(checkout.checkout_id, checkout.human_action);
|
|
1374
|
-
return checkout;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
function requiredDeliveryContactFields(selection) {
|
|
1378
|
-
if (Array.isArray(selection?.required_contact_fields)) {
|
|
1379
|
-
return selection.required_contact_fields.map((field) => String(field).trim()).filter(Boolean);
|
|
1380
|
-
}
|
|
1381
|
-
const fields = selection?.delivery?.requires_contact_fields;
|
|
1382
|
-
return Array.isArray(fields) ? fields.map((field) => String(field).trim()).filter(Boolean) : [];
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
async function createBuyerPaymentIntent(checkoutID, flags = {}) {
|
|
1386
|
-
if (!checkoutID) throw new Error("checkout_id is required");
|
|
1387
|
-
const method = String(flags.method || flags.payment_method || "alipay").toLowerCase();
|
|
1388
|
-
const provider = String(flags.provider || flags.preferred_provider || method).toLowerCase();
|
|
1389
|
-
const intent = await coreApi(`/v1/checkouts/${encodeURIComponent(checkoutID)}/payment-intents`, {
|
|
1390
|
-
method: "POST",
|
|
1391
|
-
idempotencyKey: flags.payment_idempotency_key || flags.idempotency_key || `idem_cli_payment_${cryptoRandom()}`,
|
|
1392
|
-
body: {
|
|
1393
|
-
payment_method_type: method,
|
|
1394
|
-
preferred_provider: provider
|
|
1395
|
-
}
|
|
1396
|
-
}, flags);
|
|
1397
|
-
writeState({ ...readState(), last_core_checkout_id: checkoutID, last_core_payment_intent_id: intent.payment_intent_id });
|
|
1398
|
-
return intent;
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
async function waitBuyerCheckoutAuth(checkout, flags = {}) {
|
|
1402
|
-
const checkoutID = checkout?.checkout_id || checkout;
|
|
1403
|
-
if (!checkoutID) throw new Error("checkout_id is required");
|
|
1404
|
-
const timeoutMs = Number(flags.auth_timeout || flags.timeout || 900) * 1000;
|
|
1405
|
-
const started = Date.now();
|
|
1406
|
-
let lastHeartbeatAt = 0;
|
|
1407
|
-
let current = typeof checkout === "string" ? await getBuyerCheckout(checkoutID, flags) : checkout;
|
|
1408
|
-
const authAction = current?.human_action || readCoreAuthAction(checkoutID) || null;
|
|
1409
|
-
rememberCoreAuthAction(checkoutID, authAction);
|
|
1410
|
-
while (Date.now() - started < timeoutMs) {
|
|
1411
|
-
if (current.payment_intent_id || current.identity_status === "identity_resolved" || current.next_required_action !== "auth_qr") {
|
|
1412
|
-
await maybeClaimBuyerSessionFromAuthAction(authAction, flags);
|
|
1413
|
-
return current;
|
|
1414
|
-
}
|
|
1415
|
-
lastHeartbeatAt = writeWaitHeartbeat({
|
|
1416
|
-
kind: "ItPay buyer auth",
|
|
1417
|
-
idName: "checkout_id",
|
|
1418
|
-
idValue: checkoutID,
|
|
1419
|
-
status: current.identity_status || current.next_required_action || "waiting_human_auth",
|
|
1420
|
-
action: current.human_action || null,
|
|
1421
|
-
lastHeartbeatAt,
|
|
1422
|
-
flags,
|
|
1423
|
-
command: cliCommand("buyer", "checkout", "resume", checkoutID, "--json")
|
|
1424
|
-
});
|
|
1425
|
-
await sleep(Number(flags.auth_poll_ms || flags.poll_ms || 2000));
|
|
1426
|
-
current = await getBuyerCheckout(checkoutID, flags);
|
|
1427
|
-
}
|
|
1428
|
-
await maybeClaimBuyerSessionFromAuthAction(authAction, flags);
|
|
1429
|
-
return current;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
async function maybeClaimBuyerSessionFromAuthAction(action, flags = {}) {
|
|
1433
|
-
const parsed = parseBuyerAuthActionURL(action);
|
|
1434
|
-
if (!parsed.authSessionID || !parsed.displayToken) return null;
|
|
1435
|
-
try {
|
|
1436
|
-
const response = await coreApi(`/v1/session-exchanges/auth-sessions/${encodeURIComponent(parsed.authSessionID)}/agent-session?display_token=${encodeURIComponent(parsed.displayToken)}`, {
|
|
1437
|
-
method: "POST"
|
|
1438
|
-
}, flags);
|
|
1439
|
-
const rawToken = response.raw_session_token || response.session_token || response.session?.raw_session_token;
|
|
1440
|
-
if (!rawToken) return response;
|
|
1441
|
-
writeSessionCredentials({
|
|
1442
|
-
account_id: response.buyer_account_id,
|
|
1443
|
-
device_id: response.agent_device_id,
|
|
1444
|
-
session_token: rawToken
|
|
1445
|
-
});
|
|
1446
|
-
writeConfig({
|
|
1447
|
-
api_base: coreApiBase(flags),
|
|
1448
|
-
account_id: response.buyer_account_id,
|
|
1449
|
-
device_id: response.agent_device_id
|
|
1450
|
-
});
|
|
1451
|
-
forgetCoreAuthAction(response.checkout_id);
|
|
1452
|
-
return response;
|
|
1453
|
-
} catch (error) {
|
|
1454
|
-
if (!flags.quiet) {
|
|
1455
|
-
process.stderr.write(`ItPay buyer session claim skipped: ${safeErrorMessage(error)}\n`);
|
|
1456
|
-
}
|
|
1457
|
-
return null;
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
function buyerSessionClaimStatus(claimed = null) {
|
|
1462
|
-
const config = readConfig();
|
|
1463
|
-
const hasSession = Boolean(readSessionToken());
|
|
1464
|
-
if (!claimed && !hasSession) return undefined;
|
|
1465
|
-
return {
|
|
1466
|
-
status: "buyer_session_saved",
|
|
1467
|
-
session_stored: true,
|
|
1468
|
-
buyer_account_id: claimed?.buyer_account_id || config.account_id || null,
|
|
1469
|
-
agent_device_id: claimed?.agent_device_id || config.device_id || null,
|
|
1470
|
-
token_included: false,
|
|
1471
|
-
agent_next_actions: ["reuse_buyer_session", "list_agent_read_grants"]
|
|
1472
|
-
};
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
async function maybeClaimBuyerSessionForCheckout(checkout, flags = {}) {
|
|
1476
|
-
if (!checkout?.checkout_id) return null;
|
|
1477
|
-
if (readSessionToken()) return null;
|
|
1478
|
-
if (checkout.identity_status !== "identity_resolved" && !checkout.payment_intent_id) return null;
|
|
1479
|
-
const action = checkout.human_action || readCoreAuthAction(checkout.checkout_id);
|
|
1480
|
-
return await maybeClaimBuyerSessionFromAuthAction(action, flags);
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
async function ensureBuyerSessionForVaultAccess(flags = {}) {
|
|
1484
|
-
if (readSessionToken()) return null;
|
|
1485
|
-
const checkoutID =
|
|
1486
|
-
flags.checkout ||
|
|
1487
|
-
flags.checkout_id ||
|
|
1488
|
-
readState().last_core_checkout_id ||
|
|
1489
|
-
readState().last_core_auth_checkout_id;
|
|
1490
|
-
if (checkoutID) {
|
|
1491
|
-
try {
|
|
1492
|
-
const checkout = await getBuyerCheckout(checkoutID, flags);
|
|
1493
|
-
rememberCoreAuthAction(checkout.checkout_id || checkoutID, checkout.human_action);
|
|
1494
|
-
const claimed = await maybeClaimBuyerSessionForCheckout(checkout, { ...flags, quiet: true });
|
|
1495
|
-
if (claimed || readSessionToken()) return claimed;
|
|
1496
|
-
} catch {
|
|
1497
|
-
// Continue with the locally remembered auth action below.
|
|
1498
|
-
}
|
|
1499
|
-
const action = readCoreAuthAction(checkoutID);
|
|
1500
|
-
const claimed = await maybeClaimBuyerSessionFromAuthAction(action, { ...flags, quiet: true });
|
|
1501
|
-
if (claimed || readSessionToken()) return claimed;
|
|
1502
|
-
}
|
|
1503
|
-
const state = readState();
|
|
1504
|
-
const entries = Object.entries(state.core_auth_actions || {});
|
|
1505
|
-
for (const [, action] of entries.reverse()) {
|
|
1506
|
-
const claimed = await maybeClaimBuyerSessionFromAuthAction(action, { ...flags, quiet: true });
|
|
1507
|
-
if (claimed || readSessionToken()) return claimed;
|
|
1508
|
-
}
|
|
1509
|
-
return null;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
function parseBuyerAuthActionURL(action) {
|
|
1513
|
-
if (!action || typeof action !== "object") return {};
|
|
1514
|
-
let authSessionID = String(action.auth_session_id || "").trim();
|
|
1515
|
-
if (!authSessionID && String(action.id || "").startsWith("auth_")) {
|
|
1516
|
-
authSessionID = String(action.id).trim();
|
|
1517
|
-
}
|
|
1518
|
-
let displayToken = "";
|
|
1519
|
-
let sourceURL = "";
|
|
1520
|
-
for (const rawURL of buyerAuthActionCandidateURLs(action)) {
|
|
1521
|
-
try {
|
|
1522
|
-
const parsed = new URL(rawURL);
|
|
1523
|
-
const token = parsed.searchParams.get("display_token") || "";
|
|
1524
|
-
if (token && !displayToken) displayToken = token;
|
|
1525
|
-
const match = parsed.pathname.match(/(?:^|\/)v1\/session-exchanges\/auth-sessions\/([^/?#]+)(?:\/|$)/);
|
|
1526
|
-
if (match && !authSessionID) authSessionID = decodeURIComponent(match[1]);
|
|
1527
|
-
if (!sourceURL && (match || token)) sourceURL = rawURL;
|
|
1528
|
-
if (authSessionID && displayToken) break;
|
|
1529
|
-
} catch {
|
|
1530
|
-
// Ignore non-URL display entries.
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
return { authSessionID, displayToken, sourceURL };
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
function buyerAuthActionCandidateURLs(action) {
|
|
1537
|
-
const urls = [];
|
|
1538
|
-
for (const key of ["url", "web_url", "auth_url", "oauth_start_url", "mobile_wallet_url"]) {
|
|
1539
|
-
if (action?.[key]) urls.push(String(action[key]));
|
|
1540
|
-
}
|
|
1541
|
-
const presentationDisplay = action?.presentation?.display;
|
|
1542
|
-
if (Array.isArray(presentationDisplay)) {
|
|
1543
|
-
for (const entry of presentationDisplay) {
|
|
1544
|
-
if (entry?.url) urls.push(String(entry.url));
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
if (Array.isArray(action?.display)) {
|
|
1548
|
-
for (const entry of action.display) {
|
|
1549
|
-
if (entry?.url) urls.push(String(entry.url));
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
return urls;
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
function rememberCoreAuthAction(checkoutID, action) {
|
|
1556
|
-
if (!checkoutID || action?.kind !== "auth_qr") return;
|
|
1557
|
-
const parsed = parseBuyerAuthActionURL(action);
|
|
1558
|
-
if (!parsed.authSessionID || !parsed.displayToken) return;
|
|
1559
|
-
const state = readState();
|
|
1560
|
-
const existing = state.core_auth_actions && typeof state.core_auth_actions === "object" ? state.core_auth_actions : {};
|
|
1561
|
-
const entries = Object.entries(existing).slice(-19);
|
|
1562
|
-
const next = Object.fromEntries(entries);
|
|
1563
|
-
next[checkoutID] = {
|
|
1564
|
-
kind: "auth_qr",
|
|
1565
|
-
id: action.id || action.auth_session_id || parsed.authSessionID,
|
|
1566
|
-
auth_session_id: parsed.authSessionID,
|
|
1567
|
-
url: action.url || parsed.sourceURL,
|
|
1568
|
-
web_url: action.web_url || action.url || parsed.sourceURL,
|
|
1569
|
-
expires_at: action.expires_at || null,
|
|
1570
|
-
saved_at: new Date().toISOString()
|
|
1571
|
-
};
|
|
1572
|
-
writeState({ ...state, core_auth_actions: next, last_core_auth_checkout_id: checkoutID });
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
function readCoreAuthAction(checkoutID) {
|
|
1576
|
-
if (!checkoutID) return null;
|
|
1577
|
-
const state = readState();
|
|
1578
|
-
return state.core_auth_actions?.[checkoutID] || null;
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
function forgetCoreAuthAction(checkoutID) {
|
|
1582
|
-
if (!checkoutID) return;
|
|
1583
|
-
const state = readState();
|
|
1584
|
-
if (!state.core_auth_actions?.[checkoutID]) return;
|
|
1585
|
-
const next = { ...state.core_auth_actions };
|
|
1586
|
-
delete next[checkoutID];
|
|
1587
|
-
writeState({ ...state, core_auth_actions: next });
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
async function getBuyerCheckout(checkoutID, flags = {}) {
|
|
1591
|
-
return await coreApi(`/v1/checkouts/${encodeURIComponent(checkoutID)}`, { method: "GET" }, flags);
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
async function createBuyerRefund(orderID, flags = {}) {
|
|
1595
|
-
if (flags.amount !== undefined) {
|
|
1596
|
-
throw new Error("use --amount-minor for refund amount");
|
|
1597
|
-
}
|
|
1598
|
-
if (flags.refund_scope && flags.refund_scope !== "order") {
|
|
1599
|
-
throw new Error("unsupported_refund_scope");
|
|
1600
|
-
}
|
|
1601
|
-
const order = await getBuyerOrderDetail(orderID, flags);
|
|
1602
|
-
const eligibility = order.refund_eligibility || order.order_detail?.refund_eligibility || null;
|
|
1603
|
-
if (eligibility && eligibility.likely_refundable === false && !booleanFlag(flags.confirm_policy_risk || false)) {
|
|
1604
|
-
return {
|
|
1605
|
-
status: "policy_risk_confirmation_required",
|
|
1606
|
-
order_id: orderID,
|
|
1607
|
-
refund_eligibility: eligibility,
|
|
1608
|
-
agent_next_actions: [
|
|
1609
|
-
"explain_refund_policy",
|
|
1610
|
-
"ask_human_to_confirm_policy_risk",
|
|
1611
|
-
`retry_with_${cliCommand("buyer", "refund", "create", orderID, "--confirm-policy-risk", "true", "--json")}`
|
|
1612
|
-
],
|
|
1613
|
-
submitted: false
|
|
1614
|
-
};
|
|
1615
|
-
}
|
|
1616
|
-
return await coreApi(`/v1/me/orders/${encodeURIComponent(orderID)}/refunds`, {
|
|
1617
|
-
method: "POST",
|
|
1618
|
-
headers: { "X-ItPay-Client-Surface": "cli" },
|
|
1619
|
-
idempotencyKey: flags.idempotency_key || `idem_cli_refund_create_${orderID}`,
|
|
1620
|
-
body: {
|
|
1621
|
-
refund_scope: flags.refund_scope || "order",
|
|
1622
|
-
order_line_item_ids: csvValues(flags.order_line_item_ids || flags.line_ids || flags.line_id),
|
|
1623
|
-
amount_minor: intFlag(flags.amount_minor, "amount_minor"),
|
|
1624
|
-
currency: flags.currency || "CNY",
|
|
1625
|
-
reason_code: flags.reason || flags.reason_code || "buyer_requested",
|
|
1626
|
-
reason_note: flags.note || flags.reason_note || ""
|
|
1627
|
-
}
|
|
1628
|
-
}, flags);
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
async function getBuyerOrderDetail(orderID, flags = {}) {
|
|
1632
|
-
return await coreApi(`/v1/me/orders/${encodeURIComponent(orderID)}`, { method: "GET" }, flags);
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
async function listBuyerRefunds(orderID, flags = {}) {
|
|
1636
|
-
return await coreApi(`/v1/me/orders/${encodeURIComponent(orderID)}/refunds`, { method: "GET" }, flags);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
async function getBuyerRefund(refundID, flags = {}) {
|
|
1640
|
-
return await coreApi(`/v1/me/refunds/${encodeURIComponent(refundID)}`, { method: "GET" }, flags);
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
async function cancelBuyerRefund(refundID, flags = {}) {
|
|
1644
|
-
return await coreApi(`/v1/me/refunds/${encodeURIComponent(refundID)}/cancel`, {
|
|
1645
|
-
method: "POST",
|
|
1646
|
-
headers: { "X-ItPay-Client-Surface": "cli" },
|
|
1647
|
-
idempotencyKey: flags.idempotency_key || `idem_cli_refund_cancel_${refundID}`,
|
|
1648
|
-
body: {
|
|
1649
|
-
reason_code: flags.reason || flags.reason_code || "buyer_changed_mind",
|
|
1650
|
-
reason_note: flags.note || flags.reason_note || ""
|
|
1651
|
-
}
|
|
1652
|
-
}, flags);
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
async function getBuyerPaymentIntent(paymentIntentID, flags = {}) {
|
|
1656
|
-
return await coreApi(`/v1/payment-intents/${encodeURIComponent(paymentIntentID)}`, { method: "GET" }, flags);
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
async function refreshBuyerPaymentQR(paymentIntentID, flags = {}) {
|
|
1660
|
-
const intent = await getBuyerPaymentIntent(paymentIntentID, flags);
|
|
1661
|
-
if (intent.status === "verified") return intent;
|
|
1662
|
-
const refreshURL = intent.qr_refresh_url;
|
|
1663
|
-
if (!refreshURL) {
|
|
1664
|
-
throw new Error("payment intent does not expose qr_refresh_url; refresh is supported only for refreshable Alipay sandbox QR intents");
|
|
1665
|
-
}
|
|
1666
|
-
return await coreApi(refreshURL, {
|
|
1667
|
-
method: "POST",
|
|
1668
|
-
body: {
|
|
1669
|
-
reason: normalizeQRRefreshReasonForCLI(flags.reason || flags.refresh_reason || "order_not_found")
|
|
1670
|
-
}
|
|
1671
|
-
}, flags);
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
function normalizeQRRefreshReasonForCLI(reason) {
|
|
1675
|
-
const normalized = String(reason || "").trim().toLowerCase().replaceAll("-", "_");
|
|
1676
|
-
if (["order_not_found", "qr_unavailable", "manual_refresh", "human_open"].includes(normalized)) return normalized;
|
|
1677
|
-
return "manual_refresh";
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
async function renderItPayPaymentAction(intent, flags = {}) {
|
|
1681
|
-
const action = intent?.human_action ? { ...intent.human_action } : (intent?.payment_url ? {
|
|
1682
|
-
id: intent.payment_intent_id,
|
|
1683
|
-
title: "Scan with Alipay",
|
|
1684
|
-
url: intent.payment_url,
|
|
1685
|
-
expires_at: intent.qr?.expires_at
|
|
1686
|
-
} : null);
|
|
1687
|
-
if (action) {
|
|
1688
|
-
if (intent?.qr_png_url || intent?.qr?.png_url) {
|
|
1689
|
-
action.qr_png_url = intent.qr_png_url || intent.qr.png_url;
|
|
1690
|
-
}
|
|
1691
|
-
if (intent?.mobile_wallet_url) {
|
|
1692
|
-
action.mobile_wallet_url = intent.mobile_wallet_url;
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
if (action && (intent?.qr_image_url || intent?.qr?.image_url)) {
|
|
1696
|
-
action.qr_image_url = intent.qr_image_url || intent.qr.image_url;
|
|
1697
|
-
action.display_mode = intent.qr?.scan_mode || "itpay_entry_qr";
|
|
1698
|
-
action.description = action.description || "Scan the ItPay payment entry QR; ItPay will safely hand off to Alipay.";
|
|
1699
|
-
}
|
|
1700
|
-
const result = await renderHumanAction(action, flags);
|
|
1701
|
-
if (intent && action) {
|
|
1702
|
-
intent.human_action = { ...(intent.human_action || {}), ...action };
|
|
1703
|
-
if (action.local_qr_path) intent.local_qr_path = action.local_qr_path;
|
|
1704
|
-
if (action.preferred_qr_url) intent.preferred_qr_url = action.preferred_qr_url;
|
|
1705
|
-
if (action.mobile_wallet_url) intent.mobile_wallet_url = action.mobile_wallet_url;
|
|
1706
|
-
}
|
|
1707
|
-
return result;
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
async function waitBuyerPayment(intent, flags = {}) {
|
|
1711
|
-
const paymentIntentID = intent?.payment_intent_id || intent;
|
|
1712
|
-
if (!paymentIntentID) throw new Error("payment_intent_id is required");
|
|
1713
|
-
let cursor = flags.cursor || intent?.agent_wait?.cursor || "";
|
|
1714
|
-
const waitURL = flags.wait_url || intent?.agent_wait?.wait_url || `/v1/payment-intents/${encodeURIComponent(paymentIntentID)}/events/wait`;
|
|
1715
|
-
const timeoutMs = Number(flags.timeout || 900) * 1000;
|
|
1716
|
-
const started = Date.now();
|
|
1717
|
-
let lastHeartbeatAt = 0;
|
|
1718
|
-
let lastEvent = null;
|
|
1719
|
-
while (Date.now() - started < timeoutMs) {
|
|
1720
|
-
const params = new URLSearchParams();
|
|
1721
|
-
if (cursor) params.set("cursor", cursor);
|
|
1722
|
-
params.set("timeout", String(flags.poll_timeout || "30s"));
|
|
1723
|
-
const event = await coreApi(appendURLQuery(waitURL, params), { method: "GET" }, flags);
|
|
1724
|
-
lastEvent = event;
|
|
1725
|
-
cursor = event.cursor || cursor;
|
|
1726
|
-
if (event.event_type === "payment_intent.verified") return event;
|
|
1727
|
-
if (event.event_type && event.event_type !== "wait.timeout") return event;
|
|
1728
|
-
lastHeartbeatAt = writeWaitHeartbeat({
|
|
1729
|
-
kind: "ItPay payment notify",
|
|
1730
|
-
idName: "payment_intent_id",
|
|
1731
|
-
idValue: paymentIntentID,
|
|
1732
|
-
status: event.event_type === "wait.timeout" ? "still_waiting" : (event.event_type || "still_waiting"),
|
|
1733
|
-
action: intent?.human_action || null,
|
|
1734
|
-
lastHeartbeatAt,
|
|
1735
|
-
flags,
|
|
1736
|
-
command: cliCommand("buyer", "payment", "wait", paymentIntentID, "--json")
|
|
1737
|
-
});
|
|
1738
|
-
}
|
|
1739
|
-
return lastEvent || { event_type: "wait.timeout", payment_intent_id: paymentIntentID, cursor, agent_next_actions: ["wait_payment"] };
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
async function waitBuyerDelivery(checkoutID, flags = {}) {
|
|
1743
|
-
const timeoutMs = Number(flags.delivery_timeout || 30) * 1000;
|
|
1744
|
-
const started = Date.now();
|
|
1745
|
-
let checkout = await getBuyerCheckout(checkoutID, flags);
|
|
1746
|
-
while (!isBuyerDeliveryComplete({ checkout }) && Date.now() - started < timeoutMs) {
|
|
1747
|
-
await sleep(Number(flags.delivery_poll_ms || 2000));
|
|
1748
|
-
checkout = await getBuyerCheckout(checkoutID, flags);
|
|
1749
|
-
}
|
|
1750
|
-
return { checkout, delivery: checkout.delivery || null };
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
function isBuyerDeliveryComplete(result) {
|
|
1754
|
-
const checkout = result?.checkout || result || {};
|
|
1755
|
-
const delivery = checkout.delivery || result?.delivery || {};
|
|
1756
|
-
return checkout.delivery_status === "delivered" ||
|
|
1757
|
-
delivery.status === "delivery_claimable" ||
|
|
1758
|
-
delivery.next_required_action === "check_email" ||
|
|
1759
|
-
checkout.agent_next_actions?.includes("stop_check_email");
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
function buyerRunOutput(value = {}) {
|
|
1763
|
-
return normalizeBuyerMoneyFields(stripInternalBuyerFields({
|
|
1764
|
-
schema_version: "itp.buyer.v1",
|
|
1765
|
-
docs: value.docs || buyerDocsFor(value),
|
|
1766
|
-
...value,
|
|
1767
|
-
secrets: {
|
|
1768
|
-
raw_content_included: false,
|
|
1769
|
-
claim_token_included: false,
|
|
1770
|
-
provider_raw_payload_included: false
|
|
1771
|
-
}
|
|
1772
|
-
}));
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
function normalizeBuyerMoneyFields(value) {
|
|
1776
|
-
if (Array.isArray(value)) return value.map((item) => normalizeBuyerMoneyFields(item));
|
|
1777
|
-
if (!value || typeof value !== "object") return value;
|
|
1778
|
-
const next = {};
|
|
1779
|
-
for (const [key, item] of Object.entries(value)) {
|
|
1780
|
-
next[key] = normalizeBuyerMoneyFields(item);
|
|
1781
|
-
}
|
|
1782
|
-
if (
|
|
1783
|
-
typeof next.amount === "number" &&
|
|
1784
|
-
Number.isFinite(next.amount) &&
|
|
1785
|
-
typeof next.currency === "string" &&
|
|
1786
|
-
next.currency.trim()
|
|
1787
|
-
) {
|
|
1788
|
-
const currency = next.currency.trim().toUpperCase();
|
|
1789
|
-
next.amount_minor = Number.isInteger(next.amount) ? next.amount : Math.round(next.amount);
|
|
1790
|
-
next.amount_major = Number((next.amount_minor / 100).toFixed(2));
|
|
1791
|
-
next.display_amount = `${currency} ${(next.amount_minor / 100).toFixed(2)}`;
|
|
1792
|
-
next.amount_unit = "minor";
|
|
1793
|
-
}
|
|
1794
|
-
return next;
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
function buyerDocsFor(value = {}) {
|
|
1798
|
-
const topics = new Set();
|
|
1799
|
-
const status = String(value.status || "").toLowerCase();
|
|
1800
|
-
const actions = Array.isArray(value.agent_next_actions) ? value.agent_next_actions : [];
|
|
1801
|
-
if (status.includes("catalog")) topics.add("catalog-search");
|
|
1802
|
-
if (status.includes("cart") || actions.includes("create_checkout_from_cart")) topics.add("cart-checkout");
|
|
1803
|
-
if (status.includes("checkout") || actions.includes("create_payment_intent")) topics.add("cart-checkout");
|
|
1804
|
-
if (status.includes("human_auth") || actions.includes("wait_human_auth") || value.human_action?.kind === "auth_qr" || value.checkout?.human_action?.kind === "auth_qr") {
|
|
1805
|
-
topics.add("cart-checkout");
|
|
1806
|
-
topics.add("payment-qr");
|
|
1807
|
-
}
|
|
1808
|
-
if (status.includes("waiting_user_payment") || actions.includes("wait_payment") || value.payment_intent?.human_action || value.payment_intent?.qr_image_url) {
|
|
1809
|
-
topics.add("payment-qr");
|
|
1810
|
-
topics.add("payment-wait");
|
|
1811
|
-
}
|
|
1812
|
-
if (status.includes("payment_verified") || value.payment_event?.event_type === "payment_intent.verified") topics.add("secure-delivery");
|
|
1813
|
-
if (status.includes("delivery") || value.delivery || actions.includes("stop_check_email")) {
|
|
1814
|
-
topics.add("secure-delivery");
|
|
1815
|
-
topics.add("human-claim-ui");
|
|
1816
|
-
topics.add("vault-agent-read");
|
|
1817
|
-
}
|
|
1818
|
-
if (status.includes("agent_read_grant") || value.agent_readable_grants || value.grant || actions.includes("read_agent_grant_view") || actions.includes("list_agent_read_grants")) {
|
|
1819
|
-
topics.add("vault-agent-read");
|
|
1820
|
-
}
|
|
1821
|
-
if (topics.size === 0) topics.add("quickstart");
|
|
1822
|
-
return Array.from(topics).map((topic) => buyerDocRef(topic));
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
function buyerDocRef(topic) {
|
|
1826
|
-
return {
|
|
1827
|
-
topic,
|
|
1828
|
-
command: cliCommand("docs", "show", topic, "--role", "buyer", "--json")
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
function buyerDeliveryListOutput(checkout) {
|
|
1833
|
-
const delivery = checkout.delivery || null;
|
|
1834
|
-
const checkoutID = checkout.checkout_id;
|
|
1835
|
-
return buyerRunOutput({
|
|
1836
|
-
status: delivery?.status || checkout.delivery_status,
|
|
1837
|
-
checkout_id: checkoutID,
|
|
1838
|
-
deliveries: delivery ? [{
|
|
1839
|
-
checkout_id: checkoutID,
|
|
1840
|
-
status: delivery.status,
|
|
1841
|
-
next_required_action: delivery.next_required_action,
|
|
1842
|
-
channels: delivery.channels || [],
|
|
1843
|
-
artifact_status: delivery.artifact_status,
|
|
1844
|
-
sensitive_content_redacted: delivery.sensitive_content_redacted !== false
|
|
1845
|
-
}] : [],
|
|
1846
|
-
agent_next_actions: deliveryAwareAgentNextActions(checkout),
|
|
1847
|
-
optional_agent_read_grant: optionalAgentReadGrantHint(checkoutID, checkout)
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
function deliveryAwareAgentNextActions(checkout = {}) {
|
|
1852
|
-
const actions = Array.isArray(checkout.agent_next_actions) ? [...checkout.agent_next_actions] : [];
|
|
1853
|
-
if (isBuyerDeliveryComplete(checkout) && !actions.includes("optionally_request_agent_read_grant")) {
|
|
1854
|
-
actions.push("optionally_request_agent_read_grant");
|
|
1855
|
-
}
|
|
1856
|
-
return actions;
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
function optionalAgentReadGrantHint(checkoutID, checkout = {}) {
|
|
1860
|
-
if (!checkoutID || !isBuyerDeliveryComplete(checkout)) return undefined;
|
|
1861
|
-
return {
|
|
1862
|
-
type: "optional_human_passkey_agent_read_grant",
|
|
1863
|
-
safe_for_agent: true,
|
|
1864
|
-
requires_human: true,
|
|
1865
|
-
instruction: "If the human wants you to analyze or use the delivered result, ask them to open the ItPay claim/account page, reveal with Passkey, choose 'Give to Agent / 一键给 Agent', select fields, and confirm. After they approve, do not ask for a grant id; run the probe command.",
|
|
1866
|
-
docs_command: cliCommand("docs", "show", "vault-agent-read", "--role", "buyer", "--json"),
|
|
1867
|
-
probe_command: cliCommand("buyer", "vault", "grants", "list", "--checkout", checkoutID, "--json"),
|
|
1868
|
-
read_pattern: cliCommand("buyer", "vault", "read", "--order", "<order_id>", "--artifact", "<vault_artifact_id>", "--json")
|
|
1869
|
-
};
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
async function buyerAuthStatusOutput(flags = {}) {
|
|
1873
|
-
const config = readConfig();
|
|
1874
|
-
const credentials = readCredentials();
|
|
1875
|
-
const token = readSessionToken(credentials);
|
|
1876
|
-
if (token && config.account_id) {
|
|
1877
|
-
try {
|
|
1878
|
-
const status = await coreApi("/v1/me/auth/status", { method: "GET" }, flags);
|
|
1879
|
-
return buyerRunOutput({
|
|
1880
|
-
status: "authenticated_buyer_session",
|
|
1881
|
-
authenticated: true,
|
|
1882
|
-
buyer_account_id: status.buyer_account_id || config.account_id,
|
|
1883
|
-
agent_device_id: config.device_id || null,
|
|
1884
|
-
account_status: status.account_status,
|
|
1885
|
-
sensitive_redacted: true,
|
|
1886
|
-
agent_next_actions: ["search_catalog", "view_orders", "list_agent_read_grants"]
|
|
1887
|
-
});
|
|
1888
|
-
} catch (error) {
|
|
1889
|
-
return buyerRunOutput({
|
|
1890
|
-
status: "buyer_session_invalid",
|
|
1891
|
-
authenticated: false,
|
|
1892
|
-
buyer_account_id: config.account_id || null,
|
|
1893
|
-
agent_device_id: config.device_id || null,
|
|
1894
|
-
error: safeErrorMessage(error),
|
|
1895
|
-
agent_next_actions: ["create_checkout_for_human_auth"]
|
|
1896
|
-
});
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
return buyerRunOutput({
|
|
1900
|
-
status: "public_purchase_mode",
|
|
1901
|
-
authenticated: false,
|
|
1902
|
-
auth_required_for_discovery: false,
|
|
1903
|
-
agent_next_actions: ["search_catalog", "create_checkout_for_human_auth"],
|
|
1904
|
-
note: "Catalog discovery is public. Checkout creates a human auth-to-payment QR and saves a buyer session after the human authorizes."
|
|
1905
|
-
});
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
async function listBuyerAgentReadGrants(flags = {}) {
|
|
1909
|
-
const params = new URLSearchParams();
|
|
1910
|
-
const mappings = [
|
|
1911
|
-
["checkout", "checkout_id"],
|
|
1912
|
-
["checkout_id", "checkout_id"],
|
|
1913
|
-
["order", "order_id"],
|
|
1914
|
-
["order_id", "order_id"],
|
|
1915
|
-
["artifact", "vault_artifact_id"],
|
|
1916
|
-
["vault_artifact", "vault_artifact_id"],
|
|
1917
|
-
["vault_artifact_id", "vault_artifact_id"],
|
|
1918
|
-
["line", "order_line_item_id"],
|
|
1919
|
-
["line_id", "order_line_item_id"],
|
|
1920
|
-
["order_line_item_id", "order_line_item_id"]
|
|
1921
|
-
];
|
|
1922
|
-
for (const [flagName, paramName] of mappings) {
|
|
1923
|
-
if (flags[flagName] !== undefined && flags[flagName] !== null && flags[flagName] !== false) {
|
|
1924
|
-
params.set(paramName, String(flags[flagName]));
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
const query = params.toString();
|
|
1928
|
-
return await coreApi(`/v1/me/agent-grants${query ? `?${query}` : ""}`, { method: "GET" }, flags);
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
async function readBuyerAgentReadGrant(grantID, flags = {}) {
|
|
1932
|
-
if (!grantID) throw new Error("agent_read_grant_id is required");
|
|
1933
|
-
return await coreApi(`/v1/me/agent-grants/${encodeURIComponent(grantID)}/view`, { method: "GET" }, flags);
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
async function readBuyerVaultArtifactGrant(flags = {}) {
|
|
1937
|
-
const grants = await listBuyerAgentReadGrants(flags);
|
|
1938
|
-
const list = Array.isArray(grants.agent_readable_grants) ? grants.agent_readable_grants : [];
|
|
1939
|
-
if (!list.length) {
|
|
1940
|
-
throw new Error("no active agent-readable grant found; ask the human to open the account portal and confirm one-key agent authorization with Passkey");
|
|
1941
|
-
}
|
|
1942
|
-
const grant = list[0];
|
|
1943
|
-
const grantID = grant.agent_read_grant_id || grant.agent_readGrantID;
|
|
1944
|
-
if (!grantID) throw new Error("active grant did not include agent_read_grant_id");
|
|
1945
|
-
const view = await readBuyerAgentReadGrant(grantID, flags);
|
|
1946
|
-
return {
|
|
1947
|
-
...view,
|
|
1948
|
-
discovered_grant: grant
|
|
1949
|
-
};
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
async function agentStatus(flags) {
|
|
1953
|
-
const runId = flags.run_id || readState().current_run_id;
|
|
1954
|
-
const run = readRun(runId);
|
|
1955
|
-
if (!run) {
|
|
1956
|
-
const config = readConfig();
|
|
1957
|
-
const credentials = readCredentials();
|
|
1958
|
-
const hasLocalSession = Boolean(readSessionToken(credentials));
|
|
1959
|
-
const auth = flags.refresh
|
|
1960
|
-
? await refreshBuyerSessionForStatus(flags)
|
|
1961
|
-
: {
|
|
1962
|
-
authenticated: hasLocalSession,
|
|
1963
|
-
session_verified: false,
|
|
1964
|
-
account_id: config.account_id || null,
|
|
1965
|
-
device_id: config.device_id || null,
|
|
1966
|
-
auth_source: hasLocalSession ? "local_files_unverified" : "local_files"
|
|
1967
|
-
};
|
|
1968
|
-
output({
|
|
1969
|
-
schema_version: "itp.agent.v1",
|
|
1970
|
-
status: auth.authenticated
|
|
1971
|
-
? (auth.session_verified === false ? "local_session_unverified" : "idle")
|
|
1972
|
-
: "unauthenticated",
|
|
1973
|
-
phase: auth.authenticated ? "idle" : "unauthenticated",
|
|
1974
|
-
authenticated: Boolean(auth.authenticated),
|
|
1975
|
-
session_verified: auth.session_verified !== false,
|
|
1976
|
-
auth_source: auth.auth_source,
|
|
1977
|
-
account_id: auth.account_id || null,
|
|
1978
|
-
device_id: auth.device_id || null,
|
|
1979
|
-
next: nextActionForAuthStatus(auth, flags),
|
|
1980
|
-
agent_next_actions: auth.authenticated && auth.session_verified !== false
|
|
1981
|
-
? ["search_catalog", "view_orders", "create_refund_if_needed", "list_agent_read_grants"]
|
|
1982
|
-
: ["run_status_refresh", "start_human_auth_if_unauthenticated"],
|
|
1983
|
-
note: auth.authenticated && auth.session_verified === false
|
|
1984
|
-
? "Local buyer session exists but the server has not verified it. Run the next.command before order/refund/account operations."
|
|
1985
|
-
: undefined,
|
|
1986
|
-
error: auth.error || undefined,
|
|
1987
|
-
secrets: { raw_key_included: false, session_token_included: false }
|
|
1988
|
-
});
|
|
1989
|
-
return;
|
|
1990
|
-
}
|
|
1991
|
-
const refreshed = flags.refresh ? await refreshRun(run, flags) : run;
|
|
1992
|
-
writeRun(refreshed);
|
|
1993
|
-
output(agentRunResponse(refreshed));
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
async function refreshBuyerSessionForStatus(flags = {}) {
|
|
1997
|
-
const config = readConfig();
|
|
1998
|
-
const credentials = readCredentials();
|
|
1999
|
-
if (!readSessionToken(credentials)) {
|
|
2000
|
-
return {
|
|
2001
|
-
authenticated: false,
|
|
2002
|
-
session_verified: true,
|
|
2003
|
-
account_id: config.account_id || null,
|
|
2004
|
-
device_id: config.device_id || null,
|
|
2005
|
-
auth_source: "core_no_session"
|
|
2006
|
-
};
|
|
2007
|
-
}
|
|
2008
|
-
try {
|
|
2009
|
-
const status = await coreApi("/v1/me/auth/status", { method: "GET" }, flags);
|
|
2010
|
-
return {
|
|
2011
|
-
authenticated: true,
|
|
2012
|
-
session_verified: true,
|
|
2013
|
-
account_id: status.buyer_account_id || config.account_id || null,
|
|
2014
|
-
device_id: config.device_id || null,
|
|
2015
|
-
account_status: status.account_status || null,
|
|
2016
|
-
auth_source: "core"
|
|
2017
|
-
};
|
|
2018
|
-
} catch (error) {
|
|
2019
|
-
return {
|
|
2020
|
-
authenticated: false,
|
|
2021
|
-
session_verified: true,
|
|
2022
|
-
account_id: config.account_id || null,
|
|
2023
|
-
device_id: config.device_id || null,
|
|
2024
|
-
auth_source: "core",
|
|
2025
|
-
error: safeErrorMessage(error)
|
|
2026
|
-
};
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
function nextActionForAuthStatus(auth = {}, flags = {}) {
|
|
2031
|
-
if (auth.authenticated && auth.session_verified === false) {
|
|
2032
|
-
return { type: "verify_buyer_session", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true };
|
|
2033
|
-
}
|
|
2034
|
-
if (auth.authenticated) {
|
|
2035
|
-
return { type: "buyer_ready", command: cliCommand("buyer", "auth", "status", "--json"), safe_for_agent: true };
|
|
2036
|
-
}
|
|
2037
|
-
return { type: "start_auth", command: cliCommand("setup", "--plan", "credit-300", "--method", "alipay", "--json"), safe_for_agent: true };
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
async function resume(flags) {
|
|
2041
|
-
const runId = flags.run_id || readState().current_run_id;
|
|
2042
|
-
const run = readRun(runId);
|
|
2043
|
-
if (!run) throw new Error("no active run found");
|
|
2044
|
-
if (["done", "installed"].includes(run.status) || run.phase === "done") {
|
|
2045
|
-
output(agentRunResponse(run));
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
await setup({
|
|
2049
|
-
...flags,
|
|
2050
|
-
run_id: run.run_id,
|
|
2051
|
-
resume: true,
|
|
2052
|
-
plan: run.plan_id || flags.plan,
|
|
2053
|
-
credits: run.plan_id ? flags.credits : run.credits || flags.credits,
|
|
2054
|
-
method: run.payment_method || flags.method,
|
|
2055
|
-
target: run.target || flags.target,
|
|
2056
|
-
install_runtime: run.install_runtime || flags.install_runtime
|
|
2057
|
-
});
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
async function runs(command, flags) {
|
|
2061
|
-
if (command === "current") {
|
|
2062
|
-
const run = readRun(flags.run_id || readState().current_run_id);
|
|
2063
|
-
output(run ? agentRunResponse(run) : { schema_version: "itp.agent.v1", status: "none", runs: [] });
|
|
2064
|
-
return;
|
|
2065
|
-
}
|
|
2066
|
-
if (command === "list") {
|
|
2067
|
-
output({ runs: listRuns().map(agentRunResponse) });
|
|
2068
|
-
return;
|
|
2069
|
-
}
|
|
2070
|
-
if (command === "show") {
|
|
2071
|
-
const run = readRun(flags.run_id || flags.id);
|
|
2072
|
-
if (!run) throw new Error("run not found");
|
|
2073
|
-
output(agentRunResponse(run));
|
|
2074
|
-
return;
|
|
2075
|
-
}
|
|
2076
|
-
if (command === "forget") {
|
|
2077
|
-
const runId = flags.run_id || flags.id;
|
|
2078
|
-
if (!runId || runId === "forget") throw new Error("run_id is required");
|
|
2079
|
-
const file = runPath(runId);
|
|
2080
|
-
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
2081
|
-
const state = readState();
|
|
2082
|
-
if (state.current_run_id === runId) {
|
|
2083
|
-
delete state.current_run_id;
|
|
2084
|
-
writeState(state);
|
|
2085
|
-
}
|
|
2086
|
-
output({ status: "forgotten", run_id: runId });
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
throw new Error(`unknown runs command: ${command || ""}`);
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
async function startDeviceAuth(flags) {
|
|
2093
|
-
const runtime = flags.runtime || "unknown";
|
|
2094
|
-
return await api("/api/itp/auth/device/start", {
|
|
2095
|
-
method: "POST",
|
|
2096
|
-
body: {
|
|
2097
|
-
device: {
|
|
2098
|
-
display_name: os.hostname(),
|
|
2099
|
-
runtime,
|
|
2100
|
-
os: os.platform(),
|
|
2101
|
-
arch: os.arch(),
|
|
2102
|
-
itp_version: VERSION
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
}, flags);
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
async function completeDeviceAuth(flags) {
|
|
2109
|
-
const start = await startDeviceAuth(flags);
|
|
2110
|
-
if (flags.no_wait) {
|
|
2111
|
-
writeState({ ...readState(), last_auth_id: start.auth_id });
|
|
2112
|
-
updateCurrentRun({
|
|
2113
|
-
phase: "waiting_human_auth",
|
|
2114
|
-
auth: { auth_id: start.auth_id, status: "pending", expires_at: start.expires_at },
|
|
2115
|
-
human_action: start.human_action || null,
|
|
2116
|
-
safe_summary: "Waiting for Alipay authentication scan."
|
|
2117
|
-
}, flags);
|
|
2118
|
-
await renderHumanAction(start.human_action, flags);
|
|
2119
|
-
return start;
|
|
2120
|
-
}
|
|
2121
|
-
await maybeMockApproveDeviceAuth(start.auth_id, flags);
|
|
2122
|
-
const response = await waitDeviceAuth(start.auth_id, flags, start);
|
|
2123
|
-
if (!response.auth) {
|
|
2124
|
-
throw new Error(`device auth ended without session: ${response.status || "unknown"}`);
|
|
2125
|
-
}
|
|
2126
|
-
writeSessionCredentials(response.auth);
|
|
2127
|
-
writeConfig({
|
|
2128
|
-
api_base: apiBase(flags),
|
|
2129
|
-
account_id: response.auth.account_id,
|
|
2130
|
-
device_id: response.auth.device_id,
|
|
2131
|
-
web_console_url: response.auth.web_console_url
|
|
2132
|
-
});
|
|
2133
|
-
writeState({ ...readState(), last_auth_id: start.auth_id });
|
|
2134
|
-
return { ...sanitizeAuthResponse(response.auth), auth_id: start.auth_id, status: response.status };
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
async function ensureAuthenticated(flags) {
|
|
2138
|
-
const config = readConfig();
|
|
2139
|
-
const credentials = readCredentials();
|
|
2140
|
-
if (readSessionToken(credentials)) {
|
|
2141
|
-
try {
|
|
2142
|
-
const status = await api("/api/itp/auth/status", { method: "GET" }, flags);
|
|
2143
|
-
if (status.authenticated !== false) {
|
|
2144
|
-
return {
|
|
2145
|
-
authenticated: true,
|
|
2146
|
-
account_id: status.account_id || config.account_id || null,
|
|
2147
|
-
device_id: status.device_id || config.device_id || null,
|
|
2148
|
-
newapi_user_id: status.newapi_user_id || null,
|
|
2149
|
-
session_reused: true
|
|
2150
|
-
};
|
|
2151
|
-
}
|
|
2152
|
-
} catch {
|
|
2153
|
-
deleteSessionCredential(credentials);
|
|
2154
|
-
writeCredentials(credentials);
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
const resumableRun = readRun(flags.run_id || readState().current_run_id);
|
|
2158
|
-
if ((flags.resume || flags.auth_id) && resumableRun?.auth?.auth_id && !resumableRun.account?.authenticated && !resumableRun.checkout?.checkout_id && !flags.no_wait && !flags.no_wait_auth) {
|
|
2159
|
-
await renderHumanAction(resumableRun.human_action, flags);
|
|
2160
|
-
await maybeMockApproveDeviceAuth(resumableRun.auth.auth_id, flags);
|
|
2161
|
-
const response = await waitDeviceAuth(resumableRun.auth.auth_id, flags);
|
|
2162
|
-
if (!response.auth) {
|
|
2163
|
-
return {
|
|
2164
|
-
authenticated: false,
|
|
2165
|
-
status: response.status || "waiting_human_auth",
|
|
2166
|
-
auth_id: resumableRun.auth.auth_id,
|
|
2167
|
-
expires_at: resumableRun.auth.expires_at,
|
|
2168
|
-
human_action: resumableRun.human_action
|
|
2169
|
-
};
|
|
2170
|
-
}
|
|
2171
|
-
writeSessionCredentials(response.auth);
|
|
2172
|
-
writeConfig({
|
|
2173
|
-
api_base: apiBase(flags),
|
|
2174
|
-
account_id: response.auth.account_id,
|
|
2175
|
-
device_id: response.auth.device_id,
|
|
2176
|
-
web_console_url: response.auth.web_console_url
|
|
2177
|
-
});
|
|
2178
|
-
writeState({ ...readState(), last_auth_id: resumableRun.auth.auth_id });
|
|
2179
|
-
return { ...sanitizeAuthResponse(response.auth), auth_id: resumableRun.auth.auth_id, authenticated: true, session_reused: false };
|
|
2180
|
-
}
|
|
2181
|
-
if (flags.no_wait || flags.no_wait_auth) {
|
|
2182
|
-
const start = await startDeviceAuth(flags);
|
|
2183
|
-
writeState({ ...readState(), last_auth_id: start.auth_id });
|
|
2184
|
-
updateCurrentRun({
|
|
2185
|
-
phase: "waiting_human_auth",
|
|
2186
|
-
auth: { auth_id: start.auth_id, status: "pending", expires_at: start.expires_at },
|
|
2187
|
-
human_action: start.human_action || null,
|
|
2188
|
-
safe_summary: "Waiting for Alipay authentication scan."
|
|
2189
|
-
}, flags);
|
|
2190
|
-
await renderHumanAction(start.human_action, flags);
|
|
2191
|
-
return {
|
|
2192
|
-
authenticated: false,
|
|
2193
|
-
status: "waiting_human_auth",
|
|
2194
|
-
action: "scan_alipay_auth",
|
|
2195
|
-
auth_id: start.auth_id,
|
|
2196
|
-
user_code: start.user_code,
|
|
2197
|
-
verification_uri: start.verification_uri,
|
|
2198
|
-
verification_uri_complete: start.verification_uri_complete,
|
|
2199
|
-
alipay_authorization_url: start.alipay_authorization_url,
|
|
2200
|
-
expires_at: start.expires_at,
|
|
2201
|
-
interval: start.interval,
|
|
2202
|
-
human_action: start.human_action
|
|
2203
|
-
};
|
|
2204
|
-
}
|
|
2205
|
-
const auth = await completeDeviceAuth(flags);
|
|
2206
|
-
return { ...auth, authenticated: true, session_reused: false };
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
async function maybeMockApproveDeviceAuth(authId, flags) {
|
|
2210
|
-
if (!(flags.mock_approve || process.env.ITPAY_MOCK_APPROVE === "true" || process.env.ITPAY_MOCK_APPROVE === "1")) {
|
|
2211
|
-
return;
|
|
2212
|
-
}
|
|
2213
|
-
if (!fakeTestingAllowed(flags)) {
|
|
2214
|
-
throw new Error("mock approval is developer-only and disabled for agent runs; use real Alipay sandbox authentication");
|
|
2215
|
-
}
|
|
2216
|
-
const alipayUserId = flags.alipay_user_id || process.env.ITPAY_MOCK_ALIPAY_USER_ID || `2088${crypto.randomInt(100000000000, 999999999999)}`;
|
|
2217
|
-
await api(`/api/itp/auth/device/${encodeURIComponent(authId)}/mock-approve`, {
|
|
2218
|
-
method: "POST",
|
|
2219
|
-
body: { alipay_user_id: alipayUserId }
|
|
2220
|
-
}, flags);
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
async function authDevice(command, flags) {
|
|
2224
|
-
if (command === "start") {
|
|
2225
|
-
const response = await startDeviceAuth(flags);
|
|
2226
|
-
writeState({ ...readState(), last_auth_id: response.auth_id });
|
|
2227
|
-
await renderHumanAction(response.human_action, flags);
|
|
2228
|
-
output(response);
|
|
2229
|
-
return;
|
|
2230
|
-
}
|
|
2231
|
-
if (command === "poll") {
|
|
2232
|
-
const authId = flags.auth_id || flags.device_auth_id || readState().last_auth_id;
|
|
2233
|
-
if (!authId) throw new Error("auth_id is required");
|
|
2234
|
-
const response = await waitDeviceAuth(authId, flags);
|
|
2235
|
-
if (response.auth) {
|
|
2236
|
-
writeSessionCredentials(response.auth);
|
|
2237
|
-
writeConfig({
|
|
2238
|
-
api_base: apiBase(flags),
|
|
2239
|
-
account_id: response.auth.account_id,
|
|
2240
|
-
device_id: response.auth.device_id,
|
|
2241
|
-
web_console_url: response.auth.web_console_url
|
|
2242
|
-
});
|
|
2243
|
-
}
|
|
2244
|
-
output(response.auth ? { ...sanitizeAuthResponse(response.auth), auth_id: authId, status: response.status } : response);
|
|
2245
|
-
return;
|
|
2246
|
-
}
|
|
2247
|
-
throw new Error(`unknown auth device command: ${command || ""}`);
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
async function waitDeviceAuth(authId, flags, start = null) {
|
|
2251
|
-
const started = Date.now();
|
|
2252
|
-
const timeoutMs = Number(flags.timeout || 600) * 1000;
|
|
2253
|
-
let intervalMs = 2000;
|
|
2254
|
-
let lastHeartbeatAt = 0;
|
|
2255
|
-
let lastStatus = "authorization_pending";
|
|
2256
|
-
const action = start?.human_action || null;
|
|
2257
|
-
if (start) {
|
|
2258
|
-
updateCurrentRun({
|
|
2259
|
-
phase: "waiting_human_auth",
|
|
2260
|
-
auth: { auth_id: start.auth_id, status: "pending", expires_at: start.expires_at },
|
|
2261
|
-
human_action: start.human_action || null,
|
|
2262
|
-
safe_summary: "Waiting for Alipay authentication scan."
|
|
2263
|
-
}, flags);
|
|
2264
|
-
await renderHumanAction(start.human_action, flags);
|
|
2265
|
-
if (!start.human_action) process.stderr.write(`Open Alipay auth URL: ${start.verification_uri_complete || start.alipay_authorization_url}\n`);
|
|
2266
|
-
if (start.user_code) process.stderr.write(`Alipay auth code: ${start.user_code}\n`);
|
|
2267
|
-
}
|
|
2268
|
-
while (Date.now() - started < timeoutMs) {
|
|
2269
|
-
const response = await api(`/api/itp/auth/device/${encodeURIComponent(authId)}/poll`, { method: "POST" }, flags);
|
|
2270
|
-
lastStatus = response.status || lastStatus;
|
|
2271
|
-
if (response.auth?.session_token) {
|
|
2272
|
-
return response;
|
|
2273
|
-
}
|
|
2274
|
-
if (response.status === "authorization_pending") {
|
|
2275
|
-
intervalMs = Number(response.interval || 2) * 1000;
|
|
2276
|
-
lastHeartbeatAt = writeWaitHeartbeat({
|
|
2277
|
-
kind: "Alipay authentication",
|
|
2278
|
-
idName: "auth_id",
|
|
2279
|
-
idValue: authId,
|
|
2280
|
-
status: response.status,
|
|
2281
|
-
action,
|
|
2282
|
-
lastHeartbeatAt,
|
|
2283
|
-
flags,
|
|
2284
|
-
command: cliCommand("auth", "device", "poll", authId, "--timeout", String(Math.ceil((timeoutMs - (Date.now() - started)) / 1000)), "--json")
|
|
2285
|
-
});
|
|
2286
|
-
await sleep(intervalMs);
|
|
2287
|
-
continue;
|
|
2288
|
-
}
|
|
2289
|
-
if (response.status === "approved") {
|
|
2290
|
-
return response;
|
|
2291
|
-
}
|
|
2292
|
-
if (response.status === "expired" || response.status === "consumed" || response.error) {
|
|
2293
|
-
throw new Error(response.error || `device auth ended with status: ${response.status}`);
|
|
2294
|
-
}
|
|
2295
|
-
await sleep(intervalMs);
|
|
2296
|
-
}
|
|
2297
|
-
throw new Error(`device auth timed out at status ${lastStatus}; run \`itp auth device poll ${authId} --timeout 600\``);
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
async function authLogin(flags) {
|
|
2301
|
-
const runtime = flags.runtime || "unknown";
|
|
2302
|
-
if (flags.password && !flags.password_stdin) {
|
|
2303
|
-
throw new Error("use --password-stdin to avoid leaking passwords into shell history");
|
|
2304
|
-
}
|
|
2305
|
-
const password = flags.password_stdin
|
|
2306
|
-
? fs.readFileSync(0, "utf8").trim()
|
|
2307
|
-
: undefined;
|
|
2308
|
-
if (flags.password_stdin && !password) {
|
|
2309
|
-
throw new Error("password is required on stdin");
|
|
2310
|
-
}
|
|
2311
|
-
const response = await api("/api/itp/auth/login", {
|
|
2312
|
-
method: "POST",
|
|
2313
|
-
body: {
|
|
2314
|
-
username: flags.username || undefined,
|
|
2315
|
-
password,
|
|
2316
|
-
access_token: flags.access_token || undefined,
|
|
2317
|
-
device: {
|
|
2318
|
-
display_name: os.hostname(),
|
|
2319
|
-
runtime,
|
|
2320
|
-
os: os.platform(),
|
|
2321
|
-
arch: os.arch(),
|
|
2322
|
-
itp_version: VERSION
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
}, flags);
|
|
2326
|
-
writeSessionCredentials(response);
|
|
2327
|
-
writeConfig({
|
|
2328
|
-
api_base: apiBase(flags),
|
|
2329
|
-
account_id: response.account_id,
|
|
2330
|
-
device_id: response.device_id,
|
|
2331
|
-
web_console_url: response.web_console_url
|
|
2332
|
-
});
|
|
2333
|
-
output(sanitizeAuthResponse(response));
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
async function authStatus(flags) {
|
|
2337
|
-
const config = readConfig();
|
|
2338
|
-
const credentials = readCredentials();
|
|
2339
|
-
if (!readSessionToken(credentials)) {
|
|
2340
|
-
output({ authenticated: false, account_id: config.account_id || null });
|
|
2341
|
-
return;
|
|
2342
|
-
}
|
|
2343
|
-
try {
|
|
2344
|
-
const status = await api("/api/itp/auth/status", { method: "GET" }, flags);
|
|
2345
|
-
output(status);
|
|
2346
|
-
} catch (error) {
|
|
2347
|
-
output({
|
|
2348
|
-
authenticated: false,
|
|
2349
|
-
account_id: config.account_id || null,
|
|
2350
|
-
error: error.message
|
|
2351
|
-
});
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
async function accountShow(flags) {
|
|
2356
|
-
output(await api("/api/itp/account", { method: "GET" }, flags));
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
async function accountLoginLink(flags) {
|
|
2360
|
-
const config = readConfig();
|
|
2361
|
-
const accountID = flags.account || flags.account_id || flags.buyer_account || flags.buyer_account_id || config.account_id;
|
|
2362
|
-
if (!accountID) {
|
|
2363
|
-
throw new Error("buyer account id is required; complete a buyer purchase first or pass --account-id");
|
|
2364
|
-
}
|
|
2365
|
-
if (!readSessionToken()) {
|
|
2366
|
-
throw new Error("buyer account session is required; complete first-purchase auth or run buyer checkout resume first");
|
|
2367
|
-
}
|
|
2368
|
-
const link = await coreApi("/v1/me/portal-login-links", { method: "POST" }, flags);
|
|
2369
|
-
output(buyerRunOutput({
|
|
2370
|
-
status: "account_portal_login_link_created",
|
|
2371
|
-
account_id: accountID,
|
|
2372
|
-
portal_login_link: link,
|
|
2373
|
-
login_url: link.login_url,
|
|
2374
|
-
expires_at: link.expires_at,
|
|
2375
|
-
agent_next_actions: link.agent_next_actions || ["show_login_link_to_human_only"],
|
|
2376
|
-
next: {
|
|
2377
|
-
type: "human_open_account_portal_link",
|
|
2378
|
-
safe_for_agent: false,
|
|
2379
|
-
requires_human: true,
|
|
2380
|
-
agent_must_not_open: true,
|
|
2381
|
-
instruction: "Give this one-time ItPay account portal link to the human buyer. Do not open it yourself; the human portal is redacted and protected content stays locked until human reveal."
|
|
2382
|
-
}
|
|
2383
|
-
}));
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
async function accountSetPassword(flags) {
|
|
2387
|
-
if (!flags.password_stdin) {
|
|
2388
|
-
throw new Error("use --password-stdin to avoid leaking passwords into shell history");
|
|
2389
|
-
}
|
|
2390
|
-
const password = fs.readFileSync(0, "utf8").trim();
|
|
2391
|
-
if (!password) throw new Error("password is required on stdin");
|
|
2392
|
-
output(await api("/api/itp/account/password", {
|
|
2393
|
-
method: "POST",
|
|
2394
|
-
body: { password }
|
|
2395
|
-
}, flags));
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
async function plansList(flags) {
|
|
2399
|
-
output(await api("/api/itp/plans", { method: "GET" }, flags));
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
async function plansShow(plan, flags) {
|
|
2403
|
-
if (!plan) throw new Error("plan id is required");
|
|
2404
|
-
output(await api(`/api/itp/plans/${encodeURIComponent(plan)}`, { method: "GET" }, flags));
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
async function checkoutCreate(flags) {
|
|
2408
|
-
const response = await createCheckoutResult(flags);
|
|
2409
|
-
await renderHumanAction(response.human_action, flags);
|
|
2410
|
-
output(response);
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
async function createCheckoutResult(flags) {
|
|
2414
|
-
const purchase = normalizePurchaseFlags(flags, true);
|
|
2415
|
-
const method = flags.method || "alipay";
|
|
2416
|
-
validateLivePaymentFlags(method, flags);
|
|
2417
|
-
const currentRun = readRun(flags.run_id || readState().current_run_id);
|
|
2418
|
-
const idempotencyKey = flags.idempotency_key || currentRun?.idempotency_key || cryptoRandom();
|
|
2419
|
-
const body = {
|
|
2420
|
-
payment_method: method,
|
|
2421
|
-
idempotency_key: idempotencyKey
|
|
2422
|
-
};
|
|
2423
|
-
if (purchase.plan) body.plan_id = purchase.plan;
|
|
2424
|
-
if (purchase.credits) body.credits = purchase.credits;
|
|
2425
|
-
const response = await api("/api/itp/checkout", {
|
|
2426
|
-
method: "POST",
|
|
2427
|
-
body
|
|
2428
|
-
}, flags);
|
|
2429
|
-
writeState({ ...readState(), last_checkout_id: response.checkout_id, last_grant_id: response.grant_id || null });
|
|
2430
|
-
updateCurrentRun({
|
|
2431
|
-
phase: response.grant_id ? "grant_ready" : "waiting_human_payment",
|
|
2432
|
-
plan_id: response.plan_id || purchase.plan || null,
|
|
2433
|
-
credits: response.credits || purchase.credits || null,
|
|
2434
|
-
purchase_kind: response.purchase?.kind || purchase.kind,
|
|
2435
|
-
checkout: {
|
|
2436
|
-
checkout_id: response.checkout_id,
|
|
2437
|
-
order_id: response.order_id,
|
|
2438
|
-
status: response.status,
|
|
2439
|
-
expires_at: response.expires_at,
|
|
2440
|
-
purchase: response.purchase || null
|
|
2441
|
-
},
|
|
2442
|
-
payment: {
|
|
2443
|
-
provider: method,
|
|
2444
|
-
status: response.status
|
|
2445
|
-
},
|
|
2446
|
-
grant: {
|
|
2447
|
-
...(currentRun?.grant || {}),
|
|
2448
|
-
grant_id: response.grant_id || currentRun?.grant?.grant_id || null
|
|
2449
|
-
},
|
|
2450
|
-
human_action: response.human_action || null,
|
|
2451
|
-
safe_summary: response.grant_id ? "Payment verified and grant is ready." : "Waiting for Alipay payment scan."
|
|
2452
|
-
}, flags);
|
|
2453
|
-
return response;
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
function validateLivePaymentFlags(method, flags = {}) {
|
|
2457
|
-
if (String(method).toLowerCase() === "fake" && !fakeTestingAllowed(flags)) {
|
|
2458
|
-
throw new Error("fake payment is developer-only and disabled for agent runs; use --method alipay for local, sandbox, and live testing");
|
|
2459
|
-
}
|
|
2460
|
-
if ((flags.mock_approve || process.env.ITPAY_MOCK_APPROVE === "true" || process.env.ITPAY_MOCK_APPROVE === "1") && !fakeTestingAllowed(flags)) {
|
|
2461
|
-
throw new Error("mock approval is developer-only and disabled for agent runs; use real Alipay sandbox authentication");
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
function fakeTestingAllowed(flags = {}) {
|
|
2466
|
-
return flags.allow_fake || process.env.ITP_ALLOW_FAKE_PAYMENT === "true" || process.env.ITP_ALLOW_FAKE_PAYMENT === "1";
|
|
2467
|
-
}
|
|
2468
|
-
|
|
2469
|
-
async function paymentWait(checkoutId, flags) {
|
|
2470
|
-
output(await paymentWaitResult(checkoutId, flags));
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
async function paymentWaitResult(checkoutId, flags) {
|
|
2474
|
-
if (!checkoutId) throw new Error("checkout_id is required");
|
|
2475
|
-
const started = Date.now();
|
|
2476
|
-
const timeoutMs = Number(flags.timeout || 120) * 1000;
|
|
2477
|
-
let lastRecoverAt = 0;
|
|
2478
|
-
let lastHeartbeatAt = 0;
|
|
2479
|
-
let lastResponse = null;
|
|
2480
|
-
while (Date.now() - started < timeoutMs) {
|
|
2481
|
-
let response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}`, { method: "GET" }, flags);
|
|
2482
|
-
lastResponse = response;
|
|
2483
|
-
if (isTerminalCheckoutFailure(response.status)) {
|
|
2484
|
-
throw new Error(`checkout ended with status: ${response.status}`);
|
|
2485
|
-
}
|
|
2486
|
-
if (isSuccessfulCheckout(response)) {
|
|
2487
|
-
writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
|
|
2488
|
-
updateCurrentRun({
|
|
2489
|
-
phase: "grant_ready",
|
|
2490
|
-
checkout: { checkout_id: checkoutId, order_id: response.order_id, status: response.status, expires_at: response.expires_at },
|
|
2491
|
-
grant: { grant_id: response.grant_id, installed: false },
|
|
2492
|
-
human_action: null,
|
|
2493
|
-
safe_summary: "Payment verified and grant is ready."
|
|
2494
|
-
}, flags);
|
|
2495
|
-
return { status: "grant_issued", checkout_id: checkoutId, order_id: response.order_id, grant_id: response.grant_id };
|
|
2496
|
-
}
|
|
2497
|
-
if (shouldRecoverCheckout(response.status) && Date.now() - lastRecoverAt > 5000) {
|
|
2498
|
-
lastRecoverAt = Date.now();
|
|
2499
|
-
response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/recover`, { method: "POST" }, flags);
|
|
2500
|
-
lastResponse = response;
|
|
2501
|
-
if (isTerminalCheckoutFailure(response.status)) {
|
|
2502
|
-
throw new Error(`checkout ended with status: ${response.status}`);
|
|
2503
|
-
}
|
|
2504
|
-
if (isSuccessfulCheckout(response)) {
|
|
2505
|
-
writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
|
|
2506
|
-
updateCurrentRun({
|
|
2507
|
-
phase: "grant_ready",
|
|
2508
|
-
checkout: { checkout_id: checkoutId, order_id: response.order_id, status: response.status, expires_at: response.expires_at },
|
|
2509
|
-
grant: { grant_id: response.grant_id, installed: false },
|
|
2510
|
-
human_action: null,
|
|
2511
|
-
safe_summary: "Payment verified and grant is ready."
|
|
2512
|
-
}, flags);
|
|
2513
|
-
return { status: "grant_issued", checkout_id: checkoutId, order_id: response.order_id, grant_id: response.grant_id, recovered: true };
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
lastHeartbeatAt = writeWaitHeartbeat({
|
|
2517
|
-
kind: "Alipay payment",
|
|
2518
|
-
idName: "checkout_id",
|
|
2519
|
-
idValue: checkoutId,
|
|
2520
|
-
status: response.status || "waiting",
|
|
2521
|
-
action: response.human_action || null,
|
|
2522
|
-
lastHeartbeatAt,
|
|
2523
|
-
flags,
|
|
2524
|
-
command: cliCommand("payment", "wait", checkoutId, "--timeout", String(Math.ceil((timeoutMs - (Date.now() - started)) / 1000)), "--json")
|
|
2525
|
-
});
|
|
2526
|
-
await sleep(2000);
|
|
2527
|
-
}
|
|
2528
|
-
try {
|
|
2529
|
-
const response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/recover`, { method: "POST" }, flags);
|
|
2530
|
-
lastResponse = response;
|
|
2531
|
-
if (isTerminalCheckoutFailure(response.status)) {
|
|
2532
|
-
throw new Error(`checkout ended with status: ${response.status}`);
|
|
2533
|
-
}
|
|
2534
|
-
if (isSuccessfulCheckout(response)) {
|
|
2535
|
-
writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
|
|
2536
|
-
updateCurrentRun({
|
|
2537
|
-
phase: "grant_ready",
|
|
2538
|
-
checkout: { checkout_id: checkoutId, order_id: response.order_id, status: response.status, expires_at: response.expires_at },
|
|
2539
|
-
grant: { grant_id: response.grant_id, installed: false },
|
|
2540
|
-
human_action: null,
|
|
2541
|
-
safe_summary: "Payment verified and grant is ready."
|
|
2542
|
-
}, flags);
|
|
2543
|
-
return { status: "grant_issued", checkout_id: checkoutId, order_id: response.order_id, grant_id: response.grant_id, recovered: true };
|
|
2544
|
-
}
|
|
2545
|
-
} catch (error) {
|
|
2546
|
-
if (isTerminalCheckoutFailure(lastResponse?.status)) {
|
|
2547
|
-
throw error;
|
|
2548
|
-
}
|
|
2549
|
-
// Preserve the timeout message below; recover is a best-effort final attempt.
|
|
2550
|
-
}
|
|
2551
|
-
if (lastResponse?.status) {
|
|
2552
|
-
throw new Error(`payment wait timed out at checkout status ${lastResponse.status}; run \`itp checkout recover ${checkoutId}\` later`);
|
|
2553
|
-
}
|
|
2554
|
-
throw new Error("payment wait timed out; run `itp checkout recover` later");
|
|
2555
|
-
}
|
|
2556
|
-
|
|
2557
|
-
async function checkoutRecover(checkoutId, flags) {
|
|
2558
|
-
if (!checkoutId) throw new Error("checkout_id is required");
|
|
2559
|
-
const response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/recover`, { method: "POST" }, flags);
|
|
2560
|
-
if (response.grant_id) {
|
|
2561
|
-
writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
|
|
2562
|
-
}
|
|
2563
|
-
output(response);
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
async function checkoutOpen(flags) {
|
|
2567
|
-
const state = readState();
|
|
2568
|
-
if (!state.last_checkout_id) throw new Error("no checkout found in local state");
|
|
2569
|
-
const response = await api(`/api/itp/checkout/${encodeURIComponent(state.last_checkout_id)}`, { method: "GET" }, flags);
|
|
2570
|
-
output(response.payment || response);
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
async function checkoutQR(checkoutId, flags) {
|
|
2574
|
-
if (!checkoutId) throw new Error("checkout_id is required");
|
|
2575
|
-
const response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/qr`, { method: "GET" }, flags);
|
|
2576
|
-
await renderHumanAction(response.human_action, flags);
|
|
2577
|
-
output(response);
|
|
2578
|
-
}
|
|
2579
|
-
|
|
2580
|
-
async function checkoutList(flags) {
|
|
2581
|
-
const params = new URLSearchParams();
|
|
2582
|
-
if (flags.limit) params.set("limit", flags.limit);
|
|
2583
|
-
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
2584
|
-
output(await api(`/api/itp/orders${suffix}`, { method: "GET" }, flags));
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
async function balance(flags) {
|
|
2588
|
-
output(await api("/api/itp/balance", { method: "GET" }, flags));
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
async function usage(flags) {
|
|
2592
|
-
const params = new URLSearchParams();
|
|
2593
|
-
if (flags.model) params.set("model", flags.model);
|
|
2594
|
-
if (flags.grant) params.set("grant_id", flags.grant);
|
|
2595
|
-
if (flags.grant_id) params.set("grant_id", flags.grant_id);
|
|
2596
|
-
if (flags.from) params.set("from", flags.from);
|
|
2597
|
-
if (flags.to) params.set("to", flags.to);
|
|
2598
|
-
if (flags.today) {
|
|
2599
|
-
const start = new Date();
|
|
2600
|
-
start.setHours(0, 0, 0, 0);
|
|
2601
|
-
params.set("from", Math.floor(start.getTime() / 1000).toString());
|
|
2602
|
-
}
|
|
2603
|
-
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
2604
|
-
output(await api(`/api/itp/usage${suffix}`, { method: "GET" }, flags));
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
async function grantsList(flags) {
|
|
2608
|
-
output(await api("/api/itp/grants", { method: "GET" }, flags));
|
|
2609
|
-
}
|
|
2610
|
-
|
|
2611
|
-
async function grantsShow(grantId, flags) {
|
|
2612
|
-
if (!grantId) throw new Error("grant_id is required");
|
|
2613
|
-
output(await api(`/api/itp/grants/${encodeURIComponent(grantId)}`, { method: "GET" }, flags));
|
|
2614
|
-
}
|
|
2615
|
-
|
|
2616
|
-
async function grantsInstall(grantId, flags) {
|
|
2617
|
-
output(await grantsInstallResult(grantId, flags));
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
async function grantsInstallResult(grantId, flags) {
|
|
2621
|
-
grantId = grantId || readState().last_grant_id;
|
|
2622
|
-
if (!grantId) throw new Error("grant_id is required");
|
|
2623
|
-
const target = flags.target || "generic";
|
|
2624
|
-
const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/install`, {
|
|
2625
|
-
method: "POST",
|
|
2626
|
-
body: { target }
|
|
2627
|
-
}, flags);
|
|
2628
|
-
const storedCredential = storeGrantCredential(grantId, {
|
|
2629
|
-
key: response.credential.key_once,
|
|
2630
|
-
target,
|
|
2631
|
-
base_url: response.base_url,
|
|
2632
|
-
openai_base_url: response.openai_base_url,
|
|
2633
|
-
anthropic_base_url: response.anthropic_base_url,
|
|
2634
|
-
gemini_base_url: response.gemini_base_url,
|
|
2635
|
-
models: response.models || [],
|
|
2636
|
-
install_profiles: response.install_profiles || []
|
|
2637
|
-
});
|
|
2638
|
-
const grantsDir = path.join(CONFIG_DIR, "grants");
|
|
2639
|
-
ensureConfigDir();
|
|
2640
|
-
fs.mkdirSync(grantsDir, { recursive: true });
|
|
2641
|
-
try {
|
|
2642
|
-
fs.chmodSync(grantsDir, 0o700);
|
|
2643
|
-
} catch {
|
|
2644
|
-
// Best effort; grant metadata does not contain gateway keys.
|
|
2645
|
-
}
|
|
2646
|
-
const metadataPath = path.join(grantsDir, `${grantId}.json`);
|
|
2647
|
-
fs.writeFileSync(metadataPath, JSON.stringify({
|
|
2648
|
-
grant_id: grantId,
|
|
2649
|
-
target,
|
|
2650
|
-
base_url: response.base_url,
|
|
2651
|
-
models: response.models,
|
|
2652
|
-
install_profiles: response.install_profiles
|
|
2653
|
-
}, null, 2), { mode: 0o600 });
|
|
2654
|
-
fs.chmodSync(metadataPath, 0o600);
|
|
2655
|
-
writeState({ ...readState(), last_grant_id: grantId });
|
|
2656
|
-
updateCurrentRun({
|
|
2657
|
-
phase: "grant_ready",
|
|
2658
|
-
grant: {
|
|
2659
|
-
grant_id: grantId,
|
|
2660
|
-
installed: true,
|
|
2661
|
-
credential_store: storedCredential.credential_store || null
|
|
2662
|
-
},
|
|
2663
|
-
result: {
|
|
2664
|
-
base_url: response.base_url,
|
|
2665
|
-
openai_base_url: response.openai_base_url,
|
|
2666
|
-
anthropic_base_url: response.anthropic_base_url,
|
|
2667
|
-
gemini_base_url: response.gemini_base_url
|
|
2668
|
-
},
|
|
2669
|
-
safe_summary: "Grant credential stored."
|
|
2670
|
-
}, flags);
|
|
2671
|
-
return {
|
|
2672
|
-
...response,
|
|
2673
|
-
credential: {
|
|
2674
|
-
type: response.credential.type,
|
|
2675
|
-
stored: true,
|
|
2676
|
-
credential_store: storedCredential.credential_store,
|
|
2677
|
-
warning: storedCredential.credential_warning || undefined
|
|
2678
|
-
}
|
|
2679
|
-
};
|
|
2680
|
-
}
|
|
2681
|
-
|
|
2682
|
-
async function grantsRevoke(grantId, flags) {
|
|
2683
|
-
grantId = grantId || flags.grant || readState().last_grant_id;
|
|
2684
|
-
if (!grantId) throw new Error("grant_id is required");
|
|
2685
|
-
const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/revoke`, { method: "POST" }, flags);
|
|
2686
|
-
deleteGrantCredential(grantId);
|
|
2687
|
-
const state = readState();
|
|
2688
|
-
if (state.last_grant_id === grantId) {
|
|
2689
|
-
delete state.last_grant_id;
|
|
2690
|
-
writeState(state);
|
|
2691
|
-
}
|
|
2692
|
-
output(response);
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
async function installRuntime(target, flags) {
|
|
2696
|
-
output(await installRuntimeResult(target, flags));
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
async function installRuntimeResult(target, flags) {
|
|
2700
|
-
const grantId = flags.grant || readState().last_grant_id;
|
|
2701
|
-
if (!grantId) throw new Error("grant id is required");
|
|
2702
|
-
const credentials = readGrantCredential(grantId);
|
|
2703
|
-
if (!credentials) throw new Error(`grant ${grantId} is not installed; run grants install first`);
|
|
2704
|
-
if (!credentials.key) {
|
|
2705
|
-
throw new Error(`grant ${grantId} credential key is unavailable; run keys rotate or grants install again`);
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
|
-
const dryRun = Boolean(flags.dry_run);
|
|
2709
|
-
const result = installTargetConfig(target, grantId, credentials, dryRun);
|
|
2710
|
-
const shouldTest = !dryRun && !flags.offline && !flags.no_test;
|
|
2711
|
-
const modelCheck = shouldTest
|
|
2712
|
-
? await doctorModelCheck(target, credentials)
|
|
2713
|
-
: { attempted: false, skipped: true, reason: dryRun ? "dry_run" : flags.offline ? "offline" : "no_test" };
|
|
2714
|
-
result.model_check = modelCheck;
|
|
2715
|
-
result.tested = Boolean(modelCheck.ok);
|
|
2716
|
-
if (modelCheck.attempted && !modelCheck.ok) {
|
|
2717
|
-
result.warnings.push(`Model endpoint check failed: ${modelCheck.error || modelCheck.status || "unknown error"}`);
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
if (!dryRun && !flags.offline) {
|
|
2721
|
-
await api(`/api/itp/grants/${encodeURIComponent(grantId)}/install-ack`, {
|
|
2722
|
-
method: "POST",
|
|
2723
|
-
body: {
|
|
2724
|
-
target,
|
|
2725
|
-
status: "installed",
|
|
2726
|
-
tested: result.tested,
|
|
2727
|
-
config_path: result.files[0]?.path || "",
|
|
2728
|
-
last_error: result.warnings.join("; ")
|
|
2729
|
-
}
|
|
2730
|
-
}, flags);
|
|
2731
|
-
}
|
|
2732
|
-
return result;
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
function shouldRecoverCheckout(status) {
|
|
2736
|
-
return ["paid_verified", "granting", "grant_failed"].includes(status);
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
function isSuccessfulCheckout(response) {
|
|
2740
|
-
return Boolean(response?.grant_id) && ["grant_issued", "grant_installed"].includes(response.status);
|
|
2741
|
-
}
|
|
2742
|
-
|
|
2743
|
-
function isTerminalCheckoutFailure(status) {
|
|
2744
|
-
return ["expired", "payment_failed", "verify_failed", "amount_mismatch", "revoked"].includes(status);
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
function installTargetConfig(target, grantId, credentials, dryRun) {
|
|
2748
|
-
if (target === "codex") return installCodex(grantId, credentials, dryRun);
|
|
2749
|
-
if (target === "claude-code") return installClaudeCode(grantId, credentials, dryRun);
|
|
2750
|
-
if (target === "openclaw") return installOpenClaw(grantId, credentials, dryRun);
|
|
2751
|
-
throw new Error(`unsupported install target: ${target}`);
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
function installClaudeCode(grantId, credentials, dryRun) {
|
|
2755
|
-
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
2756
|
-
const current = readJSON(settingsPath, {});
|
|
2757
|
-
current.env = {
|
|
2758
|
-
...(current.env || {}),
|
|
2759
|
-
ANTHROPIC_BASE_URL: credentials.anthropic_base_url,
|
|
2760
|
-
ANTHROPIC_AUTH_TOKEN_HELPER: `${quoteShell(currentExecutable())} token issue --grant ${quoteShell(grantId)} --stdout`
|
|
2761
|
-
};
|
|
2762
|
-
delete current.env.ANTHROPIC_API_KEY;
|
|
2763
|
-
const write = writeJSONWithBackup(settingsPath, current, dryRun);
|
|
2764
|
-
return {
|
|
2765
|
-
target: "claude-code",
|
|
2766
|
-
grant_id: grantId,
|
|
2767
|
-
status: dryRun ? "dry_run" : "installed",
|
|
2768
|
-
files: [{ path: settingsPath, action: write.action, backup_path: write.backup_path || null }],
|
|
2769
|
-
warnings: [
|
|
2770
|
-
"Claude Code will call itp as an auth token helper; no gateway key was written to settings.json."
|
|
2771
|
-
]
|
|
2772
|
-
};
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
|
-
function installCodex(grantId, credentials, dryRun) {
|
|
2776
|
-
const configPath = path.join(os.homedir(), ".codex", "config.toml");
|
|
2777
|
-
const envPath = path.join(CONFIG_DIR, "itpay.env");
|
|
2778
|
-
const existing = readText(configPath, "");
|
|
2779
|
-
const block = [
|
|
2780
|
-
'model_provider = "itpay"',
|
|
2781
|
-
'model = "openai-code-default"',
|
|
2782
|
-
"",
|
|
2783
|
-
"[model_providers.itpay]",
|
|
2784
|
-
'name = "ItPay"',
|
|
2785
|
-
`base_url = "${escapeTomlString(credentials.openai_base_url)}"`,
|
|
2786
|
-
'env_key = "ITPAY_API_KEY"'
|
|
2787
|
-
].join("\n");
|
|
2788
|
-
const nextConfig = replaceManagedBlock(existing, "itpay", block);
|
|
2789
|
-
const configWrite = writeTextWithBackup(configPath, nextConfig, 0o600, dryRun);
|
|
2790
|
-
const envWrite = writeTextWithBackup(envPath, `export ITPAY_API_KEY=${quoteShell(credentials.key)}\n`, 0o600, dryRun);
|
|
2791
|
-
return {
|
|
2792
|
-
target: "codex",
|
|
2793
|
-
grant_id: grantId,
|
|
2794
|
-
status: dryRun ? "dry_run" : "installed",
|
|
2795
|
-
files: [
|
|
2796
|
-
{ path: configPath, action: configWrite.action, backup_path: configWrite.backup_path || null },
|
|
2797
|
-
{ path: envPath, action: envWrite.action, backup_path: envWrite.backup_path || null }
|
|
2798
|
-
],
|
|
2799
|
-
warnings: [
|
|
2800
|
-
"Codex reads ITPAY_API_KEY from its process environment; source ~/.itp/itpay.env before starting Codex if your launcher does not load it."
|
|
2801
|
-
]
|
|
2802
|
-
};
|
|
2803
|
-
}
|
|
2804
|
-
|
|
2805
|
-
function installOpenClaw(grantId, credentials, dryRun) {
|
|
2806
|
-
const configPath = path.join(os.homedir(), ".openclaw", "config.json");
|
|
2807
|
-
const current = readJSON(configPath, {});
|
|
2808
|
-
current.models = current.models || {};
|
|
2809
|
-
current.models.providers = current.models.providers || {};
|
|
2810
|
-
current.models.providers.itpay = {
|
|
2811
|
-
baseUrl: credentials.openai_base_url,
|
|
2812
|
-
api: "openai-compatible",
|
|
2813
|
-
apiKey: credentials.key,
|
|
2814
|
-
models: credentials.models || []
|
|
2815
|
-
};
|
|
2816
|
-
const write = writeJSONWithBackup(configPath, current, dryRun);
|
|
2817
|
-
return {
|
|
2818
|
-
target: "openclaw",
|
|
2819
|
-
grant_id: grantId,
|
|
2820
|
-
status: dryRun ? "dry_run" : "installed",
|
|
2821
|
-
files: [{ path: configPath, action: write.action, backup_path: write.backup_path || null }],
|
|
2822
|
-
warnings: [
|
|
2823
|
-
"OpenClaw does not expose a stable token-helper contract here, so the key is written only to the user-level config file with 0600 permissions."
|
|
2824
|
-
]
|
|
2825
|
-
};
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
async function doctor(flags) {
|
|
2829
|
-
const config = readConfig();
|
|
2830
|
-
const credentials = readCredentials();
|
|
2831
|
-
const target = flags.target || null;
|
|
2832
|
-
const grantId = flags.grant || readState().last_grant_id || null;
|
|
2833
|
-
const grantCredential = grantId ? readGrantCredential(grantId) : null;
|
|
2834
|
-
output({
|
|
2835
|
-
itp_version: VERSION,
|
|
2836
|
-
api_base: config.api_base || apiBase(flags),
|
|
2837
|
-
account_id: config.account_id || null,
|
|
2838
|
-
device_id: config.device_id || null,
|
|
2839
|
-
authenticated: Boolean(readSessionToken(credentials)),
|
|
2840
|
-
grants_cached: Object.keys(credentials).filter((k) => k.startsWith("grant_")).length,
|
|
2841
|
-
grant_id: grantId,
|
|
2842
|
-
target,
|
|
2843
|
-
target_config: target ? runtimeConfigStatus(target) : null,
|
|
2844
|
-
model_check: grantCredential && !flags.offline ? await doctorModelCheck(target, grantCredential) : null,
|
|
2845
|
-
grant_credential_store: grantCredential?.credential_store || null,
|
|
2846
|
-
warning: grantCredential?.credential_warning || undefined,
|
|
2847
|
-
credential_store: {
|
|
2848
|
-
path: CREDENTIALS_PATH,
|
|
2849
|
-
mode: fileMode(CREDENTIALS_PATH),
|
|
2850
|
-
native: detectNativeCredentialStore(),
|
|
2851
|
-
fallback: true
|
|
2852
|
-
},
|
|
2853
|
-
session_credential_store: credentials.session_token_store || (credentials.session_token ? "file" : null),
|
|
2854
|
-
session_credential_warning: credentials.session_token_warning || undefined
|
|
2855
|
-
});
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
async function docs(command, rest = [], flags = {}) {
|
|
2859
|
-
const role = normalizeDocsRole(flags.role || "buyer");
|
|
2860
|
-
if (command === "list" || !command) {
|
|
2861
|
-
const docsList = listAgentDocs(role);
|
|
2862
|
-
output({
|
|
2863
|
-
schema_version: "itp.agent_doc_index.v1",
|
|
2864
|
-
role,
|
|
2865
|
-
topics: docsList.map((doc) => ({
|
|
2866
|
-
topic: doc.topic,
|
|
2867
|
-
title: doc.title,
|
|
2868
|
-
purpose: doc.purpose,
|
|
2869
|
-
command: cliCommand("docs", "show", doc.topic, "--role", role, "--json"),
|
|
2870
|
-
next_docs: Array.isArray(doc.next_docs) ? doc.next_docs.map((next) => next.topic).filter(Boolean) : []
|
|
2871
|
-
})),
|
|
2872
|
-
start_here: cliCommand("docs", "show", "quickstart", "--role", role, "--json"),
|
|
2873
|
-
search_command: cliCommand("docs", "search", "<question>", "--role", role, "--json")
|
|
2874
|
-
});
|
|
2875
|
-
return;
|
|
2876
|
-
}
|
|
2877
|
-
if (command === "show" || command === "read") {
|
|
2878
|
-
const topic = flags.topic || positionalArgs(rest)[0] || "quickstart";
|
|
2879
|
-
output(loadAgentDoc(role, topic));
|
|
2880
|
-
return;
|
|
2881
|
-
}
|
|
2882
|
-
if (command === "search") {
|
|
2883
|
-
const query = String(flags.query || flags.q || positionalArgs(rest).join(" ")).trim();
|
|
2884
|
-
if (!query) throw new Error("docs search query is required");
|
|
2885
|
-
const matches = searchAgentDocs(role, query);
|
|
2886
|
-
output({
|
|
2887
|
-
schema_version: "itp.agent_doc_search.v1",
|
|
2888
|
-
role,
|
|
2889
|
-
query,
|
|
2890
|
-
matches,
|
|
2891
|
-
fallback: matches.length ? null : {
|
|
2892
|
-
topic: "quickstart",
|
|
2893
|
-
command: cliCommand("docs", "show", "quickstart", "--role", role, "--json")
|
|
2894
|
-
}
|
|
2895
|
-
});
|
|
2896
|
-
return;
|
|
2897
|
-
}
|
|
2898
|
-
throw new Error(`unknown docs command: ${command}`);
|
|
2899
|
-
}
|
|
2900
|
-
|
|
2901
|
-
function normalizeDocsRole(role) {
|
|
2902
|
-
const normalized = String(role || "buyer").trim().toLowerCase();
|
|
2903
|
-
if (normalized === "buyer" || normalized === "itpay-buyer") return "buyer";
|
|
2904
|
-
throw new Error(`unsupported docs role: ${role}`);
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
function listAgentDocs(role) {
|
|
2908
|
-
const docsDir = resolveDocsDir(role);
|
|
2909
|
-
return fs.readdirSync(docsDir)
|
|
2910
|
-
.filter((name) => name.endsWith(".json"))
|
|
2911
|
-
.map((name) => loadAgentDoc(role, name.replace(/\.json$/, "")))
|
|
2912
|
-
.sort((a, b) => docTopicOrder(a.topic) - docTopicOrder(b.topic) || a.topic.localeCompare(b.topic));
|
|
2913
|
-
}
|
|
2914
|
-
|
|
2915
|
-
function loadAgentDoc(role, topic) {
|
|
2916
|
-
const normalizedTopic = normalizeDocTopic(topic);
|
|
2917
|
-
const file = path.join(resolveDocsDir(role), `${normalizedTopic}.json`);
|
|
2918
|
-
if (!fs.existsSync(file)) {
|
|
2919
|
-
throw new Error(`agent docs topic not found: ${normalizedTopic}`);
|
|
2920
|
-
}
|
|
2921
|
-
const doc = readJSON(file, null);
|
|
2922
|
-
if (!doc || doc.role !== role || doc.topic !== normalizedTopic) {
|
|
2923
|
-
throw new Error(`invalid agent docs topic: ${normalizedTopic}`);
|
|
2924
|
-
}
|
|
2925
|
-
return {
|
|
2926
|
-
...doc,
|
|
2927
|
-
source: {
|
|
2928
|
-
packaged_path: file,
|
|
2929
|
-
command: cliCommand("docs", "show", normalizedTopic, "--role", role, "--json")
|
|
2930
|
-
}
|
|
2931
|
-
};
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
|
-
function searchAgentDocs(role, query) {
|
|
2935
|
-
const rawQuery = String(query).toLowerCase();
|
|
2936
|
-
const terms = rawQuery.split(/\s+/).filter(Boolean);
|
|
2937
|
-
return listAgentDocs(role)
|
|
2938
|
-
.map((doc) => {
|
|
2939
|
-
const docTerms = (doc.search_terms || []).map((term) => String(term).toLowerCase()).filter(Boolean);
|
|
2940
|
-
const haystack = [
|
|
2941
|
-
doc.topic,
|
|
2942
|
-
doc.title,
|
|
2943
|
-
doc.purpose,
|
|
2944
|
-
...(doc.when_to_use || []),
|
|
2945
|
-
...(doc.agent_rules || []),
|
|
2946
|
-
...(doc.forbidden || []),
|
|
2947
|
-
...docTerms
|
|
2948
|
-
].join(" ").toLowerCase();
|
|
2949
|
-
const score = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0) +
|
|
2950
|
-
docTerms.reduce((sum, term) => sum + (rawQuery.includes(term) ? 1 : 0), 0);
|
|
2951
|
-
return { doc, score };
|
|
2952
|
-
})
|
|
2953
|
-
.filter((entry) => entry.score > 0)
|
|
2954
|
-
.sort((a, b) => b.score - a.score || docTopicOrder(a.doc.topic) - docTopicOrder(b.doc.topic))
|
|
2955
|
-
.slice(0, 5)
|
|
2956
|
-
.map((entry) => ({
|
|
2957
|
-
topic: entry.doc.topic,
|
|
2958
|
-
title: entry.doc.title,
|
|
2959
|
-
purpose: entry.doc.purpose,
|
|
2960
|
-
score: entry.score,
|
|
2961
|
-
command: cliCommand("docs", "show", entry.doc.topic, "--role", role, "--json"),
|
|
2962
|
-
next_docs: Array.isArray(entry.doc.next_docs) ? entry.doc.next_docs.map((next) => next.topic).filter(Boolean) : []
|
|
2963
|
-
}));
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
function resolveDocsDir(role) {
|
|
2967
|
-
const candidates = [
|
|
2968
|
-
process.env.ITPAY_CLI_DOCS_DIR,
|
|
2969
|
-
path.join(PACKAGE_ROOT, "docs", "agent", role),
|
|
2970
|
-
path.join(path.dirname(CLI_DIR), "share", "itpay_cli", "docs", "agent", role),
|
|
2971
|
-
path.join(process.cwd(), "docs", "agent", role)
|
|
2972
|
-
].filter(Boolean);
|
|
2973
|
-
const found = candidates.find((candidate) => fs.existsSync(candidate));
|
|
2974
|
-
if (!found) {
|
|
2975
|
-
throw new Error(`ItPay agent docs not found for role ${role}. Checked: ${candidates.join(", ")}`);
|
|
2976
|
-
}
|
|
2977
|
-
return found;
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
function normalizeDocTopic(topic) {
|
|
2981
|
-
return String(topic || "").trim().toLowerCase().replaceAll("_", "-");
|
|
2982
|
-
}
|
|
2983
|
-
|
|
2984
|
-
function docTopicOrder(topic) {
|
|
2985
|
-
const order = [
|
|
2986
|
-
"quickstart",
|
|
2987
|
-
"catalog-search",
|
|
2988
|
-
"product-recommendation",
|
|
2989
|
-
"cart-checkout",
|
|
2990
|
-
"payment-qr",
|
|
2991
|
-
"payment-wait",
|
|
2992
|
-
"qr-refresh",
|
|
2993
|
-
"secure-delivery",
|
|
2994
|
-
"human-claim-ui",
|
|
2995
|
-
"account-portal",
|
|
2996
|
-
"vault-agent-read",
|
|
2997
|
-
"recovery",
|
|
2998
|
-
"safety-policy"
|
|
2999
|
-
];
|
|
3000
|
-
const index = order.indexOf(topic);
|
|
3001
|
-
return index === -1 ? 999 : index;
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
async function skill(command, flags) {
|
|
3005
|
-
const role = normalizeSkillRole(flags.role || flags.skill || "buyer");
|
|
3006
|
-
const skillPath = resolveSkillPath(role);
|
|
3007
|
-
if (!command || command === "show" || command === "read") {
|
|
3008
|
-
const content = fs.readFileSync(skillPath, "utf8");
|
|
3009
|
-
if (flags.json) {
|
|
3010
|
-
output({ skill: "itpay-buyer", role, path: skillPath, content });
|
|
3011
|
-
} else {
|
|
3012
|
-
process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
|
|
3013
|
-
}
|
|
3014
|
-
return;
|
|
3015
|
-
}
|
|
3016
|
-
if (command === "path") {
|
|
3017
|
-
if (flags.json) {
|
|
3018
|
-
output({ skill: "itpay-buyer", role, path: skillPath });
|
|
3019
|
-
} else {
|
|
3020
|
-
process.stdout.write(`${skillPath}\n`);
|
|
3021
|
-
}
|
|
3022
|
-
return;
|
|
3023
|
-
}
|
|
3024
|
-
throw new Error(`unknown skill command: ${command}`);
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
function normalizeSkillRole(role) {
|
|
3028
|
-
const normalized = String(role || "buyer").trim().toLowerCase();
|
|
3029
|
-
if (normalized === "buyer" || normalized === "itpay-buyer") return "buyer";
|
|
3030
|
-
if (normalized === "merchant" || normalized === "itpay-merchant") {
|
|
3031
|
-
throw new Error("merchant skill is not packaged yet; use --role buyer for current external-agent tests");
|
|
3032
|
-
}
|
|
3033
|
-
throw new Error(`unsupported skill role: ${role}`);
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
function resolveSkillPath(role = "buyer") {
|
|
3037
|
-
const skillDirName = "itpay-buyer";
|
|
3038
|
-
const envPath = process.env.ITPAY_BUYER_SKILL_PATH;
|
|
3039
|
-
const candidates = [
|
|
3040
|
-
envPath,
|
|
3041
|
-
process.env.ITPAY_CLI_SKILL_PATH,
|
|
3042
|
-
path.join(PACKAGE_ROOT, "skills", skillDirName, "SKILL.md"),
|
|
3043
|
-
path.join(path.dirname(CLI_DIR), "share", "itpay_cli", "skills", skillDirName, "SKILL.md"),
|
|
3044
|
-
path.join(process.cwd(), "skills", skillDirName, "SKILL.md")
|
|
3045
|
-
].filter(Boolean);
|
|
3046
|
-
const found = candidates.find((candidate) => fs.existsSync(candidate));
|
|
3047
|
-
if (!found) {
|
|
3048
|
-
throw new Error(`ItPay skill file not found for role ${role}. Checked: ${candidates.join(", ")}`);
|
|
3049
|
-
}
|
|
3050
|
-
return found;
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
async function doctorModelCheck(target, credentials) {
|
|
3054
|
-
const baseUrl = target === "claude-code"
|
|
3055
|
-
? credentials.anthropic_base_url
|
|
3056
|
-
: target === "codex" || target === "openclaw"
|
|
3057
|
-
? credentials.openai_base_url
|
|
3058
|
-
: credentials.openai_base_url || credentials.base_url;
|
|
3059
|
-
if (!baseUrl || !credentials.key) {
|
|
3060
|
-
return { attempted: false, ok: false, error: "missing base_url or credential" };
|
|
3061
|
-
}
|
|
3062
|
-
const controller = new AbortController();
|
|
3063
|
-
const timer = setTimeout(() => controller.abort(), 3000);
|
|
3064
|
-
try {
|
|
3065
|
-
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
|
|
3066
|
-
method: "GET",
|
|
3067
|
-
headers: { Authorization: `Bearer ${credentials.key}` },
|
|
3068
|
-
signal: controller.signal
|
|
3069
|
-
});
|
|
3070
|
-
return {
|
|
3071
|
-
attempted: true,
|
|
3072
|
-
ok: response.ok,
|
|
3073
|
-
status: response.status,
|
|
3074
|
-
endpoint: `${baseUrl.replace(/\/$/, "")}/models`
|
|
3075
|
-
};
|
|
3076
|
-
} catch (error) {
|
|
3077
|
-
return {
|
|
3078
|
-
attempted: true,
|
|
3079
|
-
ok: false,
|
|
3080
|
-
endpoint: `${baseUrl.replace(/\/$/, "")}/models`,
|
|
3081
|
-
error: error.message
|
|
3082
|
-
};
|
|
3083
|
-
} finally {
|
|
3084
|
-
clearTimeout(timer);
|
|
3085
|
-
}
|
|
3086
|
-
}
|
|
3087
|
-
|
|
3088
|
-
function runtimeConfigStatus(target) {
|
|
3089
|
-
const paths = {
|
|
3090
|
-
"claude-code": [path.join(os.homedir(), ".claude", "settings.json")],
|
|
3091
|
-
codex: [path.join(os.homedir(), ".codex", "config.toml"), path.join(CONFIG_DIR, "itpay.env")],
|
|
3092
|
-
openclaw: [path.join(os.homedir(), ".openclaw", "config.json")]
|
|
3093
|
-
};
|
|
3094
|
-
return (paths[target] || []).map((file) => ({
|
|
3095
|
-
path: file,
|
|
3096
|
-
exists: fs.existsSync(file),
|
|
3097
|
-
mode: fileMode(file)
|
|
3098
|
-
}));
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
async function keys(command, flags) {
|
|
3102
|
-
const credentials = readCredentials();
|
|
3103
|
-
const grants = Object.entries(credentials)
|
|
3104
|
-
.filter(([key]) => key.startsWith("grant_"))
|
|
3105
|
-
.map(([key, value]) => ({
|
|
3106
|
-
grant_id: key.slice("grant_".length),
|
|
3107
|
-
target: value.target,
|
|
3108
|
-
base_url: value.base_url,
|
|
3109
|
-
credential_store: value.credential_store || "file",
|
|
3110
|
-
key: value.key ? maskSecret(value.key) : "stored"
|
|
3111
|
-
}));
|
|
3112
|
-
if (!command || command === "list") {
|
|
3113
|
-
output({ keys: grants });
|
|
3114
|
-
return;
|
|
3115
|
-
}
|
|
3116
|
-
if (command === "revoke") {
|
|
3117
|
-
const grantId = flags.grant || grants[0]?.grant_id;
|
|
3118
|
-
if (!grantId) throw new Error("grant id is required");
|
|
3119
|
-
const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/revoke`, { method: "POST" }, flags);
|
|
3120
|
-
deleteGrantCredential(grantId);
|
|
3121
|
-
output(response);
|
|
3122
|
-
return;
|
|
3123
|
-
}
|
|
3124
|
-
if (command === "rotate") {
|
|
3125
|
-
const grantId = flags.grant || grants[0]?.grant_id;
|
|
3126
|
-
if (!grantId) throw new Error("grant id is required");
|
|
3127
|
-
const existing = readCredentials()[`grant_${grantId}`] || {};
|
|
3128
|
-
const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/rotate`, { method: "POST" }, flags);
|
|
3129
|
-
const storedCredential = storeGrantCredential(grantId, {
|
|
3130
|
-
key: response.credential.key_once,
|
|
3131
|
-
target: existing.target || flags.target || "",
|
|
3132
|
-
base_url: response.base_url,
|
|
3133
|
-
openai_base_url: response.openai_base_url,
|
|
3134
|
-
anthropic_base_url: response.anthropic_base_url,
|
|
3135
|
-
gemini_base_url: response.gemini_base_url,
|
|
3136
|
-
models: response.models || [],
|
|
3137
|
-
install_profiles: response.install_profiles || []
|
|
3138
|
-
});
|
|
3139
|
-
output({
|
|
3140
|
-
...response,
|
|
3141
|
-
credential: {
|
|
3142
|
-
type: response.credential.type,
|
|
3143
|
-
rotated: true,
|
|
3144
|
-
stored: true,
|
|
3145
|
-
credential_store: storedCredential.credential_store,
|
|
3146
|
-
warning: storedCredential.credential_warning || undefined
|
|
3147
|
-
}
|
|
3148
|
-
});
|
|
3149
|
-
return;
|
|
3150
|
-
}
|
|
3151
|
-
throw new Error(`unknown keys command: ${command}`);
|
|
3152
|
-
}
|
|
3153
|
-
|
|
3154
|
-
async function token(command, flags) {
|
|
3155
|
-
if (command !== "issue") throw new Error(`unknown token command: ${command || ""}`);
|
|
3156
|
-
const grantId = flags.grant || readState().last_grant_id;
|
|
3157
|
-
if (!grantId) throw new Error("grant id is required");
|
|
3158
|
-
const credentials = readGrantCredential(grantId);
|
|
3159
|
-
if (!credentials?.key) throw new Error(`grant ${grantId} is not installed locally`);
|
|
3160
|
-
if (flags.stdout) {
|
|
3161
|
-
process.stdout.write(credentials.key);
|
|
3162
|
-
return;
|
|
3163
|
-
}
|
|
3164
|
-
output({
|
|
3165
|
-
grant_id: grantId,
|
|
3166
|
-
key: maskSecret(credentials.key),
|
|
3167
|
-
stdout_required_for_raw_token: true
|
|
3168
|
-
});
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
|
-
async function sync(flags) {
|
|
3172
|
-
const [account, balanceResult, grants] = await Promise.all([
|
|
3173
|
-
api("/api/itp/account", { method: "GET" }, flags),
|
|
3174
|
-
api("/api/itp/balance", { method: "GET" }, flags),
|
|
3175
|
-
api("/api/itp/grants", { method: "GET" }, flags)
|
|
3176
|
-
]);
|
|
3177
|
-
output({ account, balance: balanceResult, grants });
|
|
3178
|
-
}
|
|
3179
|
-
|
|
3180
|
-
async function refreshRun(run, flags = {}) {
|
|
3181
|
-
let next = { ...run };
|
|
3182
|
-
const credentials = readCredentials();
|
|
3183
|
-
const hasSession = Boolean(readSessionToken(credentials));
|
|
3184
|
-
|
|
3185
|
-
if (next.auth?.auth_id && !hasSession) {
|
|
3186
|
-
try {
|
|
3187
|
-
const auth = await api(`/api/itp/auth/device/${encodeURIComponent(next.auth.auth_id)}/poll`, { method: "POST" }, flags);
|
|
3188
|
-
if (auth.auth?.session_token) {
|
|
3189
|
-
writeSessionCredentials(auth.auth);
|
|
3190
|
-
writeConfig({
|
|
3191
|
-
api_base: apiBase(flags),
|
|
3192
|
-
account_id: auth.auth.account_id,
|
|
3193
|
-
device_id: auth.auth.device_id,
|
|
3194
|
-
web_console_url: auth.auth.web_console_url
|
|
3195
|
-
});
|
|
3196
|
-
next = mergeRun(next, {
|
|
3197
|
-
phase: "authenticated",
|
|
3198
|
-
status: "running",
|
|
3199
|
-
account: {
|
|
3200
|
-
authenticated: true,
|
|
3201
|
-
account_id: auth.auth.account_id,
|
|
3202
|
-
device_id: auth.auth.device_id,
|
|
3203
|
-
newapi_user_id: auth.auth.newapi_user_id || null
|
|
3204
|
-
},
|
|
3205
|
-
auth: { status: "consumed" },
|
|
3206
|
-
human_action: null,
|
|
3207
|
-
safe_summary: "Agent device authenticated."
|
|
3208
|
-
});
|
|
3209
|
-
} else if (auth.status === "authorization_pending") {
|
|
3210
|
-
next = mergeRun(next, {
|
|
3211
|
-
phase: "waiting_human_auth",
|
|
3212
|
-
status: "waiting_human_auth",
|
|
3213
|
-
auth: { status: "pending", expires_at: auth.expires_at },
|
|
3214
|
-
human_action: auth.human_action || auth.next_action?.human_action || next.human_action || null,
|
|
3215
|
-
safe_summary: "Waiting for Alipay authentication scan."
|
|
3216
|
-
});
|
|
3217
|
-
} else if (auth.status) {
|
|
3218
|
-
next = mergeRun(next, {
|
|
3219
|
-
phase: auth.status === "expired" ? "expired" : "failed",
|
|
3220
|
-
status: auth.status === "expired" ? "expired" : "failed",
|
|
3221
|
-
auth: { status: auth.status },
|
|
3222
|
-
safe_summary: auth.error || `Auth status: ${auth.status}`
|
|
3223
|
-
});
|
|
3224
|
-
}
|
|
3225
|
-
} catch (error) {
|
|
3226
|
-
next = mergeRun(next, { last_error: safeErrorMessage(error), safe_summary: "Could not refresh auth status." });
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
|
|
3230
|
-
if (readSessionToken(readCredentials())) {
|
|
3231
|
-
try {
|
|
3232
|
-
const authStatusResult = await api("/api/itp/auth/status", { method: "GET" }, flags);
|
|
3233
|
-
next = mergeRun(next, {
|
|
3234
|
-
account: {
|
|
3235
|
-
authenticated: authStatusResult.authenticated !== false,
|
|
3236
|
-
account_id: authStatusResult.account_id || next.account?.account_id || null,
|
|
3237
|
-
device_id: authStatusResult.device_id || next.account?.device_id || null,
|
|
3238
|
-
newapi_user_id: authStatusResult.newapi_user_id || null
|
|
3239
|
-
}
|
|
3240
|
-
});
|
|
3241
|
-
} catch (error) {
|
|
3242
|
-
next = mergeRun(next, { last_error: safeErrorMessage(error) });
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
|
|
3246
|
-
if (next.checkout?.checkout_id && readSessionToken(readCredentials())) {
|
|
3247
|
-
try {
|
|
3248
|
-
const checkout = await api(`/api/itp/checkout/${encodeURIComponent(next.checkout.checkout_id)}`, { method: "GET" }, flags);
|
|
3249
|
-
next = mergeRun(next, {
|
|
3250
|
-
phase: checkout.grant_id ? "grant_ready" : checkout.status === "waiting_user_payment" ? "waiting_human_payment" : next.phase,
|
|
3251
|
-
status: checkout.grant_id ? "grant_ready" : checkout.status === "waiting_user_payment" ? "waiting_human_payment" : next.status,
|
|
3252
|
-
checkout: {
|
|
3253
|
-
checkout_id: checkout.checkout_id,
|
|
3254
|
-
order_id: checkout.order_id,
|
|
3255
|
-
status: checkout.status,
|
|
3256
|
-
expires_at: checkout.expires_at
|
|
3257
|
-
},
|
|
3258
|
-
payment: { provider: next.payment_method, status: checkout.status },
|
|
3259
|
-
grant: { ...(next.grant || {}), grant_id: checkout.grant_id || next.grant?.grant_id || null },
|
|
3260
|
-
human_action: checkout.human_action || null,
|
|
3261
|
-
safe_summary: checkout.grant_id ? "Payment verified and grant is ready." : checkout.status === "waiting_user_payment" ? "Waiting for Alipay payment scan." : `Checkout status: ${checkout.status}`
|
|
3262
|
-
});
|
|
3263
|
-
} catch (error) {
|
|
3264
|
-
next = mergeRun(next, { last_error: safeErrorMessage(error), safe_summary: "Could not refresh checkout status." });
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
const grantId = next.grant?.grant_id || readState().last_grant_id;
|
|
3269
|
-
if (grantId) {
|
|
3270
|
-
const credential = readGrantCredential(grantId);
|
|
3271
|
-
if (credential?.key || credential?.credential_ref) {
|
|
3272
|
-
next = mergeRun(next, {
|
|
3273
|
-
phase: next.install_runtime ? next.phase : "grant_ready",
|
|
3274
|
-
grant: { grant_id: grantId, installed: true, credential_store: credential.credential_store || "file" },
|
|
3275
|
-
safe_summary: "Grant credential stored."
|
|
3276
|
-
});
|
|
3277
|
-
}
|
|
3278
|
-
}
|
|
3279
|
-
return next;
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
function agentRunResponse(run, extra = {}) {
|
|
3283
|
-
const status = extra.status || (run.status && run.status !== "running" ? run.status : phaseToStatus(run.phase));
|
|
3284
|
-
return {
|
|
3285
|
-
schema_version: "itp.agent.v1",
|
|
3286
|
-
status,
|
|
3287
|
-
run_id: run.run_id,
|
|
3288
|
-
phase: run.phase || status,
|
|
3289
|
-
auth_id: extra.auth_id || run.auth?.auth_id || null,
|
|
3290
|
-
account_id: extra.account_id || run.account?.account_id || null,
|
|
3291
|
-
device_id: extra.device_id || run.account?.device_id || null,
|
|
3292
|
-
checkout_id: extra.checkout_id || run.checkout?.checkout_id || null,
|
|
3293
|
-
order_id: extra.order_id || run.checkout?.order_id || null,
|
|
3294
|
-
grant_id: extra.grant_id || run.grant?.grant_id || null,
|
|
3295
|
-
plan_id: extra.plan_id || run.plan_id || null,
|
|
3296
|
-
credits: extra.credits || run.credits || run.checkout?.purchase?.credits_granted || null,
|
|
3297
|
-
purchase: extra.purchase || run.checkout?.purchase || undefined,
|
|
3298
|
-
target: extra.target || run.target || "generic",
|
|
3299
|
-
base_url: extra.base_url || run.result?.base_url || undefined,
|
|
3300
|
-
openai_base_url: extra.openai_base_url || run.result?.openai_base_url || undefined,
|
|
3301
|
-
anthropic_base_url: extra.anthropic_base_url || run.result?.anthropic_base_url || undefined,
|
|
3302
|
-
gemini_base_url: extra.gemini_base_url || run.result?.gemini_base_url || undefined,
|
|
3303
|
-
credential: extra.credential || (run.grant?.installed ? {
|
|
3304
|
-
stored: true,
|
|
3305
|
-
credential_store: run.grant?.credential_store,
|
|
3306
|
-
token_command: run.grant?.grant_id ? cliCommand("token", "issue", "--grant", run.grant.grant_id, "--stdout") : undefined,
|
|
3307
|
-
stdout_required_for_raw_token: true
|
|
3308
|
-
} : undefined),
|
|
3309
|
-
human_action: extra.human_action || run.human_action || undefined,
|
|
3310
|
-
next: extra.next || nextActionForRun(run),
|
|
3311
|
-
next_action: extra.next_action || undefined,
|
|
3312
|
-
safe_user_message: extra.safe_user_message || safeUserMessageForRun(run),
|
|
3313
|
-
safe_summary: run.safe_summary || undefined,
|
|
3314
|
-
warnings: extra.warnings || [],
|
|
3315
|
-
secrets: {
|
|
3316
|
-
raw_key_included: false,
|
|
3317
|
-
session_token_included: false
|
|
3318
|
-
},
|
|
3319
|
-
...extra
|
|
3320
|
-
};
|
|
3321
|
-
}
|
|
3322
|
-
|
|
3323
|
-
function phaseToStatus(phase) {
|
|
3324
|
-
if (phase === "waiting_human_auth") return "waiting_human_auth";
|
|
3325
|
-
if (phase === "waiting_human_payment") return "waiting_human_payment";
|
|
3326
|
-
if (phase === "grant_ready") return "grant_ready";
|
|
3327
|
-
if (phase === "done") return "done";
|
|
3328
|
-
if (phase === "expired") return "expired";
|
|
3329
|
-
if (phase === "failed") return "failed";
|
|
3330
|
-
return phase || "running";
|
|
3331
|
-
}
|
|
3332
|
-
|
|
3333
|
-
function nextActionForRun(run) {
|
|
3334
|
-
if (run.phase === "waiting_human_auth") {
|
|
3335
|
-
return { type: "show_qr_and_wait", command: resumeCommand(run, runResumeFlags(run)), retry_after_ms: 2000, safe_for_agent: true };
|
|
3336
|
-
}
|
|
3337
|
-
if (run.phase === "waiting_human_payment") {
|
|
3338
|
-
return { type: "show_qr_and_wait", command: resumeCommand(run, runResumeFlags(run, { display: "none" })), retry_after_ms: 2000, safe_for_agent: true };
|
|
3339
|
-
}
|
|
3340
|
-
if (["paid_verified", "granting", "grant_failed"].includes(run.checkout?.status)) {
|
|
3341
|
-
return { type: "recover_checkout", command: cliCommand("checkout", "recover", run.checkout.checkout_id, "--json"), safe_for_agent: true };
|
|
3342
|
-
}
|
|
3343
|
-
if (run.grant?.grant_id && !run.grant?.installed) {
|
|
3344
|
-
return { type: "install_grant", command: cliCommand("grants", "install", run.grant.grant_id, "--target", run.target || "generic", "--json"), safe_for_agent: true };
|
|
3345
|
-
}
|
|
3346
|
-
if (run.grant?.installed) {
|
|
3347
|
-
return { type: "done", safe_for_agent: true };
|
|
3348
|
-
}
|
|
3349
|
-
return { type: "resume", command: resumeCommand(run, runResumeFlags(run)), safe_for_agent: true };
|
|
3350
|
-
}
|
|
3351
|
-
|
|
3352
|
-
function runResumeFlags(run, overrides = {}) {
|
|
3353
|
-
return {
|
|
3354
|
-
host: run.agent_host || undefined,
|
|
3355
|
-
display: run.agent_display || undefined,
|
|
3356
|
-
qr_format: run.agent_qr_format || undefined,
|
|
3357
|
-
api_base: run.api_base || undefined,
|
|
3358
|
-
...overrides
|
|
3359
|
-
};
|
|
3360
|
-
}
|
|
3361
|
-
|
|
3362
|
-
function resumeCommand(run, flags = {}, options = {}) {
|
|
3363
|
-
const args = ["resume", "--run-id", run.run_id, "--json"];
|
|
3364
|
-
appendPassthroughFlag(args, flags, "host");
|
|
3365
|
-
appendPassthroughFlag(args, flags, "display");
|
|
3366
|
-
appendPassthroughFlag(args, flags, "qr_format", "qr-format");
|
|
3367
|
-
appendPassthroughFlag(args, flags, "api_base", "api-base");
|
|
3368
|
-
appendPassthroughFlag(args, flags, "api_timeout", "api-timeout");
|
|
3369
|
-
if (options.no_wait_payment) args.push("--no-wait-payment");
|
|
3370
|
-
return cliCommand(...args);
|
|
3371
|
-
}
|
|
3372
|
-
|
|
3373
|
-
function appendPassthroughFlag(args, flags, key, flagName = key) {
|
|
3374
|
-
const value = flags[key];
|
|
3375
|
-
if (value === undefined || value === null || value === false) return;
|
|
3376
|
-
args.push(`--${flagName}`);
|
|
3377
|
-
if (value !== true) args.push(String(value));
|
|
3378
|
-
}
|
|
3379
|
-
|
|
3380
|
-
function cliCommand(...args) {
|
|
3381
|
-
const override = process.env.ITP_COMMAND;
|
|
3382
|
-
const base = override
|
|
3383
|
-
? override
|
|
3384
|
-
: `${shellQuote(process.execPath)} ${shellQuote(CLI_FILE)}`;
|
|
3385
|
-
return [base, ...args.map((arg) => shellQuote(String(arg)))].join(" ");
|
|
3386
|
-
}
|
|
3387
|
-
|
|
3388
|
-
function shellQuote(value) {
|
|
3389
|
-
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
|
|
3390
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
3391
|
-
}
|
|
3392
|
-
|
|
3393
|
-
function safeUserMessageForRun(run) {
|
|
3394
|
-
if (run.phase === "waiting_human_auth") return "Please scan the Alipay authentication QR. I will continue automatically after approval.";
|
|
3395
|
-
if (run.phase === "waiting_human_payment") return "Please scan the Alipay payment QR. I will continue automatically after payment is verified.";
|
|
3396
|
-
if (run.grant?.installed) return "Payment verified. The API credential is stored locally.";
|
|
3397
|
-
return run.safe_summary || "ITPay setup is in progress.";
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
|
-
function safeErrorMessage(error) {
|
|
3401
|
-
return String(error?.message || error || "unknown error")
|
|
3402
|
-
.replace(/itp_sess_[A-Za-z0-9_-]+/g, "itp_sess_****")
|
|
3403
|
-
.replace(/sk-[A-Za-z0-9_-]+/g, "sk-****");
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
function humanActionSummaryLines(action) {
|
|
3407
|
-
if (!action?.url) return [];
|
|
3408
|
-
const lines = [
|
|
3409
|
-
"ITP HUMAN ACTION REQUIRED",
|
|
3410
|
-
`Title: ${action.title || "Scan with Alipay"}`,
|
|
3411
|
-
`URL: ${action.url}`
|
|
3412
|
-
];
|
|
3413
|
-
if (action.local_qr_path) {
|
|
3414
|
-
lines.push(`Local QR image: ${action.local_qr_path}`);
|
|
3415
|
-
}
|
|
3416
|
-
if (action.qr_png_url) {
|
|
3417
|
-
lines.push(`QR PNG: ${action.qr_png_url}`);
|
|
3418
|
-
}
|
|
3419
|
-
if (action.qr_image_url) {
|
|
3420
|
-
lines.push(`QR image: ${action.qr_image_url}`);
|
|
3421
|
-
}
|
|
3422
|
-
if (action.mobile_wallet_url) {
|
|
3423
|
-
lines.push(`Mobile wallet link: ${action.mobile_wallet_url}`);
|
|
3424
|
-
}
|
|
3425
|
-
if (action.oauth_start_url) {
|
|
3426
|
-
lines.push(`Alipay auth fallback: ${action.oauth_start_url}`);
|
|
3427
|
-
}
|
|
3428
|
-
if (action.fallback_text && !action.fallback_text.includes(action.url)) {
|
|
3429
|
-
lines.push(`Fallback: ${action.fallback_text}`);
|
|
3430
|
-
}
|
|
3431
|
-
if (action.expires_at) {
|
|
3432
|
-
lines.push(`Expires at: ${formatActionTime(action.expires_at)}`);
|
|
3433
|
-
}
|
|
3434
|
-
return lines;
|
|
3435
|
-
}
|
|
3436
|
-
|
|
3437
|
-
function writeHumanActionSummary(action, suffix = "") {
|
|
3438
|
-
const lines = humanActionSummaryLines(action);
|
|
3439
|
-
if (!lines.length) return;
|
|
3440
|
-
process.stderr.write(`\n${lines.join("\n")}${suffix ? `\n${suffix}` : ""}\n\n`);
|
|
3441
|
-
}
|
|
3442
|
-
|
|
3443
|
-
function waitHeartbeatMs(flags = {}) {
|
|
3444
|
-
if (flags.quiet || process.env.ITP_WAIT_HEARTBEAT_SECONDS === "0") return 0;
|
|
3445
|
-
return Math.max(5000, Number(flags.heartbeat || process.env.ITP_WAIT_HEARTBEAT_SECONDS || 20) * 1000);
|
|
3446
|
-
}
|
|
3447
|
-
|
|
3448
|
-
function writeWaitHeartbeat({ kind, idName, idValue, status, action, lastHeartbeatAt, flags, command }) {
|
|
3449
|
-
const heartbeatMs = waitHeartbeatMs(flags);
|
|
3450
|
-
if (!heartbeatMs) return lastHeartbeatAt;
|
|
3451
|
-
const now = Date.now();
|
|
3452
|
-
if (now - lastHeartbeatAt < heartbeatMs) return lastHeartbeatAt;
|
|
3453
|
-
const lines = [
|
|
3454
|
-
`ITP waiting for ${kind}: ${idName}=${idValue} status=${status}`,
|
|
3455
|
-
action?.url ? `URL: ${action.url}` : null,
|
|
3456
|
-
action?.local_qr_path ? `Local QR image: ${action.local_qr_path}` : null,
|
|
3457
|
-
action?.qr_png_url ? `QR PNG: ${action.qr_png_url}` : null,
|
|
3458
|
-
action?.qr_image_url ? `QR image: ${action.qr_image_url}` : null,
|
|
3459
|
-
action?.mobile_wallet_url ? `Mobile wallet link: ${action.mobile_wallet_url}` : null,
|
|
3460
|
-
action?.oauth_start_url ? `Alipay auth fallback: ${action.oauth_start_url}` : null,
|
|
3461
|
-
command ? `Resume command: ${command}` : null
|
|
3462
|
-
].filter(Boolean);
|
|
3463
|
-
process.stderr.write(`\n${lines.join("\n")}\n\n`);
|
|
3464
|
-
return now;
|
|
3465
|
-
}
|
|
3466
|
-
|
|
3467
|
-
async function renderHumanAction(action, flags = {}) {
|
|
3468
|
-
if (!action?.url) return null;
|
|
3469
|
-
const qrImageURL = preferredHumanActionQRURL(action);
|
|
3470
|
-
const mode = String(flags.display || process.env.ITP_DISPLAY || "").toLowerCase() || "auto";
|
|
3471
|
-
annotateHumanActionPresentation(action, qrImageURL);
|
|
3472
|
-
if (flags.json || mode === "none" || mode === "json") {
|
|
3473
|
-
if (qrImageURL && shouldPrepareLocalQRForJSON(mode, flags, action)) {
|
|
3474
|
-
const localPath = await prepareLocalQRFile(action, qrImageURL, flags, mode !== "file" && !flags.qr_file && !process.env.ITP_QR_FILE);
|
|
3475
|
-
if (localPath) {
|
|
3476
|
-
attachAgentQRImage(action, qrImageURL, localPath);
|
|
3477
|
-
persistHumanAction(action, flags);
|
|
3478
|
-
return { rendered: false, mode: "json-local-qr", outputs: [localPath] };
|
|
3479
|
-
}
|
|
3480
|
-
}
|
|
3481
|
-
if (!qrImageURL && shouldGenerateLocalQRFromActionURL(action, mode, flags)) {
|
|
3482
|
-
const localPath = await prepareLocalQRFromActionURL(action, flags, mode !== "file" && !flags.qr_file && !process.env.ITP_QR_FILE);
|
|
3483
|
-
if (localPath) {
|
|
3484
|
-
attachAgentLocalQR(action, localPath);
|
|
3485
|
-
annotateHumanActionPresentation(action, "");
|
|
3486
|
-
persistHumanAction(action, flags);
|
|
3487
|
-
return { rendered: false, mode: "json-local-url-qr", outputs: [localPath] };
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
return { rendered: false, mode: flags.json ? "json" : mode };
|
|
3491
|
-
}
|
|
3492
|
-
const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
|
|
3493
|
-
if (["discord", "telegram", "whatsapp"].includes(host)) {
|
|
3494
|
-
return { rendered: false, mode: "chat-json", host };
|
|
3495
|
-
}
|
|
3496
|
-
if (shouldUseAgentTextQR(flags)) {
|
|
3497
|
-
if (qrImageURL) {
|
|
3498
|
-
const localPath = await prepareLocalQRFile(action, qrImageURL, flags, true);
|
|
3499
|
-
attachAgentQRImage(action, qrImageURL, localPath);
|
|
3500
|
-
persistHumanAction(action, flags);
|
|
3501
|
-
return { rendered: false, mode: localPath ? "agent-local-image-qr" : "agent-image-qr", host, outputs: [localPath || "preferred_qr_url"] };
|
|
3502
|
-
}
|
|
3503
|
-
if (shouldGenerateLocalQRFromActionURL(action, mode, flags)) {
|
|
3504
|
-
const localPath = await prepareLocalQRFromActionURL(action, flags, true);
|
|
3505
|
-
if (localPath) {
|
|
3506
|
-
attachAgentLocalQR(action, localPath);
|
|
3507
|
-
annotateHumanActionPresentation(action, "");
|
|
3508
|
-
persistHumanAction(action, flags);
|
|
3509
|
-
return { rendered: false, mode: "agent-local-url-qr", host, outputs: [localPath] };
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
process.stderr.write(`No QR image available. Open action URL: ${action.url}\n`);
|
|
3513
|
-
persistHumanAction(action, flags);
|
|
3514
|
-
return { rendered: false, mode: "agent-url-fallback", host, outputs: ["action_url"] };
|
|
3515
|
-
}
|
|
3516
|
-
|
|
3517
|
-
const renderResult = { rendered: false, mode, outputs: [] };
|
|
3518
|
-
writeHumanActionSummary(action);
|
|
3519
|
-
if ((mode === "auto" || mode === "browser") && shouldOpenBrowser(flags)) {
|
|
3520
|
-
if (openBrowser(action.mobile_wallet_url || qrImageURL || action.url)) {
|
|
3521
|
-
renderResult.rendered = true;
|
|
3522
|
-
renderResult.outputs.push("browser");
|
|
3523
|
-
}
|
|
3524
|
-
if (mode === "browser") return renderResult;
|
|
3525
|
-
}
|
|
3526
|
-
|
|
3527
|
-
if (mode === "file" || flags.qr_file || process.env.ITP_QR_FILE) {
|
|
3528
|
-
const file = flags.qr_file || process.env.ITP_QR_FILE || defaultQRFilePath(action, qrImageURL);
|
|
3529
|
-
if (qrImageURL) {
|
|
3530
|
-
await downloadQRImage(qrImageURL, file, flags);
|
|
3531
|
-
action.local_qr_path = file;
|
|
3532
|
-
action.local_qr_mime = qrMimeType(qrImageURL, action);
|
|
3533
|
-
process.stderr.write(`Alipay QR image: ${file}\n`);
|
|
3534
|
-
renderResult.rendered = true;
|
|
3535
|
-
renderResult.outputs.push(file);
|
|
3536
|
-
if (mode === "file") return renderResult;
|
|
3537
|
-
} else {
|
|
3538
|
-
const localPath = shouldGenerateLocalQRFromActionURL(action, mode, flags)
|
|
3539
|
-
? await prepareLocalQRFromActionURL(action, flags, false)
|
|
3540
|
-
: "";
|
|
3541
|
-
if (localPath) {
|
|
3542
|
-
attachAgentLocalQR(action, localPath);
|
|
3543
|
-
annotateHumanActionPresentation(action, "");
|
|
3544
|
-
process.stderr.write(`Alipay action QR image: ${localPath}\n`);
|
|
3545
|
-
renderResult.rendered = true;
|
|
3546
|
-
renderResult.outputs.push(localPath);
|
|
3547
|
-
if (mode === "file") return renderResult;
|
|
3548
|
-
} else {
|
|
3549
|
-
process.stderr.write(`No QR image available. Open action URL: ${action.url}\n`);
|
|
3550
|
-
}
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
if (qrImageURL) {
|
|
3555
|
-
process.stderr.write(`Alipay QR image URL: ${qrImageURL}\n`);
|
|
3556
|
-
if (action.mobile_wallet_url) process.stderr.write(`Mobile wallet link: ${action.mobile_wallet_url}\n`);
|
|
3557
|
-
renderResult.outputs.push("preferred_qr_url");
|
|
3558
|
-
return renderResult;
|
|
3559
|
-
}
|
|
3560
|
-
|
|
3561
|
-
if ((mode === "auto" || mode === "terminal") && shouldRenderTerminalQR(host)) {
|
|
3562
|
-
const qr = await QRCode.toString(action.url, {
|
|
3563
|
-
type: terminalQRType(flags, host),
|
|
3564
|
-
small: true,
|
|
3565
|
-
errorCorrectionLevel: "M"
|
|
3566
|
-
});
|
|
3567
|
-
process.stderr.write(`\n${action.title || "Scan with Alipay"}\n`);
|
|
3568
|
-
if (action.description) process.stderr.write(`${action.description}\n`);
|
|
3569
|
-
process.stderr.write(`${qr}\n`);
|
|
3570
|
-
process.stderr.write(`Alipay action URL: ${action.url}\n`);
|
|
3571
|
-
if (action.expires_at) process.stderr.write(`Expires at: ${formatActionTime(action.expires_at)}\n`);
|
|
3572
|
-
renderResult.rendered = true;
|
|
3573
|
-
renderResult.outputs.push("terminal");
|
|
3574
|
-
return renderResult;
|
|
3575
|
-
}
|
|
3576
|
-
|
|
3577
|
-
process.stderr.write(`${action.fallback_text || `Open Alipay URL: ${action.url}`}\n`);
|
|
3578
|
-
renderResult.outputs.push("url");
|
|
3579
|
-
return renderResult;
|
|
3580
|
-
}
|
|
3581
|
-
|
|
3582
|
-
function preferredHumanActionQRURL(action) {
|
|
3583
|
-
return action?.qr_png_url ||
|
|
3584
|
-
humanActionPresentationURL(action, "qr_png_url") ||
|
|
3585
|
-
action?.qr_image_url ||
|
|
3586
|
-
action?.qr?.png_url ||
|
|
3587
|
-
action?.qr?.image_url ||
|
|
3588
|
-
humanActionPresentationURL(action, "qr_svg_url") ||
|
|
3589
|
-
"";
|
|
3590
|
-
}
|
|
3591
|
-
|
|
3592
|
-
function humanActionPresentationURL(action, type) {
|
|
3593
|
-
const display = action?.presentation?.display;
|
|
3594
|
-
if (!Array.isArray(display)) return "";
|
|
3595
|
-
const found = display.find((item) => item?.type === type && item?.url);
|
|
3596
|
-
return found?.url || "";
|
|
3597
|
-
}
|
|
3598
|
-
|
|
3599
|
-
function annotateHumanActionPresentation(action, qrImageURL) {
|
|
3600
|
-
if (!action) return action;
|
|
3601
|
-
if (qrImageURL) {
|
|
3602
|
-
action.preferred_qr_url = qrImageURL;
|
|
3603
|
-
action.preferred_qr_mime = qrMimeType(qrImageURL, action);
|
|
3604
|
-
}
|
|
3605
|
-
const mobileURL = action.mobile_wallet_url || humanActionPresentationURL(action, "mobile_wallet_url");
|
|
3606
|
-
if (mobileURL) action.mobile_wallet_url = mobileURL;
|
|
3607
|
-
action.agent_display_hint = {
|
|
3608
|
-
primary: action.local_qr_path ? "local_qr_path" : (action.qr_png_url ? "qr_png_url" : "preferred_qr_url"),
|
|
3609
|
-
desktop: action.kind === "auth_qr"
|
|
3610
|
-
? "Show local_qr_path when present; otherwise show the ItPay first-purchase entry URL. This QR starts login/registration/profile authorization and should continue to payment for the same checkout after approval; it is not payment proof."
|
|
3611
|
-
: "Show local_qr_path when present; otherwise show qr_png_url/preferred_qr_url directly. This is an ItPay-hosted human QR image and may render the native provider payment code; do not render your own QR from payment_entry_url.",
|
|
3612
|
-
mobile: "Show mobile_wallet_url as a clickable human-only fallback when present.",
|
|
3613
|
-
proof: "Only payment_intent.verified proves payment. QR display or page open is not payment proof."
|
|
3614
|
-
};
|
|
3615
|
-
return action;
|
|
3616
|
-
}
|
|
3617
|
-
|
|
3618
|
-
function shouldPrepareLocalQRForJSON(mode, flags = {}, action = {}) {
|
|
3619
|
-
if (mode === "file" || flags.qr_file || process.env.ITP_QR_FILE) return true;
|
|
3620
|
-
if (action?.qr_png_url || action?.preferred_qr_url || action?.qr_image_url) return true;
|
|
3621
|
-
return false;
|
|
3622
|
-
}
|
|
3623
|
-
|
|
3624
|
-
function shouldGenerateLocalQRFromActionURL(action = {}, mode = "", flags = {}) {
|
|
3625
|
-
if (!action?.url) return false;
|
|
3626
|
-
if (action.kind === "auth_qr") return true;
|
|
3627
|
-
return mode === "file" || Boolean(flags.qr_file || process.env.ITP_QR_FILE);
|
|
3628
|
-
}
|
|
3629
|
-
|
|
3630
|
-
async function prepareLocalQRFile(action, qrImageURL, flags = {}, optional = false) {
|
|
3631
|
-
if (!qrImageURL) return "";
|
|
3632
|
-
const file = flags.qr_file || process.env.ITP_QR_FILE || defaultQRFilePath(action, qrImageURL);
|
|
3633
|
-
try {
|
|
3634
|
-
await downloadQRImage(qrImageURL, file, flags);
|
|
3635
|
-
} catch (error) {
|
|
3636
|
-
if (!optional) throw error;
|
|
3637
|
-
action.local_qr_error = safeErrorMessage(error);
|
|
3638
|
-
return "";
|
|
3639
|
-
}
|
|
3640
|
-
action.local_qr_path = file;
|
|
3641
|
-
action.local_qr_mime = qrMimeType(qrImageURL, action);
|
|
3642
|
-
return file;
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
async function prepareLocalQRFromActionURL(action, flags = {}, optional = false) {
|
|
3646
|
-
if (!action?.url) return "";
|
|
3647
|
-
const file = flags.qr_file || process.env.ITP_QR_FILE || defaultGeneratedQRFilePath(action);
|
|
3648
|
-
try {
|
|
3649
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
3650
|
-
await QRCode.toFile(file, action.url, {
|
|
3651
|
-
type: "png",
|
|
3652
|
-
errorCorrectionLevel: "M",
|
|
3653
|
-
margin: 2,
|
|
3654
|
-
width: 512
|
|
3655
|
-
});
|
|
3656
|
-
} catch (error) {
|
|
3657
|
-
if (!optional) throw error;
|
|
3658
|
-
action.local_qr_error = safeErrorMessage(error);
|
|
3659
|
-
return "";
|
|
3660
|
-
}
|
|
3661
|
-
action.local_qr_path = file;
|
|
3662
|
-
action.local_qr_mime = "image/png";
|
|
3663
|
-
return file;
|
|
3664
|
-
}
|
|
3665
|
-
|
|
3666
|
-
function defaultQRFilePath(action, qrImageURL) {
|
|
3667
|
-
const id = sanitizeFilename(action?.payment_intent_id || action?.id || "qr");
|
|
3668
|
-
return path.join(os.tmpdir(), `itp-${id}.${qrFileExtension(qrImageURL, action)}`);
|
|
3669
|
-
}
|
|
3670
|
-
|
|
3671
|
-
function defaultGeneratedQRFilePath(action) {
|
|
3672
|
-
const id = sanitizeFilename(action?.payment_intent_id || action?.id || "qr");
|
|
3673
|
-
return path.join(os.tmpdir(), `itp-${id}.png`);
|
|
3674
|
-
}
|
|
3675
|
-
|
|
3676
|
-
function qrFileExtension(qrImageURL, action = {}) {
|
|
3677
|
-
const mime = qrMimeType(qrImageURL, action);
|
|
3678
|
-
if (mime === "image/png") return "png";
|
|
3679
|
-
if (mime === "image/svg+xml") return "svg";
|
|
3680
|
-
return "img";
|
|
3681
|
-
}
|
|
3682
|
-
|
|
3683
|
-
function qrMimeType(qrImageURL, action = {}) {
|
|
3684
|
-
if (qrImageURL && action?.qr_png_url && qrImageURL === action.qr_png_url) return "image/png";
|
|
3685
|
-
if (String(qrImageURL || "").toLowerCase().includes(".png")) return "image/png";
|
|
3686
|
-
if (String(qrImageURL || "").toLowerCase().includes(".svg")) return "image/svg+xml";
|
|
3687
|
-
return action?.preferred_qr_mime || "image/png";
|
|
3688
|
-
}
|
|
3689
|
-
|
|
3690
|
-
function sanitizeFilename(value) {
|
|
3691
|
-
return String(value || "qr").replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 96) || "qr";
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
|
-
function formatActionTime(value) {
|
|
3695
|
-
if (value === null || value === undefined || value === "") return "";
|
|
3696
|
-
if (typeof value === "number" || /^[0-9]+$/.test(String(value))) {
|
|
3697
|
-
const numeric = Number(value);
|
|
3698
|
-
const millis = numeric > 100000000000 ? numeric : numeric * 1000;
|
|
3699
|
-
const date = new Date(millis);
|
|
3700
|
-
if (!Number.isNaN(date.getTime())) return date.toISOString();
|
|
3701
|
-
}
|
|
3702
|
-
const date = new Date(String(value));
|
|
3703
|
-
if (!Number.isNaN(date.getTime())) return date.toISOString();
|
|
3704
|
-
return String(value);
|
|
3705
|
-
}
|
|
3706
|
-
|
|
3707
|
-
function shouldUseAgentTextQR(flags = {}) {
|
|
3708
|
-
const mode = String(flags.display || process.env.ITP_DISPLAY || "").toLowerCase() || "auto";
|
|
3709
|
-
const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
|
|
3710
|
-
return mode === "chat" || mode === "agent" || (["gemini", "gemini-cli"].includes(host) && mode === "auto");
|
|
3711
|
-
}
|
|
3712
|
-
|
|
3713
|
-
function shouldReturnAfterAgentTextQR(flags = {}) {
|
|
3714
|
-
if (flags.wait || flags.wait_human) return false;
|
|
3715
|
-
return shouldUseAgentTextQR(flags);
|
|
3716
|
-
}
|
|
3717
|
-
|
|
3718
|
-
function attachAgentQRImage(action, qrImageURL, localPath = "") {
|
|
3719
|
-
if (!action || !qrImageURL) return action;
|
|
3720
|
-
action.preferred_qr_url = qrImageURL;
|
|
3721
|
-
if (qrMimeType(qrImageURL, action) === "image/png") {
|
|
3722
|
-
action.qr_png_url = action.qr_png_url || qrImageURL;
|
|
3723
|
-
} else {
|
|
3724
|
-
action.qr_image_url = action.qr_image_url || qrImageURL;
|
|
3725
|
-
}
|
|
3726
|
-
if (!Array.isArray(action.display)) {
|
|
3727
|
-
action.display = [];
|
|
3728
|
-
}
|
|
3729
|
-
if (!action.display.some((item) => item?.type === "image")) {
|
|
3730
|
-
action.display.push({
|
|
3731
|
-
type: "image",
|
|
3732
|
-
format: qrFileExtension(qrImageURL, action),
|
|
3733
|
-
url: qrImageURL,
|
|
3734
|
-
local_path: localPath || undefined,
|
|
3735
|
-
instructions: "Render local_path when present; otherwise render this ItPay-hosted QR image for the human to scan. ItPay may render a native provider payment code inside the image, but the agent must not request, decode, or expose provider payloads. Do not encode payment_entry_url or mobile_wallet_url into your own QR."
|
|
3736
|
-
});
|
|
3737
|
-
}
|
|
3738
|
-
return action;
|
|
3739
|
-
}
|
|
3740
|
-
|
|
3741
|
-
function attachAgentLocalQR(action, localPath = "") {
|
|
3742
|
-
if (!action || !localPath) return action;
|
|
3743
|
-
if (!Array.isArray(action.display)) {
|
|
3744
|
-
action.display = [];
|
|
3745
|
-
}
|
|
3746
|
-
if (!action.display.some((item) => item?.type === "image" && item?.local_path === localPath)) {
|
|
3747
|
-
action.display.push({
|
|
3748
|
-
type: "image",
|
|
3749
|
-
format: "png",
|
|
3750
|
-
local_path: localPath,
|
|
3751
|
-
instructions: "Render this local QR image for the human to scan. It encodes the ItPay human action URL, not a provider raw QR payload."
|
|
3752
|
-
});
|
|
3753
|
-
}
|
|
3754
|
-
return action;
|
|
3755
|
-
}
|
|
3756
|
-
|
|
3757
|
-
async function downloadQRImage(qrImageURL, file, flags = {}) {
|
|
3758
|
-
const controller = new AbortController();
|
|
3759
|
-
const timer = setTimeout(() => controller.abort(), apiTimeoutMs(flags));
|
|
3760
|
-
let response;
|
|
3761
|
-
try {
|
|
3762
|
-
response = await fetch(qrImageURL, { method: "GET", signal: controller.signal });
|
|
3763
|
-
} catch (error) {
|
|
3764
|
-
if (error?.name === "AbortError") {
|
|
3765
|
-
throw new Error(`QR image download timed out: ${qrImageURL}`);
|
|
3766
|
-
}
|
|
3767
|
-
throw error;
|
|
3768
|
-
} finally {
|
|
3769
|
-
clearTimeout(timer);
|
|
3770
|
-
}
|
|
3771
|
-
if (!response.ok) {
|
|
3772
|
-
throw new Error(`QR image download failed: ${response.status}`);
|
|
3773
|
-
}
|
|
3774
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
3775
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
3776
|
-
fs.writeFileSync(file, bytes);
|
|
3777
|
-
}
|
|
3778
|
-
|
|
3779
|
-
function persistHumanAction(action, flags = {}) {
|
|
3780
|
-
const runId = flags.run_id || readState().current_run_id;
|
|
3781
|
-
if (!runId || !action?.id) return;
|
|
3782
|
-
const run = readRun(runId);
|
|
3783
|
-
if (!run?.human_action?.id || run.human_action.id !== action.id) return;
|
|
3784
|
-
writeRun(mergeRun(run, { human_action: action }));
|
|
3785
|
-
}
|
|
3786
|
-
|
|
3787
|
-
function shouldRenderTerminalQR(host) {
|
|
3788
|
-
if (process.stderr.isTTY) return true;
|
|
3789
|
-
return ["gemini", "gemini-cli"].includes(host);
|
|
3790
|
-
}
|
|
3791
|
-
|
|
3792
|
-
function terminalQRType(flags = {}, host = "") {
|
|
3793
|
-
const requested = String(flags.qr_format || process.env.ITP_QR_FORMAT || "").toLowerCase();
|
|
3794
|
-
if (requested === "unicode" || requested === "utf8") return "utf8";
|
|
3795
|
-
if (requested === "ansi" || requested === "terminal") return "terminal";
|
|
3796
|
-
if (["gemini", "gemini-cli"].includes(host)) return "utf8";
|
|
3797
|
-
return "terminal";
|
|
3798
|
-
}
|
|
3799
|
-
|
|
3800
|
-
function shouldOpenBrowser(flags = {}) {
|
|
3801
|
-
if (flags.no_open_browser || process.env.ITP_OPEN_BROWSER === "false" || process.env.ITP_OPEN_BROWSER === "0") return false;
|
|
3802
|
-
if (flags.open_browser || process.env.ITP_OPEN_BROWSER === "true" || process.env.ITP_OPEN_BROWSER === "1") return true;
|
|
3803
|
-
if (process.env.SSH_CONNECTION || process.env.CI) return false;
|
|
3804
|
-
return Boolean(process.stderr.isTTY && (process.platform === "darwin" || process.platform === "win32" || process.env.DISPLAY || process.env.WAYLAND_DISPLAY || process.env.WSL_DISTRO_NAME));
|
|
3805
|
-
}
|
|
3806
|
-
|
|
3807
|
-
function openBrowser(targetURL) {
|
|
3808
|
-
try {
|
|
3809
|
-
if (process.platform === "darwin") {
|
|
3810
|
-
execFileSync("open", [targetURL], { stdio: "ignore", timeout: 2000 });
|
|
3811
|
-
return true;
|
|
3812
|
-
}
|
|
3813
|
-
if (process.platform === "win32") {
|
|
3814
|
-
execFileSync("cmd", ["/c", "start", "", targetURL], { stdio: "ignore", timeout: 2000 });
|
|
3815
|
-
return true;
|
|
3816
|
-
}
|
|
3817
|
-
if (process.env.WSL_DISTRO_NAME && commandExists("cmd.exe")) {
|
|
3818
|
-
execFileSync("cmd.exe", ["/c", "start", "", targetURL], { stdio: "ignore", timeout: 2000 });
|
|
3819
|
-
return true;
|
|
3820
|
-
}
|
|
3821
|
-
if (commandExists("xdg-open")) {
|
|
3822
|
-
execFileSync("xdg-open", [targetURL], { stdio: "ignore", timeout: 2000 });
|
|
3823
|
-
return true;
|
|
3824
|
-
}
|
|
3825
|
-
} catch {
|
|
3826
|
-
return false;
|
|
3827
|
-
}
|
|
3828
|
-
return false;
|
|
3829
|
-
}
|
|
3830
|
-
|
|
3831
|
-
async function admin(command, rest, flags) {
|
|
3832
|
-
if (command === "orders") {
|
|
3833
|
-
const params = new URLSearchParams();
|
|
3834
|
-
for (const key of ["account_id", "status", "provider", "limit"]) {
|
|
3835
|
-
if (flags[key]) params.set(key, flags[key]);
|
|
3836
|
-
}
|
|
3837
|
-
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
3838
|
-
output(await api(`/api/itp/admin/orders${suffix}`, { method: "GET" }, flags));
|
|
3839
|
-
return;
|
|
3840
|
-
}
|
|
3841
|
-
if (command === "payment-events") {
|
|
3842
|
-
const params = new URLSearchParams();
|
|
3843
|
-
for (const key of ["order_id", "checkout_id", "out_trade_no", "provider", "event_type", "signature_verified", "limit"]) {
|
|
3844
|
-
if (flags[key]) params.set(key, flags[key]);
|
|
3845
|
-
}
|
|
3846
|
-
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
3847
|
-
output(await api(`/api/itp/admin/payment-events${suffix}`, { method: "GET" }, flags));
|
|
3848
|
-
return;
|
|
3849
|
-
}
|
|
3850
|
-
if (command === "outbox") {
|
|
3851
|
-
const params = new URLSearchParams();
|
|
3852
|
-
for (const key of ["status", "event_type", "aggregate_id", "limit"]) {
|
|
3853
|
-
if (flags[key]) params.set(key, flags[key]);
|
|
3854
|
-
}
|
|
3855
|
-
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
3856
|
-
output(await api(`/api/itp/admin/outbox${suffix}`, { method: "GET" }, flags));
|
|
3857
|
-
return;
|
|
3858
|
-
}
|
|
3859
|
-
if (command === "process-outbox") {
|
|
3860
|
-
output(await api("/api/itp/admin/outbox/process", { method: "POST" }, flags));
|
|
3861
|
-
return;
|
|
3862
|
-
}
|
|
3863
|
-
if (command === "recover-order") {
|
|
3864
|
-
const orderId = rest[0];
|
|
3865
|
-
if (!orderId) throw new Error("order_id is required");
|
|
3866
|
-
output(await api(`/api/itp/admin/orders/${encodeURIComponent(orderId)}/recover`, { method: "POST" }, flags));
|
|
3867
|
-
return;
|
|
3868
|
-
}
|
|
3869
|
-
throw new Error(`unknown admin command: ${command || ""}`);
|
|
3870
|
-
}
|
|
3871
|
-
|
|
3872
|
-
async function coreApi(pathname, options = {}, flags = {}) {
|
|
3873
|
-
const headers = { "Content-Type": "application/json" };
|
|
3874
|
-
const credentials = readCredentials();
|
|
3875
|
-
if (flags.access_token) {
|
|
3876
|
-
const raw = String(flags.access_token);
|
|
3877
|
-
headers.Authorization = raw.toLowerCase().startsWith("bearer ") ? raw : `Bearer ${raw}`;
|
|
3878
|
-
} else {
|
|
3879
|
-
const sessionToken = readSessionToken(credentials);
|
|
3880
|
-
if (sessionToken) headers.Authorization = `Bearer ${sessionToken}`;
|
|
3881
|
-
}
|
|
3882
|
-
if (options.idempotencyKey || flags.idempotency_key) {
|
|
3883
|
-
headers["Idempotency-Key"] = String(options.idempotencyKey || flags.idempotency_key);
|
|
3884
|
-
}
|
|
3885
|
-
if (!options.noAgentHeaders) {
|
|
3886
|
-
headers["X-ItPay-Agent-Fingerprint"] = coreAgentFingerprint(flags);
|
|
3887
|
-
headers["X-ItPay-Agent-Name"] = coreAgentDisplayName(flags);
|
|
3888
|
-
}
|
|
3889
|
-
if (options.ops) {
|
|
3890
|
-
headers["X-ItPay-Ops-Token"] = sandboxOpsToken(flags);
|
|
3891
|
-
}
|
|
3892
|
-
Object.assign(headers, options.headers || {});
|
|
3893
|
-
if (flags.request_id) headers["X-Request-ID"] = String(flags.request_id);
|
|
3894
|
-
if (flags.correlation_id) headers["X-Correlation-ID"] = String(flags.correlation_id);
|
|
3895
|
-
const targetURL = coreURL(pathname, flags);
|
|
3896
|
-
const timeoutMs = apiTimeoutMs(flags);
|
|
3897
|
-
const controller = new AbortController();
|
|
3898
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3899
|
-
let response;
|
|
3900
|
-
try {
|
|
3901
|
-
response = await fetch(targetURL, {
|
|
3902
|
-
method: options.method || "GET",
|
|
3903
|
-
headers,
|
|
3904
|
-
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
3905
|
-
signal: controller.signal
|
|
3906
|
-
});
|
|
3907
|
-
} catch (error) {
|
|
3908
|
-
if (error?.name === "AbortError") {
|
|
3909
|
-
throw new Error(`request timed out after ${Math.ceil(timeoutMs / 1000)}s: ${safeRequestTarget(targetURL)}`);
|
|
3910
|
-
}
|
|
3911
|
-
throw new Error(`network request failed: ${safeRequestTarget(targetURL)}: ${safeErrorMessage(error)}`);
|
|
3912
|
-
} finally {
|
|
3913
|
-
clearTimeout(timer);
|
|
3914
|
-
}
|
|
3915
|
-
const text = await response.text();
|
|
3916
|
-
let payload = {};
|
|
3917
|
-
if (text) {
|
|
3918
|
-
try {
|
|
3919
|
-
payload = JSON.parse(text);
|
|
3920
|
-
} catch {
|
|
3921
|
-
payload = { text };
|
|
3922
|
-
}
|
|
3923
|
-
}
|
|
3924
|
-
if (!response.ok || payload.success === false) {
|
|
3925
|
-
const error = new Error(payload.error || payload.message || `request failed: ${response.status}`);
|
|
3926
|
-
error.status = response.status;
|
|
3927
|
-
if (response.status === 401 && String(pathname).startsWith("/v1/me/")) {
|
|
3928
|
-
error.message = "buyer session required or expired; run status --refresh --json, then run setup --method alipay --json if unauthenticated";
|
|
3929
|
-
error.next = [
|
|
3930
|
-
{ type: "verify_buyer_session", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true },
|
|
3931
|
-
{ type: "start_auth_if_needed", command: cliCommand("setup", "--method", "alipay", "--json"), safe_for_agent: true }
|
|
3932
|
-
];
|
|
3933
|
-
}
|
|
3934
|
-
throw error;
|
|
3935
|
-
}
|
|
3936
|
-
return payload.data ?? payload;
|
|
3937
|
-
}
|
|
3938
|
-
|
|
3939
|
-
function safeRequestTarget(target) {
|
|
3940
|
-
try {
|
|
3941
|
-
const url = new URL(String(target));
|
|
3942
|
-
return `${url.origin}${url.pathname}`;
|
|
3943
|
-
} catch {
|
|
3944
|
-
return String(target).split("?")[0];
|
|
3945
|
-
}
|
|
3946
|
-
}
|
|
3947
|
-
|
|
3948
|
-
function coreAgentFingerprint(flags = {}) {
|
|
3949
|
-
const explicit = flags.agent_fingerprint || flags.agent_device_fingerprint || process.env.ITPAY_AGENT_FINGERPRINT || process.env.ITPAY_AGENT_DEVICE_FINGERPRINT;
|
|
3950
|
-
if (explicit) return String(explicit);
|
|
3951
|
-
const state = readState();
|
|
3952
|
-
if (state.core_agent_fingerprint) return state.core_agent_fingerprint;
|
|
3953
|
-
const fingerprint = `itp_cli_${cryptoRandom()}`;
|
|
3954
|
-
writeState({ ...state, core_agent_fingerprint: fingerprint });
|
|
3955
|
-
return fingerprint;
|
|
3956
|
-
}
|
|
3957
|
-
|
|
3958
|
-
function coreAgentDisplayName(flags = {}) {
|
|
3959
|
-
return String(flags.agent_name || flags.agent_display_name || process.env.ITPAY_AGENT_NAME || "ItPay CLI buyer agent");
|
|
3960
|
-
}
|
|
3961
|
-
|
|
3962
|
-
function coreURL(pathname, flags = {}) {
|
|
3963
|
-
if (/^https?:\/\//i.test(String(pathname))) return String(pathname);
|
|
3964
|
-
return `${coreApiBase(flags)}${pathname.startsWith("/") ? "" : "/"}${pathname}`;
|
|
3965
|
-
}
|
|
3966
|
-
|
|
3967
|
-
function coreApiBase(flags = {}, config = readConfig()) {
|
|
3968
|
-
const base = flags.api_base || flags.core_api_base || process.env.ITPAY_API_BASE || process.env.ITPAY_CORE_API_BASE || process.env.ITPAY_CORE_BASE_URL || config.api_base || "https://dev.api.itpay.ai";
|
|
3969
|
-
return String(base).replace(/\/$/, "");
|
|
3970
|
-
}
|
|
3971
|
-
|
|
3972
|
-
function sandboxOpsToken(flags = {}) {
|
|
3973
|
-
const token = flags.ops_token || flags.sandbox_ops_token || process.env.ITPAY_SANDBOX_OPS_TOKEN || process.env.ITPAY_OPS_TOKEN;
|
|
3974
|
-
if (!token) throw new Error("sandbox ops token is required; set ITPAY_SANDBOX_OPS_TOKEN or pass --ops-token");
|
|
3975
|
-
return String(token);
|
|
3976
|
-
}
|
|
3977
|
-
|
|
3978
|
-
function queryString(params) {
|
|
3979
|
-
const raw = params.toString();
|
|
3980
|
-
return raw ? `?${raw}` : "";
|
|
3981
|
-
}
|
|
3982
|
-
|
|
3983
|
-
function appendURLQuery(target, params) {
|
|
3984
|
-
const suffix = params.toString();
|
|
3985
|
-
if (!suffix) return target;
|
|
3986
|
-
return `${target}${String(target).includes("?") ? "&" : "?"}${suffix}`;
|
|
3987
|
-
}
|
|
3988
|
-
|
|
3989
|
-
function positional(values, index) {
|
|
3990
|
-
const value = values[index];
|
|
3991
|
-
if (!value || String(value).startsWith("--")) return "";
|
|
3992
|
-
return String(value);
|
|
3993
|
-
}
|
|
3994
|
-
|
|
3995
|
-
function positionalArgs(values = []) {
|
|
3996
|
-
const result = [];
|
|
3997
|
-
for (let i = 0; i < values.length; i += 1) {
|
|
3998
|
-
const value = values[i];
|
|
3999
|
-
if (!value) continue;
|
|
4000
|
-
if (String(value).startsWith("--")) {
|
|
4001
|
-
const next = values[i + 1];
|
|
4002
|
-
if (next && !String(next).startsWith("--")) i += 1;
|
|
4003
|
-
continue;
|
|
4004
|
-
}
|
|
4005
|
-
result.push(String(value));
|
|
4006
|
-
}
|
|
4007
|
-
return result;
|
|
4008
|
-
}
|
|
4009
|
-
|
|
4010
|
-
function stripInternalBuyerFields(value) {
|
|
4011
|
-
if (Array.isArray(value)) return value.map(stripInternalBuyerFields);
|
|
4012
|
-
if (!value || typeof value !== "object") return value;
|
|
4013
|
-
const result = {};
|
|
4014
|
-
for (const [key, nested] of Object.entries(value)) {
|
|
4015
|
-
if (key === "next_actions") continue;
|
|
4016
|
-
result[key] = stripInternalBuyerFields(nested);
|
|
4017
|
-
}
|
|
4018
|
-
return result;
|
|
4019
|
-
}
|
|
4020
|
-
|
|
4021
|
-
async function api(pathname, options, flags = {}) {
|
|
4022
|
-
const config = readConfig();
|
|
4023
|
-
const credentials = readCredentials();
|
|
4024
|
-
const base = apiBase(flags, config);
|
|
4025
|
-
const headers = { "Content-Type": "application/json" };
|
|
4026
|
-
if (flags.access_token) {
|
|
4027
|
-
headers.Authorization = String(flags.access_token);
|
|
4028
|
-
} else {
|
|
4029
|
-
const sessionToken = readSessionToken(credentials);
|
|
4030
|
-
if (sessionToken) {
|
|
4031
|
-
headers.Authorization = `Bearer ${sessionToken}`;
|
|
4032
|
-
}
|
|
4033
|
-
}
|
|
4034
|
-
if (flags.new_api_user || flags.new_api_user_id || flags.user_id) {
|
|
4035
|
-
headers["New-Api-User"] = String(flags.new_api_user || flags.new_api_user_id || flags.user_id);
|
|
4036
|
-
}
|
|
4037
|
-
const timeoutMs = apiTimeoutMs(flags);
|
|
4038
|
-
const controller = new AbortController();
|
|
4039
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4040
|
-
let response;
|
|
4041
|
-
try {
|
|
4042
|
-
response = await fetch(`${base}${pathname}`, {
|
|
4043
|
-
method: options.method,
|
|
4044
|
-
headers,
|
|
4045
|
-
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
4046
|
-
signal: controller.signal
|
|
4047
|
-
});
|
|
4048
|
-
} catch (error) {
|
|
4049
|
-
if (error?.name === "AbortError") {
|
|
4050
|
-
throw new Error(`request timed out after ${Math.ceil(timeoutMs / 1000)}s: ${pathname}`);
|
|
4051
|
-
}
|
|
4052
|
-
throw error;
|
|
4053
|
-
} finally {
|
|
4054
|
-
clearTimeout(timer);
|
|
4055
|
-
}
|
|
4056
|
-
const payload = await response.json().catch(() => ({}));
|
|
4057
|
-
if (!response.ok || payload.success === false) {
|
|
4058
|
-
throw new Error(payload.message || `request failed: ${response.status}`);
|
|
4059
|
-
}
|
|
4060
|
-
return payload.data ?? payload;
|
|
4061
|
-
}
|
|
4062
|
-
|
|
4063
|
-
function apiTimeoutMs(flags = {}) {
|
|
4064
|
-
const seconds = Number(flags.api_timeout || process.env.ITP_API_TIMEOUT_SECONDS || 45);
|
|
4065
|
-
if (!Number.isFinite(seconds) || seconds <= 0) return 45000;
|
|
4066
|
-
return Math.max(5000, seconds * 1000);
|
|
4067
|
-
}
|
|
4068
|
-
|
|
4069
|
-
function parseFlags(args) {
|
|
4070
|
-
const flags = {};
|
|
4071
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
4072
|
-
const arg = args[i];
|
|
4073
|
-
if (!arg.startsWith("--")) continue;
|
|
4074
|
-
const key = arg.slice(2).replaceAll("-", "_");
|
|
4075
|
-
const next = args[i + 1];
|
|
4076
|
-
if (!next || next.startsWith("--")) {
|
|
4077
|
-
flags[key] = true;
|
|
4078
|
-
} else {
|
|
4079
|
-
flags[key] = next;
|
|
4080
|
-
i += 1;
|
|
4081
|
-
}
|
|
4082
|
-
}
|
|
4083
|
-
return flags;
|
|
4084
|
-
}
|
|
4085
|
-
|
|
4086
|
-
function normalizePurchaseFlags(flags, required = false) {
|
|
4087
|
-
const plan = typeof flags.plan === "string" ? flags.plan.trim() : "";
|
|
4088
|
-
const rawCredits = flags.credits ?? flags.credit ?? null;
|
|
4089
|
-
const hasCredits = rawCredits !== null && rawCredits !== undefined && rawCredits !== false;
|
|
4090
|
-
if (plan && hasCredits) {
|
|
4091
|
-
throw new Error("use either --plan or --credits, not both");
|
|
4092
|
-
}
|
|
4093
|
-
if (hasCredits) {
|
|
4094
|
-
const credits = Number(rawCredits);
|
|
4095
|
-
if (!Number.isInteger(credits) || credits < 20) {
|
|
4096
|
-
throw new Error("--credits must be an integer greater than or equal to 20");
|
|
4097
|
-
}
|
|
4098
|
-
return {
|
|
4099
|
-
kind: "custom",
|
|
4100
|
-
plan: null,
|
|
4101
|
-
credits,
|
|
4102
|
-
key: `credits-${credits}`
|
|
4103
|
-
};
|
|
4104
|
-
}
|
|
4105
|
-
if (plan) {
|
|
4106
|
-
if (plan === "coding-100") {
|
|
4107
|
-
throw new Error("coding-100 is disabled; use credit-100, credit-300, credit-500, or --credits <amount>");
|
|
4108
|
-
}
|
|
4109
|
-
return {
|
|
4110
|
-
kind: "plan",
|
|
4111
|
-
plan,
|
|
4112
|
-
credits: null,
|
|
4113
|
-
key: `plan-${plan}`
|
|
4114
|
-
};
|
|
4115
|
-
}
|
|
4116
|
-
if (required) {
|
|
4117
|
-
throw new Error("choose a purchase: --credits <integer >=20> or --plan credit-100|credit-300|credit-500");
|
|
4118
|
-
}
|
|
4119
|
-
return { kind: null, plan: null, credits: null, key: "none" };
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
|
-
function apiBase(flags = {}, config = readConfig()) {
|
|
4123
|
-
return (flags.api_base || config.api_base || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
4124
|
-
}
|
|
4125
|
-
|
|
4126
|
-
function readConfig() {
|
|
4127
|
-
return readJSON(CONFIG_PATH, {});
|
|
4128
|
-
}
|
|
4129
|
-
|
|
4130
|
-
function packageVersion() {
|
|
4131
|
-
try {
|
|
4132
|
-
return JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8")).version || "0.0.0";
|
|
4133
|
-
} catch {
|
|
4134
|
-
return "0.0.0";
|
|
4135
|
-
}
|
|
4136
|
-
}
|
|
4137
|
-
|
|
4138
|
-
function writeConfig(value) {
|
|
4139
|
-
writeJSON0600(CONFIG_PATH, value);
|
|
4140
|
-
}
|
|
4141
|
-
|
|
4142
|
-
function readState() {
|
|
4143
|
-
return readJSON(STATE_PATH, {});
|
|
4144
|
-
}
|
|
4145
|
-
|
|
4146
|
-
function writeState(value) {
|
|
4147
|
-
writeJSON0600(STATE_PATH, value);
|
|
4148
|
-
}
|
|
4149
|
-
|
|
4150
|
-
function runPath(runId) {
|
|
4151
|
-
return path.join(RUNS_DIR, `${runId}.json`);
|
|
4152
|
-
}
|
|
4153
|
-
|
|
4154
|
-
function readRun(runId) {
|
|
4155
|
-
if (!runId) return null;
|
|
4156
|
-
return readJSON(runPath(runId), null);
|
|
4157
|
-
}
|
|
4158
|
-
|
|
4159
|
-
function listRuns() {
|
|
4160
|
-
if (!fs.existsSync(RUNS_DIR)) return [];
|
|
4161
|
-
return fs.readdirSync(RUNS_DIR)
|
|
4162
|
-
.filter((name) => name.endsWith(".json"))
|
|
4163
|
-
.map((name) => readJSON(path.join(RUNS_DIR, name), null))
|
|
4164
|
-
.filter(Boolean)
|
|
4165
|
-
.sort((a, b) => String(b.updated_at || "").localeCompare(String(a.updated_at || "")));
|
|
4166
|
-
}
|
|
4167
|
-
|
|
4168
|
-
function writeRun(run) {
|
|
4169
|
-
if (!run?.run_id) throw new Error("run_id is required");
|
|
4170
|
-
ensureConfigDir();
|
|
4171
|
-
fs.mkdirSync(RUNS_DIR, { recursive: true });
|
|
4172
|
-
try {
|
|
4173
|
-
fs.chmodSync(RUNS_DIR, 0o700);
|
|
4174
|
-
} catch {
|
|
4175
|
-
// Best effort on platforms without POSIX modes.
|
|
4176
|
-
}
|
|
4177
|
-
const next = {
|
|
4178
|
-
...run,
|
|
4179
|
-
schema_version: "itp.run.v1",
|
|
4180
|
-
updated_at: new Date().toISOString()
|
|
4181
|
-
};
|
|
4182
|
-
const file = runPath(next.run_id);
|
|
4183
|
-
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
4184
|
-
fs.writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
4185
|
-
fs.renameSync(tmp, file);
|
|
4186
|
-
try {
|
|
4187
|
-
fs.chmodSync(file, 0o600);
|
|
4188
|
-
} catch {
|
|
4189
|
-
// Best effort on platforms without POSIX modes.
|
|
4190
|
-
}
|
|
4191
|
-
writeState({ ...readState(), current_run_id: next.run_id });
|
|
4192
|
-
return next;
|
|
4193
|
-
}
|
|
4194
|
-
|
|
4195
|
-
function mergeRun(run, patch) {
|
|
4196
|
-
return {
|
|
4197
|
-
...(run || {}),
|
|
4198
|
-
...patch,
|
|
4199
|
-
auth: patch.auth === undefined ? run?.auth : { ...(run?.auth || {}), ...(patch.auth || {}) },
|
|
4200
|
-
account: patch.account === undefined ? run?.account : { ...(run?.account || {}), ...(patch.account || {}) },
|
|
4201
|
-
checkout: patch.checkout === undefined ? run?.checkout : { ...(run?.checkout || {}), ...(patch.checkout || {}) },
|
|
4202
|
-
payment: patch.payment === undefined ? run?.payment : { ...(run?.payment || {}), ...(patch.payment || {}) },
|
|
4203
|
-
grant: patch.grant === undefined ? run?.grant : { ...(run?.grant || {}), ...(patch.grant || {}) },
|
|
4204
|
-
result: patch.result === undefined ? run?.result : { ...(run?.result || {}), ...(patch.result || {}) }
|
|
4205
|
-
};
|
|
4206
|
-
}
|
|
4207
|
-
|
|
4208
|
-
function updateRun(run, patch) {
|
|
4209
|
-
return writeRun(mergeRun(run, patch));
|
|
4210
|
-
}
|
|
4211
|
-
|
|
4212
|
-
function updateCurrentRun(patch, flags = {}) {
|
|
4213
|
-
const run = readRun(flags.run_id || readState().current_run_id);
|
|
4214
|
-
if (!run) return null;
|
|
4215
|
-
return writeRun(mergeRun(run, patch));
|
|
4216
|
-
}
|
|
4217
|
-
|
|
4218
|
-
function prepareSetupRun(flags, options) {
|
|
4219
|
-
const explicitRunId = flags.run_id || null;
|
|
4220
|
-
const state = readState();
|
|
4221
|
-
let run = explicitRunId ? readRun(explicitRunId) : (!flags.new_run ? readRun(state.current_run_id) : null);
|
|
4222
|
-
const reusable = run
|
|
4223
|
-
&& !["done", "installed", "failed", "cancelled"].includes(run.status)
|
|
4224
|
-
&& run.phase !== "done"
|
|
4225
|
-
&& (!run.plan_id || run.plan_id === options.plan)
|
|
4226
|
-
&& (options.plan || !run.credits || Number(run.credits) === Number(options.credits || 0))
|
|
4227
|
-
&& (!run.payment_method || run.payment_method === options.method);
|
|
4228
|
-
if (reusable) {
|
|
4229
|
-
return writeRun(mergeRun(run, {
|
|
4230
|
-
target: options.target,
|
|
4231
|
-
plan_id: options.plan,
|
|
4232
|
-
credits: options.credits,
|
|
4233
|
-
purchase_kind: options.plan ? "plan" : "custom",
|
|
4234
|
-
payment_method: options.method,
|
|
4235
|
-
agent_host: flags.host || run.agent_host || null,
|
|
4236
|
-
agent_display: flags.display || run.agent_display || null,
|
|
4237
|
-
agent_qr_format: flags.qr_format || run.agent_qr_format || null,
|
|
4238
|
-
install_runtime: Boolean(options.install_runtime),
|
|
4239
|
-
status: "running"
|
|
4240
|
-
}));
|
|
4241
|
-
}
|
|
4242
|
-
if (flags.resume && explicitRunId && !run) {
|
|
4243
|
-
throw new Error(`run not found: ${explicitRunId}`);
|
|
4244
|
-
}
|
|
4245
|
-
const runId = explicitRunId || `run_${cryptoRandom()}`;
|
|
4246
|
-
return writeRun({
|
|
4247
|
-
schema_version: "itp.run.v1",
|
|
4248
|
-
run_id: runId,
|
|
4249
|
-
created_at: new Date().toISOString(),
|
|
4250
|
-
api_base: apiBase(flags),
|
|
4251
|
-
target: options.target,
|
|
4252
|
-
install_runtime: Boolean(options.install_runtime),
|
|
4253
|
-
plan_id: options.plan,
|
|
4254
|
-
credits: options.credits,
|
|
4255
|
-
purchase_kind: options.plan ? "plan" : "custom",
|
|
4256
|
-
payment_method: options.method,
|
|
4257
|
-
agent_host: flags.host || null,
|
|
4258
|
-
agent_display: flags.display || null,
|
|
4259
|
-
agent_qr_format: flags.qr_format || null,
|
|
4260
|
-
idempotency_key: flags.idempotency_key || `setup:${runId}:${options.plan || `credits-${options.credits}`}`,
|
|
4261
|
-
phase: "new",
|
|
4262
|
-
status: "running",
|
|
4263
|
-
safe_summary: "Setup started."
|
|
4264
|
-
});
|
|
4265
|
-
}
|
|
4266
|
-
|
|
4267
|
-
async function withStateLock(fn) {
|
|
4268
|
-
ensureConfigDir();
|
|
4269
|
-
const staleMs = 10 * 60 * 1000;
|
|
4270
|
-
try {
|
|
4271
|
-
const stat = fs.statSync(LOCK_PATH);
|
|
4272
|
-
const lock = readJSON(LOCK_PATH, {});
|
|
4273
|
-
if ((lock.pid && !processIsRunning(lock.pid)) || Date.now() - stat.mtimeMs > staleMs) {
|
|
4274
|
-
fs.unlinkSync(LOCK_PATH);
|
|
4275
|
-
}
|
|
4276
|
-
} catch {
|
|
4277
|
-
// No lock or unreadable stale state.
|
|
4278
|
-
}
|
|
4279
|
-
let fd;
|
|
4280
|
-
try {
|
|
4281
|
-
fd = fs.openSync(LOCK_PATH, "wx", 0o600);
|
|
4282
|
-
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
|
|
4283
|
-
} catch {
|
|
4284
|
-
const error = new Error(`another itp setup/status operation is running; if no other ItPay CLI is active, remove the stale lock and retry: rm -f ${LOCK_PATH}`);
|
|
4285
|
-
error.next = [
|
|
4286
|
-
{ type: "check_status", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true },
|
|
4287
|
-
{ type: "clear_stale_lock", command: `rm -f ${shellQuote(LOCK_PATH)}`, safe_for_agent: true }
|
|
4288
|
-
];
|
|
4289
|
-
throw error;
|
|
4290
|
-
} finally {
|
|
4291
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
4292
|
-
}
|
|
4293
|
-
try {
|
|
4294
|
-
return await fn();
|
|
4295
|
-
} finally {
|
|
4296
|
-
try {
|
|
4297
|
-
fs.unlinkSync(LOCK_PATH);
|
|
4298
|
-
} catch {
|
|
4299
|
-
// Best effort cleanup.
|
|
4300
|
-
}
|
|
4301
|
-
}
|
|
4302
|
-
}
|
|
4303
|
-
|
|
4304
|
-
function readCredentials() {
|
|
4305
|
-
return readJSON(CREDENTIALS_PATH, {});
|
|
4306
|
-
}
|
|
4307
|
-
|
|
4308
|
-
function writeCredentials(value) {
|
|
4309
|
-
writeJSON0600(CREDENTIALS_PATH, value);
|
|
4310
|
-
}
|
|
4311
|
-
|
|
4312
|
-
function writeJSON0600(file, value) {
|
|
4313
|
-
ensureConfigDir();
|
|
4314
|
-
fs.writeFileSync(file, JSON.stringify(value, null, 2), { mode: 0o600 });
|
|
4315
|
-
fs.chmodSync(file, 0o600);
|
|
4316
|
-
}
|
|
4317
|
-
|
|
4318
|
-
function writeSessionCredentials(response) {
|
|
4319
|
-
const currentAccountId = readConfig().account_id;
|
|
4320
|
-
let credentials = deleteSessionCredential(readCredentials());
|
|
4321
|
-
if (currentAccountId && currentAccountId !== response.account_id) {
|
|
4322
|
-
for (const key of Object.keys(credentials)) {
|
|
4323
|
-
if (key.startsWith("grant_")) {
|
|
4324
|
-
const grantId = key.slice("grant_".length);
|
|
4325
|
-
deleteGrantCredential(grantId);
|
|
4326
|
-
}
|
|
4327
|
-
}
|
|
4328
|
-
credentials = {};
|
|
4329
|
-
}
|
|
4330
|
-
writeCredentials({ ...credentials, ...storeSessionCredential(response) });
|
|
4331
|
-
}
|
|
4332
|
-
|
|
4333
|
-
function storeSessionCredential(response) {
|
|
4334
|
-
const token = response.session_token;
|
|
4335
|
-
const ref = `itpay:session:${response.account_id}:${response.device_id}`;
|
|
4336
|
-
const nativeStore = writeNativeSecret(ref, token);
|
|
4337
|
-
if (nativeStore.ok) {
|
|
4338
|
-
return {
|
|
4339
|
-
session_token_store: nativeStore.store,
|
|
4340
|
-
session_token_ref: nativeStore.ref
|
|
4341
|
-
};
|
|
4342
|
-
}
|
|
4343
|
-
return {
|
|
4344
|
-
session_token: token,
|
|
4345
|
-
session_token_store: "file",
|
|
4346
|
-
session_token_warning: nativeStore.error
|
|
4347
|
-
};
|
|
4348
|
-
}
|
|
4349
|
-
|
|
4350
|
-
function readSessionToken(credentials = readCredentials()) {
|
|
4351
|
-
if (credentials.session_token) {
|
|
4352
|
-
return credentials.session_token;
|
|
4353
|
-
}
|
|
4354
|
-
if (credentials.session_token_store && credentials.session_token_ref) {
|
|
4355
|
-
return readNativeSecret(credentials.session_token_store, credentials.session_token_ref);
|
|
4356
|
-
}
|
|
4357
|
-
return "";
|
|
4358
|
-
}
|
|
4359
|
-
|
|
4360
|
-
function deleteSessionCredential(credentials) {
|
|
4361
|
-
if (!credentials) {
|
|
4362
|
-
return {};
|
|
4363
|
-
}
|
|
4364
|
-
if (credentials.session_token_store && credentials.session_token_ref) {
|
|
4365
|
-
deleteNativeSecret(credentials.session_token_store, credentials.session_token_ref);
|
|
4366
|
-
}
|
|
4367
|
-
delete credentials.session_token;
|
|
4368
|
-
delete credentials.session_token_store;
|
|
4369
|
-
delete credentials.session_token_ref;
|
|
4370
|
-
delete credentials.session_token_warning;
|
|
4371
|
-
return credentials;
|
|
4372
|
-
}
|
|
4373
|
-
|
|
4374
|
-
function sanitizeAuthResponse(response) {
|
|
4375
|
-
const { session_token, ...safe } = response;
|
|
4376
|
-
return { ...safe, session_stored: Boolean(session_token) };
|
|
4377
|
-
}
|
|
4378
|
-
|
|
4379
|
-
function storeGrantCredential(grantId, credential) {
|
|
4380
|
-
const credentials = readCredentials();
|
|
4381
|
-
const key = credential.key;
|
|
4382
|
-
const record = { ...credential };
|
|
4383
|
-
delete record.key;
|
|
4384
|
-
const nativeStore = writeNativeSecret(grantSecretRef(grantId), key);
|
|
4385
|
-
if (nativeStore.ok) {
|
|
4386
|
-
record.credential_store = nativeStore.store;
|
|
4387
|
-
record.credential_ref = nativeStore.ref;
|
|
4388
|
-
} else {
|
|
4389
|
-
record.key = key;
|
|
4390
|
-
record.credential_store = "file";
|
|
4391
|
-
record.credential_warning = nativeStore.error;
|
|
4392
|
-
}
|
|
4393
|
-
credentials[`grant_${grantId}`] = record;
|
|
4394
|
-
writeCredentials(credentials);
|
|
4395
|
-
return record;
|
|
4396
|
-
}
|
|
4397
|
-
|
|
4398
|
-
function readGrantCredential(grantId) {
|
|
4399
|
-
const record = readCredentials()[`grant_${grantId}`];
|
|
4400
|
-
if (!record) return null;
|
|
4401
|
-
if (record.key) return record;
|
|
4402
|
-
const key = readNativeSecret(record.credential_store, record.credential_ref);
|
|
4403
|
-
return key ? { ...record, key } : record;
|
|
4404
|
-
}
|
|
4405
|
-
|
|
4406
|
-
function deleteGrantCredential(grantId) {
|
|
4407
|
-
const credentials = readCredentials();
|
|
4408
|
-
const record = credentials[`grant_${grantId}`];
|
|
4409
|
-
if (record?.credential_store && record?.credential_ref) {
|
|
4410
|
-
deleteNativeSecret(record.credential_store, record.credential_ref);
|
|
4411
|
-
}
|
|
4412
|
-
delete credentials[`grant_${grantId}`];
|
|
4413
|
-
writeCredentials(credentials);
|
|
4414
|
-
}
|
|
4415
|
-
|
|
4416
|
-
function grantSecretRef(grantId) {
|
|
4417
|
-
return `itpay:${grantId}`;
|
|
4418
|
-
}
|
|
4419
|
-
|
|
4420
|
-
function detectNativeCredentialStore() {
|
|
4421
|
-
if (!shouldUseNativeCredentialStore()) return "file";
|
|
4422
|
-
if (process.platform === "darwin" && commandExists("security")) return "macos-keychain";
|
|
4423
|
-
if (process.platform === "linux" && commandExists("secret-tool")) return "secret-tool";
|
|
4424
|
-
return "unavailable";
|
|
4425
|
-
}
|
|
4426
|
-
|
|
4427
|
-
function writeNativeSecret(ref, secret) {
|
|
4428
|
-
if (!secret) return { ok: false, error: "empty secret" };
|
|
4429
|
-
if (!shouldUseNativeCredentialStore()) {
|
|
4430
|
-
return { ok: false, error: "native credential store disabled for non-interactive agent host" };
|
|
4431
|
-
}
|
|
4432
|
-
if (process.platform === "darwin" && commandExists("security")) {
|
|
4433
|
-
try {
|
|
4434
|
-
execFileSync("security", [
|
|
4435
|
-
"add-generic-password",
|
|
4436
|
-
"-a",
|
|
4437
|
-
ref,
|
|
4438
|
-
"-s",
|
|
4439
|
-
"ItPay",
|
|
4440
|
-
"-w",
|
|
4441
|
-
secret,
|
|
4442
|
-
"-U"
|
|
4443
|
-
], { stdio: "ignore" });
|
|
4444
|
-
return { ok: true, store: "macos-keychain", ref };
|
|
4445
|
-
} catch (error) {
|
|
4446
|
-
return { ok: false, error: `macOS Keychain unavailable: ${error.message}` };
|
|
4447
|
-
}
|
|
4448
|
-
}
|
|
4449
|
-
if (process.platform === "linux" && commandExists("secret-tool")) {
|
|
4450
|
-
try {
|
|
4451
|
-
execFileSync("secret-tool", [
|
|
4452
|
-
"store",
|
|
4453
|
-
"--label=ItPay",
|
|
4454
|
-
"service",
|
|
4455
|
-
"ItPay",
|
|
4456
|
-
"account",
|
|
4457
|
-
ref
|
|
4458
|
-
], { input: secret, stdio: ["pipe", "ignore", "ignore"] });
|
|
4459
|
-
return { ok: true, store: "secret-tool", ref };
|
|
4460
|
-
} catch (error) {
|
|
4461
|
-
return { ok: false, error: `secret-tool unavailable: ${error.message}` };
|
|
4462
|
-
}
|
|
4463
|
-
}
|
|
4464
|
-
return { ok: false, error: "native credential store unavailable" };
|
|
4465
|
-
}
|
|
4466
|
-
|
|
4467
|
-
function shouldUseNativeCredentialStore() {
|
|
4468
|
-
const store = String(process.env.ITP_CREDENTIAL_STORE || "").toLowerCase();
|
|
4469
|
-
if (store === "file") return false;
|
|
4470
|
-
if (store === "native" || store === "keychain" || store === "secret-tool") return true;
|
|
4471
|
-
const disabled = String(process.env.ITP_DISABLE_NATIVE_CREDENTIAL_STORE || "").toLowerCase();
|
|
4472
|
-
if (["1", "true", "yes"].includes(disabled)) return false;
|
|
4473
|
-
if (process.env.CODEX_CI || process.env.CODEX_SHELL || process.env.CODEX_THREAD_ID) return false;
|
|
4474
|
-
if (process.env.CI && !process.env.GITHUB_ACTIONS) return false;
|
|
4475
|
-
return true;
|
|
4476
|
-
}
|
|
4477
|
-
|
|
4478
|
-
function readNativeSecret(store, ref) {
|
|
4479
|
-
if (!store || !ref) return "";
|
|
4480
|
-
try {
|
|
4481
|
-
if (store === "macos-keychain") {
|
|
4482
|
-
return execFileSync("security", [
|
|
4483
|
-
"find-generic-password",
|
|
4484
|
-
"-a",
|
|
4485
|
-
ref,
|
|
4486
|
-
"-s",
|
|
4487
|
-
"ItPay",
|
|
4488
|
-
"-w"
|
|
4489
|
-
], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
4490
|
-
}
|
|
4491
|
-
if (store === "secret-tool") {
|
|
4492
|
-
return execFileSync("secret-tool", [
|
|
4493
|
-
"lookup",
|
|
4494
|
-
"service",
|
|
4495
|
-
"ItPay",
|
|
4496
|
-
"account",
|
|
4497
|
-
ref
|
|
4498
|
-
], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
4499
|
-
}
|
|
4500
|
-
} catch {
|
|
4501
|
-
return "";
|
|
4502
|
-
}
|
|
4503
|
-
return "";
|
|
4504
|
-
}
|
|
4505
|
-
|
|
4506
|
-
function deleteNativeSecret(store, ref) {
|
|
4507
|
-
try {
|
|
4508
|
-
if (store === "macos-keychain") {
|
|
4509
|
-
execFileSync("security", [
|
|
4510
|
-
"delete-generic-password",
|
|
4511
|
-
"-a",
|
|
4512
|
-
ref,
|
|
4513
|
-
"-s",
|
|
4514
|
-
"ItPay"
|
|
4515
|
-
], { stdio: "ignore" });
|
|
4516
|
-
}
|
|
4517
|
-
if (store === "secret-tool") {
|
|
4518
|
-
execFileSync("secret-tool", [
|
|
4519
|
-
"clear",
|
|
4520
|
-
"service",
|
|
4521
|
-
"ItPay",
|
|
4522
|
-
"account",
|
|
4523
|
-
ref
|
|
4524
|
-
], { stdio: "ignore" });
|
|
4525
|
-
}
|
|
4526
|
-
} catch {
|
|
4527
|
-
// The local record is still removed; missing native secrets are harmless.
|
|
4528
|
-
}
|
|
4529
|
-
}
|
|
4530
|
-
|
|
4531
|
-
function commandExists(command) {
|
|
4532
|
-
try {
|
|
4533
|
-
execFileSync("which", [command], { stdio: "ignore" });
|
|
4534
|
-
return true;
|
|
4535
|
-
} catch {
|
|
4536
|
-
return false;
|
|
4537
|
-
}
|
|
4538
|
-
}
|
|
4539
|
-
|
|
4540
|
-
function processIsRunning(pid) {
|
|
4541
|
-
const numericPid = Number(pid);
|
|
4542
|
-
if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
|
|
4543
|
-
try {
|
|
4544
|
-
process.kill(numericPid, 0);
|
|
4545
|
-
return true;
|
|
4546
|
-
} catch {
|
|
4547
|
-
return false;
|
|
4548
|
-
}
|
|
4549
|
-
}
|
|
4550
|
-
|
|
4551
|
-
function ensureConfigDir() {
|
|
4552
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
4553
|
-
try {
|
|
4554
|
-
fs.chmodSync(CONFIG_DIR, 0o700);
|
|
4555
|
-
} catch {
|
|
4556
|
-
// Best effort; individual secret files are still written as 0600.
|
|
4557
|
-
}
|
|
4558
|
-
}
|
|
4559
|
-
|
|
4560
|
-
function readText(file, fallback) {
|
|
4561
|
-
try {
|
|
4562
|
-
return fs.readFileSync(file, "utf8");
|
|
4563
|
-
} catch {
|
|
4564
|
-
return fallback;
|
|
4565
|
-
}
|
|
4566
|
-
}
|
|
4567
|
-
|
|
4568
|
-
function readJSON(file, fallback) {
|
|
4569
|
-
try {
|
|
4570
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
4571
|
-
} catch {
|
|
4572
|
-
return fallback;
|
|
4573
|
-
}
|
|
4574
|
-
}
|
|
4575
|
-
|
|
4576
|
-
function writeJSONWithBackup(file, value, dryRun) {
|
|
4577
|
-
return writeTextWithBackup(file, `${JSON.stringify(value, null, 2)}\n`, 0o600, dryRun);
|
|
4578
|
-
}
|
|
4579
|
-
|
|
4580
|
-
function writeTextWithBackup(file, content, mode, dryRun) {
|
|
4581
|
-
const backupPath = fs.existsSync(file) ? `${file}.itp-bak-${Date.now()}` : "";
|
|
4582
|
-
if (dryRun) {
|
|
4583
|
-
return { action: fs.existsSync(file) ? "would_update" : "would_create", backup_path: backupPath || null };
|
|
4584
|
-
}
|
|
4585
|
-
const dir = path.dirname(file);
|
|
4586
|
-
if (dir === CONFIG_DIR) {
|
|
4587
|
-
ensureConfigDir();
|
|
4588
|
-
} else {
|
|
4589
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
4590
|
-
}
|
|
4591
|
-
if (backupPath) {
|
|
4592
|
-
fs.copyFileSync(file, backupPath);
|
|
4593
|
-
fs.chmodSync(backupPath, mode);
|
|
4594
|
-
}
|
|
4595
|
-
fs.writeFileSync(file, content, { mode });
|
|
4596
|
-
fs.chmodSync(file, mode);
|
|
4597
|
-
return { action: backupPath ? "updated" : "created", backup_path: backupPath || null };
|
|
4598
|
-
}
|
|
4599
|
-
|
|
4600
|
-
function fileMode(file) {
|
|
4601
|
-
try {
|
|
4602
|
-
return `0${(fs.statSync(file).mode & 0o777).toString(8)}`;
|
|
4603
|
-
} catch {
|
|
4604
|
-
return null;
|
|
4605
|
-
}
|
|
4606
|
-
}
|
|
4607
|
-
|
|
4608
|
-
function replaceManagedBlock(source, name, block) {
|
|
4609
|
-
const start = `# >>> itp ${name}`;
|
|
4610
|
-
const end = `# <<< itp ${name}`;
|
|
4611
|
-
const managed = `${start}\n${block.trim()}\n${end}`;
|
|
4612
|
-
const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`, "m");
|
|
4613
|
-
const trimmed = source.trimEnd();
|
|
4614
|
-
if (pattern.test(source)) return source.replace(pattern, managed);
|
|
4615
|
-
return `${trimmed}${trimmed ? "\n\n" : ""}${managed}\n`;
|
|
4616
|
-
}
|
|
4617
|
-
|
|
4618
|
-
function escapeRegExp(value) {
|
|
4619
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4620
|
-
}
|
|
4621
|
-
|
|
4622
|
-
function escapeTomlString(value) {
|
|
4623
|
-
return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
4624
|
-
}
|
|
4625
|
-
|
|
4626
|
-
function currentExecutable() {
|
|
4627
|
-
return process.argv[1] || "itp";
|
|
4628
|
-
}
|
|
4629
|
-
|
|
4630
|
-
function quoteShell(value) {
|
|
4631
|
-
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
4632
|
-
}
|
|
4633
|
-
|
|
4634
|
-
function output(value) {
|
|
4635
|
-
console.log(JSON.stringify(value, null, 2));
|
|
4636
|
-
}
|
|
4637
|
-
|
|
4638
|
-
function outputError(error) {
|
|
4639
|
-
const payload = { success: false, message: safeErrorMessage(error) };
|
|
4640
|
-
if (error?.next) payload.next = error.next;
|
|
4641
|
-
console.error(JSON.stringify(payload, null, 2));
|
|
4642
|
-
}
|
|
4643
|
-
|
|
4644
|
-
function maskSecret(secret) {
|
|
4645
|
-
if (!secret) return "";
|
|
4646
|
-
if (secret.length <= 8) return "********";
|
|
4647
|
-
return `${secret.slice(0, 4)}********${secret.slice(-4)}`;
|
|
4648
|
-
}
|
|
4649
|
-
|
|
4650
|
-
function cryptoRandom() {
|
|
4651
|
-
return crypto.randomUUID();
|
|
4652
|
-
}
|
|
4653
|
-
|
|
4654
|
-
function sleep(ms) {
|
|
4655
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4656
|
-
}
|