@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/lib/runtime.js
ADDED
|
@@ -0,0 +1,1532 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { api, coreApi } from "./http.js";
|
|
5
|
+
import {
|
|
6
|
+
buyerAuthStatusOutput, buyerRunOutput, getBuyerCheckout, getBuyerPaymentIntent, isBuyerDeliveryComplete, noRecoverableContext,
|
|
7
|
+
recoverableBuyerContextForStatus, recoverableIntentCheckGuidance
|
|
8
|
+
} from "./buyer.js";
|
|
9
|
+
import { renderHumanAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
|
|
10
|
+
import {
|
|
11
|
+
CREDENTIALS_PATH, CONFIG_DIR, RUNS_DIR, VERSION, apiBase, apiTimeoutMs, appendURLQuery, booleanFlag, cliCommand, commandExists,
|
|
12
|
+
cryptoRandom, csvValues, currentExecutable, deleteGrantCredential, deleteSessionCredential, detectNativeCredentialStore,
|
|
13
|
+
escapeTomlString, fileMode, intFlag, listRuns, maskSecret, mergeRun, output, parseFlags, positional, prepareSetupRun,
|
|
14
|
+
queryString, quoteShell, readConfig, readCredentials, readGrantCredential, readJSON, readRun, readSessionToken, readState,
|
|
15
|
+
readText, replaceManagedBlock, safeErrorMessage, sanitizeAuthResponse, shellQuote, sleep, storeGrantCredential,
|
|
16
|
+
updateCurrentRun, updateRun, withStateLock, writeConfig, writeCredentials, writeJSONWithBackup, writeRun,
|
|
17
|
+
writeSessionCredentials, writeState, writeTextWithBackup
|
|
18
|
+
} from "./env.js";
|
|
19
|
+
|
|
20
|
+
async function authRegister(flags) {
|
|
21
|
+
const response = await completeDeviceAuth(flags);
|
|
22
|
+
output(response);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function setup(flags) {
|
|
26
|
+
return await withStateLock(async () => {
|
|
27
|
+
const target = flags.target || flags.runtime || "generic";
|
|
28
|
+
const purchase = normalizePurchaseFlags(flags, true);
|
|
29
|
+
const plan = purchase.plan || null;
|
|
30
|
+
const credits = purchase.credits || null;
|
|
31
|
+
const method = flags.method || "alipay";
|
|
32
|
+
validateLivePaymentFlags(method, flags);
|
|
33
|
+
const shouldInstallRuntime = Boolean(flags.install_runtime || flags.install_config || flags.write_runtime_config) && !flags.no_runtime_install && !flags.no_install;
|
|
34
|
+
if (shouldInstallRuntime && target === "generic") {
|
|
35
|
+
throw new Error("--install-runtime requires --target codex, --target claude-code, or --target openclaw");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let run = prepareSetupRun(flags, { target, plan, credits, method, install_runtime: shouldInstallRuntime });
|
|
39
|
+
const setupFlags = { ...flags, runtime: target, run_id: run.run_id };
|
|
40
|
+
if (shouldReturnAfterAgentTextQR(setupFlags)) {
|
|
41
|
+
setupFlags.no_wait_auth = true;
|
|
42
|
+
setupFlags.no_wait_payment = true;
|
|
43
|
+
}
|
|
44
|
+
run = updateRun(run, { phase: "checking_auth", status: "running", api_base: apiBase(setupFlags) });
|
|
45
|
+
|
|
46
|
+
const auth = await ensureAuthenticated(setupFlags);
|
|
47
|
+
if (!auth.authenticated) {
|
|
48
|
+
run = mergeRun(run, {
|
|
49
|
+
phase: "waiting_human_auth",
|
|
50
|
+
status: "waiting_human_auth",
|
|
51
|
+
auth: {
|
|
52
|
+
auth_id: auth.auth_id,
|
|
53
|
+
status: "pending",
|
|
54
|
+
expires_at: auth.expires_at
|
|
55
|
+
},
|
|
56
|
+
human_action: auth.human_action || null,
|
|
57
|
+
safe_summary: "Waiting for Alipay authentication scan."
|
|
58
|
+
});
|
|
59
|
+
writeRun(run);
|
|
60
|
+
output(agentRunResponse(run, {
|
|
61
|
+
status: "waiting_human_auth",
|
|
62
|
+
action: "scan_alipay_auth",
|
|
63
|
+
auth_id: auth.auth_id,
|
|
64
|
+
user_code: auth.user_code,
|
|
65
|
+
verification_uri: auth.verification_uri,
|
|
66
|
+
verification_uri_complete: auth.verification_uri_complete,
|
|
67
|
+
alipay_authorization_url: auth.alipay_authorization_url,
|
|
68
|
+
expires_at: auth.expires_at,
|
|
69
|
+
interval: auth.interval,
|
|
70
|
+
human_action: auth.human_action,
|
|
71
|
+
next_action: {
|
|
72
|
+
type: "show_qr_and_wait",
|
|
73
|
+
command: resumeCommand(run, setupFlags, { no_wait_payment: Boolean(setupFlags.no_wait_payment) }),
|
|
74
|
+
retry_after_ms: Number(auth.interval || 2) * 1000
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
run = mergeRun(run, {
|
|
81
|
+
phase: "authenticated",
|
|
82
|
+
status: "running",
|
|
83
|
+
account: {
|
|
84
|
+
authenticated: true,
|
|
85
|
+
account_id: auth.account_id,
|
|
86
|
+
device_id: auth.device_id,
|
|
87
|
+
newapi_user_id: auth.newapi_user_id || null,
|
|
88
|
+
session_reused: Boolean(auth.session_reused)
|
|
89
|
+
},
|
|
90
|
+
auth: {
|
|
91
|
+
...(run.auth || {}),
|
|
92
|
+
status: "consumed"
|
|
93
|
+
},
|
|
94
|
+
human_action: null,
|
|
95
|
+
safe_summary: "Agent device authenticated."
|
|
96
|
+
});
|
|
97
|
+
writeRun(run);
|
|
98
|
+
|
|
99
|
+
let checkout = run.checkout?.checkout_id
|
|
100
|
+
? await api(`/api/itp/checkout/${encodeURIComponent(run.checkout.checkout_id)}`, { method: "GET" }, setupFlags)
|
|
101
|
+
: null;
|
|
102
|
+
if (!checkout || isTerminalCheckoutFailure(checkout.status)) {
|
|
103
|
+
checkout = await createCheckoutResult({
|
|
104
|
+
...setupFlags,
|
|
105
|
+
plan,
|
|
106
|
+
credits,
|
|
107
|
+
method,
|
|
108
|
+
idempotency_key: flags.idempotency_key || run.idempotency_key
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
run = mergeRun(run, {
|
|
113
|
+
phase: checkout.grant_id ? "grant_ready" : "waiting_human_payment",
|
|
114
|
+
status: checkout.grant_id ? "grant_ready" : "waiting_human_payment",
|
|
115
|
+
plan_id: checkout.plan_id || plan,
|
|
116
|
+
credits: checkout.credits || credits,
|
|
117
|
+
purchase_kind: checkout.purchase?.kind || purchase.kind,
|
|
118
|
+
checkout: {
|
|
119
|
+
checkout_id: checkout.checkout_id,
|
|
120
|
+
order_id: checkout.order_id,
|
|
121
|
+
status: checkout.status,
|
|
122
|
+
expires_at: checkout.expires_at,
|
|
123
|
+
purchase: checkout.purchase || null
|
|
124
|
+
},
|
|
125
|
+
payment: {
|
|
126
|
+
provider: method,
|
|
127
|
+
status: checkout.status
|
|
128
|
+
},
|
|
129
|
+
grant: {
|
|
130
|
+
...(run.grant || {}),
|
|
131
|
+
grant_id: checkout.grant_id || run.grant?.grant_id || null
|
|
132
|
+
},
|
|
133
|
+
human_action: checkout.human_action || null,
|
|
134
|
+
safe_summary: checkout.grant_id ? "Payment verified and grant is ready." : "Waiting for Alipay payment scan."
|
|
135
|
+
});
|
|
136
|
+
writeRun(run);
|
|
137
|
+
|
|
138
|
+
if (checkout.human_action) {
|
|
139
|
+
await renderHumanAction(checkout.human_action, setupFlags);
|
|
140
|
+
} else if (checkout.payment?.cashier_url) {
|
|
141
|
+
process.stderr.write(`Open Alipay payment URL: ${checkout.payment.cashier_url}\n`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!checkout.grant_id && (setupFlags.no_wait_payment || setupFlags.no_wait)) {
|
|
145
|
+
output(agentRunResponse(run, {
|
|
146
|
+
status: "waiting_human_payment",
|
|
147
|
+
action: "scan_alipay_payment",
|
|
148
|
+
account_id: auth.account_id,
|
|
149
|
+
device_id: auth.device_id,
|
|
150
|
+
checkout_id: checkout.checkout_id,
|
|
151
|
+
order_id: checkout.order_id,
|
|
152
|
+
plan_id: checkout.plan_id || plan,
|
|
153
|
+
credits: checkout.credits || credits,
|
|
154
|
+
purchase: checkout.purchase || null,
|
|
155
|
+
expires_at: checkout.expires_at,
|
|
156
|
+
payment: checkout.payment,
|
|
157
|
+
human_action: checkout.human_action,
|
|
158
|
+
next_action: checkout.next_action || {
|
|
159
|
+
type: "show_qr_and_wait",
|
|
160
|
+
command: resumeCommand(run, setupFlags),
|
|
161
|
+
retry_after_ms: 2000
|
|
162
|
+
}
|
|
163
|
+
}));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const payment = checkout.grant_id
|
|
168
|
+
? { status: checkout.status, checkout_id: checkout.checkout_id, order_id: checkout.order_id, grant_id: checkout.grant_id }
|
|
169
|
+
: await paymentWaitResult(checkout.checkout_id, setupFlags);
|
|
170
|
+
run = mergeRun(readRun(run.run_id) || run, {
|
|
171
|
+
phase: "grant_ready",
|
|
172
|
+
checkout: {
|
|
173
|
+
...(run.checkout || {}),
|
|
174
|
+
checkout_id: payment.checkout_id,
|
|
175
|
+
order_id: payment.order_id,
|
|
176
|
+
status: payment.status
|
|
177
|
+
},
|
|
178
|
+
payment: {
|
|
179
|
+
provider: method,
|
|
180
|
+
status: payment.status
|
|
181
|
+
},
|
|
182
|
+
grant: {
|
|
183
|
+
grant_id: payment.grant_id,
|
|
184
|
+
installed: false
|
|
185
|
+
},
|
|
186
|
+
human_action: null,
|
|
187
|
+
safe_summary: "Payment verified and grant is ready."
|
|
188
|
+
});
|
|
189
|
+
writeRun(run);
|
|
190
|
+
|
|
191
|
+
const grant = await grantsInstallResult(payment.grant_id, { ...setupFlags, target });
|
|
192
|
+
const runtimeInstall = shouldInstallRuntime
|
|
193
|
+
? await installRuntimeResult(target, {
|
|
194
|
+
...setupFlags,
|
|
195
|
+
grant: payment.grant_id,
|
|
196
|
+
no_test: flags.test ? false : true
|
|
197
|
+
})
|
|
198
|
+
: {
|
|
199
|
+
status: "skipped",
|
|
200
|
+
reason: "runtime_config_install_is_opt_in",
|
|
201
|
+
command: target === "generic"
|
|
202
|
+
? `${cliCommand("install")} <target> --grant ${shellQuote(payment.grant_id)} --json`
|
|
203
|
+
: cliCommand("install", target, "--grant", payment.grant_id, "--json")
|
|
204
|
+
};
|
|
205
|
+
const tokenCommand = cliCommand("token", "issue", "--grant", payment.grant_id, "--stdout");
|
|
206
|
+
run = mergeRun(readRun(run.run_id) || run, {
|
|
207
|
+
phase: shouldInstallRuntime ? "done" : "grant_ready",
|
|
208
|
+
status: shouldInstallRuntime ? "installed" : "grant_ready",
|
|
209
|
+
grant: {
|
|
210
|
+
grant_id: payment.grant_id,
|
|
211
|
+
installed: true,
|
|
212
|
+
credential_store: grant.credential?.credential_store || null
|
|
213
|
+
},
|
|
214
|
+
result: {
|
|
215
|
+
base_url: grant.base_url,
|
|
216
|
+
openai_base_url: grant.openai_base_url,
|
|
217
|
+
anthropic_base_url: grant.anthropic_base_url,
|
|
218
|
+
gemini_base_url: grant.gemini_base_url
|
|
219
|
+
},
|
|
220
|
+
safe_summary: shouldInstallRuntime ? "Runtime configured." : "Grant credential stored."
|
|
221
|
+
});
|
|
222
|
+
writeRun(run);
|
|
223
|
+
|
|
224
|
+
output(agentRunResponse(run, {
|
|
225
|
+
status: shouldInstallRuntime ? "installed" : "grant_ready",
|
|
226
|
+
account_id: auth.account_id,
|
|
227
|
+
device_id: auth.device_id,
|
|
228
|
+
checkout_id: checkout.checkout_id,
|
|
229
|
+
order_id: checkout.order_id,
|
|
230
|
+
plan_id: checkout.plan_id || plan,
|
|
231
|
+
credits: checkout.credits || credits,
|
|
232
|
+
purchase: checkout.purchase || null,
|
|
233
|
+
grant_id: payment.grant_id,
|
|
234
|
+
target,
|
|
235
|
+
base_url: grant.base_url,
|
|
236
|
+
openai_base_url: grant.openai_base_url,
|
|
237
|
+
anthropic_base_url: grant.anthropic_base_url,
|
|
238
|
+
gemini_base_url: grant.gemini_base_url,
|
|
239
|
+
credential: {
|
|
240
|
+
stored: true,
|
|
241
|
+
credential_store: grant.credential?.credential_store,
|
|
242
|
+
warning: grant.credential?.warning,
|
|
243
|
+
token_command: tokenCommand,
|
|
244
|
+
stdout_required_for_raw_token: true
|
|
245
|
+
},
|
|
246
|
+
auth,
|
|
247
|
+
checkout,
|
|
248
|
+
payment,
|
|
249
|
+
grant_install: grant,
|
|
250
|
+
runtime_install: runtimeInstall,
|
|
251
|
+
next_action: shouldInstallRuntime
|
|
252
|
+
? null
|
|
253
|
+
: {
|
|
254
|
+
type: "configure_agent_optional",
|
|
255
|
+
token_command: tokenCommand,
|
|
256
|
+
runtime_install_command: runtimeInstall.command
|
|
257
|
+
}
|
|
258
|
+
}));
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function agentStatus(flags) {
|
|
263
|
+
const runId = flags.run_id || readState().current_run_id;
|
|
264
|
+
const run = readRun(runId);
|
|
265
|
+
if (!run) {
|
|
266
|
+
const config = readConfig();
|
|
267
|
+
const credentials = readCredentials();
|
|
268
|
+
const hasLocalSession = Boolean(readSessionToken(credentials));
|
|
269
|
+
const auth = flags.refresh
|
|
270
|
+
? await refreshBuyerSessionForStatus(flags)
|
|
271
|
+
: {
|
|
272
|
+
authenticated: hasLocalSession,
|
|
273
|
+
session_verified: false,
|
|
274
|
+
account_id: config.account_id || null,
|
|
275
|
+
device_id: config.device_id || null,
|
|
276
|
+
auth_source: hasLocalSession ? "local_files_unverified" : "local_files"
|
|
277
|
+
};
|
|
278
|
+
output({
|
|
279
|
+
schema_version: "itp.agent.v1",
|
|
280
|
+
status: auth.authenticated
|
|
281
|
+
? (auth.session_verified === false ? "local_session_unverified" : "idle")
|
|
282
|
+
: "unauthenticated",
|
|
283
|
+
phase: auth.authenticated ? "idle" : "unauthenticated",
|
|
284
|
+
authenticated: Boolean(auth.authenticated),
|
|
285
|
+
session_verified: auth.session_verified !== false,
|
|
286
|
+
auth_source: auth.auth_source,
|
|
287
|
+
account_id: auth.account_id || null,
|
|
288
|
+
device_id: auth.device_id || null,
|
|
289
|
+
next: nextActionForAuthStatus(auth, flags),
|
|
290
|
+
recoverable_context: noRecoverableContext(),
|
|
291
|
+
agent_next_actions: auth.authenticated && auth.session_verified !== false
|
|
292
|
+
? ["search_catalog", "view_orders", "create_refund_if_needed", "list_agent_read_grants"]
|
|
293
|
+
: ["run_status_refresh", "start_human_auth_if_unauthenticated"],
|
|
294
|
+
note: auth.authenticated && auth.session_verified === false
|
|
295
|
+
? "Local buyer session exists but the server has not verified it. Run the next.command before order/refund/account operations."
|
|
296
|
+
: undefined,
|
|
297
|
+
error: auth.error || undefined,
|
|
298
|
+
secrets: { raw_key_included: false, session_token_included: false }
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const refreshed = flags.refresh ? await refreshRun(run, flags) : run;
|
|
303
|
+
writeRun(refreshed);
|
|
304
|
+
output(agentRunResponse(refreshed, {
|
|
305
|
+
recoverable_context: recoverableRuntimeRunContext(refreshed)
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function refreshBuyerSessionForStatus(flags = {}) {
|
|
310
|
+
const config = readConfig();
|
|
311
|
+
const credentials = readCredentials();
|
|
312
|
+
if (!readSessionToken(credentials)) {
|
|
313
|
+
return {
|
|
314
|
+
authenticated: false,
|
|
315
|
+
session_verified: true,
|
|
316
|
+
account_id: config.account_id || null,
|
|
317
|
+
device_id: config.device_id || null,
|
|
318
|
+
auth_source: "core_no_session"
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const status = await coreApi("/v1/me/auth/status", { method: "GET" }, flags);
|
|
323
|
+
return {
|
|
324
|
+
authenticated: true,
|
|
325
|
+
session_verified: true,
|
|
326
|
+
account_id: status.buyer_account_id || config.account_id || null,
|
|
327
|
+
device_id: config.device_id || null,
|
|
328
|
+
account_status: status.account_status || null,
|
|
329
|
+
auth_source: "core"
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
return {
|
|
333
|
+
authenticated: false,
|
|
334
|
+
session_verified: true,
|
|
335
|
+
account_id: config.account_id || null,
|
|
336
|
+
device_id: config.device_id || null,
|
|
337
|
+
auth_source: "core",
|
|
338
|
+
error: safeErrorMessage(error)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function nextActionForAuthStatus(auth = {}, flags = {}) {
|
|
344
|
+
if (auth.authenticated && auth.session_verified === false) {
|
|
345
|
+
return { type: "verify_buyer_session", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true };
|
|
346
|
+
}
|
|
347
|
+
if (auth.authenticated) {
|
|
348
|
+
return { type: "buyer_ready", command: cliCommand("buyer", "auth", "status", "--json"), safe_for_agent: true };
|
|
349
|
+
}
|
|
350
|
+
return { type: "start_auth", command: cliCommand("setup", "--plan", "credit-300", "--method", "alipay", "--json"), safe_for_agent: true };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function resume(flags) {
|
|
354
|
+
const runId = flags.run_id || readState().current_run_id;
|
|
355
|
+
const run = readRun(runId);
|
|
356
|
+
if (!run) throw new Error("no active run found");
|
|
357
|
+
if (["done", "installed"].includes(run.status) || run.phase === "done") {
|
|
358
|
+
output(agentRunResponse(run));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
await setup({
|
|
362
|
+
...flags,
|
|
363
|
+
run_id: run.run_id,
|
|
364
|
+
resume: true,
|
|
365
|
+
plan: run.plan_id || flags.plan,
|
|
366
|
+
credits: run.plan_id ? flags.credits : run.credits || flags.credits,
|
|
367
|
+
method: run.payment_method || flags.method,
|
|
368
|
+
target: run.target || flags.target,
|
|
369
|
+
install_runtime: run.install_runtime || flags.install_runtime
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function runs(command, flags) {
|
|
374
|
+
if (command === "current") {
|
|
375
|
+
const run = readRun(flags.run_id || readState().current_run_id);
|
|
376
|
+
output(run ? agentRunResponse(run) : { schema_version: "itp.agent.v1", status: "none", runs: [] });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (command === "list") {
|
|
380
|
+
output({ runs: listRuns().map(agentRunResponse) });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (command === "show") {
|
|
384
|
+
const run = readRun(flags.run_id || flags.id);
|
|
385
|
+
if (!run) throw new Error("run not found");
|
|
386
|
+
output(agentRunResponse(run));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (command === "forget") {
|
|
390
|
+
const runId = flags.run_id || flags.id;
|
|
391
|
+
if (!runId || runId === "forget") throw new Error("run_id is required");
|
|
392
|
+
const file = runPath(runId);
|
|
393
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
394
|
+
const state = readState();
|
|
395
|
+
if (state.current_run_id === runId) {
|
|
396
|
+
delete state.current_run_id;
|
|
397
|
+
writeState(state);
|
|
398
|
+
}
|
|
399
|
+
output({ status: "forgotten", run_id: runId });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
throw new Error(`unknown runs command: ${command || ""}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function startDeviceAuth(flags) {
|
|
406
|
+
const runtime = flags.runtime || "unknown";
|
|
407
|
+
return await api("/api/itp/auth/device/start", {
|
|
408
|
+
method: "POST",
|
|
409
|
+
body: {
|
|
410
|
+
device: {
|
|
411
|
+
display_name: os.hostname(),
|
|
412
|
+
runtime,
|
|
413
|
+
os: os.platform(),
|
|
414
|
+
arch: os.arch(),
|
|
415
|
+
itp_version: VERSION
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}, flags);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function completeDeviceAuth(flags) {
|
|
422
|
+
const start = await startDeviceAuth(flags);
|
|
423
|
+
if (flags.no_wait) {
|
|
424
|
+
writeState({ ...readState(), last_auth_id: start.auth_id });
|
|
425
|
+
updateCurrentRun({
|
|
426
|
+
phase: "waiting_human_auth",
|
|
427
|
+
auth: { auth_id: start.auth_id, status: "pending", expires_at: start.expires_at },
|
|
428
|
+
human_action: start.human_action || null,
|
|
429
|
+
safe_summary: "Waiting for Alipay authentication scan."
|
|
430
|
+
}, flags);
|
|
431
|
+
await renderHumanAction(start.human_action, flags);
|
|
432
|
+
return start;
|
|
433
|
+
}
|
|
434
|
+
await maybeMockApproveDeviceAuth(start.auth_id, flags);
|
|
435
|
+
const response = await waitDeviceAuth(start.auth_id, flags, start);
|
|
436
|
+
if (!response.auth) {
|
|
437
|
+
throw new Error(`device auth ended without session: ${response.status || "unknown"}`);
|
|
438
|
+
}
|
|
439
|
+
writeSessionCredentials(response.auth);
|
|
440
|
+
writeConfig({
|
|
441
|
+
api_base: apiBase(flags),
|
|
442
|
+
account_id: response.auth.account_id,
|
|
443
|
+
device_id: response.auth.device_id,
|
|
444
|
+
web_console_url: response.auth.web_console_url
|
|
445
|
+
});
|
|
446
|
+
writeState({ ...readState(), last_auth_id: start.auth_id });
|
|
447
|
+
return { ...sanitizeAuthResponse(response.auth), auth_id: start.auth_id, status: response.status };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function ensureAuthenticated(flags) {
|
|
451
|
+
const config = readConfig();
|
|
452
|
+
const credentials = readCredentials();
|
|
453
|
+
if (readSessionToken(credentials)) {
|
|
454
|
+
try {
|
|
455
|
+
const status = await api("/api/itp/auth/status", { method: "GET" }, flags);
|
|
456
|
+
if (status.authenticated !== false) {
|
|
457
|
+
return {
|
|
458
|
+
authenticated: true,
|
|
459
|
+
account_id: status.account_id || config.account_id || null,
|
|
460
|
+
device_id: status.device_id || config.device_id || null,
|
|
461
|
+
newapi_user_id: status.newapi_user_id || null,
|
|
462
|
+
session_reused: true
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
deleteSessionCredential(credentials);
|
|
467
|
+
writeCredentials(credentials);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const resumableRun = readRun(flags.run_id || readState().current_run_id);
|
|
471
|
+
if ((flags.resume || flags.auth_id) && resumableRun?.auth?.auth_id && !resumableRun.account?.authenticated && !resumableRun.checkout?.checkout_id && !flags.no_wait && !flags.no_wait_auth) {
|
|
472
|
+
await renderHumanAction(resumableRun.human_action, flags);
|
|
473
|
+
await maybeMockApproveDeviceAuth(resumableRun.auth.auth_id, flags);
|
|
474
|
+
const response = await waitDeviceAuth(resumableRun.auth.auth_id, flags);
|
|
475
|
+
if (!response.auth) {
|
|
476
|
+
return {
|
|
477
|
+
authenticated: false,
|
|
478
|
+
status: response.status || "waiting_human_auth",
|
|
479
|
+
auth_id: resumableRun.auth.auth_id,
|
|
480
|
+
expires_at: resumableRun.auth.expires_at,
|
|
481
|
+
human_action: resumableRun.human_action
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
writeSessionCredentials(response.auth);
|
|
485
|
+
writeConfig({
|
|
486
|
+
api_base: apiBase(flags),
|
|
487
|
+
account_id: response.auth.account_id,
|
|
488
|
+
device_id: response.auth.device_id,
|
|
489
|
+
web_console_url: response.auth.web_console_url
|
|
490
|
+
});
|
|
491
|
+
writeState({ ...readState(), last_auth_id: resumableRun.auth.auth_id });
|
|
492
|
+
return { ...sanitizeAuthResponse(response.auth), auth_id: resumableRun.auth.auth_id, authenticated: true, session_reused: false };
|
|
493
|
+
}
|
|
494
|
+
if (flags.no_wait || flags.no_wait_auth) {
|
|
495
|
+
const start = await startDeviceAuth(flags);
|
|
496
|
+
writeState({ ...readState(), last_auth_id: start.auth_id });
|
|
497
|
+
updateCurrentRun({
|
|
498
|
+
phase: "waiting_human_auth",
|
|
499
|
+
auth: { auth_id: start.auth_id, status: "pending", expires_at: start.expires_at },
|
|
500
|
+
human_action: start.human_action || null,
|
|
501
|
+
safe_summary: "Waiting for Alipay authentication scan."
|
|
502
|
+
}, flags);
|
|
503
|
+
await renderHumanAction(start.human_action, flags);
|
|
504
|
+
return {
|
|
505
|
+
authenticated: false,
|
|
506
|
+
status: "waiting_human_auth",
|
|
507
|
+
action: "scan_alipay_auth",
|
|
508
|
+
auth_id: start.auth_id,
|
|
509
|
+
user_code: start.user_code,
|
|
510
|
+
verification_uri: start.verification_uri,
|
|
511
|
+
verification_uri_complete: start.verification_uri_complete,
|
|
512
|
+
alipay_authorization_url: start.alipay_authorization_url,
|
|
513
|
+
expires_at: start.expires_at,
|
|
514
|
+
interval: start.interval,
|
|
515
|
+
human_action: start.human_action
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const auth = await completeDeviceAuth(flags);
|
|
519
|
+
return { ...auth, authenticated: true, session_reused: false };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function maybeMockApproveDeviceAuth(authId, flags) {
|
|
523
|
+
if (!(flags.mock_approve || process.env.ITPAY_MOCK_APPROVE === "true" || process.env.ITPAY_MOCK_APPROVE === "1")) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (!fakeTestingAllowed(flags)) {
|
|
527
|
+
throw new Error("mock approval is developer-only and disabled for agent runs; use real Alipay sandbox authentication");
|
|
528
|
+
}
|
|
529
|
+
const alipayUserId = flags.alipay_user_id || process.env.ITPAY_MOCK_ALIPAY_USER_ID || `2088${crypto.randomInt(100000000000, 999999999999)}`;
|
|
530
|
+
await api(`/api/itp/auth/device/${encodeURIComponent(authId)}/mock-approve`, {
|
|
531
|
+
method: "POST",
|
|
532
|
+
body: { alipay_user_id: alipayUserId }
|
|
533
|
+
}, flags);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function authDevice(command, flags) {
|
|
537
|
+
if (command === "start") {
|
|
538
|
+
const response = await startDeviceAuth(flags);
|
|
539
|
+
writeState({ ...readState(), last_auth_id: response.auth_id });
|
|
540
|
+
await renderHumanAction(response.human_action, flags);
|
|
541
|
+
output(response);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (command === "poll") {
|
|
545
|
+
const authId = flags.auth_id || flags.device_auth_id || readState().last_auth_id;
|
|
546
|
+
if (!authId) throw new Error("auth_id is required");
|
|
547
|
+
const response = await waitDeviceAuth(authId, flags);
|
|
548
|
+
if (response.auth) {
|
|
549
|
+
writeSessionCredentials(response.auth);
|
|
550
|
+
writeConfig({
|
|
551
|
+
api_base: apiBase(flags),
|
|
552
|
+
account_id: response.auth.account_id,
|
|
553
|
+
device_id: response.auth.device_id,
|
|
554
|
+
web_console_url: response.auth.web_console_url
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
output(response.auth ? { ...sanitizeAuthResponse(response.auth), auth_id: authId, status: response.status } : response);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
throw new Error(`unknown auth device command: ${command || ""}`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function waitDeviceAuth(authId, flags, start = null) {
|
|
564
|
+
const started = Date.now();
|
|
565
|
+
const timeoutMs = Number(flags.timeout || 600) * 1000;
|
|
566
|
+
let intervalMs = 2000;
|
|
567
|
+
let lastHeartbeatAt = 0;
|
|
568
|
+
let lastStatus = "authorization_pending";
|
|
569
|
+
const action = start?.human_action || null;
|
|
570
|
+
if (start) {
|
|
571
|
+
updateCurrentRun({
|
|
572
|
+
phase: "waiting_human_auth",
|
|
573
|
+
auth: { auth_id: start.auth_id, status: "pending", expires_at: start.expires_at },
|
|
574
|
+
human_action: start.human_action || null,
|
|
575
|
+
safe_summary: "Waiting for Alipay authentication scan."
|
|
576
|
+
}, flags);
|
|
577
|
+
await renderHumanAction(start.human_action, flags);
|
|
578
|
+
if (!start.human_action) process.stderr.write(`Open Alipay auth URL: ${start.verification_uri_complete || start.alipay_authorization_url}\n`);
|
|
579
|
+
if (start.user_code) process.stderr.write(`Alipay auth code: ${start.user_code}\n`);
|
|
580
|
+
}
|
|
581
|
+
while (Date.now() - started < timeoutMs) {
|
|
582
|
+
const response = await api(`/api/itp/auth/device/${encodeURIComponent(authId)}/poll`, { method: "POST" }, flags);
|
|
583
|
+
lastStatus = response.status || lastStatus;
|
|
584
|
+
if (response.auth?.session_token) {
|
|
585
|
+
return response;
|
|
586
|
+
}
|
|
587
|
+
if (response.status === "authorization_pending") {
|
|
588
|
+
intervalMs = Number(response.interval || 2) * 1000;
|
|
589
|
+
lastHeartbeatAt = writeWaitHeartbeat({
|
|
590
|
+
kind: "Alipay authentication",
|
|
591
|
+
idName: "auth_id",
|
|
592
|
+
idValue: authId,
|
|
593
|
+
status: response.status,
|
|
594
|
+
action,
|
|
595
|
+
lastHeartbeatAt,
|
|
596
|
+
flags,
|
|
597
|
+
command: cliCommand("auth", "device", "poll", authId, "--timeout", String(Math.ceil((timeoutMs - (Date.now() - started)) / 1000)), "--json")
|
|
598
|
+
});
|
|
599
|
+
await sleep(intervalMs);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (response.status === "approved") {
|
|
603
|
+
return response;
|
|
604
|
+
}
|
|
605
|
+
if (response.status === "expired" || response.status === "consumed" || response.error) {
|
|
606
|
+
throw new Error(response.error || `device auth ended with status: ${response.status}`);
|
|
607
|
+
}
|
|
608
|
+
await sleep(intervalMs);
|
|
609
|
+
}
|
|
610
|
+
throw new Error(`device auth timed out at status ${lastStatus}; run \`itp auth device poll ${authId} --timeout 600\``);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function authLogin(flags) {
|
|
614
|
+
const runtime = flags.runtime || "unknown";
|
|
615
|
+
if (flags.password && !flags.password_stdin) {
|
|
616
|
+
throw new Error("use --password-stdin to avoid leaking passwords into shell history");
|
|
617
|
+
}
|
|
618
|
+
const password = flags.password_stdin
|
|
619
|
+
? fs.readFileSync(0, "utf8").trim()
|
|
620
|
+
: undefined;
|
|
621
|
+
if (flags.password_stdin && !password) {
|
|
622
|
+
throw new Error("password is required on stdin");
|
|
623
|
+
}
|
|
624
|
+
const response = await api("/api/itp/auth/login", {
|
|
625
|
+
method: "POST",
|
|
626
|
+
body: {
|
|
627
|
+
username: flags.username || undefined,
|
|
628
|
+
password,
|
|
629
|
+
access_token: flags.access_token || undefined,
|
|
630
|
+
device: {
|
|
631
|
+
display_name: os.hostname(),
|
|
632
|
+
runtime,
|
|
633
|
+
os: os.platform(),
|
|
634
|
+
arch: os.arch(),
|
|
635
|
+
itp_version: VERSION
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}, flags);
|
|
639
|
+
writeSessionCredentials(response);
|
|
640
|
+
writeConfig({
|
|
641
|
+
api_base: apiBase(flags),
|
|
642
|
+
account_id: response.account_id,
|
|
643
|
+
device_id: response.device_id,
|
|
644
|
+
web_console_url: response.web_console_url
|
|
645
|
+
});
|
|
646
|
+
output(sanitizeAuthResponse(response));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function authStatus(flags) {
|
|
650
|
+
const config = readConfig();
|
|
651
|
+
const credentials = readCredentials();
|
|
652
|
+
if (!readSessionToken(credentials)) {
|
|
653
|
+
output({ authenticated: false, account_id: config.account_id || null });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
const status = await api("/api/itp/auth/status", { method: "GET" }, flags);
|
|
658
|
+
output(status);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
output({
|
|
661
|
+
authenticated: false,
|
|
662
|
+
account_id: config.account_id || null,
|
|
663
|
+
error: error.message
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function accountShow(flags) {
|
|
669
|
+
output(await api("/api/itp/account", { method: "GET" }, flags));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function accountLoginLink(flags) {
|
|
673
|
+
const config = readConfig();
|
|
674
|
+
const accountID = flags.account || flags.account_id || flags.buyer_account || flags.buyer_account_id || config.account_id;
|
|
675
|
+
if (!accountID) {
|
|
676
|
+
throw new Error("buyer account id is required; complete a buyer purchase first or pass --account-id");
|
|
677
|
+
}
|
|
678
|
+
if (!readSessionToken()) {
|
|
679
|
+
throw new Error("buyer account session is required; complete first-purchase auth or run buyer checkout resume first");
|
|
680
|
+
}
|
|
681
|
+
const link = await coreApi("/v1/me/portal-login-links", { method: "POST" }, flags);
|
|
682
|
+
output(buyerRunOutput({
|
|
683
|
+
status: "account_portal_login_link_created",
|
|
684
|
+
account_id: accountID,
|
|
685
|
+
portal_login_link: link,
|
|
686
|
+
login_url: link.login_url,
|
|
687
|
+
expires_at: link.expires_at,
|
|
688
|
+
agent_next_actions: link.agent_next_actions || ["show_login_link_to_human_only"],
|
|
689
|
+
next: {
|
|
690
|
+
type: "human_open_account_portal_link",
|
|
691
|
+
safe_for_agent: false,
|
|
692
|
+
requires_human: true,
|
|
693
|
+
agent_must_not_open: true,
|
|
694
|
+
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."
|
|
695
|
+
}
|
|
696
|
+
}));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function accountSetPassword(flags) {
|
|
700
|
+
if (!flags.password_stdin) {
|
|
701
|
+
throw new Error("use --password-stdin to avoid leaking passwords into shell history");
|
|
702
|
+
}
|
|
703
|
+
const password = fs.readFileSync(0, "utf8").trim();
|
|
704
|
+
if (!password) throw new Error("password is required on stdin");
|
|
705
|
+
output(await api("/api/itp/account/password", {
|
|
706
|
+
method: "POST",
|
|
707
|
+
body: { password }
|
|
708
|
+
}, flags));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function plansList(flags) {
|
|
712
|
+
output(await api("/api/itp/plans", { method: "GET" }, flags));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function plansShow(plan, flags) {
|
|
716
|
+
if (!plan) throw new Error("plan id is required");
|
|
717
|
+
output(await api(`/api/itp/plans/${encodeURIComponent(plan)}`, { method: "GET" }, flags));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function checkoutCreate(flags) {
|
|
721
|
+
const response = await createCheckoutResult(flags);
|
|
722
|
+
await renderHumanAction(response.human_action, flags);
|
|
723
|
+
output(response);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function createCheckoutResult(flags) {
|
|
727
|
+
const purchase = normalizePurchaseFlags(flags, true);
|
|
728
|
+
const method = flags.method || "alipay";
|
|
729
|
+
validateLivePaymentFlags(method, flags);
|
|
730
|
+
const currentRun = readRun(flags.run_id || readState().current_run_id);
|
|
731
|
+
const idempotencyKey = flags.idempotency_key || currentRun?.idempotency_key || cryptoRandom();
|
|
732
|
+
const body = {
|
|
733
|
+
payment_method: method,
|
|
734
|
+
idempotency_key: idempotencyKey
|
|
735
|
+
};
|
|
736
|
+
if (purchase.plan) body.plan_id = purchase.plan;
|
|
737
|
+
if (purchase.credits) body.credits = purchase.credits;
|
|
738
|
+
const response = await api("/api/itp/checkout", {
|
|
739
|
+
method: "POST",
|
|
740
|
+
body
|
|
741
|
+
}, flags);
|
|
742
|
+
writeState({ ...readState(), last_checkout_id: response.checkout_id, last_grant_id: response.grant_id || null });
|
|
743
|
+
updateCurrentRun({
|
|
744
|
+
phase: response.grant_id ? "grant_ready" : "waiting_human_payment",
|
|
745
|
+
plan_id: response.plan_id || purchase.plan || null,
|
|
746
|
+
credits: response.credits || purchase.credits || null,
|
|
747
|
+
purchase_kind: response.purchase?.kind || purchase.kind,
|
|
748
|
+
checkout: {
|
|
749
|
+
checkout_id: response.checkout_id,
|
|
750
|
+
order_id: response.order_id,
|
|
751
|
+
status: response.status,
|
|
752
|
+
expires_at: response.expires_at,
|
|
753
|
+
purchase: response.purchase || null
|
|
754
|
+
},
|
|
755
|
+
payment: {
|
|
756
|
+
provider: method,
|
|
757
|
+
status: response.status
|
|
758
|
+
},
|
|
759
|
+
grant: {
|
|
760
|
+
...(currentRun?.grant || {}),
|
|
761
|
+
grant_id: response.grant_id || currentRun?.grant?.grant_id || null
|
|
762
|
+
},
|
|
763
|
+
human_action: response.human_action || null,
|
|
764
|
+
safe_summary: response.grant_id ? "Payment verified and grant is ready." : "Waiting for Alipay payment scan."
|
|
765
|
+
}, flags);
|
|
766
|
+
return response;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function validateLivePaymentFlags(method, flags = {}) {
|
|
770
|
+
if (String(method).toLowerCase() === "fake" && !fakeTestingAllowed(flags)) {
|
|
771
|
+
throw new Error("fake payment is developer-only and disabled for agent runs; use --method alipay for local, sandbox, and live testing");
|
|
772
|
+
}
|
|
773
|
+
if ((flags.mock_approve || process.env.ITPAY_MOCK_APPROVE === "true" || process.env.ITPAY_MOCK_APPROVE === "1") && !fakeTestingAllowed(flags)) {
|
|
774
|
+
throw new Error("mock approval is developer-only and disabled for agent runs; use real Alipay sandbox authentication");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function fakeTestingAllowed(flags = {}) {
|
|
779
|
+
return flags.allow_fake || process.env.ITP_ALLOW_FAKE_PAYMENT === "true" || process.env.ITP_ALLOW_FAKE_PAYMENT === "1";
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function paymentWait(checkoutId, flags) {
|
|
783
|
+
output(await paymentWaitResult(checkoutId, flags));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function paymentWaitResult(checkoutId, flags) {
|
|
787
|
+
if (!checkoutId) throw new Error("checkout_id is required");
|
|
788
|
+
const started = Date.now();
|
|
789
|
+
const timeoutMs = Number(flags.timeout || 120) * 1000;
|
|
790
|
+
let lastRecoverAt = 0;
|
|
791
|
+
let lastHeartbeatAt = 0;
|
|
792
|
+
let lastResponse = null;
|
|
793
|
+
while (Date.now() - started < timeoutMs) {
|
|
794
|
+
let response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}`, { method: "GET" }, flags);
|
|
795
|
+
lastResponse = response;
|
|
796
|
+
if (isTerminalCheckoutFailure(response.status)) {
|
|
797
|
+
throw new Error(`checkout ended with status: ${response.status}`);
|
|
798
|
+
}
|
|
799
|
+
if (isSuccessfulCheckout(response)) {
|
|
800
|
+
writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
|
|
801
|
+
updateCurrentRun({
|
|
802
|
+
phase: "grant_ready",
|
|
803
|
+
checkout: { checkout_id: checkoutId, order_id: response.order_id, status: response.status, expires_at: response.expires_at },
|
|
804
|
+
grant: { grant_id: response.grant_id, installed: false },
|
|
805
|
+
human_action: null,
|
|
806
|
+
safe_summary: "Payment verified and grant is ready."
|
|
807
|
+
}, flags);
|
|
808
|
+
return { status: "grant_issued", checkout_id: checkoutId, order_id: response.order_id, grant_id: response.grant_id };
|
|
809
|
+
}
|
|
810
|
+
if (shouldRecoverCheckout(response.status) && Date.now() - lastRecoverAt > 5000) {
|
|
811
|
+
lastRecoverAt = Date.now();
|
|
812
|
+
response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/recover`, { method: "POST" }, flags);
|
|
813
|
+
lastResponse = response;
|
|
814
|
+
if (isTerminalCheckoutFailure(response.status)) {
|
|
815
|
+
throw new Error(`checkout ended with status: ${response.status}`);
|
|
816
|
+
}
|
|
817
|
+
if (isSuccessfulCheckout(response)) {
|
|
818
|
+
writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
|
|
819
|
+
updateCurrentRun({
|
|
820
|
+
phase: "grant_ready",
|
|
821
|
+
checkout: { checkout_id: checkoutId, order_id: response.order_id, status: response.status, expires_at: response.expires_at },
|
|
822
|
+
grant: { grant_id: response.grant_id, installed: false },
|
|
823
|
+
human_action: null,
|
|
824
|
+
safe_summary: "Payment verified and grant is ready."
|
|
825
|
+
}, flags);
|
|
826
|
+
return { status: "grant_issued", checkout_id: checkoutId, order_id: response.order_id, grant_id: response.grant_id, recovered: true };
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
lastHeartbeatAt = writeWaitHeartbeat({
|
|
830
|
+
kind: "Alipay payment",
|
|
831
|
+
idName: "checkout_id",
|
|
832
|
+
idValue: checkoutId,
|
|
833
|
+
status: response.status || "waiting",
|
|
834
|
+
action: response.human_action || null,
|
|
835
|
+
lastHeartbeatAt,
|
|
836
|
+
flags,
|
|
837
|
+
command: cliCommand("payment", "wait", checkoutId, "--timeout", String(Math.ceil((timeoutMs - (Date.now() - started)) / 1000)), "--json")
|
|
838
|
+
});
|
|
839
|
+
await sleep(2000);
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
const response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/recover`, { method: "POST" }, flags);
|
|
843
|
+
lastResponse = response;
|
|
844
|
+
if (isTerminalCheckoutFailure(response.status)) {
|
|
845
|
+
throw new Error(`checkout ended with status: ${response.status}`);
|
|
846
|
+
}
|
|
847
|
+
if (isSuccessfulCheckout(response)) {
|
|
848
|
+
writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
|
|
849
|
+
updateCurrentRun({
|
|
850
|
+
phase: "grant_ready",
|
|
851
|
+
checkout: { checkout_id: checkoutId, order_id: response.order_id, status: response.status, expires_at: response.expires_at },
|
|
852
|
+
grant: { grant_id: response.grant_id, installed: false },
|
|
853
|
+
human_action: null,
|
|
854
|
+
safe_summary: "Payment verified and grant is ready."
|
|
855
|
+
}, flags);
|
|
856
|
+
return { status: "grant_issued", checkout_id: checkoutId, order_id: response.order_id, grant_id: response.grant_id, recovered: true };
|
|
857
|
+
}
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (isTerminalCheckoutFailure(lastResponse?.status)) {
|
|
860
|
+
throw error;
|
|
861
|
+
}
|
|
862
|
+
// Preserve the timeout message below; recover is a best-effort final attempt.
|
|
863
|
+
}
|
|
864
|
+
if (lastResponse?.status) {
|
|
865
|
+
throw new Error(`payment wait timed out at checkout status ${lastResponse.status}; run \`itp checkout recover ${checkoutId}\` later`);
|
|
866
|
+
}
|
|
867
|
+
throw new Error("payment wait timed out; run `itp checkout recover` later");
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function checkoutRecover(checkoutId, flags) {
|
|
871
|
+
if (!checkoutId) throw new Error("checkout_id is required");
|
|
872
|
+
const response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/recover`, { method: "POST" }, flags);
|
|
873
|
+
if (response.grant_id) {
|
|
874
|
+
writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
|
|
875
|
+
}
|
|
876
|
+
output(response);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function checkoutOpen(flags) {
|
|
880
|
+
const state = readState();
|
|
881
|
+
if (!state.last_checkout_id) throw new Error("no checkout found in local state");
|
|
882
|
+
const response = await api(`/api/itp/checkout/${encodeURIComponent(state.last_checkout_id)}`, { method: "GET" }, flags);
|
|
883
|
+
output(response.payment || response);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async function checkoutQR(checkoutId, flags) {
|
|
887
|
+
if (!checkoutId) throw new Error("checkout_id is required");
|
|
888
|
+
const response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/qr`, { method: "GET" }, flags);
|
|
889
|
+
await renderHumanAction(response.human_action, flags);
|
|
890
|
+
output(response);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function checkoutList(flags) {
|
|
894
|
+
const params = new URLSearchParams();
|
|
895
|
+
if (flags.limit) params.set("limit", flags.limit);
|
|
896
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
897
|
+
output(await api(`/api/itp/orders${suffix}`, { method: "GET" }, flags));
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function balance(flags) {
|
|
901
|
+
output(await api("/api/itp/balance", { method: "GET" }, flags));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
async function usage(flags) {
|
|
905
|
+
const params = new URLSearchParams();
|
|
906
|
+
if (flags.model) params.set("model", flags.model);
|
|
907
|
+
if (flags.grant) params.set("grant_id", flags.grant);
|
|
908
|
+
if (flags.grant_id) params.set("grant_id", flags.grant_id);
|
|
909
|
+
if (flags.from) params.set("from", flags.from);
|
|
910
|
+
if (flags.to) params.set("to", flags.to);
|
|
911
|
+
if (flags.today) {
|
|
912
|
+
const start = new Date();
|
|
913
|
+
start.setHours(0, 0, 0, 0);
|
|
914
|
+
params.set("from", Math.floor(start.getTime() / 1000).toString());
|
|
915
|
+
}
|
|
916
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
917
|
+
output(await api(`/api/itp/usage${suffix}`, { method: "GET" }, flags));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function grantsList(flags) {
|
|
921
|
+
output(await api("/api/itp/grants", { method: "GET" }, flags));
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function grantsShow(grantId, flags) {
|
|
925
|
+
if (!grantId) throw new Error("grant_id is required");
|
|
926
|
+
output(await api(`/api/itp/grants/${encodeURIComponent(grantId)}`, { method: "GET" }, flags));
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async function grantsInstall(grantId, flags) {
|
|
930
|
+
output(await grantsInstallResult(grantId, flags));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async function grantsInstallResult(grantId, flags) {
|
|
934
|
+
grantId = grantId || readState().last_grant_id;
|
|
935
|
+
if (!grantId) throw new Error("grant_id is required");
|
|
936
|
+
const target = flags.target || "generic";
|
|
937
|
+
const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/install`, {
|
|
938
|
+
method: "POST",
|
|
939
|
+
body: { target }
|
|
940
|
+
}, flags);
|
|
941
|
+
const storedCredential = storeGrantCredential(grantId, {
|
|
942
|
+
key: response.credential.key_once,
|
|
943
|
+
target,
|
|
944
|
+
base_url: response.base_url,
|
|
945
|
+
openai_base_url: response.openai_base_url,
|
|
946
|
+
anthropic_base_url: response.anthropic_base_url,
|
|
947
|
+
gemini_base_url: response.gemini_base_url,
|
|
948
|
+
models: response.models || [],
|
|
949
|
+
install_profiles: response.install_profiles || []
|
|
950
|
+
});
|
|
951
|
+
const grantsDir = path.join(CONFIG_DIR, "grants");
|
|
952
|
+
ensureConfigDir();
|
|
953
|
+
fs.mkdirSync(grantsDir, { recursive: true });
|
|
954
|
+
try {
|
|
955
|
+
fs.chmodSync(grantsDir, 0o700);
|
|
956
|
+
} catch {
|
|
957
|
+
// Best effort; grant metadata does not contain gateway keys.
|
|
958
|
+
}
|
|
959
|
+
const metadataPath = path.join(grantsDir, `${grantId}.json`);
|
|
960
|
+
fs.writeFileSync(metadataPath, JSON.stringify({
|
|
961
|
+
grant_id: grantId,
|
|
962
|
+
target,
|
|
963
|
+
base_url: response.base_url,
|
|
964
|
+
models: response.models,
|
|
965
|
+
install_profiles: response.install_profiles
|
|
966
|
+
}, null, 2), { mode: 0o600 });
|
|
967
|
+
fs.chmodSync(metadataPath, 0o600);
|
|
968
|
+
writeState({ ...readState(), last_grant_id: grantId });
|
|
969
|
+
updateCurrentRun({
|
|
970
|
+
phase: "grant_ready",
|
|
971
|
+
grant: {
|
|
972
|
+
grant_id: grantId,
|
|
973
|
+
installed: true,
|
|
974
|
+
credential_store: storedCredential.credential_store || null
|
|
975
|
+
},
|
|
976
|
+
result: {
|
|
977
|
+
base_url: response.base_url,
|
|
978
|
+
openai_base_url: response.openai_base_url,
|
|
979
|
+
anthropic_base_url: response.anthropic_base_url,
|
|
980
|
+
gemini_base_url: response.gemini_base_url
|
|
981
|
+
},
|
|
982
|
+
safe_summary: "Grant credential stored."
|
|
983
|
+
}, flags);
|
|
984
|
+
return {
|
|
985
|
+
...response,
|
|
986
|
+
credential: {
|
|
987
|
+
type: response.credential.type,
|
|
988
|
+
stored: true,
|
|
989
|
+
credential_store: storedCredential.credential_store,
|
|
990
|
+
warning: storedCredential.credential_warning || undefined
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async function grantsRevoke(grantId, flags) {
|
|
996
|
+
grantId = grantId || flags.grant || readState().last_grant_id;
|
|
997
|
+
if (!grantId) throw new Error("grant_id is required");
|
|
998
|
+
const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/revoke`, { method: "POST" }, flags);
|
|
999
|
+
deleteGrantCredential(grantId);
|
|
1000
|
+
const state = readState();
|
|
1001
|
+
if (state.last_grant_id === grantId) {
|
|
1002
|
+
delete state.last_grant_id;
|
|
1003
|
+
writeState(state);
|
|
1004
|
+
}
|
|
1005
|
+
output(response);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function installRuntime(target, flags) {
|
|
1009
|
+
output(await installRuntimeResult(target, flags));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function installRuntimeResult(target, flags) {
|
|
1013
|
+
const grantId = flags.grant || readState().last_grant_id;
|
|
1014
|
+
if (!grantId) throw new Error("grant id is required");
|
|
1015
|
+
const credentials = readGrantCredential(grantId);
|
|
1016
|
+
if (!credentials) throw new Error(`grant ${grantId} is not installed; run grants install first`);
|
|
1017
|
+
if (!credentials.key) {
|
|
1018
|
+
throw new Error(`grant ${grantId} credential key is unavailable; run keys rotate or grants install again`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const dryRun = Boolean(flags.dry_run);
|
|
1022
|
+
const result = installTargetConfig(target, grantId, credentials, dryRun);
|
|
1023
|
+
const shouldTest = !dryRun && !flags.offline && !flags.no_test;
|
|
1024
|
+
const modelCheck = shouldTest
|
|
1025
|
+
? await doctorModelCheck(target, credentials)
|
|
1026
|
+
: { attempted: false, skipped: true, reason: dryRun ? "dry_run" : flags.offline ? "offline" : "no_test" };
|
|
1027
|
+
result.model_check = modelCheck;
|
|
1028
|
+
result.tested = Boolean(modelCheck.ok);
|
|
1029
|
+
if (modelCheck.attempted && !modelCheck.ok) {
|
|
1030
|
+
result.warnings.push(`Model endpoint check failed: ${modelCheck.error || modelCheck.status || "unknown error"}`);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (!dryRun && !flags.offline) {
|
|
1034
|
+
await api(`/api/itp/grants/${encodeURIComponent(grantId)}/install-ack`, {
|
|
1035
|
+
method: "POST",
|
|
1036
|
+
body: {
|
|
1037
|
+
target,
|
|
1038
|
+
status: "installed",
|
|
1039
|
+
tested: result.tested,
|
|
1040
|
+
config_path: result.files[0]?.path || "",
|
|
1041
|
+
last_error: result.warnings.join("; ")
|
|
1042
|
+
}
|
|
1043
|
+
}, flags);
|
|
1044
|
+
}
|
|
1045
|
+
return result;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function shouldRecoverCheckout(status) {
|
|
1049
|
+
return ["paid_verified", "granting", "grant_failed"].includes(status);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function isSuccessfulCheckout(response) {
|
|
1053
|
+
return Boolean(response?.grant_id) && ["grant_issued", "grant_installed"].includes(response.status);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function isTerminalCheckoutFailure(status) {
|
|
1057
|
+
return ["expired", "payment_failed", "verify_failed", "amount_mismatch", "revoked"].includes(status);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function installTargetConfig(target, grantId, credentials, dryRun) {
|
|
1061
|
+
if (target === "codex") return installCodex(grantId, credentials, dryRun);
|
|
1062
|
+
if (target === "claude-code") return installClaudeCode(grantId, credentials, dryRun);
|
|
1063
|
+
if (target === "openclaw") return installOpenClaw(grantId, credentials, dryRun);
|
|
1064
|
+
throw new Error(`unsupported install target: ${target}`);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function installClaudeCode(grantId, credentials, dryRun) {
|
|
1068
|
+
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
1069
|
+
const current = readJSON(settingsPath, {});
|
|
1070
|
+
current.env = {
|
|
1071
|
+
...(current.env || {}),
|
|
1072
|
+
ANTHROPIC_BASE_URL: credentials.anthropic_base_url,
|
|
1073
|
+
ANTHROPIC_AUTH_TOKEN_HELPER: `${quoteShell(currentExecutable())} token issue --grant ${quoteShell(grantId)} --stdout`
|
|
1074
|
+
};
|
|
1075
|
+
delete current.env.ANTHROPIC_API_KEY;
|
|
1076
|
+
const write = writeJSONWithBackup(settingsPath, current, dryRun);
|
|
1077
|
+
return {
|
|
1078
|
+
target: "claude-code",
|
|
1079
|
+
grant_id: grantId,
|
|
1080
|
+
status: dryRun ? "dry_run" : "installed",
|
|
1081
|
+
files: [{ path: settingsPath, action: write.action, backup_path: write.backup_path || null }],
|
|
1082
|
+
warnings: [
|
|
1083
|
+
"Claude Code will call itp as an auth token helper; no gateway key was written to settings.json."
|
|
1084
|
+
]
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function installCodex(grantId, credentials, dryRun) {
|
|
1089
|
+
const configPath = path.join(os.homedir(), ".codex", "config.toml");
|
|
1090
|
+
const envPath = path.join(CONFIG_DIR, "itpay.env");
|
|
1091
|
+
const existing = readText(configPath, "");
|
|
1092
|
+
const block = [
|
|
1093
|
+
'model_provider = "itpay"',
|
|
1094
|
+
'model = "openai-code-default"',
|
|
1095
|
+
"",
|
|
1096
|
+
"[model_providers.itpay]",
|
|
1097
|
+
'name = "ItPay"',
|
|
1098
|
+
`base_url = "${escapeTomlString(credentials.openai_base_url)}"`,
|
|
1099
|
+
'env_key = "ITPAY_API_KEY"'
|
|
1100
|
+
].join("\n");
|
|
1101
|
+
const nextConfig = replaceManagedBlock(existing, "itpay", block);
|
|
1102
|
+
const configWrite = writeTextWithBackup(configPath, nextConfig, 0o600, dryRun);
|
|
1103
|
+
const envWrite = writeTextWithBackup(envPath, `export ITPAY_API_KEY=${quoteShell(credentials.key)}\n`, 0o600, dryRun);
|
|
1104
|
+
return {
|
|
1105
|
+
target: "codex",
|
|
1106
|
+
grant_id: grantId,
|
|
1107
|
+
status: dryRun ? "dry_run" : "installed",
|
|
1108
|
+
files: [
|
|
1109
|
+
{ path: configPath, action: configWrite.action, backup_path: configWrite.backup_path || null },
|
|
1110
|
+
{ path: envPath, action: envWrite.action, backup_path: envWrite.backup_path || null }
|
|
1111
|
+
],
|
|
1112
|
+
warnings: [
|
|
1113
|
+
"Codex reads ITPAY_API_KEY from its process environment; source ~/.itp/itpay.env before starting Codex if your launcher does not load it."
|
|
1114
|
+
]
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function installOpenClaw(grantId, credentials, dryRun) {
|
|
1119
|
+
const configPath = path.join(os.homedir(), ".openclaw", "config.json");
|
|
1120
|
+
const current = readJSON(configPath, {});
|
|
1121
|
+
current.models = current.models || {};
|
|
1122
|
+
current.models.providers = current.models.providers || {};
|
|
1123
|
+
current.models.providers.itpay = {
|
|
1124
|
+
baseUrl: credentials.openai_base_url,
|
|
1125
|
+
api: "openai-compatible",
|
|
1126
|
+
apiKey: credentials.key,
|
|
1127
|
+
models: credentials.models || []
|
|
1128
|
+
};
|
|
1129
|
+
const write = writeJSONWithBackup(configPath, current, dryRun);
|
|
1130
|
+
return {
|
|
1131
|
+
target: "openclaw",
|
|
1132
|
+
grant_id: grantId,
|
|
1133
|
+
status: dryRun ? "dry_run" : "installed",
|
|
1134
|
+
files: [{ path: configPath, action: write.action, backup_path: write.backup_path || null }],
|
|
1135
|
+
warnings: [
|
|
1136
|
+
"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."
|
|
1137
|
+
]
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async function doctor(flags) {
|
|
1142
|
+
const config = readConfig();
|
|
1143
|
+
const credentials = readCredentials();
|
|
1144
|
+
const target = flags.target || null;
|
|
1145
|
+
const grantId = flags.grant || readState().last_grant_id || null;
|
|
1146
|
+
const grantCredential = grantId ? readGrantCredential(grantId) : null;
|
|
1147
|
+
output({
|
|
1148
|
+
itp_version: VERSION,
|
|
1149
|
+
api_base: config.api_base || apiBase(flags),
|
|
1150
|
+
account_id: config.account_id || null,
|
|
1151
|
+
device_id: config.device_id || null,
|
|
1152
|
+
authenticated: Boolean(readSessionToken(credentials)),
|
|
1153
|
+
grants_cached: Object.keys(credentials).filter((k) => k.startsWith("grant_")).length,
|
|
1154
|
+
grant_id: grantId,
|
|
1155
|
+
target,
|
|
1156
|
+
target_config: target ? runtimeConfigStatus(target) : null,
|
|
1157
|
+
model_check: grantCredential && !flags.offline ? await doctorModelCheck(target, grantCredential) : null,
|
|
1158
|
+
grant_credential_store: grantCredential?.credential_store || null,
|
|
1159
|
+
warning: grantCredential?.credential_warning || undefined,
|
|
1160
|
+
credential_store: {
|
|
1161
|
+
path: CREDENTIALS_PATH,
|
|
1162
|
+
mode: fileMode(CREDENTIALS_PATH),
|
|
1163
|
+
native: detectNativeCredentialStore(),
|
|
1164
|
+
fallback: true
|
|
1165
|
+
},
|
|
1166
|
+
session_credential_store: credentials.session_token_store || (credentials.session_token ? "file" : null),
|
|
1167
|
+
session_credential_warning: credentials.session_token_warning || undefined
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
async function doctorModelCheck(target, credentials) {
|
|
1172
|
+
const baseUrl = target === "claude-code"
|
|
1173
|
+
? credentials.anthropic_base_url
|
|
1174
|
+
: target === "codex" || target === "openclaw"
|
|
1175
|
+
? credentials.openai_base_url
|
|
1176
|
+
: credentials.openai_base_url || credentials.base_url;
|
|
1177
|
+
if (!baseUrl || !credentials.key) {
|
|
1178
|
+
return { attempted: false, ok: false, error: "missing base_url or credential" };
|
|
1179
|
+
}
|
|
1180
|
+
const controller = new AbortController();
|
|
1181
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
1182
|
+
try {
|
|
1183
|
+
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
|
|
1184
|
+
method: "GET",
|
|
1185
|
+
headers: { Authorization: `Bearer ${credentials.key}` },
|
|
1186
|
+
signal: controller.signal
|
|
1187
|
+
});
|
|
1188
|
+
return {
|
|
1189
|
+
attempted: true,
|
|
1190
|
+
ok: response.ok,
|
|
1191
|
+
status: response.status,
|
|
1192
|
+
endpoint: `${baseUrl.replace(/\/$/, "")}/models`
|
|
1193
|
+
};
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
return {
|
|
1196
|
+
attempted: true,
|
|
1197
|
+
ok: false,
|
|
1198
|
+
endpoint: `${baseUrl.replace(/\/$/, "")}/models`,
|
|
1199
|
+
error: error.message
|
|
1200
|
+
};
|
|
1201
|
+
} finally {
|
|
1202
|
+
clearTimeout(timer);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function runtimeConfigStatus(target) {
|
|
1207
|
+
const paths = {
|
|
1208
|
+
"claude-code": [path.join(os.homedir(), ".claude", "settings.json")],
|
|
1209
|
+
codex: [path.join(os.homedir(), ".codex", "config.toml"), path.join(CONFIG_DIR, "itpay.env")],
|
|
1210
|
+
openclaw: [path.join(os.homedir(), ".openclaw", "config.json")]
|
|
1211
|
+
};
|
|
1212
|
+
return (paths[target] || []).map((file) => ({
|
|
1213
|
+
path: file,
|
|
1214
|
+
exists: fs.existsSync(file),
|
|
1215
|
+
mode: fileMode(file)
|
|
1216
|
+
}));
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async function keys(command, flags) {
|
|
1220
|
+
const credentials = readCredentials();
|
|
1221
|
+
const grants = Object.entries(credentials)
|
|
1222
|
+
.filter(([key]) => key.startsWith("grant_"))
|
|
1223
|
+
.map(([key, value]) => ({
|
|
1224
|
+
grant_id: key.slice("grant_".length),
|
|
1225
|
+
target: value.target,
|
|
1226
|
+
base_url: value.base_url,
|
|
1227
|
+
credential_store: value.credential_store || "file",
|
|
1228
|
+
key: value.key ? maskSecret(value.key) : "stored"
|
|
1229
|
+
}));
|
|
1230
|
+
if (!command || command === "list") {
|
|
1231
|
+
output({ keys: grants });
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (command === "revoke") {
|
|
1235
|
+
const grantId = flags.grant || grants[0]?.grant_id;
|
|
1236
|
+
if (!grantId) throw new Error("grant id is required");
|
|
1237
|
+
const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/revoke`, { method: "POST" }, flags);
|
|
1238
|
+
deleteGrantCredential(grantId);
|
|
1239
|
+
output(response);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (command === "rotate") {
|
|
1243
|
+
const grantId = flags.grant || grants[0]?.grant_id;
|
|
1244
|
+
if (!grantId) throw new Error("grant id is required");
|
|
1245
|
+
const existing = readCredentials()[`grant_${grantId}`] || {};
|
|
1246
|
+
const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/rotate`, { method: "POST" }, flags);
|
|
1247
|
+
const storedCredential = storeGrantCredential(grantId, {
|
|
1248
|
+
key: response.credential.key_once,
|
|
1249
|
+
target: existing.target || flags.target || "",
|
|
1250
|
+
base_url: response.base_url,
|
|
1251
|
+
openai_base_url: response.openai_base_url,
|
|
1252
|
+
anthropic_base_url: response.anthropic_base_url,
|
|
1253
|
+
gemini_base_url: response.gemini_base_url,
|
|
1254
|
+
models: response.models || [],
|
|
1255
|
+
install_profiles: response.install_profiles || []
|
|
1256
|
+
});
|
|
1257
|
+
output({
|
|
1258
|
+
...response,
|
|
1259
|
+
credential: {
|
|
1260
|
+
type: response.credential.type,
|
|
1261
|
+
rotated: true,
|
|
1262
|
+
stored: true,
|
|
1263
|
+
credential_store: storedCredential.credential_store,
|
|
1264
|
+
warning: storedCredential.credential_warning || undefined
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
throw new Error(`unknown keys command: ${command}`);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
async function token(command, flags) {
|
|
1273
|
+
if (command !== "issue") throw new Error(`unknown token command: ${command || ""}`);
|
|
1274
|
+
const grantId = flags.grant || readState().last_grant_id;
|
|
1275
|
+
if (!grantId) throw new Error("grant id is required");
|
|
1276
|
+
const credentials = readGrantCredential(grantId);
|
|
1277
|
+
if (!credentials?.key) throw new Error(`grant ${grantId} is not installed locally`);
|
|
1278
|
+
if (flags.stdout) {
|
|
1279
|
+
process.stdout.write(credentials.key);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
output({
|
|
1283
|
+
grant_id: grantId,
|
|
1284
|
+
key: maskSecret(credentials.key),
|
|
1285
|
+
stdout_required_for_raw_token: true
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async function sync(flags) {
|
|
1290
|
+
const [account, balanceResult, grants] = await Promise.all([
|
|
1291
|
+
api("/api/itp/account", { method: "GET" }, flags),
|
|
1292
|
+
api("/api/itp/balance", { method: "GET" }, flags),
|
|
1293
|
+
api("/api/itp/grants", { method: "GET" }, flags)
|
|
1294
|
+
]);
|
|
1295
|
+
output({ account, balance: balanceResult, grants });
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async function refreshRun(run, flags = {}) {
|
|
1299
|
+
let next = { ...run };
|
|
1300
|
+
const credentials = readCredentials();
|
|
1301
|
+
const hasSession = Boolean(readSessionToken(credentials));
|
|
1302
|
+
|
|
1303
|
+
if (next.auth?.auth_id && !hasSession) {
|
|
1304
|
+
try {
|
|
1305
|
+
const auth = await api(`/api/itp/auth/device/${encodeURIComponent(next.auth.auth_id)}/poll`, { method: "POST" }, flags);
|
|
1306
|
+
if (auth.auth?.session_token) {
|
|
1307
|
+
writeSessionCredentials(auth.auth);
|
|
1308
|
+
writeConfig({
|
|
1309
|
+
api_base: apiBase(flags),
|
|
1310
|
+
account_id: auth.auth.account_id,
|
|
1311
|
+
device_id: auth.auth.device_id,
|
|
1312
|
+
web_console_url: auth.auth.web_console_url
|
|
1313
|
+
});
|
|
1314
|
+
next = mergeRun(next, {
|
|
1315
|
+
phase: "authenticated",
|
|
1316
|
+
status: "running",
|
|
1317
|
+
account: {
|
|
1318
|
+
authenticated: true,
|
|
1319
|
+
account_id: auth.auth.account_id,
|
|
1320
|
+
device_id: auth.auth.device_id,
|
|
1321
|
+
newapi_user_id: auth.auth.newapi_user_id || null
|
|
1322
|
+
},
|
|
1323
|
+
auth: { status: "consumed" },
|
|
1324
|
+
human_action: null,
|
|
1325
|
+
safe_summary: "Agent device authenticated."
|
|
1326
|
+
});
|
|
1327
|
+
} else if (auth.status === "authorization_pending") {
|
|
1328
|
+
next = mergeRun(next, {
|
|
1329
|
+
phase: "waiting_human_auth",
|
|
1330
|
+
status: "waiting_human_auth",
|
|
1331
|
+
auth: { status: "pending", expires_at: auth.expires_at },
|
|
1332
|
+
human_action: auth.human_action || auth.next_action?.human_action || next.human_action || null,
|
|
1333
|
+
safe_summary: "Waiting for Alipay authentication scan."
|
|
1334
|
+
});
|
|
1335
|
+
} else if (auth.status) {
|
|
1336
|
+
next = mergeRun(next, {
|
|
1337
|
+
phase: auth.status === "expired" ? "expired" : "failed",
|
|
1338
|
+
status: auth.status === "expired" ? "expired" : "failed",
|
|
1339
|
+
auth: { status: auth.status },
|
|
1340
|
+
safe_summary: auth.error || `Auth status: ${auth.status}`
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
next = mergeRun(next, { last_error: safeErrorMessage(error), safe_summary: "Could not refresh auth status." });
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (readSessionToken(readCredentials())) {
|
|
1349
|
+
try {
|
|
1350
|
+
const authStatusResult = await api("/api/itp/auth/status", { method: "GET" }, flags);
|
|
1351
|
+
next = mergeRun(next, {
|
|
1352
|
+
account: {
|
|
1353
|
+
authenticated: authStatusResult.authenticated !== false,
|
|
1354
|
+
account_id: authStatusResult.account_id || next.account?.account_id || null,
|
|
1355
|
+
device_id: authStatusResult.device_id || next.account?.device_id || null,
|
|
1356
|
+
newapi_user_id: authStatusResult.newapi_user_id || null
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
next = mergeRun(next, { last_error: safeErrorMessage(error) });
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
if (next.checkout?.checkout_id && readSessionToken(readCredentials())) {
|
|
1365
|
+
try {
|
|
1366
|
+
const checkout = await api(`/api/itp/checkout/${encodeURIComponent(next.checkout.checkout_id)}`, { method: "GET" }, flags);
|
|
1367
|
+
next = mergeRun(next, {
|
|
1368
|
+
phase: checkout.grant_id ? "grant_ready" : checkout.status === "waiting_user_payment" ? "waiting_human_payment" : next.phase,
|
|
1369
|
+
status: checkout.grant_id ? "grant_ready" : checkout.status === "waiting_user_payment" ? "waiting_human_payment" : next.status,
|
|
1370
|
+
checkout: {
|
|
1371
|
+
checkout_id: checkout.checkout_id,
|
|
1372
|
+
order_id: checkout.order_id,
|
|
1373
|
+
status: checkout.status,
|
|
1374
|
+
expires_at: checkout.expires_at
|
|
1375
|
+
},
|
|
1376
|
+
payment: { provider: next.payment_method, status: checkout.status },
|
|
1377
|
+
grant: { ...(next.grant || {}), grant_id: checkout.grant_id || next.grant?.grant_id || null },
|
|
1378
|
+
human_action: checkout.human_action || null,
|
|
1379
|
+
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}`
|
|
1380
|
+
});
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
next = mergeRun(next, { last_error: safeErrorMessage(error), safe_summary: "Could not refresh checkout status." });
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const grantId = next.grant?.grant_id || readState().last_grant_id;
|
|
1387
|
+
if (grantId) {
|
|
1388
|
+
const credential = readGrantCredential(grantId);
|
|
1389
|
+
if (credential?.key || credential?.credential_ref) {
|
|
1390
|
+
next = mergeRun(next, {
|
|
1391
|
+
phase: next.install_runtime ? next.phase : "grant_ready",
|
|
1392
|
+
grant: { grant_id: grantId, installed: true, credential_store: credential.credential_store || "file" },
|
|
1393
|
+
safe_summary: "Grant credential stored."
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return next;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function agentRunResponse(run, extra = {}) {
|
|
1401
|
+
const status = extra.status || (run.status && run.status !== "running" ? run.status : phaseToStatus(run.phase));
|
|
1402
|
+
return {
|
|
1403
|
+
schema_version: "itp.agent.v1",
|
|
1404
|
+
status,
|
|
1405
|
+
run_id: run.run_id,
|
|
1406
|
+
phase: run.phase || status,
|
|
1407
|
+
auth_id: extra.auth_id || run.auth?.auth_id || null,
|
|
1408
|
+
account_id: extra.account_id || run.account?.account_id || null,
|
|
1409
|
+
device_id: extra.device_id || run.account?.device_id || null,
|
|
1410
|
+
checkout_id: extra.checkout_id || run.checkout?.checkout_id || null,
|
|
1411
|
+
order_id: extra.order_id || run.checkout?.order_id || null,
|
|
1412
|
+
grant_id: extra.grant_id || run.grant?.grant_id || null,
|
|
1413
|
+
plan_id: extra.plan_id || run.plan_id || null,
|
|
1414
|
+
credits: extra.credits || run.credits || run.checkout?.purchase?.credits_granted || null,
|
|
1415
|
+
purchase: extra.purchase || run.checkout?.purchase || undefined,
|
|
1416
|
+
target: extra.target || run.target || "generic",
|
|
1417
|
+
base_url: extra.base_url || run.result?.base_url || undefined,
|
|
1418
|
+
openai_base_url: extra.openai_base_url || run.result?.openai_base_url || undefined,
|
|
1419
|
+
anthropic_base_url: extra.anthropic_base_url || run.result?.anthropic_base_url || undefined,
|
|
1420
|
+
gemini_base_url: extra.gemini_base_url || run.result?.gemini_base_url || undefined,
|
|
1421
|
+
credential: extra.credential || (run.grant?.installed ? {
|
|
1422
|
+
stored: true,
|
|
1423
|
+
credential_store: run.grant?.credential_store,
|
|
1424
|
+
token_command: run.grant?.grant_id ? cliCommand("token", "issue", "--grant", run.grant.grant_id, "--stdout") : undefined,
|
|
1425
|
+
stdout_required_for_raw_token: true
|
|
1426
|
+
} : undefined),
|
|
1427
|
+
human_action: extra.human_action || run.human_action || undefined,
|
|
1428
|
+
next: extra.next || nextActionForRun(run),
|
|
1429
|
+
next_action: extra.next_action || undefined,
|
|
1430
|
+
recoverable_context: extra.recoverable_context || undefined,
|
|
1431
|
+
safe_user_message: extra.safe_user_message || safeUserMessageForRun(run),
|
|
1432
|
+
safe_summary: run.safe_summary || undefined,
|
|
1433
|
+
warnings: extra.warnings || [],
|
|
1434
|
+
secrets: {
|
|
1435
|
+
raw_key_included: false,
|
|
1436
|
+
session_token_included: false
|
|
1437
|
+
},
|
|
1438
|
+
...extra
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function phaseToStatus(phase) {
|
|
1443
|
+
if (phase === "waiting_human_auth") return "waiting_human_auth";
|
|
1444
|
+
if (phase === "waiting_human_payment") return "waiting_human_payment";
|
|
1445
|
+
if (phase === "grant_ready") return "grant_ready";
|
|
1446
|
+
if (phase === "done") return "done";
|
|
1447
|
+
if (phase === "expired") return "expired";
|
|
1448
|
+
if (phase === "failed") return "failed";
|
|
1449
|
+
return phase || "running";
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function nextActionForRun(run) {
|
|
1453
|
+
if (run.phase === "waiting_human_auth") {
|
|
1454
|
+
return { type: "show_qr_and_wait", command: resumeCommand(run, runResumeFlags(run)), retry_after_ms: 2000, safe_for_agent: true };
|
|
1455
|
+
}
|
|
1456
|
+
if (run.phase === "waiting_human_payment") {
|
|
1457
|
+
return { type: "show_qr_and_wait", command: resumeCommand(run, runResumeFlags(run, { display: "none" })), retry_after_ms: 2000, safe_for_agent: true };
|
|
1458
|
+
}
|
|
1459
|
+
if (["paid_verified", "granting", "grant_failed"].includes(run.checkout?.status)) {
|
|
1460
|
+
return { type: "recover_checkout", command: cliCommand("checkout", "recover", run.checkout.checkout_id, "--json"), safe_for_agent: true };
|
|
1461
|
+
}
|
|
1462
|
+
if (run.grant?.grant_id && !run.grant?.installed) {
|
|
1463
|
+
return { type: "install_grant", command: cliCommand("grants", "install", run.grant.grant_id, "--target", run.target || "generic", "--json"), safe_for_agent: true };
|
|
1464
|
+
}
|
|
1465
|
+
if (run.grant?.installed) {
|
|
1466
|
+
return { type: "done", safe_for_agent: true };
|
|
1467
|
+
}
|
|
1468
|
+
return { type: "resume", command: resumeCommand(run, runResumeFlags(run)), safe_for_agent: true };
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function runResumeFlags(run, overrides = {}) {
|
|
1472
|
+
return {
|
|
1473
|
+
host: run.agent_host || undefined,
|
|
1474
|
+
display: run.agent_display || undefined,
|
|
1475
|
+
qr_format: run.agent_qr_format || undefined,
|
|
1476
|
+
api_base: run.api_base || undefined,
|
|
1477
|
+
...overrides
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function resumeCommand(run, flags = {}, options = {}) {
|
|
1482
|
+
const args = ["resume", "--run-id", run.run_id, "--json"];
|
|
1483
|
+
appendPassthroughFlag(args, flags, "host");
|
|
1484
|
+
appendPassthroughFlag(args, flags, "display");
|
|
1485
|
+
appendPassthroughFlag(args, flags, "qr_format", "qr-format");
|
|
1486
|
+
appendPassthroughFlag(args, flags, "api_base", "api-base");
|
|
1487
|
+
appendPassthroughFlag(args, flags, "api_timeout", "api-timeout");
|
|
1488
|
+
if (options.no_wait_payment) args.push("--no-wait-payment");
|
|
1489
|
+
return cliCommand(...args);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function appendPassthroughFlag(args, flags, key, flagName = key) {
|
|
1493
|
+
const value = flags[key];
|
|
1494
|
+
if (value === undefined || value === null || value === false) return;
|
|
1495
|
+
args.push(`--${flagName}`);
|
|
1496
|
+
if (value !== true) args.push(String(value));
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function safeUserMessageForRun(run) {
|
|
1500
|
+
if (run.phase === "waiting_human_auth") return "Please scan the Alipay authentication QR. I will continue automatically after approval.";
|
|
1501
|
+
if (run.phase === "waiting_human_payment") return "Please scan the Alipay payment QR. I will continue automatically after payment is verified.";
|
|
1502
|
+
if (run.grant?.installed) return "Payment verified. The API credential is stored locally.";
|
|
1503
|
+
return run.safe_summary || "ITPay setup is in progress.";
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function recoverableRuntimeRunContext(run = {}) {
|
|
1507
|
+
if (!run || !run.run_id) return noRecoverableContext();
|
|
1508
|
+
const status = String(run.status || run.phase || "").toLowerCase();
|
|
1509
|
+
const terminal = ["done", "installed", "failed", "expired"].includes(status) || run.phase === "done";
|
|
1510
|
+
if (terminal) return noRecoverableContext();
|
|
1511
|
+
return {
|
|
1512
|
+
found: true,
|
|
1513
|
+
kind: "runtime_run",
|
|
1514
|
+
run_id: run.run_id,
|
|
1515
|
+
status: run.status || null,
|
|
1516
|
+
phase: run.phase || null,
|
|
1517
|
+
checkout_id: run.checkout?.checkout_id || null,
|
|
1518
|
+
order_id: run.checkout?.order_id || null,
|
|
1519
|
+
grant_id: run.grant?.grant_id || null,
|
|
1520
|
+
resume_command: cliCommand("resume", "--run-id", run.run_id, "--json"),
|
|
1521
|
+
status_command: cliCommand("status", "--refresh", "--run-id", run.run_id, "--json"),
|
|
1522
|
+
intent_check: recoverableIntentCheckGuidance(run),
|
|
1523
|
+
safe_choices: [
|
|
1524
|
+
{ choice: "continue_old_task", when: "当前用户是在继续这次 setup/auth/payment/grant 安装流程。", action: cliCommand("resume", "--run-id", run.run_id, "--json") },
|
|
1525
|
+
{ choice: "ignore_old_task", when: "当前用户明确要求新的、不相关的任务。", action: "按当前用户意图继续,不要自动 resume。" },
|
|
1526
|
+
{ choice: "ask_human", when: "无法判断旧 run 是否相关,或涉及付款/授权/退款。", action: "向用户确认是否继续旧 run。" }
|
|
1527
|
+
],
|
|
1528
|
+
guidance: "发现未完成 run 后,先判断它是否属于当前用户意图。相关则 resume;明确不相关可忽略;不确定必须问人类。"
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
export { authRegister, setup, agentStatus, refreshBuyerSessionForStatus, nextActionForAuthStatus, resume, runs, startDeviceAuth, completeDeviceAuth, ensureAuthenticated, maybeMockApproveDeviceAuth, authDevice, waitDeviceAuth, authLogin, authStatus, accountShow, accountLoginLink, accountSetPassword, plansList, plansShow, checkoutCreate, createCheckoutResult, validateLivePaymentFlags, fakeTestingAllowed, paymentWait, paymentWaitResult, checkoutRecover, checkoutOpen, checkoutQR, checkoutList, balance, usage, grantsList, grantsShow, grantsInstall, grantsInstallResult, grantsRevoke, installRuntime, installRuntimeResult, shouldRecoverCheckout, isSuccessfulCheckout, isTerminalCheckoutFailure, installTargetConfig, installClaudeCode, installCodex, installOpenClaw, doctor, doctorModelCheck, runtimeConfigStatus, keys, token, sync, refreshRun, agentRunResponse, phaseToStatus, nextActionForRun, runResumeFlags, resumeCommand, appendPassthroughFlag, safeUserMessageForRun, recoverableRuntimeRunContext };
|