@tokensrc/codex 1.13.4 → 1.14.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/README.md +79 -3
- package/dist/client.d.ts +29 -1
- package/dist/client.js +221 -2
- package/dist/client.js.map +1 -1
- package/dist/command-directory.d.ts +1 -0
- package/dist/command-directory.js +3 -2
- package/dist/command-directory.js.map +1 -1
- package/dist/commands.d.ts +8 -2
- package/dist/commands.js +567 -40
- package/dist/commands.js.map +1 -1
- package/dist/console-status.js +1 -1
- package/dist/console-status.js.map +1 -1
- package/dist/credential-supervisor.d.ts +17 -0
- package/dist/credential-supervisor.js +84 -0
- package/dist/credential-supervisor.js.map +1 -0
- package/dist/credentials.d.ts +75 -3
- package/dist/credentials.js +357 -12
- package/dist/credentials.js.map +1 -1
- package/dist/enrollment-envelope.d.ts +18 -0
- package/dist/enrollment-envelope.js +112 -0
- package/dist/enrollment-envelope.js.map +1 -0
- package/dist/enrollment-flow.d.ts +27 -0
- package/dist/enrollment-flow.js +224 -0
- package/dist/enrollment-flow.js.map +1 -0
- package/dist/enrollment-login.d.ts +26 -0
- package/dist/enrollment-login.js +104 -0
- package/dist/enrollment-login.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/dist/managed-credential-v3-envelope.d.ts +60 -0
- package/dist/managed-credential-v3-envelope.js +234 -0
- package/dist/managed-credential-v3-envelope.js.map +1 -0
- package/dist/managed-credential-v3-flow.d.ts +45 -0
- package/dist/managed-credential-v3-flow.js +499 -0
- package/dist/managed-credential-v3-flow.js.map +1 -0
- package/dist/managed-credential-v3-journal.d.ts +192 -0
- package/dist/managed-credential-v3-journal.js +153 -0
- package/dist/managed-credential-v3-journal.js.map +1 -0
- package/dist/managed-credential-v3.d.ts +117 -0
- package/dist/managed-credential-v3.js +423 -0
- package/dist/managed-credential-v3.js.map +1 -0
- package/dist/migration-login-batch.d.ts +66 -0
- package/dist/migration-login-batch.js +202 -0
- package/dist/migration-login-batch.js.map +1 -0
- package/dist/private-file.d.ts +10 -0
- package/dist/private-file.js +77 -2
- package/dist/private-file.js.map +1 -1
- package/dist/profile.d.ts +10 -0
- package/dist/profile.js +55 -2
- package/dist/profile.js.map +1 -1
- package/dist/protocol.d.ts +1070 -0
- package/dist/protocol.js +560 -0
- package/dist/protocol.js.map +1 -1
- package/dist/retirement-flow.d.ts +38 -0
- package/dist/retirement-flow.js +78 -0
- package/dist/retirement-flow.js.map +1 -0
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +29 -4
- package/dist/runtime.js.map +1 -1
- package/dist/selection.d.ts +10 -1
- package/dist/selection.js +38 -2
- package/dist/selection.js.map +1 -1
- package/dist/short-lived-delivery-flow.d.ts +41 -0
- package/dist/short-lived-delivery-flow.js +141 -0
- package/dist/short-lived-delivery-flow.js.map +1 -0
- package/dist/short-lived-delivery-journal.d.ts +34 -0
- package/dist/short-lived-delivery-journal.js +137 -0
- package/dist/short-lived-delivery-journal.js.map +1 -0
- package/dist/short-lived-delivery.d.ts +26 -0
- package/dist/short-lived-delivery.js +139 -0
- package/dist/short-lived-delivery.js.map +1 -0
- package/dist/tui.js +23 -0
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +6 -2
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
import { getStoragePath, loadAccounts, setStoragePath, setStoragePathDirect } from "codex-multi-auth/storage";
|
|
4
5
|
import { normalizeCodexServiceUrl } from "./client.js";
|
|
5
6
|
import { CodexServiceError } from "./errors.js";
|
|
6
|
-
import { backupCodexFileCredentials, codexFileCredentialsMatchAccount, inspectLocalCodexCredentialStatus, resetManagedCodexFileCredentials, restoreCodexFileCredentials, writeCodexChatgptFileCredentials } from "./credentials.js";
|
|
7
|
+
import { backupCodexFileCredentials, codexFileCredentialsMatchAccount, discardCodexFileCredentialBackup, inspectLocalCodexCredentialStatus, readManagedCodexLineageState, resetManagedCodexFileCredentials, restoreCodexFileCredentials, writeCodexChatgptFileCredentials } from "./credentials.js";
|
|
7
8
|
import { CODEX_DISTRIBUTION_SCHEMA_VERSION } from "./protocol.js";
|
|
8
9
|
import { formatCodexAccountDisplayIdentity, resolveCodexAdminAccount, resolveCodexAccountDisplayName } from "./account-display.js";
|
|
9
10
|
import { loadCodexQuotaUploads } from "./quota-cache.js";
|
|
10
11
|
import { cleanupLegacyCodexProvider } from "./legacy-cleanup.js";
|
|
11
|
-
import {
|
|
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
|
+
import { executeManagedCodexV3Login, executeManagedCodexV3Upload, reportManagedCodexV3Credentials } from "./managed-credential-v3-flow.js";
|
|
16
|
+
import { readManagedCodexV3State } from "./managed-credential-v3.js";
|
|
17
|
+
import { profileManagedCredentialsV3Enabled } from "./profile.js";
|
|
18
|
+
import { executeCodexAssignmentRetirementLocalDelete } from "./retirement-flow.js";
|
|
19
|
+
import { clearCodexSelection, loadCodexSelection, rememberCodexSelectionProviderAccount, saveCodexSelection } from "./selection.js";
|
|
12
20
|
import { installCodexShortcuts, removeCodexShortcuts } from "./shortcuts.js";
|
|
13
21
|
import { isCodexAccountAvailable, renderCodexAccountDashboard, selectCodexAccount } from "./tui.js";
|
|
22
|
+
import { readCodexShortLivedDeliveryJournal } from "./short-lived-delivery-journal.js";
|
|
23
|
+
import { superviseCodexShortLivedCredentials } from "./credential-supervisor.js";
|
|
14
24
|
export const NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE = "当前用户没有绑定可用的 ChatGPT 账号。请前往 OA 系统提交 IT11 工单,申请独享或共享的 ChatGPT 个人账号。";
|
|
25
|
+
// The Server refreshes a user-bound lineage only inside its final 60-second
|
|
26
|
+
// credential safety window. Asking for a new delivery earlier would merely
|
|
27
|
+
// re-encrypt the same access/ID tokens and must not be treated as a renewal.
|
|
28
|
+
const SERVER_MANAGED_SHORT_LIVED_REFRESH_WINDOW_MS = 60_000;
|
|
29
|
+
const SCHEMA_V2_LEGACY_MANAGEMENT_DISABLED = "schema_v2_legacy_management_disabled";
|
|
30
|
+
export function effectiveCodexCredentialRefreshBefore(requestedMs, serverManagedShortLived) {
|
|
31
|
+
return serverManagedShortLived
|
|
32
|
+
? Math.min(requestedMs, SERVER_MANAGED_SHORT_LIVED_REFRESH_WINDOW_MS)
|
|
33
|
+
: requestedMs;
|
|
34
|
+
}
|
|
15
35
|
async function json(command, deps) {
|
|
16
36
|
if (command.optsWithGlobals().json)
|
|
17
37
|
return true;
|
|
@@ -30,6 +50,27 @@ function success(value) {
|
|
|
30
50
|
function importFailures(credentials) {
|
|
31
51
|
return credentials.filter(({ status }) => status === "REJECTED" || status === "UNKNOWN");
|
|
32
52
|
}
|
|
53
|
+
function schemaV2LifecycleEnabled(config) {
|
|
54
|
+
const capabilities = config.profile?.userBoundRefreshTokens;
|
|
55
|
+
return capabilities?.delivery === true || capabilities?.migrationPlans === true;
|
|
56
|
+
}
|
|
57
|
+
function managedCredentialsV3Enabled(config) {
|
|
58
|
+
return profileManagedCredentialsV3Enabled(config.profile);
|
|
59
|
+
}
|
|
60
|
+
function codexConfigPathForEnv(env) {
|
|
61
|
+
const codexHome = env.CODEX_HOME?.trim();
|
|
62
|
+
return codexHome ? join(codexHome, "config.toml") : undefined;
|
|
63
|
+
}
|
|
64
|
+
function assertLegacyManagementEnabled(config) {
|
|
65
|
+
if (!schemaV2LifecycleEnabled(config))
|
|
66
|
+
return;
|
|
67
|
+
throw new CodexServiceError("Schema-v2 user-bound credentials are enabled; legacy refresh-token pool management is disabled.", SCHEMA_V2_LEGACY_MANAGEMENT_DISABLED, 409);
|
|
68
|
+
}
|
|
69
|
+
function assignmentCredentialStatus(assignment) {
|
|
70
|
+
if (assignment.deliveryEligible)
|
|
71
|
+
return "AVAILABLE";
|
|
72
|
+
return ["NEEDS_REAUTH", "LOST", "DIVERGED", "COMPROMISED"].includes(assignment.lineageStatus ?? "") ? "NEEDS_REAUTH" : "TEMPORARILY_UNAVAILABLE";
|
|
73
|
+
}
|
|
33
74
|
function serviceConnection(options = {}) {
|
|
34
75
|
return {
|
|
35
76
|
...(options.server ? { serverUrl: options.server } : {}),
|
|
@@ -64,15 +105,62 @@ function formatAdminAccount(account) {
|
|
|
64
105
|
})} (${account.id})`;
|
|
65
106
|
}
|
|
66
107
|
async function loadAccountData(deps, options = {}) {
|
|
67
|
-
const response = await deps.clientFor(serviceConnection(options)).listAccounts();
|
|
68
108
|
const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
|
|
109
|
+
if (managedCredentialsV3Enabled(deps.config)) {
|
|
110
|
+
const v3Assignments = await deps.clientFor(serviceConnection(options)).listV3CredentialAssignments();
|
|
111
|
+
const accounts = v3Assignments.items.map((assignment) => ({
|
|
112
|
+
id: assignment.managedAccountId,
|
|
113
|
+
accountId: assignment.assignmentId,
|
|
114
|
+
label: assignment.label,
|
|
115
|
+
isDefault: true,
|
|
116
|
+
accessExpiresAt: Date.parse(assignment.credentialExpiresAt),
|
|
117
|
+
credentialStatus: assignment.lineageStatus === "NEEDS_REAUTH"
|
|
118
|
+
? "NEEDS_REAUTH"
|
|
119
|
+
: assignment.authorityMode === "CLIENT_MANAGED" ? "AVAILABLE" : "TEMPORARILY_UNAVAILABLE",
|
|
120
|
+
lineageStatus: assignment.lineageStatus === "STALE" ? "SYNC_LAGGING" : assignment.lineageStatus,
|
|
121
|
+
quota: null
|
|
122
|
+
}));
|
|
123
|
+
return {
|
|
124
|
+
response: { schemaVersion: CODEX_DISTRIBUTION_SCHEMA_VERSION, accounts,
|
|
125
|
+
defaultAccountId: accounts[0]?.id ?? null },
|
|
126
|
+
selectedAccountId: previous?.account.managedAccountId,
|
|
127
|
+
v3Assignments
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (deps.config.profile?.userBoundRefreshTokens?.delivery === true) {
|
|
131
|
+
const assignments = await deps.clientFor(serviceConnection(options)).listCredentialAssignments();
|
|
132
|
+
const accounts = assignments.items.map((assignment) => ({
|
|
133
|
+
id: assignment.managedAccountId,
|
|
134
|
+
accountId: assignment.assignmentId,
|
|
135
|
+
label: assignment.label,
|
|
136
|
+
isDefault: previous?.account.assignmentId === assignment.assignmentId,
|
|
137
|
+
...(assignment.credentialExpiresAt
|
|
138
|
+
? { accessExpiresAt: Date.parse(assignment.credentialExpiresAt) }
|
|
139
|
+
: {}),
|
|
140
|
+
credentialStatus: assignmentCredentialStatus(assignment),
|
|
141
|
+
lineageStatus: assignment.lineageStatus ?? undefined,
|
|
142
|
+
quota: null
|
|
143
|
+
}));
|
|
144
|
+
return {
|
|
145
|
+
response: {
|
|
146
|
+
schemaVersion: CODEX_DISTRIBUTION_SCHEMA_VERSION,
|
|
147
|
+
accounts,
|
|
148
|
+
defaultAccountId: previous?.account.managedAccountId ?? null
|
|
149
|
+
},
|
|
150
|
+
selectedAccountId: previous?.account.managedAccountId,
|
|
151
|
+
assignments
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const response = await deps.clientFor(serviceConnection(options)).listAccounts();
|
|
69
155
|
return {
|
|
70
156
|
response,
|
|
71
157
|
selectedAccountId: previous?.account.id ?? response.defaultAccountId ?? undefined
|
|
72
158
|
};
|
|
73
159
|
}
|
|
74
160
|
async function chooseAccount(deps, requested, options = {}) {
|
|
75
|
-
const
|
|
161
|
+
const data = await loadAccountData(deps, options);
|
|
162
|
+
const { response, selectedAccountId } = data;
|
|
163
|
+
const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
|
|
76
164
|
const cli = deps.config.branding.cliName === "numa"
|
|
77
165
|
? "numa codex use"
|
|
78
166
|
: `${deps.config.branding.cliName} use`;
|
|
@@ -88,39 +176,240 @@ async function chooseAccount(deps, requested, options = {}) {
|
|
|
88
176
|
label: account.label,
|
|
89
177
|
providerAccountId: account.accountId
|
|
90
178
|
});
|
|
179
|
+
const assignment = data.assignments?.items.find((item) => item.managedAccountId === account.id && item.assignmentId === account.accountId);
|
|
180
|
+
const retainedProviderAccountId = assignment
|
|
181
|
+
&& previous?.account.assignmentId === assignment.assignmentId
|
|
182
|
+
? previous.account.providerAccountId
|
|
183
|
+
: undefined;
|
|
184
|
+
const v3Assignment = data.v3Assignments?.items.find((item) => item.managedAccountId === account.id && item.assignmentId === account.accountId);
|
|
91
185
|
const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
|
|
92
186
|
serviceUrl: options.server ?? deps.config.connection.serverUrl,
|
|
93
187
|
allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
|
|
94
|
-
account:
|
|
188
|
+
account: v3Assignment ? {
|
|
189
|
+
id: v3Assignment.managedAccountId,
|
|
190
|
+
accountId: v3Assignment.providerAccountId,
|
|
191
|
+
label: accountLabel,
|
|
192
|
+
distributionMode: "CLIENT_MANAGED_V3",
|
|
193
|
+
assignmentId: v3Assignment.assignmentId,
|
|
194
|
+
managedAccountId: v3Assignment.managedAccountId,
|
|
195
|
+
providerAccountId: v3Assignment.providerAccountId
|
|
196
|
+
} : assignment ? {
|
|
197
|
+
id: assignment.managedAccountId,
|
|
198
|
+
label: accountLabel,
|
|
199
|
+
distributionMode: "SERVER_MANAGED_SHORT_LIVED",
|
|
200
|
+
assignmentId: assignment.assignmentId,
|
|
201
|
+
managedAccountId: assignment.managedAccountId,
|
|
202
|
+
...(retainedProviderAccountId ? {
|
|
203
|
+
accountId: retainedProviderAccountId,
|
|
204
|
+
providerAccountId: retainedProviderAccountId
|
|
205
|
+
} : {})
|
|
206
|
+
} : {
|
|
207
|
+
id: account.id,
|
|
208
|
+
accountId: account.accountId,
|
|
209
|
+
label: accountLabel,
|
|
210
|
+
distributionMode: "SCHEMA_V1"
|
|
211
|
+
}
|
|
95
212
|
});
|
|
96
213
|
return { account, selection };
|
|
97
214
|
}
|
|
98
|
-
async function installCredentials(deps,
|
|
99
|
-
const
|
|
100
|
-
await cleanupLegacyCodexProvider();
|
|
101
|
-
|
|
215
|
+
async function installCredentials(deps, selection, service) {
|
|
216
|
+
const client = deps.clientFor(service);
|
|
217
|
+
await cleanupLegacyCodexProvider(codexConfigPathForEnv(deps.env));
|
|
218
|
+
if (managedCredentialsV3Enabled(deps.config)) {
|
|
219
|
+
const state = await readManagedCodexV3State(deps.env);
|
|
220
|
+
if (!state || state.phase !== "INSTALLED" || state.authorityMode !== "CLIENT_MANAGED"
|
|
221
|
+
|| selection.account.distributionMode !== "CLIENT_MANAGED_V3"
|
|
222
|
+
|| selection.account.assignmentId !== state.assignmentId
|
|
223
|
+
|| selection.account.managedAccountId !== state.managedAccountId
|
|
224
|
+
|| selection.account.providerAccountId !== state.providerAccountId) {
|
|
225
|
+
throw new CodexServiceError("The selected v3 assignment does not match acknowledged local credentials; run `numa codex login` or `numa codex upload`.", "lineage_version_conflict", 409);
|
|
226
|
+
}
|
|
227
|
+
const assignments = await client.listV3CredentialAssignments();
|
|
228
|
+
const current = assignments.items[0];
|
|
229
|
+
if (!current || current.assignmentId !== state.assignmentId || current.lineageId !== state.lineageId
|
|
230
|
+
|| current.authorityMode !== "CLIENT_MANAGED") {
|
|
231
|
+
throw new CodexServiceError("The v3 credential assignment is no longer active.", "codex_assignment_not_found", 404);
|
|
232
|
+
}
|
|
233
|
+
return { expiresAt: Date.parse(state.credentialExpiresAt), selection };
|
|
234
|
+
}
|
|
235
|
+
if (deps.config.profile?.userBoundRefreshTokens?.delivery === true) {
|
|
236
|
+
await discardCodexFileCredentialBackup(deps.env);
|
|
237
|
+
if (selection.account.distributionMode !== "SERVER_MANAGED_SHORT_LIVED"
|
|
238
|
+
|| !selection.account.assignmentId || !selection.account.managedAccountId) {
|
|
239
|
+
throw new CodexServiceError("The saved Codex selection is not a schema-v2 credential assignment.", "codex_assignment_not_found", 404);
|
|
240
|
+
}
|
|
241
|
+
const assignments = await client.listCredentialAssignments();
|
|
242
|
+
const assignment = assignments.items.find((item) => item.assignmentId === selection.account.assignmentId
|
|
243
|
+
&& item.managedAccountId === selection.account.managedAccountId);
|
|
244
|
+
if (!assignment) {
|
|
245
|
+
throw new CodexServiceError("The selected schema-v2 credential assignment is no longer active.", "codex_assignment_not_found", 404);
|
|
246
|
+
}
|
|
247
|
+
const state = await readManagedCodexLineageState(deps.env);
|
|
248
|
+
const journal = await readCodexShortLivedDeliveryJournal(deps.env);
|
|
249
|
+
const recoverable = journal?.tenantId === deps.config.connection.tenantId
|
|
250
|
+
&& journal.assignmentId === assignment.assignmentId
|
|
251
|
+
|| state?.assignmentId === assignment.assignmentId
|
|
252
|
+
&& (state.tenantId === deps.config.connection.tenantId || state.tenantId === null)
|
|
253
|
+
&& (state.phase === "INSTALLING"
|
|
254
|
+
|| state.phase === "INSTALLED_PENDING_ACK" && Date.parse(state.ackExpiresAt) > Date.now());
|
|
255
|
+
if (!assignment.deliveryEligible && !recoverable) {
|
|
256
|
+
throw new CodexServiceError("The selected schema-v2 credential assignment is not ready for delivery.", "short_lived_credential_lineage_not_ready", 409);
|
|
257
|
+
}
|
|
258
|
+
const delivered = await executeCodexShortLivedCredentialDelivery({
|
|
259
|
+
client,
|
|
260
|
+
tenantId: deps.config.connection.tenantId,
|
|
261
|
+
assignmentId: assignment.assignmentId,
|
|
262
|
+
allowNewDelivery: assignment.deliveryEligible,
|
|
263
|
+
env: deps.env
|
|
264
|
+
});
|
|
265
|
+
const updated = await rememberCodexSelectionProviderAccount(deps.config.persistence.configDirectory, assignment.assignmentId, delivered.providerAccountId);
|
|
266
|
+
return { expiresAt: Date.parse(delivered.credentialExpiresAt), selection: updated };
|
|
267
|
+
}
|
|
268
|
+
await backupCodexFileCredentials(deps.env);
|
|
269
|
+
const response = await client.getLoginCredentials(selection.account.id);
|
|
102
270
|
await writeCodexChatgptFileCredentials({
|
|
103
271
|
accessToken: response.accessToken,
|
|
104
272
|
idToken: response.idToken,
|
|
105
273
|
accountId: response.account.accountId
|
|
106
|
-
});
|
|
107
|
-
return { expiresAt: response.expiresAt };
|
|
274
|
+
}, Date.now(), deps.env);
|
|
275
|
+
return { expiresAt: response.expiresAt, selection };
|
|
108
276
|
}
|
|
109
277
|
async function selectedCredentials(deps, requested, options = {}) {
|
|
278
|
+
if (managedCredentialsV3Enabled(deps.config)) {
|
|
279
|
+
const state = await readManagedCodexV3State(deps.env);
|
|
280
|
+
if (!state || state.phase !== "INSTALLED" || state.authorityMode !== "CLIENT_MANAGED") {
|
|
281
|
+
throw new CodexServiceError("No acknowledged v3 Codex credential is installed; run `numa codex login` or `numa codex upload`.", "managed_codex_v3_login_required", 409);
|
|
282
|
+
}
|
|
283
|
+
if (requested && ![
|
|
284
|
+
state.assignmentId,
|
|
285
|
+
state.managedAccountId,
|
|
286
|
+
state.providerAccountId,
|
|
287
|
+
state.lineageId
|
|
288
|
+
].includes(requested)) {
|
|
289
|
+
throw new CodexServiceError("The requested account is not the active v3 lineage.", "codex_account_not_found", 404);
|
|
290
|
+
}
|
|
291
|
+
const assignments = await deps.clientFor(serviceConnection(options)).listV3CredentialAssignments();
|
|
292
|
+
const assignment = assignments.items[0];
|
|
293
|
+
if (!assignment || assignment.assignmentId !== state.assignmentId
|
|
294
|
+
|| assignment.managedAccountId !== state.managedAccountId
|
|
295
|
+
|| assignment.providerAccountId !== state.providerAccountId
|
|
296
|
+
|| assignment.lineageId !== state.lineageId
|
|
297
|
+
|| assignment.authorityMode !== "CLIENT_MANAGED") {
|
|
298
|
+
throw new CodexServiceError("The local v3 lineage no longer matches the server assignment.", "lineage_version_conflict", 409);
|
|
299
|
+
}
|
|
300
|
+
const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
|
|
301
|
+
serviceUrl: options.server ?? deps.config.connection.serverUrl,
|
|
302
|
+
allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
|
|
303
|
+
account: {
|
|
304
|
+
id: state.managedAccountId,
|
|
305
|
+
accountId: state.providerAccountId,
|
|
306
|
+
label: assignment.label,
|
|
307
|
+
distributionMode: "CLIENT_MANAGED_V3",
|
|
308
|
+
assignmentId: state.assignmentId,
|
|
309
|
+
managedAccountId: state.managedAccountId,
|
|
310
|
+
providerAccountId: state.providerAccountId
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
return { selection, expiresAt: Date.parse(state.credentialExpiresAt) };
|
|
314
|
+
}
|
|
110
315
|
let selection = await loadCodexSelection(deps.config.persistence.configDirectory);
|
|
111
316
|
const currentServiceUrl = options.server ?? deps.config.connection.serverUrl;
|
|
112
317
|
const currentAllowsInsecureHttp = Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp);
|
|
113
318
|
const serviceChanged = selection !== undefined && !sameServiceUrl(selection.serviceUrl, selection.allowInsecurePoc, currentServiceUrl, currentAllowsInsecureHttp);
|
|
114
|
-
|
|
319
|
+
const requestedMatchesSelection = selection !== undefined && requested !== undefined
|
|
320
|
+
&& [
|
|
321
|
+
selection.account.id,
|
|
322
|
+
selection.account.accountId,
|
|
323
|
+
selection.account.assignmentId,
|
|
324
|
+
selection.account.managedAccountId,
|
|
325
|
+
selection.account.providerAccountId,
|
|
326
|
+
selection.account.label
|
|
327
|
+
].includes(requested);
|
|
328
|
+
if (!selection || serviceChanged || requested && !requestedMatchesSelection) {
|
|
115
329
|
selection = (await chooseAccount(deps, requested, options)).selection;
|
|
116
330
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
331
|
+
if (deps.config.profile?.userBoundRefreshTokens?.delivery === true) {
|
|
332
|
+
if (selection.account.distributionMode !== "SERVER_MANAGED_SHORT_LIVED") {
|
|
333
|
+
selection = (await chooseAccount(deps, requested, options)).selection;
|
|
334
|
+
}
|
|
335
|
+
const assignments = await deps.clientFor({
|
|
336
|
+
serverUrl: selection.serviceUrl,
|
|
337
|
+
allowInsecureHttp: selection.allowInsecurePoc
|
|
338
|
+
}).listCredentialAssignments();
|
|
339
|
+
const assignment = assignments.items.find((item) => item.assignmentId === selection.account.assignmentId
|
|
340
|
+
&& item.managedAccountId === selection.account.managedAccountId);
|
|
341
|
+
if (!assignment) {
|
|
342
|
+
throw new CodexServiceError("The selected schema-v2 credential assignment is no longer active.", "codex_assignment_not_found", 404);
|
|
343
|
+
}
|
|
344
|
+
const state = await readManagedCodexLineageState(deps.env);
|
|
345
|
+
const journal = await readCodexShortLivedDeliveryJournal(deps.env);
|
|
346
|
+
const stateMatches = state !== undefined
|
|
347
|
+
&& (state.tenantId === deps.config.connection.tenantId || state.tenantId === null)
|
|
348
|
+
&& state.assignmentId === assignment.assignmentId
|
|
349
|
+
&& state.managedAccountId === assignment.managedAccountId;
|
|
350
|
+
const local = await inspectLocalCodexCredentialStatus(stateMatches ? state.providerAccountId : selection.account.providerAccountId, deps.env);
|
|
351
|
+
const pending = journal?.tenantId === deps.config.connection.tenantId
|
|
352
|
+
&& journal.assignmentId === assignment.assignmentId
|
|
353
|
+
|| stateMatches && state.phase !== "INSTALLED";
|
|
354
|
+
if (!assignment.deliveryEligible || !stateMatches || !local.usable
|
|
355
|
+
|| local.matchesExpectedAccount !== true || pending) {
|
|
356
|
+
return installCredentials(deps, selection, {
|
|
357
|
+
serverUrl: selection.serviceUrl,
|
|
358
|
+
allowInsecureHttp: selection.allowInsecurePoc
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const matchingState = state;
|
|
362
|
+
if (selection.account.providerAccountId !== matchingState.providerAccountId) {
|
|
363
|
+
selection = await rememberCodexSelectionProviderAccount(deps.config.persistence.configDirectory, assignment.assignmentId, matchingState.providerAccountId);
|
|
364
|
+
}
|
|
365
|
+
return { selection, expiresAt: local.expiresAt ?? Date.parse(matchingState.credentialExpiresAt) };
|
|
366
|
+
}
|
|
367
|
+
if (!selection.account.accountId) {
|
|
368
|
+
throw new CodexServiceError("The saved Codex selection has no provider account.", "invalid_configuration");
|
|
369
|
+
}
|
|
370
|
+
if (!(await codexFileCredentialsMatchAccount(selection.account.accountId, deps.env))) {
|
|
371
|
+
return installCredentials(deps, selection, {
|
|
119
372
|
serverUrl: selection.serviceUrl,
|
|
120
373
|
allowInsecureHttp: selection.allowInsecurePoc
|
|
121
374
|
});
|
|
122
375
|
}
|
|
123
|
-
|
|
376
|
+
const local = await inspectLocalCodexCredentialStatus(selection.account.accountId, deps.env);
|
|
377
|
+
return { selection, expiresAt: local.expiresAt };
|
|
378
|
+
}
|
|
379
|
+
async function launchWithSelectedCredentials(deps, selected, launch) {
|
|
380
|
+
if (managedCredentialsV3Enabled(deps.config)) {
|
|
381
|
+
try {
|
|
382
|
+
return await launch();
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
const report = await reportManagedCodexV3Credentials({
|
|
386
|
+
client: deps.clientFor({
|
|
387
|
+
serverUrl: selected.selection.serviceUrl,
|
|
388
|
+
allowInsecureHttp: selected.selection.allowInsecurePoc
|
|
389
|
+
}),
|
|
390
|
+
tenantId: deps.config.connection.tenantId,
|
|
391
|
+
env: deps.env
|
|
392
|
+
});
|
|
393
|
+
if (!report.reported && report.warningCode) {
|
|
394
|
+
deps.errorOutput.write(`Codex credential observation deferred (${report.warningCode}); local credentials were not marked invalid.\n`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (deps.config.profile?.userBoundRefreshTokens?.delivery !== true)
|
|
399
|
+
return launch();
|
|
400
|
+
let current = selected;
|
|
401
|
+
return superviseCodexShortLivedCredentials({
|
|
402
|
+
initialExpiresAt: current.expiresAt,
|
|
403
|
+
launch: (signal) => launch(signal),
|
|
404
|
+
renew: async () => {
|
|
405
|
+
const installed = await installCredentials(deps, current.selection, {
|
|
406
|
+
serverUrl: current.selection.serviceUrl,
|
|
407
|
+
allowInsecureHttp: current.selection.allowInsecurePoc
|
|
408
|
+
});
|
|
409
|
+
current = installed;
|
|
410
|
+
return { expiresAt: installed.expiresAt };
|
|
411
|
+
}
|
|
412
|
+
});
|
|
124
413
|
}
|
|
125
414
|
export function registerCodexCommands(program, deps, options = {}) {
|
|
126
415
|
const codex = options.nested === false
|
|
@@ -147,21 +436,67 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
147
436
|
else
|
|
148
437
|
deps.output.write("Logged out.\n");
|
|
149
438
|
});
|
|
439
|
+
if (!includeAuthCommands && managedCredentialsV3Enabled(deps.config))
|
|
440
|
+
serviceOptions(codex.command("login")
|
|
441
|
+
.description("Run an isolated official Codex login and install client-managed credentials"))
|
|
442
|
+
.option("--device-auth", "use the official Codex device authorization flow")
|
|
443
|
+
.action(async (opts, command) => {
|
|
444
|
+
if (!managedCredentialsV3Enabled(deps.config)) {
|
|
445
|
+
throw new CodexServiceError("Managed Codex credential v3 is not enabled by the verified platform profile.", "managed_codex_v3_not_enabled", 409);
|
|
446
|
+
}
|
|
447
|
+
const result = await executeManagedCodexV3Login({
|
|
448
|
+
client: deps.clientFor(serviceConnection(opts)),
|
|
449
|
+
tenantId: deps.config.connection.tenantId,
|
|
450
|
+
env: deps.env,
|
|
451
|
+
deviceAuth: opts.deviceAuth
|
|
452
|
+
});
|
|
453
|
+
const assignments = await deps.clientFor(serviceConnection(opts)).listV3CredentialAssignments();
|
|
454
|
+
const assignment = assignments.items.find((item) => item.assignmentId === result.assignmentId);
|
|
455
|
+
await saveCodexSelection(deps.config.persistence.configDirectory, {
|
|
456
|
+
serviceUrl: opts.server ?? deps.config.connection.serverUrl,
|
|
457
|
+
allowInsecurePoc: Boolean(opts.insecurePoc || opts.insecureHttp || deps.config.connection.allowInsecureHttp),
|
|
458
|
+
account: {
|
|
459
|
+
id: result.managedAccountId,
|
|
460
|
+
accountId: result.providerAccountId,
|
|
461
|
+
label: assignment?.label ?? "Codex account",
|
|
462
|
+
distributionMode: "CLIENT_MANAGED_V3",
|
|
463
|
+
assignmentId: result.assignmentId,
|
|
464
|
+
managedAccountId: result.managedAccountId,
|
|
465
|
+
providerAccountId: result.providerAccountId
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
const output = {
|
|
469
|
+
enrollmentId: result.enrollmentId,
|
|
470
|
+
assignmentId: result.assignmentId,
|
|
471
|
+
managedAccountId: result.managedAccountId,
|
|
472
|
+
providerAccountId: result.providerAccountId,
|
|
473
|
+
lineageId: result.lineageId,
|
|
474
|
+
generationVersion: result.generationVersion,
|
|
475
|
+
authorityMode: result.authorityMode,
|
|
476
|
+
credentialExpiresAt: result.credentialExpiresAt
|
|
477
|
+
};
|
|
478
|
+
if (await json(command, deps))
|
|
479
|
+
print(deps.output, success(output));
|
|
480
|
+
else
|
|
481
|
+
deps.output.write(`Codex login installed and acknowledged for ${result.providerAccountId}; generation=${result.generationVersion}.\n`);
|
|
482
|
+
});
|
|
150
483
|
serviceOptions(codex.command("list").description("List available Codex accounts without downloading credentials"))
|
|
151
484
|
.action(async (opts, command) => {
|
|
152
|
-
const { response: result, selectedAccountId } = await loadAccountData(deps, opts);
|
|
485
|
+
const { response: result, selectedAccountId, assignments, v3Assignments } = await loadAccountData(deps, opts);
|
|
153
486
|
const hasAvailableAccount = result.accounts.some(isCodexAccountAvailable);
|
|
487
|
+
const shouldRequestAccount = result.accounts.length === 0
|
|
488
|
+
|| !hasAvailableAccount && assignments === undefined && v3Assignments === undefined;
|
|
154
489
|
if (await json(command, deps))
|
|
155
490
|
print(deps.output, success({
|
|
156
491
|
...result,
|
|
157
|
-
...(
|
|
492
|
+
...(shouldRequestAccount ? { guidance: { code: "chatgpt_account_request_required", ticket_template: "IT11", message: NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE } } : {})
|
|
158
493
|
}));
|
|
159
494
|
else if (result.accounts.length === 0) {
|
|
160
495
|
deps.output.write(`【需要申请 ChatGPT 账号】\n${NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE}\n`);
|
|
161
496
|
}
|
|
162
497
|
else {
|
|
163
498
|
deps.output.write(renderCodexAccountDashboard(result.accounts, selectedAccountId));
|
|
164
|
-
if (
|
|
499
|
+
if (shouldRequestAccount) {
|
|
165
500
|
deps.output.write(`【需要申请 ChatGPT 账号】\n${NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE}\n`);
|
|
166
501
|
}
|
|
167
502
|
}
|
|
@@ -169,7 +504,7 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
169
504
|
serviceOptions(codex.command("use [account]").description("Select an account and update the official Codex auth.json"))
|
|
170
505
|
.action(async (account, opts, command) => {
|
|
171
506
|
const result = await chooseAccount(deps, account, opts);
|
|
172
|
-
await installCredentials(deps, result.
|
|
507
|
+
await installCredentials(deps, result.selection, serviceConnection(opts));
|
|
173
508
|
if (await json(command, deps))
|
|
174
509
|
print(deps.output, success({ account: result.account, auth_mode: "chatgpt" }));
|
|
175
510
|
else
|
|
@@ -183,8 +518,8 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
183
518
|
if (opts.selectAccount)
|
|
184
519
|
await clearCodexSelection(deps.config.persistence.configDirectory);
|
|
185
520
|
const selected = await selectedCredentials(deps, opts.account, opts);
|
|
186
|
-
deps.errorOutput.write(`Using ChatGPT account: ${selected.account.label} (${selected.account.id})\n`);
|
|
187
|
-
process.exitCode = await deps.run(args);
|
|
521
|
+
deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
|
|
522
|
+
process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.run(args, { signal }));
|
|
188
523
|
});
|
|
189
524
|
serviceOptions(codex.command("app [workspace]").description("Validate and launch the official Codex desktop app"))
|
|
190
525
|
.option("-a, --account <id>", "account id")
|
|
@@ -195,8 +530,8 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
195
530
|
if (opts.selectAccount)
|
|
196
531
|
await clearCodexSelection(deps.config.persistence.configDirectory);
|
|
197
532
|
const selected = await selectedCredentials(deps, opts.account, opts);
|
|
198
|
-
deps.errorOutput.write(`Using ChatGPT account: ${selected.account.label} (${selected.account.id})\n`);
|
|
199
|
-
process.exitCode = await deps.app(workspace);
|
|
533
|
+
deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
|
|
534
|
+
process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.app(workspace, { signal }));
|
|
200
535
|
});
|
|
201
536
|
codex.command("status").description("Show selection, desktop app, and official auth.json status")
|
|
202
537
|
.option("--remote", "include remote accounts")
|
|
@@ -247,8 +582,10 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
247
582
|
codex.command("reset").description("Reset selection and managed official credentials")
|
|
248
583
|
.action(async (_options, command) => {
|
|
249
584
|
const selection = await loadCodexSelection(deps.config.persistence.configDirectory);
|
|
250
|
-
const cleanup = await cleanupLegacyCodexProvider();
|
|
251
|
-
|
|
585
|
+
const cleanup = await cleanupLegacyCodexProvider(codexConfigPathForEnv(deps.env));
|
|
586
|
+
if (schemaV2LifecycleEnabled(deps.config))
|
|
587
|
+
await discardCodexFileCredentialBackup(deps.env);
|
|
588
|
+
const auth = await resetManagedCodexFileCredentials(selection?.account.providerAccountId ?? selection?.account.accountId, deps.env);
|
|
252
589
|
await clearCodexSelection(deps.config.persistence.configDirectory);
|
|
253
590
|
if (await json(command, deps))
|
|
254
591
|
print(deps.output, success({ reset: true, auth_action: auth.action, legacy_provider_removed: cleanup.changed }));
|
|
@@ -264,7 +601,8 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
264
601
|
.option("--replace-login", "confirm replacement")
|
|
265
602
|
.action(async (account, opts, command) => {
|
|
266
603
|
if (opts.restore) {
|
|
267
|
-
|
|
604
|
+
assertLegacyManagementEnabled(deps.config);
|
|
605
|
+
const result = await restoreCodexFileCredentials(deps.env);
|
|
268
606
|
if (await json(command, deps))
|
|
269
607
|
print(deps.output, success({ action: result.action }));
|
|
270
608
|
else
|
|
@@ -275,56 +613,124 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
275
613
|
throw new Error("auth-agent replaces the current login; add --replace-login to confirm.");
|
|
276
614
|
if (opts.credentialStore !== "file")
|
|
277
615
|
throw new Error("ChatGPT auth.json 同步目前仅支持 --credential-store file。");
|
|
278
|
-
|
|
616
|
+
if (managedCredentialsV3Enabled(deps.config)) {
|
|
617
|
+
const interval = Number(opts.interval) * 1_000;
|
|
618
|
+
if (!Number.isFinite(interval) || interval <= 0)
|
|
619
|
+
throw new Error("--interval must be positive.");
|
|
620
|
+
do {
|
|
621
|
+
const result = await reportManagedCodexV3Credentials({
|
|
622
|
+
client: deps.clientFor(serviceConnection(opts)),
|
|
623
|
+
tenantId: deps.config.connection.tenantId,
|
|
624
|
+
env: deps.env
|
|
625
|
+
});
|
|
626
|
+
const event = {
|
|
627
|
+
level: result.reported ? "INFO" : "WARN",
|
|
628
|
+
event: result.reported ? "credentials.observed" : "credentials.observation_deferred",
|
|
629
|
+
at: new Date().toISOString(),
|
|
630
|
+
...(result.generationVersion ? { generationVersion: result.generationVersion } : {}),
|
|
631
|
+
...(result.warningCode ? { warningCode: result.warningCode } : {})
|
|
632
|
+
};
|
|
633
|
+
if (await json(command, deps))
|
|
634
|
+
print(deps.output, success(event));
|
|
635
|
+
else
|
|
636
|
+
deps.output.write(`${event.at} ${event.level} ${event.event}${result.warningCode ? ` (${result.warningCode})` : ""}\n`);
|
|
637
|
+
if (opts.once)
|
|
638
|
+
break;
|
|
639
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
640
|
+
} while (true);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
let selected = await selectedCredentials(deps, account, opts);
|
|
279
644
|
const interval = Number(opts.interval) * 1_000;
|
|
280
|
-
const
|
|
645
|
+
const requestedRefreshBefore = Number(opts.refreshBefore) * 1_000;
|
|
281
646
|
if (!Number.isFinite(interval) || interval <= 0)
|
|
282
647
|
throw new Error("--interval must be positive.");
|
|
283
|
-
if (!Number.isFinite(
|
|
648
|
+
if (!Number.isFinite(requestedRefreshBefore) || requestedRefreshBefore <= 0) {
|
|
284
649
|
throw new Error("--refresh-before must be positive.");
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
: undefined;
|
|
650
|
+
}
|
|
651
|
+
const refreshBefore = effectiveCodexCredentialRefreshBefore(requestedRefreshBefore, deps.config.profile?.userBoundRefreshTokens?.delivery === true);
|
|
652
|
+
let expiresAt = selected.expiresAt;
|
|
289
653
|
do {
|
|
290
654
|
const timestamp = Date.now();
|
|
291
655
|
if (expiresAt === undefined || expiresAt <= timestamp + refreshBefore) {
|
|
292
|
-
|
|
656
|
+
selected = await installCredentials(deps, selected.selection, serviceConnection(opts));
|
|
657
|
+
expiresAt = selected.expiresAt;
|
|
293
658
|
const event = {
|
|
294
659
|
level: "INFO",
|
|
295
660
|
event: "credentials.installed",
|
|
296
661
|
at: new Date(timestamp).toISOString(),
|
|
297
|
-
account: selection.account.id,
|
|
662
|
+
account: selected.selection.account.id,
|
|
298
663
|
expiresAt
|
|
299
664
|
};
|
|
300
665
|
if (await json(command, deps))
|
|
301
666
|
print(deps.output, success(event));
|
|
302
667
|
else
|
|
303
|
-
deps.output.write(`${event.at} INFO credentials.installed ${selection.account.label}\n`);
|
|
668
|
+
deps.output.write(`${event.at} INFO credentials.installed ${selected.selection.account.label}\n`);
|
|
304
669
|
}
|
|
305
670
|
else {
|
|
306
671
|
const event = {
|
|
307
672
|
level: "INFO",
|
|
308
673
|
event: "credentials.current",
|
|
309
674
|
at: new Date(timestamp).toISOString(),
|
|
310
|
-
account: selection.account.id,
|
|
675
|
+
account: selected.selection.account.id,
|
|
311
676
|
expiresAt,
|
|
312
677
|
refreshAt: expiresAt - refreshBefore
|
|
313
678
|
};
|
|
314
679
|
if (await json(command, deps))
|
|
315
680
|
print(deps.output, success(event));
|
|
316
681
|
else
|
|
317
|
-
deps.output.write(`${event.at} INFO credentials.current ${selection.account.label}; refresh not due\n`);
|
|
682
|
+
deps.output.write(`${event.at} INFO credentials.current ${selected.selection.account.label}; refresh not due\n`);
|
|
318
683
|
}
|
|
319
684
|
if (opts.once)
|
|
320
685
|
break;
|
|
321
686
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
322
687
|
} while (true);
|
|
323
688
|
});
|
|
324
|
-
serviceOptions(codex.command("upload").description("
|
|
689
|
+
serviceOptions(codex.command("upload").description("Enroll official Codex credentials (v3) or upload the legacy local pool"))
|
|
325
690
|
.option("--storage <path>", "account storage path")
|
|
326
691
|
.option("--source <name>", "upload source")
|
|
327
692
|
.action(async (opts, command) => {
|
|
693
|
+
if (managedCredentialsV3Enabled(deps.config)) {
|
|
694
|
+
if (opts.storage || opts.source) {
|
|
695
|
+
throw new CodexServiceError("Managed credential v3 upload reads only the official CODEX_HOME/auth.json.", "invalid_configuration");
|
|
696
|
+
}
|
|
697
|
+
const result = await executeManagedCodexV3Upload({
|
|
698
|
+
client: deps.clientFor(serviceConnection(opts)),
|
|
699
|
+
tenantId: deps.config.connection.tenantId,
|
|
700
|
+
env: deps.env
|
|
701
|
+
});
|
|
702
|
+
const assignments = await deps.clientFor(serviceConnection(opts)).listV3CredentialAssignments();
|
|
703
|
+
const assignment = assignments.items.find((item) => item.assignmentId === result.assignmentId);
|
|
704
|
+
await saveCodexSelection(deps.config.persistence.configDirectory, {
|
|
705
|
+
serviceUrl: opts.server ?? deps.config.connection.serverUrl,
|
|
706
|
+
allowInsecurePoc: Boolean(opts.insecurePoc || opts.insecureHttp || deps.config.connection.allowInsecureHttp),
|
|
707
|
+
account: {
|
|
708
|
+
id: result.managedAccountId,
|
|
709
|
+
accountId: result.providerAccountId,
|
|
710
|
+
label: assignment?.label ?? "Codex account",
|
|
711
|
+
distributionMode: "CLIENT_MANAGED_V3",
|
|
712
|
+
assignmentId: result.assignmentId,
|
|
713
|
+
managedAccountId: result.managedAccountId,
|
|
714
|
+
providerAccountId: result.providerAccountId
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
const output = {
|
|
718
|
+
enrollmentId: result.enrollmentId,
|
|
719
|
+
assignmentId: result.assignmentId,
|
|
720
|
+
managedAccountId: result.managedAccountId,
|
|
721
|
+
providerAccountId: result.providerAccountId,
|
|
722
|
+
lineageId: result.lineageId,
|
|
723
|
+
generationVersion: result.generationVersion,
|
|
724
|
+
authorityMode: result.authorityMode,
|
|
725
|
+
credentialExpiresAt: result.credentialExpiresAt
|
|
726
|
+
};
|
|
727
|
+
if (await json(command, deps))
|
|
728
|
+
print(deps.output, success(output));
|
|
729
|
+
else
|
|
730
|
+
deps.output.write(`Official Codex credentials enrolled, installed, and acknowledged; generation=${result.generationVersion}.\n`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
assertLegacyManagementEnabled(deps.config);
|
|
328
734
|
if (opts.storage)
|
|
329
735
|
setStoragePathDirect(opts.storage);
|
|
330
736
|
else
|
|
@@ -382,6 +788,124 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
382
788
|
if (failures.length > 0)
|
|
383
789
|
process.exitCode = 1;
|
|
384
790
|
});
|
|
791
|
+
const enrollment = codex.command("enrollment")
|
|
792
|
+
.description("Execute administrator-created one-shot Codex login intents");
|
|
793
|
+
serviceOptions(enrollment.command("status <intent>").description("Show redacted enrollment intent status"))
|
|
794
|
+
.action(async (intent, opts, command) => {
|
|
795
|
+
const result = await deps.clientFor(serviceConnection(opts)).getEnrollmentIntent(intent);
|
|
796
|
+
if (await json(command, deps)) {
|
|
797
|
+
print(deps.output, { ok: true, ...result });
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
deps.output.write(`${result.intent.id} ${result.intent.type} ${result.intent.status}`
|
|
801
|
+
+ ` revision=${result.intent.revision}\n`);
|
|
802
|
+
deps.output.write(`expires=${result.intent.expiresAt}`
|
|
803
|
+
+ `${result.intent.assignmentId ? ` assignment=${result.intent.assignmentId}` : ""}`
|
|
804
|
+
+ `${result.intent.managedAccountId ? ` account=${result.intent.managedAccountId}` : ""}\n`);
|
|
805
|
+
if (!result.capabilities.candidateUpload) {
|
|
806
|
+
deps.output.write("安全候选上传尚未启用;当前任务只能查询或取消,不会启动 OpenAI 登录。\n");
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
serviceOptions(enrollment.command("login <intent>")
|
|
810
|
+
.description("Run one isolated official Codex login for an enrollment intent"))
|
|
811
|
+
.option("--device-auth", "use the official Codex device authorization flow")
|
|
812
|
+
.action(async (intent, _opts, command) => {
|
|
813
|
+
const result = await executeCodexEnrollmentLogin({
|
|
814
|
+
client: deps.clientFor(serviceConnection(_opts)),
|
|
815
|
+
tenantId: deps.config.connection.tenantId,
|
|
816
|
+
intentId: intent,
|
|
817
|
+
deviceAuth: _opts.deviceAuth
|
|
818
|
+
});
|
|
819
|
+
if (await json(command, deps))
|
|
820
|
+
print(deps.output, { ok: true, ...result });
|
|
821
|
+
else {
|
|
822
|
+
deps.output.write(`${result.intent.id} 登录候选已处理;状态=${result.intent.status}`
|
|
823
|
+
+ `${result.result.managedAccountId ? `,账号=${result.result.managedAccountId}` : ""}`
|
|
824
|
+
+ `${result.result.lineageId ? `,lineage=${result.result.lineageId}` : ""}`
|
|
825
|
+
+ "。\n");
|
|
826
|
+
if (result.result.outcome === "TEMPORARILY_UNAVAILABLE") {
|
|
827
|
+
deps.output.write("同一进程内的安全重试已耗尽;请取消并新建任务,不能用另一次登录覆盖已保留的候选。\n");
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
serviceOptions(enrollment.command("login-batch <planId>")
|
|
832
|
+
.description("Sequentially execute isolated logins from a migration plan queue")
|
|
833
|
+
.option("--device-auth", "use the official Codex device authorization flow")
|
|
834
|
+
.option("--dry-run", "show the redacted sequential plan without starting login")
|
|
835
|
+
.option("--page-size <count>", "migration queue page size", "100"))
|
|
836
|
+
.action(async (planId, opts, command) => {
|
|
837
|
+
if (deps.config.profile?.userBoundRefreshTokens?.migrationPlans !== true) {
|
|
838
|
+
throw new CodexServiceError("The selected tenant has not enabled schema-v2 migration login plans.", "capability_not_enabled", 409);
|
|
839
|
+
}
|
|
840
|
+
const pageSize = Number(opts.pageSize);
|
|
841
|
+
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
|
|
842
|
+
throw new CodexServiceError("--page-size must be an integer from 1 to 100.", "invalid_configuration");
|
|
843
|
+
}
|
|
844
|
+
const batch = await executeCodexMigrationLoginBatch({
|
|
845
|
+
client: deps.clientFor(serviceConnection(opts)),
|
|
846
|
+
tenantId: deps.config.connection.tenantId,
|
|
847
|
+
planId,
|
|
848
|
+
deviceAuth: opts.deviceAuth,
|
|
849
|
+
dryRun: opts.dryRun,
|
|
850
|
+
pageSize
|
|
851
|
+
});
|
|
852
|
+
const failed = batch.counts.failed > 0
|
|
853
|
+
|| ["TARGET_UNCERTAIN", "AUTHENTICATION_ERROR", "SERVICE_ERROR", "LOGIN_ERROR"]
|
|
854
|
+
.includes(batch.stopReason);
|
|
855
|
+
if (await json(command, deps)) {
|
|
856
|
+
print(deps.output, { ok: !failed, ...batch });
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
deps.output.write(`migration=${batch.plan.id} status=${batch.plan.status} revision=${batch.plan.revision}`
|
|
860
|
+
+ ` stop=${batch.stopReason}${batch.dryRun ? " dry-run" : ""}\n`);
|
|
861
|
+
deps.output.write(`capacity: verifiedMinimum=${batch.capacity.verifiedMinimum}, verifiedLimit=unknown,`
|
|
862
|
+
+ ` sequentialRequired=${batch.capacity.sequentialRequired}; verifiedMinimum 不是上限。\n`);
|
|
863
|
+
for (const item of batch.items) {
|
|
864
|
+
deps.output.write(`${item.assignmentId} ${item.action}/${item.status}`
|
|
865
|
+
+ `${item.intentId ? ` intent=${item.intentId}` : ""}`
|
|
866
|
+
+ `${item.outcome ? ` outcome=${item.outcome}` : ""}`
|
|
867
|
+
+ `${item.skipReason ? ` skip=${item.skipReason}` : ""}`
|
|
868
|
+
+ `${item.safeErrorCode ? ` error=${item.safeErrorCode}` : ""}\n`);
|
|
869
|
+
}
|
|
870
|
+
deps.output.write(`summary: attempted=${batch.counts.attempted}, completed=${batch.counts.completed},`
|
|
871
|
+
+ ` failed=${batch.counts.failed}, skipped=${batch.counts.skipped}.\n`);
|
|
872
|
+
}
|
|
873
|
+
if (failed)
|
|
874
|
+
process.exitCode = 1;
|
|
875
|
+
});
|
|
876
|
+
serviceOptions(enrollment.command("cancel <intent>").description("Cancel an enrollment intent"))
|
|
877
|
+
.option("--idempotency-key <key>", "reuse a prior cancellation idempotency key")
|
|
878
|
+
.action(async (intent, opts, command) => {
|
|
879
|
+
const client = deps.clientFor(serviceConnection(opts));
|
|
880
|
+
const current = await client.getEnrollmentIntent(intent);
|
|
881
|
+
const result = await client.cancelEnrollmentIntent(intent, {
|
|
882
|
+
idempotencyKey: opts.idempotencyKey?.trim() || randomUUID(),
|
|
883
|
+
revision: current.intent.revision
|
|
884
|
+
});
|
|
885
|
+
if (await json(command, deps))
|
|
886
|
+
print(deps.output, { ok: true, ...result });
|
|
887
|
+
else
|
|
888
|
+
deps.output.write(`${result.intent.id} 已取消;状态=${result.intent.status}。\n`);
|
|
889
|
+
});
|
|
890
|
+
const retirement = codex.command("retirement")
|
|
891
|
+
.description("Remove an exact locally installed lineage for a Server retirement request");
|
|
892
|
+
serviceOptions(retirement.command("acknowledge <retirement>")
|
|
893
|
+
.description("Delete the matching managed short-lived credentials and append local evidence")
|
|
894
|
+
.option("--idempotency-key <key>", "reuse a prior local deletion acknowledgement key"))
|
|
895
|
+
.action(async (retirementId, opts, command) => {
|
|
896
|
+
const result = await executeCodexAssignmentRetirementLocalDelete({
|
|
897
|
+
client: deps.clientFor(serviceConnection(opts)),
|
|
898
|
+
retirementId,
|
|
899
|
+
env: deps.env,
|
|
900
|
+
...(opts.idempotencyKey?.trim() ? { idempotencyKey: opts.idempotencyKey.trim() } : {})
|
|
901
|
+
});
|
|
902
|
+
if (await json(command, deps))
|
|
903
|
+
print(deps.output, { ok: true, ...result });
|
|
904
|
+
else
|
|
905
|
+
deps.output.write(`本机匹配的短效凭据已删除并记录证据;retirement=${result.retirement.id}`
|
|
906
|
+
+ `,revision=${result.retirement.revision},状态=${result.retirement.status}。\n`
|
|
907
|
+
+ "该证据不代表其他终端已清理;最终退役仍等待全部已披露短效凭据自然过期。\n");
|
|
908
|
+
});
|
|
385
909
|
const admin = codex.command("admin")
|
|
386
910
|
.description("Administer server-managed Codex accounts and assignments");
|
|
387
911
|
const adminAccounts = admin.command("accounts")
|
|
@@ -425,6 +949,7 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
425
949
|
serviceOptions(adminAccounts.command("refresh <account>")
|
|
426
950
|
.description("Request one server-side refresh-token exchange"))
|
|
427
951
|
.action(async (account, opts, command) => {
|
|
952
|
+
assertLegacyManagementEnabled(deps.config);
|
|
428
953
|
const client = deps.clientFor(serviceConnection(opts));
|
|
429
954
|
const resolved = await resolveAdminAccount(client, account);
|
|
430
955
|
const result = await client.refreshAdminAccount(resolved.id);
|
|
@@ -468,6 +993,7 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
468
993
|
.description("Bind a ChatGPT/Codex account to a Keycloak user"))
|
|
469
994
|
.option("--default", "make this the user's default Codex account")
|
|
470
995
|
.action(async (account, subject, opts, command) => {
|
|
996
|
+
assertLegacyManagementEnabled(deps.config);
|
|
471
997
|
const client = deps.clientFor(serviceConnection(opts));
|
|
472
998
|
const resolved = await resolveAdminAccount(client, account);
|
|
473
999
|
const result = await client.bindAdminAssignment({
|
|
@@ -484,6 +1010,7 @@ export function registerCodexCommands(program, deps, options = {}) {
|
|
|
484
1010
|
serviceOptions(adminAssignments.command("unbind <assignment>")
|
|
485
1011
|
.description("Remove a ChatGPT/Codex account assignment"))
|
|
486
1012
|
.action(async (assignment, opts, command) => {
|
|
1013
|
+
assertLegacyManagementEnabled(deps.config);
|
|
487
1014
|
const client = deps.clientFor(serviceConnection(opts));
|
|
488
1015
|
const assignmentList = await client.listAdminAssignments();
|
|
489
1016
|
const target = assignmentList.assignments.find((item) => item.id === assignment);
|