@tokensrc/codex 1.14.16 → 1.14.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -71
- package/dist/client.d.ts +21 -16
- package/dist/client.js +198 -116
- package/dist/client.js.map +1 -1
- package/dist/command-directory.js +3 -2
- package/dist/command-directory.js.map +1 -1
- package/dist/commands.d.ts +0 -1
- package/dist/commands.js +479 -273
- package/dist/commands.js.map +1 -1
- package/dist/credentials.d.ts +3 -75
- package/dist/credentials.js +2 -338
- package/dist/credentials.js.map +1 -1
- package/dist/index.d.ts +2 -8
- package/dist/index.js +2 -8
- package/dist/index.js.map +1 -1
- package/dist/managed-credential-v3-envelope.d.ts +38 -14
- package/dist/managed-credential-v3-envelope.js +116 -25
- package/dist/managed-credential-v3-envelope.js.map +1 -1
- package/dist/managed-credential-v3-flow.d.ts +0 -17
- package/dist/managed-credential-v3-flow.js +107 -110
- package/dist/managed-credential-v3-flow.js.map +1 -1
- package/dist/managed-credential-v3.d.ts +0 -8
- package/dist/managed-credential-v3.js +0 -23
- package/dist/managed-credential-v3.js.map +1 -1
- package/dist/profile.d.ts +2 -6
- package/dist/profile.js +26 -23
- package/dist/profile.js.map +1 -1
- package/dist/protocol.d.ts +1284 -799
- package/dist/protocol.js +494 -277
- package/dist/protocol.js.map +1 -1
- package/dist/request-bound-device-v6-flow.d.ts +42 -0
- package/dist/request-bound-device-v6-flow.js +403 -0
- package/dist/request-bound-device-v6-flow.js.map +1 -0
- package/dist/request-bound-device-v6-state.d.ts +397 -0
- package/dist/request-bound-device-v6-state.js +221 -0
- package/dist/request-bound-device-v6-state.js.map +1 -0
- package/dist/selection.d.ts +5 -0
- package/dist/selection.js +20 -2
- package/dist/selection.js.map +1 -1
- package/dist/token-pool-v4-upload-flow.js +5 -1
- package/dist/token-pool-v4-upload-flow.js.map +1 -1
- package/package.json +2 -2
- package/dist/credential-supervisor.d.ts +0 -17
- package/dist/credential-supervisor.js +0 -84
- package/dist/credential-supervisor.js.map +0 -1
- package/dist/enrollment-envelope.d.ts +0 -18
- package/dist/enrollment-envelope.js +0 -112
- package/dist/enrollment-envelope.js.map +0 -1
- package/dist/enrollment-flow.d.ts +0 -27
- package/dist/enrollment-flow.js +0 -224
- package/dist/enrollment-flow.js.map +0 -1
- package/dist/migration-login-batch.d.ts +0 -66
- package/dist/migration-login-batch.js +0 -202
- package/dist/migration-login-batch.js.map +0 -1
- package/dist/retirement-flow.d.ts +0 -38
- package/dist/retirement-flow.js +0 -78
- package/dist/retirement-flow.js.map +0 -1
- package/dist/short-lived-delivery-flow.d.ts +0 -41
- package/dist/short-lived-delivery-flow.js +0 -141
- package/dist/short-lived-delivery-flow.js.map +0 -1
- package/dist/short-lived-delivery-journal.d.ts +0 -34
- package/dist/short-lived-delivery-journal.js +0 -137
- package/dist/short-lived-delivery-journal.js.map +0 -1
- package/dist/short-lived-delivery.d.ts +0 -26
- package/dist/short-lived-delivery.js +0 -139
- package/dist/short-lived-delivery.js.map +0 -1
package/dist/commands.js
CHANGED
|
@@ -4,31 +4,24 @@ import { join } from "node:path";
|
|
|
4
4
|
import { getStoragePath, loadAccounts, setStoragePath, setStoragePathDirect } from "codex-multi-auth/storage";
|
|
5
5
|
import { normalizeCodexServiceUrl } from "./client.js";
|
|
6
6
|
import { CodexServiceError } from "./errors.js";
|
|
7
|
-
import { backupCodexFileCredentials, codexFileCredentialsMatchAccount,
|
|
7
|
+
import { backupCodexFileCredentials, codexFileCredentialsMatchAccount, inspectLocalCodexCredentialStatus, resetManagedCodexFileCredentials, restoreCodexFileCredentials, writeCodexChatgptFileCredentials } from "./credentials.js";
|
|
8
8
|
import { CODEX_DISTRIBUTION_SCHEMA_VERSION } from "./protocol.js";
|
|
9
9
|
import { formatCodexAccountDisplayIdentity, resolveCodexAdminAccount, resolveCodexAccountDisplayName } from "./account-display.js";
|
|
10
10
|
import { loadCodexQuotaUploads } from "./quota-cache.js";
|
|
11
11
|
import { cleanupLegacyCodexProvider } from "./legacy-cleanup.js";
|
|
12
|
-
import { executeCodexEnrollmentLogin } from "./enrollment-flow.js";
|
|
13
|
-
import { executeCodexMigrationLoginBatch } from "./migration-login-batch.js";
|
|
14
|
-
import { executeCodexShortLivedCredentialDelivery } from "./short-lived-delivery-flow.js";
|
|
15
12
|
import { executeCodexTokenPoolV4Delivery, reconcilePendingManagedCodexV3Operation, reportManagedCodexV3Credentials } from "./managed-credential-v3-flow.js";
|
|
16
13
|
import { resolveManagedCodexV3AssignmentTarget } from "./managed-credential-v3-assignment.js";
|
|
17
14
|
import { executeCodexTokenPoolV4Login } from "./token-pool-v4-upload-flow.js";
|
|
18
15
|
import { readManagedCodexV3State } from "./managed-credential-v3.js";
|
|
19
|
-
import { profileAccountTokenPoolV4Enabled, profileMultiDeviceCredentialSyncEnabled } from "./profile.js";
|
|
16
|
+
import { profileAccountTokenPoolV4Enabled, profileMultiDeviceCredentialSyncEnabled, profileRequestBoundDeviceCredentialsEnabled } from "./profile.js";
|
|
20
17
|
import { superviseCodexV5Credentials, synchronizeCodexV5SelectionOnce } from "./multi-device-v5-flow.js";
|
|
21
|
-
import {
|
|
22
|
-
import { clearCodexSelection, loadCodexSelection, rememberCodexSelectionProviderAccount, saveCodexSelection } from "./selection.js";
|
|
18
|
+
import { clearCodexSelection, loadCodexSelection, saveCodexSelection } from "./selection.js";
|
|
23
19
|
import { installCodexShortcuts, removeCodexShortcuts } from "./shortcuts.js";
|
|
24
20
|
import { isCodexAccountAvailable, isCodexSubscriptionExpired, renderCodexAccountDashboard, selectCodexAccount } from "./tui.js";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
21
|
+
import { ensureCodexV6DeviceCredentials, reconcileCodexV6DeviceAccess, reportCodexV6DeviceCredentials, requireAvailableCodexV6DeviceAccess } from "./request-bound-device-v6-flow.js";
|
|
22
|
+
import { getOrCreateCodexInstallationId } from "./multi-device-v5-state.js";
|
|
23
|
+
import { resetCodexV6LocalState } from "./request-bound-device-v6-state.js";
|
|
27
24
|
export const NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE = "当前用户没有绑定可用的 ChatGPT 账号。请前往 OA 系统提交 IT11 工单,申请独享或共享的 ChatGPT 个人账号。";
|
|
28
|
-
// The Server refreshes a user-bound lineage only inside its final 60-second
|
|
29
|
-
// credential safety window. Asking for a new delivery earlier would merely
|
|
30
|
-
// re-encrypt the same access/ID tokens and must not be treated as a renewal.
|
|
31
|
-
const SERVER_MANAGED_SHORT_LIVED_REFRESH_WINDOW_MS = 60_000;
|
|
32
25
|
const SCHEMA_V2_LEGACY_MANAGEMENT_DISABLED = "schema_v2_legacy_management_disabled";
|
|
33
26
|
function findV3Assignment(input) {
|
|
34
27
|
return input.assignments.items.find((item) => item.assignmentId === input.assignmentId
|
|
@@ -36,11 +29,6 @@ function findV3Assignment(input) {
|
|
|
36
29
|
&& item.providerAccountId === input.providerAccountId
|
|
37
30
|
&& item.lineageId === input.lineageId);
|
|
38
31
|
}
|
|
39
|
-
export function effectiveCodexCredentialRefreshBefore(requestedMs, serverManagedShortLived) {
|
|
40
|
-
return serverManagedShortLived
|
|
41
|
-
? Math.min(requestedMs, SERVER_MANAGED_SHORT_LIVED_REFRESH_WINDOW_MS)
|
|
42
|
-
: requestedMs;
|
|
43
|
-
}
|
|
44
32
|
async function json(command, deps) {
|
|
45
33
|
if (command.optsWithGlobals().json)
|
|
46
34
|
return true;
|
|
@@ -59,31 +47,28 @@ function success(value) {
|
|
|
59
47
|
function importFailures(credentials) {
|
|
60
48
|
return credentials.filter(({ status }) => status === "REJECTED" || status === "UNKNOWN");
|
|
61
49
|
}
|
|
62
|
-
function schemaV2LifecycleEnabled(config) {
|
|
63
|
-
const capabilities = config.profile?.userBoundRefreshTokens;
|
|
64
|
-
return capabilities?.delivery === true || capabilities?.migrationPlans === true;
|
|
65
|
-
}
|
|
66
50
|
function accountTokenPoolV4Enabled(config) {
|
|
67
51
|
return profileAccountTokenPoolV4Enabled(config.profile);
|
|
68
52
|
}
|
|
69
53
|
function multiDeviceCredentialSyncEnabled(config) {
|
|
70
54
|
return profileMultiDeviceCredentialSyncEnabled(config.profile);
|
|
71
55
|
}
|
|
56
|
+
function requestBoundDeviceCredentialsEnabled(config) {
|
|
57
|
+
return profileRequestBoundDeviceCredentialsEnabled(config.profile);
|
|
58
|
+
}
|
|
59
|
+
function assertRequestBoundDeviceCredentialsEnabled(config) {
|
|
60
|
+
if (requestBoundDeviceCredentialsEnabled(config))
|
|
61
|
+
return;
|
|
62
|
+
throw new CodexServiceError("平台尚未启用请求绑定的独立设备 Token Set。", "codex_request_bound_device_v6_not_enabled", 409);
|
|
63
|
+
}
|
|
72
64
|
function codexConfigPathForEnv(env) {
|
|
73
65
|
const codexHome = env.CODEX_HOME?.trim();
|
|
74
66
|
return codexHome ? join(codexHome, "config.toml") : undefined;
|
|
75
67
|
}
|
|
76
68
|
function assertLegacyManagementEnabled(config) {
|
|
77
|
-
if (!
|
|
69
|
+
if (!requestBoundDeviceCredentialsEnabled(config))
|
|
78
70
|
return;
|
|
79
|
-
throw new CodexServiceError("
|
|
80
|
-
}
|
|
81
|
-
function assignmentCredentialStatus(assignment) {
|
|
82
|
-
if (isCodexSubscriptionExpired(assignment))
|
|
83
|
-
return "EXPIRED";
|
|
84
|
-
if (assignment.deliveryEligible)
|
|
85
|
-
return "AVAILABLE";
|
|
86
|
-
return ["NEEDS_REAUTH", "LOST", "DIVERGED", "COMPROMISED"].includes(assignment.lineageStatus ?? "") ? "NEEDS_REAUTH" : "TEMPORARILY_UNAVAILABLE";
|
|
71
|
+
throw new CodexServiceError("Request-bound per-device credentials are enabled; legacy direct assignment management is disabled.", SCHEMA_V2_LEGACY_MANAGEMENT_DISABLED, 409);
|
|
87
72
|
}
|
|
88
73
|
function assertCodexSubscriptionActive(subscription, label) {
|
|
89
74
|
if (!isCodexSubscriptionExpired(subscription))
|
|
@@ -175,6 +160,61 @@ function parseSubscriptionRenewalExpiry(value) {
|
|
|
175
160
|
}
|
|
176
161
|
return expiresAt;
|
|
177
162
|
}
|
|
163
|
+
function parseRequestInstant(value, option) {
|
|
164
|
+
const normalized = value.trim();
|
|
165
|
+
const timestamp = Date.parse(normalized);
|
|
166
|
+
if (!/(?:Z|[+-]\d{2}:\d{2})$/iu.test(normalized) || !Number.isFinite(timestamp)) {
|
|
167
|
+
throw new CodexServiceError(`${option} 必须是带时区的 ISO-8601 时间。`, "invalid_codex_account_request");
|
|
168
|
+
}
|
|
169
|
+
return new Date(timestamp).toISOString();
|
|
170
|
+
}
|
|
171
|
+
function parsePositiveInteger(value, option, maximum = 32) {
|
|
172
|
+
const parsed = Number.parseInt(value, 10);
|
|
173
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > maximum || String(parsed) !== value.trim()) {
|
|
174
|
+
throw new CodexServiceError(`${option} 必须是 1 到 ${maximum} 之间的整数。`, "invalid_codex_account_request");
|
|
175
|
+
}
|
|
176
|
+
return parsed;
|
|
177
|
+
}
|
|
178
|
+
function parseNonNegativeInteger(value, option) {
|
|
179
|
+
const parsed = Number.parseInt(value, 10);
|
|
180
|
+
if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== value.trim()) {
|
|
181
|
+
throw new CodexServiceError(`${option} 必须是非负整数。`, "invalid_codex_account_request");
|
|
182
|
+
}
|
|
183
|
+
return parsed;
|
|
184
|
+
}
|
|
185
|
+
function v6Platform() {
|
|
186
|
+
return ["darwin", "linux", "win32"].includes(process.platform)
|
|
187
|
+
? process.platform : "unknown";
|
|
188
|
+
}
|
|
189
|
+
function formatV6Request(request) {
|
|
190
|
+
const expiresAt = Date.parse(request.validUntil ?? request.requestedValidUntil);
|
|
191
|
+
const remainingDays = Math.ceil((expiresAt - Date.now()) / 86_400_000);
|
|
192
|
+
const expiryNotice = remainingDays <= 0 ? "EXPIRED"
|
|
193
|
+
: remainingDays <= 7 ? `URGENT:${remainingDays}d`
|
|
194
|
+
: remainingDays <= 30 ? `EXPIRING:${remainingDays}d` : `${remainingDays}d`;
|
|
195
|
+
return `${request.id} ${request.status}/${request.availability}`
|
|
196
|
+
+ ` user=${request.username} (${request.userSubject})`
|
|
197
|
+
+ ` devices=${request.activeBindingCount}+${request.pendingBindingCount}`
|
|
198
|
+
+ `/${request.approvedDeviceLimit ?? request.requestedDeviceCount}`
|
|
199
|
+
+ ` valid-until=${request.validUntil ?? request.requestedValidUntil}`
|
|
200
|
+
+ ` expiry=${expiryNotice}`
|
|
201
|
+
+ ` version=${request.version}`;
|
|
202
|
+
}
|
|
203
|
+
function formatV6Binding(binding) {
|
|
204
|
+
return `${binding.id} ${binding.status} device=${binding.deviceLabel}`
|
|
205
|
+
+ ` (${binding.installationId}) token-set=${binding.credentialLineageId ?? "WAITING"}`
|
|
206
|
+
+ `${binding.accountExpiresAt ? ` account-expires=${binding.accountExpiresAt}` : ""}`
|
|
207
|
+
+ `${binding.replacementDueAt ? ` replace-before=${binding.replacementDueAt}` : ""}`
|
|
208
|
+
+ `${binding.predecessorBindingId ? ` replaces=${binding.predecessorBindingId}` : ""}`;
|
|
209
|
+
}
|
|
210
|
+
async function v6DeviceAccess(deps, options = {}, deviceLabel) {
|
|
211
|
+
return reconcileCodexV6DeviceAccess({
|
|
212
|
+
client: deps.clientFor(serviceConnection(options)),
|
|
213
|
+
tenantId: deps.config.connection.tenantId,
|
|
214
|
+
env: deps.env,
|
|
215
|
+
deviceLabel
|
|
216
|
+
});
|
|
217
|
+
}
|
|
178
218
|
async function loadAccountData(deps, options = {}, allowV4Replacement = false) {
|
|
179
219
|
const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
|
|
180
220
|
if (accountTokenPoolV4Enabled(deps.config)) {
|
|
@@ -221,31 +261,6 @@ async function loadAccountData(deps, options = {}, allowV4Replacement = false) {
|
|
|
221
261
|
preferredV3Assignments
|
|
222
262
|
};
|
|
223
263
|
}
|
|
224
|
-
if (deps.config.profile?.userBoundRefreshTokens?.delivery === true) {
|
|
225
|
-
const assignments = await deps.clientFor(serviceConnection(options)).listCredentialAssignments();
|
|
226
|
-
const accounts = assignments.items.map((assignment) => ({
|
|
227
|
-
id: assignment.managedAccountId,
|
|
228
|
-
accountId: assignment.assignmentId,
|
|
229
|
-
label: assignment.label,
|
|
230
|
-
isDefault: previous?.account.assignmentId === assignment.assignmentId,
|
|
231
|
-
...(assignment.credentialExpiresAt
|
|
232
|
-
? { accessExpiresAt: Date.parse(assignment.credentialExpiresAt) }
|
|
233
|
-
: {}),
|
|
234
|
-
...subscriptionSummary(assignment),
|
|
235
|
-
credentialStatus: assignmentCredentialStatus(assignment),
|
|
236
|
-
lineageStatus: assignment.lineageStatus ?? undefined,
|
|
237
|
-
quota: null
|
|
238
|
-
}));
|
|
239
|
-
return {
|
|
240
|
-
response: {
|
|
241
|
-
schemaVersion: CODEX_DISTRIBUTION_SCHEMA_VERSION,
|
|
242
|
-
accounts,
|
|
243
|
-
defaultAccountId: previous?.account.managedAccountId ?? null
|
|
244
|
-
},
|
|
245
|
-
selectedAccountId: previous?.account.managedAccountId,
|
|
246
|
-
assignments
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
264
|
const response = await deps.clientFor(serviceConnection(options)).listAccounts();
|
|
250
265
|
return {
|
|
251
266
|
response,
|
|
@@ -374,39 +389,6 @@ async function installCredentials(deps, selection, service, allowV4Replacement =
|
|
|
374
389
|
});
|
|
375
390
|
return { expiresAt: Date.parse(delivered.credentialExpiresAt), selection };
|
|
376
391
|
}
|
|
377
|
-
if (deps.config.profile?.userBoundRefreshTokens?.delivery === true) {
|
|
378
|
-
await discardCodexFileCredentialBackup(deps.env);
|
|
379
|
-
if (selection.account.distributionMode !== "SERVER_MANAGED_SHORT_LIVED"
|
|
380
|
-
|| !selection.account.assignmentId || !selection.account.managedAccountId) {
|
|
381
|
-
throw new CodexServiceError("The saved Codex selection is not a schema-v2 credential assignment.", "codex_assignment_not_found", 404);
|
|
382
|
-
}
|
|
383
|
-
const assignments = await client.listCredentialAssignments();
|
|
384
|
-
const assignment = assignments.items.find((item) => item.assignmentId === selection.account.assignmentId
|
|
385
|
-
&& item.managedAccountId === selection.account.managedAccountId);
|
|
386
|
-
if (!assignment) {
|
|
387
|
-
throw new CodexServiceError("The selected schema-v2 credential assignment is no longer active.", "codex_assignment_not_found", 404);
|
|
388
|
-
}
|
|
389
|
-
const state = await readManagedCodexLineageState(deps.env);
|
|
390
|
-
const journal = await readCodexShortLivedDeliveryJournal(deps.env);
|
|
391
|
-
const recoverable = journal?.tenantId === deps.config.connection.tenantId
|
|
392
|
-
&& journal.assignmentId === assignment.assignmentId
|
|
393
|
-
|| state?.assignmentId === assignment.assignmentId
|
|
394
|
-
&& (state.tenantId === deps.config.connection.tenantId || state.tenantId === null)
|
|
395
|
-
&& (state.phase === "INSTALLING"
|
|
396
|
-
|| state.phase === "INSTALLED_PENDING_ACK" && Date.parse(state.ackExpiresAt) > Date.now());
|
|
397
|
-
if (!assignment.deliveryEligible && !recoverable) {
|
|
398
|
-
throw new CodexServiceError("The selected schema-v2 credential assignment is not ready for delivery.", "short_lived_credential_lineage_not_ready", 409);
|
|
399
|
-
}
|
|
400
|
-
const delivered = await executeCodexShortLivedCredentialDelivery({
|
|
401
|
-
client,
|
|
402
|
-
tenantId: deps.config.connection.tenantId,
|
|
403
|
-
assignmentId: assignment.assignmentId,
|
|
404
|
-
allowNewDelivery: assignment.deliveryEligible,
|
|
405
|
-
env: deps.env
|
|
406
|
-
});
|
|
407
|
-
const updated = await rememberCodexSelectionProviderAccount(deps.config.persistence.configDirectory, assignment.assignmentId, delivered.providerAccountId);
|
|
408
|
-
return { expiresAt: Date.parse(delivered.credentialExpiresAt), selection: updated };
|
|
409
|
-
}
|
|
410
392
|
await backupCodexFileCredentials(deps.env);
|
|
411
393
|
const response = await client.getLoginCredentials(selection.account.id);
|
|
412
394
|
await writeCodexChatgptFileCredentials({
|
|
@@ -417,6 +399,45 @@ async function installCredentials(deps, selection, service, allowV4Replacement =
|
|
|
417
399
|
return { expiresAt: response.expiresAt, selection };
|
|
418
400
|
}
|
|
419
401
|
async function selectedCredentials(deps, requested, options = {}) {
|
|
402
|
+
if (requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
403
|
+
const client = deps.clientFor(serviceConnection(options));
|
|
404
|
+
const ensured = await ensureCodexV6DeviceCredentials({
|
|
405
|
+
client,
|
|
406
|
+
tenantId: deps.config.connection.tenantId,
|
|
407
|
+
env: deps.env
|
|
408
|
+
});
|
|
409
|
+
if (ensured.replacementDeferredWarningCode) {
|
|
410
|
+
deps.errorOutput.write(`新设备 Token Set 暂时无法交付 (${ensured.replacementDeferredWarningCode});`
|
|
411
|
+
+ "本次继续使用尚有效的旧 Token Set,稍后会自动重试。\n");
|
|
412
|
+
}
|
|
413
|
+
const binding = ensured.binding;
|
|
414
|
+
if (requested && ![
|
|
415
|
+
binding.id,
|
|
416
|
+
binding.requestId,
|
|
417
|
+
binding.managedAccountId,
|
|
418
|
+
binding.providerAccountId,
|
|
419
|
+
binding.credentialLineageId
|
|
420
|
+
].includes(requested)) {
|
|
421
|
+
throw new CodexServiceError("当前设备只能使用管理员为它绑定的独立 Token Set。", "codex_device_binding_not_found", 404);
|
|
422
|
+
}
|
|
423
|
+
const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
|
|
424
|
+
serviceUrl: options.server ?? deps.config.connection.serverUrl,
|
|
425
|
+
allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
|
|
426
|
+
account: {
|
|
427
|
+
id: binding.managedAccountId,
|
|
428
|
+
accountId: binding.providerAccountId,
|
|
429
|
+
label: `${binding.deviceLabel} / ${binding.requestId}`,
|
|
430
|
+
distributionMode: "REQUEST_BOUND_DEVICE_V6",
|
|
431
|
+
managedAccountId: binding.managedAccountId,
|
|
432
|
+
providerAccountId: binding.providerAccountId,
|
|
433
|
+
requestId: binding.requestId,
|
|
434
|
+
bindingId: binding.id,
|
|
435
|
+
installationId: binding.installationId,
|
|
436
|
+
credentialLineageId: binding.credentialLineageId
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
return { selection, expiresAt: Date.parse(ensured.credentialExpiresAt) };
|
|
440
|
+
}
|
|
420
441
|
if (accountTokenPoolV4Enabled(deps.config)) {
|
|
421
442
|
const client = deps.clientFor(serviceConnection(options));
|
|
422
443
|
const recovered = await reconcilePendingManagedCodexV3Operation({
|
|
@@ -525,43 +546,6 @@ async function selectedCredentials(deps, requested, options = {}) {
|
|
|
525
546
|
if (!selection || serviceChanged || requested && !requestedMatchesSelection) {
|
|
526
547
|
selection = (await chooseAccount(deps, requested, options)).selection;
|
|
527
548
|
}
|
|
528
|
-
if (deps.config.profile?.userBoundRefreshTokens?.delivery === true) {
|
|
529
|
-
if (selection.account.distributionMode !== "SERVER_MANAGED_SHORT_LIVED") {
|
|
530
|
-
selection = (await chooseAccount(deps, requested, options)).selection;
|
|
531
|
-
}
|
|
532
|
-
const assignments = await deps.clientFor({
|
|
533
|
-
serverUrl: selection.serviceUrl,
|
|
534
|
-
allowInsecureHttp: selection.allowInsecurePoc
|
|
535
|
-
}).listCredentialAssignments();
|
|
536
|
-
const assignment = assignments.items.find((item) => item.assignmentId === selection.account.assignmentId
|
|
537
|
-
&& item.managedAccountId === selection.account.managedAccountId);
|
|
538
|
-
if (!assignment) {
|
|
539
|
-
throw new CodexServiceError("The selected schema-v2 credential assignment is no longer active.", "codex_assignment_not_found", 404);
|
|
540
|
-
}
|
|
541
|
-
assertCodexSubscriptionActive(assignment, assignment.label);
|
|
542
|
-
const state = await readManagedCodexLineageState(deps.env);
|
|
543
|
-
const journal = await readCodexShortLivedDeliveryJournal(deps.env);
|
|
544
|
-
const stateMatches = state !== undefined
|
|
545
|
-
&& (state.tenantId === deps.config.connection.tenantId || state.tenantId === null)
|
|
546
|
-
&& state.assignmentId === assignment.assignmentId
|
|
547
|
-
&& state.managedAccountId === assignment.managedAccountId;
|
|
548
|
-
const local = await inspectLocalCodexCredentialStatus(stateMatches ? state.providerAccountId : selection.account.providerAccountId, deps.env);
|
|
549
|
-
const pending = journal?.tenantId === deps.config.connection.tenantId
|
|
550
|
-
&& journal.assignmentId === assignment.assignmentId
|
|
551
|
-
|| stateMatches && state.phase !== "INSTALLED";
|
|
552
|
-
if (!assignment.deliveryEligible || !stateMatches || !local.usable
|
|
553
|
-
|| local.matchesExpectedAccount !== true || pending) {
|
|
554
|
-
return installCredentials(deps, selection, {
|
|
555
|
-
serverUrl: selection.serviceUrl,
|
|
556
|
-
allowInsecureHttp: selection.allowInsecurePoc
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
const matchingState = state;
|
|
560
|
-
if (selection.account.providerAccountId !== matchingState.providerAccountId) {
|
|
561
|
-
selection = await rememberCodexSelectionProviderAccount(deps.config.persistence.configDirectory, assignment.assignmentId, matchingState.providerAccountId);
|
|
562
|
-
}
|
|
563
|
-
return { selection, expiresAt: local.expiresAt ?? Date.parse(matchingState.credentialExpiresAt) };
|
|
564
|
-
}
|
|
565
549
|
if (!selection.account.accountId) {
|
|
566
550
|
throw new CodexServiceError("The saved Codex selection has no provider account.", "invalid_configuration");
|
|
567
551
|
}
|
|
@@ -592,6 +576,24 @@ async function selectedCredentials(deps, requested, options = {}) {
|
|
|
592
576
|
return { selection, expiresAt: local.expiresAt };
|
|
593
577
|
}
|
|
594
578
|
async function launchWithSelectedCredentials(deps, selected, launch) {
|
|
579
|
+
if (requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
580
|
+
try {
|
|
581
|
+
return await launch();
|
|
582
|
+
}
|
|
583
|
+
finally {
|
|
584
|
+
const report = await reportCodexV6DeviceCredentials({
|
|
585
|
+
client: deps.clientFor({
|
|
586
|
+
serverUrl: selected.selection.serviceUrl,
|
|
587
|
+
allowInsecureHttp: selected.selection.allowInsecurePoc
|
|
588
|
+
}),
|
|
589
|
+
tenantId: deps.config.connection.tenantId,
|
|
590
|
+
env: deps.env
|
|
591
|
+
});
|
|
592
|
+
if (!report.reported && report.warningCode) {
|
|
593
|
+
deps.errorOutput.write(`Codex 独立设备 Token 代际上报暂缓 (${report.warningCode});其他设备不会受影响。\n`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
595
597
|
if (multiDeviceCredentialSyncEnabled(deps.config)) {
|
|
596
598
|
const client = deps.clientFor({
|
|
597
599
|
serverUrl: selected.selection.serviceUrl,
|
|
@@ -636,21 +638,7 @@ async function launchWithSelectedCredentials(deps, selected, launch) {
|
|
|
636
638
|
}
|
|
637
639
|
}
|
|
638
640
|
}
|
|
639
|
-
|
|
640
|
-
return launch();
|
|
641
|
-
let current = selected;
|
|
642
|
-
return superviseCodexShortLivedCredentials({
|
|
643
|
-
initialExpiresAt: current.expiresAt,
|
|
644
|
-
launch: (signal) => launch(signal),
|
|
645
|
-
renew: async () => {
|
|
646
|
-
const installed = await installCredentials(deps, current.selection, {
|
|
647
|
-
serverUrl: current.selection.serviceUrl,
|
|
648
|
-
allowInsecureHttp: current.selection.allowInsecurePoc
|
|
649
|
-
});
|
|
650
|
-
current = installed;
|
|
651
|
-
return { expiresAt: installed.expiresAt };
|
|
652
|
-
}
|
|
653
|
-
});
|
|
641
|
+
return launch();
|
|
654
642
|
}
|
|
655
643
|
export function registerCodexCommands(program, deps, options = {}) {
|
|
656
644
|
const codex = options.nested === false
|
|
@@ -677,18 +665,25 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
677
665
|
else
|
|
678
666
|
deps.output.write("Logged out.\n");
|
|
679
667
|
});
|
|
680
|
-
if (!includeAuthCommands && accountTokenPoolV4Enabled(deps.config))
|
|
681
|
-
serviceOptions(codex.command("login")
|
|
682
|
-
.description(
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
.option("--
|
|
686
|
-
|
|
687
|
-
|
|
668
|
+
if (!includeAuthCommands && accountTokenPoolV4Enabled(deps.config)) {
|
|
669
|
+
let managedLogin = serviceOptions(codex.command("login")
|
|
670
|
+
.description(requestBoundDeviceCredentialsEnabled(deps.config)
|
|
671
|
+
? "Run an isolated official Codex login and add one complete token set to the unbound pool"
|
|
672
|
+
: "Run an isolated official Codex login and enroll an unbound or explicitly assigned credential"))
|
|
673
|
+
.option("--device-auth", "use the official Codex device authorization flow");
|
|
674
|
+
if (!requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
675
|
+
managedLogin = managedLogin
|
|
676
|
+
.option("--assign-to <username-or-email>", "assign by exact username or email instead of adding to the unbound pool")
|
|
677
|
+
.option("--assign-to-subject <uuid>", "assign directly to a canonical Keycloak subject UUID")
|
|
678
|
+
.option("--yes", "confirm direct subject assignment");
|
|
679
|
+
}
|
|
680
|
+
managedLogin.action(async (opts, command) => {
|
|
688
681
|
if (!accountTokenPoolV4Enabled(deps.config)) {
|
|
689
682
|
throw new CodexServiceError("The Codex v4 account token pool is not enabled by the verified platform profile.", "codex_token_pool_v4_not_enabled", 409);
|
|
690
683
|
}
|
|
691
|
-
const target =
|
|
684
|
+
const target = requestBoundDeviceCredentialsEnabled(deps.config)
|
|
685
|
+
? { mode: "UNBOUND_POOL" }
|
|
686
|
+
: resolveManagedCodexV3AssignmentTarget(opts);
|
|
692
687
|
const result = await executeCodexTokenPoolV4Login({
|
|
693
688
|
client: deps.clientFor(serviceConnection(opts)),
|
|
694
689
|
tenantId: deps.config.connection.tenantId,
|
|
@@ -715,8 +710,131 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
715
710
|
else
|
|
716
711
|
deps.output.write(`Codex credential uploaded to token pool; status=${result.status}, placement=${result.placement}.\n`);
|
|
717
712
|
});
|
|
713
|
+
}
|
|
714
|
+
const requests = codex.command("request")
|
|
715
|
+
.description("Apply for Codex access and manage request-bound device token sets");
|
|
716
|
+
serviceOptions(requests.command("create")
|
|
717
|
+
.description("Submit an account request and identify this device as the initial binding candidate")
|
|
718
|
+
.requiredOption("--reason <text>", "申请原因,至少 5 个字符")
|
|
719
|
+
.requiredOption("--until <timestamp>", "申请有效期结束时间,必须包含时区")
|
|
720
|
+
.option("--devices <count>", "申请的设备数量", "1")
|
|
721
|
+
.option("--device-label <label>", "当前设备名称")
|
|
722
|
+
.option("--idempotency-key <key>", "重试时复用同一个幂等键"))
|
|
723
|
+
.action(async (opts, command) => {
|
|
724
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
725
|
+
const requestedValidUntil = parseRequestInstant(opts.until, "--until");
|
|
726
|
+
if (Date.parse(requestedValidUntil) <= Date.now())
|
|
727
|
+
throw new CodexServiceError("--until 必须晚于当前时间。", "invalid_codex_account_request");
|
|
728
|
+
const installationId = await getOrCreateCodexInstallationId(deps.env);
|
|
729
|
+
const clientRequestId = randomUUID();
|
|
730
|
+
const result = await deps.clientFor(serviceConnection(opts)).createV6AccountRequest({
|
|
731
|
+
schemaVersion: 6,
|
|
732
|
+
clientRequestId,
|
|
733
|
+
reason: opts.reason,
|
|
734
|
+
requestedDeviceCount: parsePositiveInteger(opts.devices, "--devices"),
|
|
735
|
+
requestedValidUntil,
|
|
736
|
+
initialDevice: {
|
|
737
|
+
installationId,
|
|
738
|
+
label: (opts.deviceLabel?.trim() || hostname() || "Codex device").slice(0, 128),
|
|
739
|
+
platform: v6Platform()
|
|
740
|
+
}
|
|
741
|
+
}, opts.idempotencyKey?.trim() || clientRequestId);
|
|
742
|
+
if (await json(command, deps))
|
|
743
|
+
print(deps.output, success({
|
|
744
|
+
...result, installationId
|
|
745
|
+
}));
|
|
746
|
+
else
|
|
747
|
+
deps.output.write(`申请已提交:${formatV6Request(result.request)}\n`
|
|
748
|
+
+ `首台设备候选:${installationId}。审批后每个设备将获得独立完整 Token Set。\n`);
|
|
749
|
+
});
|
|
750
|
+
serviceOptions(requests.command("list").description("List the current user's Codex account requests"))
|
|
751
|
+
.action(async (opts, command) => {
|
|
752
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
753
|
+
const result = await deps.clientFor(serviceConnection(opts)).listV6AccountRequests();
|
|
754
|
+
if (await json(command, deps))
|
|
755
|
+
print(deps.output, success(result));
|
|
756
|
+
else if (result.items.length === 0)
|
|
757
|
+
deps.output.write("当前用户没有 ChatGPT/Codex 账号申请。\n");
|
|
758
|
+
else
|
|
759
|
+
for (const item of result.items)
|
|
760
|
+
deps.output.write(`${formatV6Request(item)}\n`);
|
|
761
|
+
});
|
|
762
|
+
serviceOptions(requests.command("show <requestId>").description("Show one account request"))
|
|
763
|
+
.action(async (requestId, opts, command) => {
|
|
764
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
765
|
+
const result = await deps.clientFor(serviceConnection(opts)).getV6AccountRequest(requestId);
|
|
766
|
+
if (await json(command, deps))
|
|
767
|
+
print(deps.output, success(result));
|
|
768
|
+
else
|
|
769
|
+
deps.output.write(`${formatV6Request(result.request)}\n`);
|
|
770
|
+
});
|
|
771
|
+
serviceOptions(requests.command("bindings <requestId>")
|
|
772
|
+
.description("List independent device token-set bindings under one request"))
|
|
773
|
+
.action(async (requestId, opts, command) => {
|
|
774
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
775
|
+
const result = await deps.clientFor(serviceConnection(opts)).listV6DeviceBindings(requestId);
|
|
776
|
+
if (await json(command, deps))
|
|
777
|
+
print(deps.output, success(result));
|
|
778
|
+
else if (result.items.length === 0)
|
|
779
|
+
deps.output.write("该申请尚无设备绑定。\n");
|
|
780
|
+
else
|
|
781
|
+
for (const binding of result.items)
|
|
782
|
+
deps.output.write(`${formatV6Binding(binding)}\n`);
|
|
783
|
+
});
|
|
784
|
+
serviceOptions(requests.command("reconcile")
|
|
785
|
+
.description("Reconcile this installation and automatically adopt one legacy distributed token set")
|
|
786
|
+
.option("--device-label <label>", "当前设备名称"))
|
|
787
|
+
.action(async (opts, command) => {
|
|
788
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
789
|
+
const access = await v6DeviceAccess(deps, opts, opts.deviceLabel);
|
|
790
|
+
if (await json(command, deps))
|
|
791
|
+
print(deps.output, success({
|
|
792
|
+
installationId: access.installationId, ...access.response
|
|
793
|
+
}));
|
|
794
|
+
else if (access.response.accessStatus === "AVAILABLE") {
|
|
795
|
+
const { request, binding } = requireAvailableCodexV6DeviceAccess(access);
|
|
796
|
+
deps.output.write(`${formatV6Request(request)}\n设备绑定:${binding.id} (${binding.installationId})`
|
|
797
|
+
+ `,迁移结果=${access.response.migration}。\n`);
|
|
798
|
+
}
|
|
799
|
+
else
|
|
800
|
+
requireAvailableCodexV6DeviceAccess(access);
|
|
801
|
+
});
|
|
802
|
+
serviceOptions(requests.command("cancel <requestId>")
|
|
803
|
+
.description("Cancel a still-submitted account request")
|
|
804
|
+
.requiredOption("--version <number>", "当前请求版本")
|
|
805
|
+
.option("--idempotency-key <key>", "重试时复用同一个幂等键"))
|
|
806
|
+
.action(async (requestId, opts, command) => {
|
|
807
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
808
|
+
const result = await deps.clientFor(serviceConnection(opts)).cancelV6AccountRequest(requestId, { schemaVersion: 6, expectedRequestVersion: parseNonNegativeInteger(opts.version, "--version") }, opts.idempotencyKey?.trim() || randomUUID());
|
|
809
|
+
if (await json(command, deps))
|
|
810
|
+
print(deps.output, success(result));
|
|
811
|
+
else
|
|
812
|
+
deps.output.write(`申请已取消:${formatV6Request(result.request)}\n`);
|
|
813
|
+
});
|
|
718
814
|
serviceOptions(codex.command("list").description("List available Codex accounts without downloading credentials"))
|
|
719
815
|
.action(async (opts, command) => {
|
|
816
|
+
if (requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
817
|
+
const access = await v6DeviceAccess(deps, opts);
|
|
818
|
+
if (await json(command, deps)) {
|
|
819
|
+
print(deps.output, success({
|
|
820
|
+
credentialModel: "REQUEST_BOUND_DEVICE_V6",
|
|
821
|
+
installationId: access.installationId,
|
|
822
|
+
...access.response
|
|
823
|
+
}));
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (access.response.accessStatus !== "AVAILABLE") {
|
|
827
|
+
requireAvailableCodexV6DeviceAccess(access);
|
|
828
|
+
}
|
|
829
|
+
const { request, binding } = requireAvailableCodexV6DeviceAccess(access);
|
|
830
|
+
deps.output.write(`${formatV6Request(request)}\n`
|
|
831
|
+
+ `${binding.id} device=${binding.deviceLabel} (${binding.installationId})`
|
|
832
|
+
+ ` token-set=${binding.credentialLineageId} ${binding.status}\n`);
|
|
833
|
+
if (access.response.migration === "AUTO_MIGRATED") {
|
|
834
|
+
deps.output.write("既有本机凭据已自动转换为该申请下的第一个独立设备绑定。\n");
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
720
838
|
const { response: result, selectedAccountId, assignments, v3Assignments } = await loadAccountData(deps, opts);
|
|
721
839
|
const hasAvailableAccount = result.accounts.some(isCodexAccountAvailable);
|
|
722
840
|
const shouldRequestAccount = result.accounts.length === 0
|
|
@@ -739,6 +857,21 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
739
857
|
serviceOptions(codex.command("use [account]").description("Select an account and update the official Codex auth.json"))
|
|
740
858
|
.option("--takeover", "explicitly replace a v4 credential active on another host")
|
|
741
859
|
.action(async (account, opts, command) => {
|
|
860
|
+
if (requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
861
|
+
if (opts.takeover)
|
|
862
|
+
throw new CodexServiceError("v6 每台设备使用独立 Token Set,不支持接管其他设备的凭据。", "codex_device_takeover_not_allowed", 409);
|
|
863
|
+
const selected = await selectedCredentials(deps, account, opts);
|
|
864
|
+
if (await json(command, deps))
|
|
865
|
+
print(deps.output, success({
|
|
866
|
+
account: selected.selection.account,
|
|
867
|
+
auth_mode: "chatgpt",
|
|
868
|
+
credential_model: "REQUEST_BOUND_DEVICE_V6"
|
|
869
|
+
}));
|
|
870
|
+
else
|
|
871
|
+
deps.output.write(`已安装当前设备的独立 Token Set:${selected.selection.account.label}`
|
|
872
|
+
+ ` (${selected.selection.account.bindingId})\n`);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
742
875
|
const v4 = accountTokenPoolV4Enabled(deps.config);
|
|
743
876
|
if (opts.takeover && !v4)
|
|
744
877
|
throw new CodexServiceError("Credential takeover requires the v4 token-pool capability.", "codex_token_pool_v4_not_enabled", 409);
|
|
@@ -822,9 +955,8 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
822
955
|
.action(async (_options, command) => {
|
|
823
956
|
const selection = await loadCodexSelection(deps.config.persistence.configDirectory);
|
|
824
957
|
const cleanup = await cleanupLegacyCodexProvider(codexConfigPathForEnv(deps.env));
|
|
825
|
-
if (schemaV2LifecycleEnabled(deps.config))
|
|
826
|
-
await discardCodexFileCredentialBackup(deps.env);
|
|
827
958
|
const auth = await resetManagedCodexFileCredentials(selection?.account.providerAccountId ?? selection?.account.accountId, deps.env);
|
|
959
|
+
await resetCodexV6LocalState(deps.env);
|
|
828
960
|
await clearCodexSelection(deps.config.persistence.configDirectory);
|
|
829
961
|
if (await json(command, deps))
|
|
830
962
|
print(deps.output, success({ reset: true, auth_action: auth.action, legacy_provider_removed: cleanup.changed }));
|
|
@@ -852,6 +984,40 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
852
984
|
throw new Error("auth-agent replaces the current login; add --replace-login to confirm.");
|
|
853
985
|
if (opts.credentialStore !== "file")
|
|
854
986
|
throw new Error("ChatGPT auth.json 同步目前仅支持 --credential-store file。");
|
|
987
|
+
if (requestBoundDeviceCredentialsEnabled(deps.config)) {
|
|
988
|
+
const interval = Number(opts.interval) * 1_000;
|
|
989
|
+
if (!Number.isFinite(interval) || interval <= 0)
|
|
990
|
+
throw new Error("--interval must be positive.");
|
|
991
|
+
do {
|
|
992
|
+
await ensureCodexV6DeviceCredentials({
|
|
993
|
+
client: deps.clientFor(serviceConnection(opts)),
|
|
994
|
+
tenantId: deps.config.connection.tenantId,
|
|
995
|
+
env: deps.env
|
|
996
|
+
});
|
|
997
|
+
const result = await reportCodexV6DeviceCredentials({
|
|
998
|
+
client: deps.clientFor(serviceConnection(opts)),
|
|
999
|
+
tenantId: deps.config.connection.tenantId,
|
|
1000
|
+
env: deps.env
|
|
1001
|
+
});
|
|
1002
|
+
const event = {
|
|
1003
|
+
level: result.warningCode ? "WARN" : "INFO",
|
|
1004
|
+
event: result.reported
|
|
1005
|
+
? "device_token_set.generation_reported"
|
|
1006
|
+
: "device_token_set.current",
|
|
1007
|
+
at: new Date().toISOString(),
|
|
1008
|
+
...(result.warningCode ? { warningCode: result.warningCode } : {})
|
|
1009
|
+
};
|
|
1010
|
+
if (await json(command, deps))
|
|
1011
|
+
print(deps.output, success(event));
|
|
1012
|
+
else
|
|
1013
|
+
deps.output.write(`${event.at} ${event.level} ${event.event}`
|
|
1014
|
+
+ `${result.warningCode ? ` (${result.warningCode})` : ""}; other devices unaffected\n`);
|
|
1015
|
+
if (opts.once)
|
|
1016
|
+
break;
|
|
1017
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
1018
|
+
} while (true);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
855
1021
|
if (accountTokenPoolV4Enabled(deps.config)) {
|
|
856
1022
|
const interval = Number(opts.interval) * 1_000;
|
|
857
1023
|
if (!Number.isFinite(interval) || interval <= 0)
|
|
@@ -887,7 +1053,7 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
887
1053
|
if (!Number.isFinite(requestedRefreshBefore) || requestedRefreshBefore <= 0) {
|
|
888
1054
|
throw new Error("--refresh-before must be positive.");
|
|
889
1055
|
}
|
|
890
|
-
const refreshBefore =
|
|
1056
|
+
const refreshBefore = requestedRefreshBefore;
|
|
891
1057
|
let expiresAt = selected.expiresAt;
|
|
892
1058
|
do {
|
|
893
1059
|
const timestamp = Date.now();
|
|
@@ -990,128 +1156,168 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
990
1156
|
if (failures.length > 0)
|
|
991
1157
|
process.exitCode = 1;
|
|
992
1158
|
});
|
|
993
|
-
if (schemaV2LifecycleEnabled(deps.config)) {
|
|
994
|
-
const enrollment = codex.command("enrollment")
|
|
995
|
-
.description("Execute administrator-created one-shot Codex login intents");
|
|
996
|
-
serviceOptions(enrollment.command("status <intent>").description("Show redacted enrollment intent status"))
|
|
997
|
-
.action(async (intent, opts, command) => {
|
|
998
|
-
const result = await deps.clientFor(serviceConnection(opts)).getEnrollmentIntent(intent);
|
|
999
|
-
if (await json(command, deps)) {
|
|
1000
|
-
print(deps.output, { ok: true, ...result });
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
deps.output.write(`${result.intent.id} ${result.intent.type} ${result.intent.status}`
|
|
1004
|
-
+ ` revision=${result.intent.revision}\n`);
|
|
1005
|
-
deps.output.write(`expires=${result.intent.expiresAt}`
|
|
1006
|
-
+ `${result.intent.assignmentId ? ` assignment=${result.intent.assignmentId}` : ""}`
|
|
1007
|
-
+ `${result.intent.managedAccountId ? ` account=${result.intent.managedAccountId}` : ""}\n`);
|
|
1008
|
-
if (!result.capabilities.candidateUpload) {
|
|
1009
|
-
deps.output.write("安全候选上传尚未启用;当前任务只能查询或取消,不会启动 OpenAI 登录。\n");
|
|
1010
|
-
}
|
|
1011
|
-
});
|
|
1012
|
-
serviceOptions(enrollment.command("login <intent>")
|
|
1013
|
-
.description("Run one isolated official Codex login for an enrollment intent"))
|
|
1014
|
-
.option("--device-auth", "use the official Codex device authorization flow")
|
|
1015
|
-
.action(async (intent, _opts, command) => {
|
|
1016
|
-
const result = await executeCodexEnrollmentLogin({
|
|
1017
|
-
client: deps.clientFor(serviceConnection(_opts)),
|
|
1018
|
-
tenantId: deps.config.connection.tenantId,
|
|
1019
|
-
intentId: intent,
|
|
1020
|
-
deviceAuth: _opts.deviceAuth
|
|
1021
|
-
});
|
|
1022
|
-
if (await json(command, deps))
|
|
1023
|
-
print(deps.output, { ok: true, ...result });
|
|
1024
|
-
else {
|
|
1025
|
-
deps.output.write(`${result.intent.id} 登录候选已处理;状态=${result.intent.status}`
|
|
1026
|
-
+ `${result.result.managedAccountId ? `,账号=${result.result.managedAccountId}` : ""}`
|
|
1027
|
-
+ `${result.result.lineageId ? `,lineage=${result.result.lineageId}` : ""}`
|
|
1028
|
-
+ "。\n");
|
|
1029
|
-
if (result.result.outcome === "TEMPORARILY_UNAVAILABLE") {
|
|
1030
|
-
deps.output.write("同一进程内的安全重试已耗尽;请取消并新建任务,不能用另一次登录覆盖已保留的候选。\n");
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
});
|
|
1034
|
-
serviceOptions(enrollment.command("login-batch <planId>")
|
|
1035
|
-
.description("Sequentially execute isolated logins from a migration plan queue")
|
|
1036
|
-
.option("--device-auth", "use the official Codex device authorization flow")
|
|
1037
|
-
.option("--dry-run", "show the redacted sequential plan without starting login")
|
|
1038
|
-
.option("--page-size <count>", "migration queue page size", "100"))
|
|
1039
|
-
.action(async (planId, opts, command) => {
|
|
1040
|
-
if (deps.config.profile?.userBoundRefreshTokens?.migrationPlans !== true) {
|
|
1041
|
-
throw new CodexServiceError("The selected tenant has not enabled schema-v2 migration login plans.", "capability_not_enabled", 409);
|
|
1042
|
-
}
|
|
1043
|
-
const pageSize = Number(opts.pageSize);
|
|
1044
|
-
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
|
|
1045
|
-
throw new CodexServiceError("--page-size must be an integer from 1 to 100.", "invalid_configuration");
|
|
1046
|
-
}
|
|
1047
|
-
const batch = await executeCodexMigrationLoginBatch({
|
|
1048
|
-
client: deps.clientFor(serviceConnection(opts)),
|
|
1049
|
-
tenantId: deps.config.connection.tenantId,
|
|
1050
|
-
planId,
|
|
1051
|
-
deviceAuth: opts.deviceAuth,
|
|
1052
|
-
dryRun: opts.dryRun,
|
|
1053
|
-
pageSize
|
|
1054
|
-
});
|
|
1055
|
-
const failed = batch.counts.failed > 0
|
|
1056
|
-
|| ["TARGET_UNCERTAIN", "AUTHENTICATION_ERROR", "SERVICE_ERROR", "LOGIN_ERROR"]
|
|
1057
|
-
.includes(batch.stopReason);
|
|
1058
|
-
if (await json(command, deps)) {
|
|
1059
|
-
print(deps.output, { ok: !failed, ...batch });
|
|
1060
|
-
}
|
|
1061
|
-
else {
|
|
1062
|
-
deps.output.write(`migration=${batch.plan.id} status=${batch.plan.status} revision=${batch.plan.revision}`
|
|
1063
|
-
+ ` stop=${batch.stopReason}${batch.dryRun ? " dry-run" : ""}\n`);
|
|
1064
|
-
deps.output.write(`capacity: verifiedMinimum=${batch.capacity.verifiedMinimum}, verifiedLimit=unknown,`
|
|
1065
|
-
+ ` sequentialRequired=${batch.capacity.sequentialRequired}; verifiedMinimum 不是上限。\n`);
|
|
1066
|
-
for (const item of batch.items) {
|
|
1067
|
-
deps.output.write(`${item.assignmentId} ${item.action}/${item.status}`
|
|
1068
|
-
+ `${item.intentId ? ` intent=${item.intentId}` : ""}`
|
|
1069
|
-
+ `${item.outcome ? ` outcome=${item.outcome}` : ""}`
|
|
1070
|
-
+ `${item.skipReason ? ` skip=${item.skipReason}` : ""}`
|
|
1071
|
-
+ `${item.safeErrorCode ? ` error=${item.safeErrorCode}` : ""}\n`);
|
|
1072
|
-
}
|
|
1073
|
-
deps.output.write(`summary: attempted=${batch.counts.attempted}, completed=${batch.counts.completed},`
|
|
1074
|
-
+ ` failed=${batch.counts.failed}, skipped=${batch.counts.skipped}.\n`);
|
|
1075
|
-
}
|
|
1076
|
-
if (failed)
|
|
1077
|
-
process.exitCode = 1;
|
|
1078
|
-
});
|
|
1079
|
-
serviceOptions(enrollment.command("cancel <intent>").description("Cancel an enrollment intent"))
|
|
1080
|
-
.option("--idempotency-key <key>", "reuse a prior cancellation idempotency key")
|
|
1081
|
-
.action(async (intent, opts, command) => {
|
|
1082
|
-
const client = deps.clientFor(serviceConnection(opts));
|
|
1083
|
-
const current = await client.getEnrollmentIntent(intent);
|
|
1084
|
-
const result = await client.cancelEnrollmentIntent(intent, {
|
|
1085
|
-
idempotencyKey: opts.idempotencyKey?.trim() || randomUUID(),
|
|
1086
|
-
revision: current.intent.revision
|
|
1087
|
-
});
|
|
1088
|
-
if (await json(command, deps))
|
|
1089
|
-
print(deps.output, { ok: true, ...result });
|
|
1090
|
-
else
|
|
1091
|
-
deps.output.write(`${result.intent.id} 已取消;状态=${result.intent.status}。\n`);
|
|
1092
|
-
});
|
|
1093
|
-
const retirement = codex.command("retirement")
|
|
1094
|
-
.description("Remove an exact locally installed lineage for a Server retirement request");
|
|
1095
|
-
serviceOptions(retirement.command("acknowledge <retirement>")
|
|
1096
|
-
.description("Delete the matching managed short-lived credentials and append local evidence")
|
|
1097
|
-
.option("--idempotency-key <key>", "reuse a prior local deletion acknowledgement key"))
|
|
1098
|
-
.action(async (retirementId, opts, command) => {
|
|
1099
|
-
const result = await executeCodexAssignmentRetirementLocalDelete({
|
|
1100
|
-
client: deps.clientFor(serviceConnection(opts)),
|
|
1101
|
-
retirementId,
|
|
1102
|
-
env: deps.env,
|
|
1103
|
-
...(opts.idempotencyKey?.trim() ? { idempotencyKey: opts.idempotencyKey.trim() } : {})
|
|
1104
|
-
});
|
|
1105
|
-
if (await json(command, deps))
|
|
1106
|
-
print(deps.output, { ok: true, ...result });
|
|
1107
|
-
else
|
|
1108
|
-
deps.output.write(`本机匹配的短效凭据已删除并记录证据;retirement=${result.retirement.id}`
|
|
1109
|
-
+ `,revision=${result.retirement.revision},状态=${result.retirement.status}。\n`
|
|
1110
|
-
+ "该证据不代表其他终端已清理;最终退役仍等待全部已披露短效凭据自然过期。\n");
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
1159
|
const admin = codex.command("admin")
|
|
1114
1160
|
.description("Administer server-managed Codex accounts and assignments");
|
|
1161
|
+
const adminRequests = admin.command("requests")
|
|
1162
|
+
.description("Review account requests and create independent device token-set bindings");
|
|
1163
|
+
serviceOptions(adminRequests.command("list").description("List tenant requests, ordered by nearest expiry")
|
|
1164
|
+
.option("--expiring-within <days>", "只显示指定天数内到期的非终态申请"))
|
|
1165
|
+
.action(async (opts, command) => {
|
|
1166
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
1167
|
+
const expiresBefore = opts.expiringWithin
|
|
1168
|
+
? new Date(Date.now() + parsePositiveInteger(opts.expiringWithin, "--expiring-within", 3_650) * 86_400_000).toISOString()
|
|
1169
|
+
: undefined;
|
|
1170
|
+
const result = await deps.clientFor(serviceConnection(opts)).listV6AdminAccountRequests(expiresBefore ? { expiresBefore, includeTerminal: false } : {});
|
|
1171
|
+
const items = [...result.items].sort((left, right) => Date.parse(left.validUntil ?? left.requestedValidUntil) - Date.parse(right.validUntil ?? right.requestedValidUntil));
|
|
1172
|
+
if (await json(command, deps))
|
|
1173
|
+
print(deps.output, success({ ...result, items }));
|
|
1174
|
+
else if (items.length === 0)
|
|
1175
|
+
deps.output.write(opts.expiringWithin
|
|
1176
|
+
? `未来 ${opts.expiringWithin} 天内没有即将到期的 ChatGPT/Codex 申请。\n`
|
|
1177
|
+
: "当前租户没有 ChatGPT/Codex 账号申请。\n");
|
|
1178
|
+
else
|
|
1179
|
+
for (const item of items)
|
|
1180
|
+
deps.output.write(`${formatV6Request(item)}\n`);
|
|
1181
|
+
});
|
|
1182
|
+
serviceOptions(adminRequests.command("replacement-candidates")
|
|
1183
|
+
.description("List device bindings whose ChatGPT account needs pre-expiry replacement")
|
|
1184
|
+
.option("--within <days>", "替换观察窗口天数", "30"))
|
|
1185
|
+
.action(async (opts, command) => {
|
|
1186
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
1187
|
+
const replaceBefore = new Date(Date.now() + parsePositiveInteger(opts.within, "--within", 3_650) * 86_400_000).toISOString();
|
|
1188
|
+
const result = await deps.clientFor(serviceConnection(opts))
|
|
1189
|
+
.listV6AdminReplacementCandidates({ replaceBefore });
|
|
1190
|
+
const items = [...result.items].sort((left, right) => Date.parse(left.replaceBefore) - Date.parse(right.replaceBefore));
|
|
1191
|
+
if (await json(command, deps))
|
|
1192
|
+
print(deps.output, success({ ...result, items }));
|
|
1193
|
+
else if (items.length === 0) {
|
|
1194
|
+
deps.output.write(`未来 ${opts.within} 天没有需要提前替换的设备账号。\n`);
|
|
1195
|
+
}
|
|
1196
|
+
else
|
|
1197
|
+
for (const item of items) {
|
|
1198
|
+
deps.output.write(`${item.recommendedReason} replace-before=${item.replaceBefore} `
|
|
1199
|
+
+ `request=${item.request.id} ${formatV6Binding(item.binding)}\n`);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
serviceOptions(adminRequests.command("show <requestId>").description("Show one tenant account request"))
|
|
1203
|
+
.action(async (requestId, opts, command) => {
|
|
1204
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
1205
|
+
const client = deps.clientFor(serviceConnection(opts));
|
|
1206
|
+
const [requestResult, bindingResult] = await Promise.all([
|
|
1207
|
+
client.getV6AdminAccountRequest(requestId),
|
|
1208
|
+
client.listV6AdminDeviceBindings(requestId)
|
|
1209
|
+
]);
|
|
1210
|
+
if (await json(command, deps))
|
|
1211
|
+
print(deps.output, success({
|
|
1212
|
+
request: requestResult.request, bindings: bindingResult.items
|
|
1213
|
+
}));
|
|
1214
|
+
else {
|
|
1215
|
+
deps.output.write(`${formatV6Request(requestResult.request)}\n`);
|
|
1216
|
+
for (const binding of bindingResult.items) {
|
|
1217
|
+
deps.output.write(`- ${formatV6Binding(binding)}\n`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
serviceOptions(adminRequests.command("review <requestId>")
|
|
1222
|
+
.description("Approve or reject an account request")
|
|
1223
|
+
.requiredOption("--decision <decision>", "APPROVE 或 REJECT")
|
|
1224
|
+
.requiredOption("--reason <text>", "审批原因,至少 5 个字符")
|
|
1225
|
+
.requiredOption("--version <number>", "当前请求版本")
|
|
1226
|
+
.option("--device-limit <count>", "批准的设备数量")
|
|
1227
|
+
.option("--valid-from <timestamp>", "批准生效时间,必须包含时区")
|
|
1228
|
+
.option("--valid-until <timestamp>", "批准到期时间,必须包含时区")
|
|
1229
|
+
.option("--idempotency-key <key>", "重试时复用同一个幂等键"))
|
|
1230
|
+
.action(async (requestId, opts, command) => {
|
|
1231
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
1232
|
+
const decision = opts.decision.trim().toUpperCase();
|
|
1233
|
+
if (!["APPROVE", "REJECT"].includes(decision)) {
|
|
1234
|
+
throw new CodexServiceError("--decision 必须是 APPROVE 或 REJECT。", "invalid_codex_account_request_review");
|
|
1235
|
+
}
|
|
1236
|
+
const approve = decision === "APPROVE";
|
|
1237
|
+
if (approve && (!opts.deviceLimit || !opts.validFrom || !opts.validUntil)) {
|
|
1238
|
+
throw new CodexServiceError("批准申请必须同时提供 --device-limit、--valid-from 和 --valid-until。", "invalid_codex_account_request_review");
|
|
1239
|
+
}
|
|
1240
|
+
const validFrom = approve ? parseRequestInstant(opts.validFrom, "--valid-from") : null;
|
|
1241
|
+
const validUntil = approve ? parseRequestInstant(opts.validUntil, "--valid-until") : null;
|
|
1242
|
+
if (validFrom && validUntil && Date.parse(validUntil) <= Date.parse(validFrom)) {
|
|
1243
|
+
throw new CodexServiceError("--valid-until 必须晚于 --valid-from。", "invalid_codex_account_request_review");
|
|
1244
|
+
}
|
|
1245
|
+
const result = await deps.clientFor(serviceConnection(opts)).reviewV6AdminAccountRequest(requestId, {
|
|
1246
|
+
schemaVersion: 6,
|
|
1247
|
+
expectedRequestVersion: parseNonNegativeInteger(opts.version, "--version"),
|
|
1248
|
+
decision: decision,
|
|
1249
|
+
reason: opts.reason,
|
|
1250
|
+
approvedDeviceLimit: approve
|
|
1251
|
+
? parsePositiveInteger(opts.deviceLimit, "--device-limit") : null,
|
|
1252
|
+
validFrom,
|
|
1253
|
+
validUntil
|
|
1254
|
+
}, opts.idempotencyKey?.trim() || randomUUID());
|
|
1255
|
+
if (await json(command, deps))
|
|
1256
|
+
print(deps.output, success(result));
|
|
1257
|
+
else
|
|
1258
|
+
deps.output.write(`审批完成:${formatV6Request(result.request)}\n`);
|
|
1259
|
+
});
|
|
1260
|
+
serviceOptions(adminRequests.command("bind-device <requestId>")
|
|
1261
|
+
.description("Bind another installation with its own independent complete token set")
|
|
1262
|
+
.requiredOption("--installation <id>", "用户设备上显示的 installationId")
|
|
1263
|
+
.requiredOption("--device-label <label>", "设备名称")
|
|
1264
|
+
.requiredOption("--version <number>", "当前请求版本")
|
|
1265
|
+
.option("--platform <platform>", "darwin|linux|win32|unknown", "unknown")
|
|
1266
|
+
.option("--pool-entry <id>", "立即预留的独立 Token Pool entry;省略则进入等待")
|
|
1267
|
+
.option("--idempotency-key <key>", "重试时复用同一个幂等键"))
|
|
1268
|
+
.action(async (requestId, opts, command) => {
|
|
1269
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
1270
|
+
const platform = opts.platform.trim().toLowerCase();
|
|
1271
|
+
if (!["darwin", "linux", "win32", "unknown"]
|
|
1272
|
+
.includes(platform)) {
|
|
1273
|
+
throw new CodexServiceError("--platform 必须是 darwin、linux、win32 或 unknown。", "invalid_codex_device_binding");
|
|
1274
|
+
}
|
|
1275
|
+
const result = await deps.clientFor(serviceConnection(opts)).createV6AdminDeviceBinding(requestId, {
|
|
1276
|
+
schemaVersion: 6,
|
|
1277
|
+
expectedRequestVersion: parseNonNegativeInteger(opts.version, "--version"),
|
|
1278
|
+
device: {
|
|
1279
|
+
installationId: opts.installation,
|
|
1280
|
+
label: opts.deviceLabel,
|
|
1281
|
+
platform: platform
|
|
1282
|
+
},
|
|
1283
|
+
poolEntryId: opts.poolEntry?.trim() || null
|
|
1284
|
+
}, opts.idempotencyKey?.trim() || randomUUID());
|
|
1285
|
+
if (await json(command, deps))
|
|
1286
|
+
print(deps.output, success(result));
|
|
1287
|
+
else
|
|
1288
|
+
deps.output.write(`设备已绑定:${result.binding.id} installation=${result.binding.installationId}`
|
|
1289
|
+
+ ` token-set=${result.binding.credentialLineageId ?? "WAITING_FOR_TOKEN"}\n`);
|
|
1290
|
+
});
|
|
1291
|
+
serviceOptions(adminRequests.command("replace-device-token <requestId> <bindingId>")
|
|
1292
|
+
.description("Prepare an independent successor token set and switch only after device ACK")
|
|
1293
|
+
.requiredOption("--version <number>", "当前请求版本")
|
|
1294
|
+
.requiredOption("--reason <reason>", "SUBSCRIPTION_EXPIRING、CREDENTIAL_UNHEALTHY 或 ADMIN_REQUESTED")
|
|
1295
|
+
.requiredOption("--replace-before <timestamp>", "最迟替换时间,必须包含时区")
|
|
1296
|
+
.option("--pool-entry <id>", "立即预留的新独立 Token Pool entry;省略则进入等待")
|
|
1297
|
+
.option("--idempotency-key <key>", "重试时复用同一个幂等键"))
|
|
1298
|
+
.action(async (requestId, bindingId, opts, command) => {
|
|
1299
|
+
assertRequestBoundDeviceCredentialsEnabled(deps.config);
|
|
1300
|
+
const reason = opts.reason.trim().toUpperCase();
|
|
1301
|
+
if (!["SUBSCRIPTION_EXPIRING", "CREDENTIAL_UNHEALTHY", "ADMIN_REQUESTED"]
|
|
1302
|
+
.includes(reason)) {
|
|
1303
|
+
throw new CodexServiceError("--reason 必须是 SUBSCRIPTION_EXPIRING、CREDENTIAL_UNHEALTHY 或 ADMIN_REQUESTED。", "invalid_codex_device_replacement");
|
|
1304
|
+
}
|
|
1305
|
+
const result = await deps.clientFor(serviceConnection(opts)).replaceV6AdminDeviceToken(requestId, bindingId, {
|
|
1306
|
+
schemaVersion: 6,
|
|
1307
|
+
expectedRequestVersion: parseNonNegativeInteger(opts.version, "--version"),
|
|
1308
|
+
reason: reason,
|
|
1309
|
+
replaceBefore: parseRequestInstant(opts.replaceBefore, "--replace-before"),
|
|
1310
|
+
poolEntryId: opts.poolEntry?.trim() || null
|
|
1311
|
+
}, opts.idempotencyKey?.trim() || randomUUID());
|
|
1312
|
+
if (await json(command, deps))
|
|
1313
|
+
print(deps.output, success(result));
|
|
1314
|
+
else
|
|
1315
|
+
deps.output.write("已准备设备 Token 替换:current=" + result.currentBinding.id
|
|
1316
|
+
+ " successor=" + result.replacementBinding.id
|
|
1317
|
+
+ " status=" + result.replacementBinding.status
|
|
1318
|
+
+ " token-set=" + (result.replacementBinding.credentialLineageId ?? "WAITING_FOR_TOKEN")
|
|
1319
|
+
+ "。\n旧 Token Set 会保持有效,直到该设备安装并 ACK 新 Token Set。\n");
|
|
1320
|
+
});
|
|
1115
1321
|
const adminAccounts = admin.command("accounts")
|
|
1116
1322
|
.description("Inspect accounts, credential history, and refresh state");
|
|
1117
1323
|
serviceOptions(adminAccounts.command("list").description("List all tenant Codex accounts"))
|