@bitkyc08/opencodex 2.49.0 → 2.51.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/AGENTS_INSTALL.md +9 -1
- package/README.md +3 -0
- package/bin/ocx.mjs +222 -71
- package/gui/dist/assets/index-D7BdZpZm.js +115 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/qoder/adapter.ts +69 -1
- package/src/adapters/qoder/scaffold-guard.ts +233 -0
- package/src/claude/agents-inject.ts +29 -5
- package/src/claude/desktop-3p.ts +31 -3
- package/src/claude/gateway-cache.ts +12 -21
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +43 -1
- package/src/cli/claude-agent-startup-sync.ts +26 -1
- package/src/cli/claude.ts +138 -20
- package/src/cli/config-command.ts +67 -1
- package/src/cli/connect.ts +181 -14
- package/src/cli/dispatch.ts +53 -9
- package/src/cli/doctor.ts +9 -2
- package/src/cli/ensure-desired-integrations.ts +10 -0
- package/src/cli/gui-pair-client.ts +1 -12
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +367 -0
- package/src/cli/index.ts +99 -31
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/cli/registry.ts +43 -3
- package/src/cli/status.ts +325 -5
- package/src/cli/version-skew.ts +4 -1
- package/src/cli.ts +2 -2
- package/src/client/catalog-compatibility.ts +192 -0
- package/src/client/connect.ts +31 -0
- package/src/client/hub-client.ts +52 -0
- package/src/client/hub-state.ts +214 -0
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +50 -13
- package/src/codex/auth-api.ts +156 -28
- package/src/codex/auth-context.ts +21 -0
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +71 -2
- package/src/codex/catalog/sync.ts +99 -0
- package/src/codex/codex-write-lock.ts +11 -2
- package/src/codex/desired-state.ts +47 -1
- package/src/codex/inject-coordination.ts +10 -5
- package/src/codex/inject.ts +29 -12
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +54 -8
- package/src/codex/routing.ts +48 -1
- package/src/codex/runtime.ts +37 -3
- package/src/codex/sync.ts +29 -9
- package/src/codex/warmup.ts +21 -4
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +184 -12
- package/src/generated/compatibility-version.json +188 -116
- package/src/grok/status.ts +9 -1
- package/src/integrations/config-io.ts +54 -1
- package/src/lib/bun-runtime.ts +1 -1
- package/src/lib/errors.ts +8 -0
- package/src/lib/gui-pair-capability.ts +27 -0
- package/src/lib/local-destinations.ts +162 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/privacy.ts +25 -0
- package/src/lib/process-control.ts +130 -20
- package/src/lib/service-secrets.ts +28 -0
- package/src/lib/test-home-guard.ts +49 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/quota.ts +20 -1
- package/src/providers/registry.ts +35 -6
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +10 -7
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +12 -6
- package/src/server/hub-state.ts +98 -0
- package/src/server/images.ts +2 -2
- package/src/server/index.ts +149 -8
- package/src/server/management/api-access.ts +14 -3
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/cursor-integration-routes.ts +13 -4
- package/src/server/management/logs-usage-routes.ts +4 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +9 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +1 -1
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/request-log.ts +10 -0
- package/src/server/responses/codex-auth-error.ts +18 -1
- package/src/server/responses/codex-ws-exchange.ts +36 -4
- package/src/server/responses/codex-ws-wire.ts +76 -5
- package/src/server/responses/compact.ts +28 -11
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +201 -48
- package/src/server/responses/policy-fallback.ts +13 -3
- package/src/server/search.ts +2 -2
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +965 -68
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +93 -4
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/update/badge.ts +3 -2
- package/src/update/index.ts +317 -64
- package/src/update/install-detection.d.mts +6 -0
- package/src/update/install-detection.mjs +73 -0
- package/src/update/job.ts +101 -49
- package/src/update/pnpm-global-install.d.mts +144 -0
- package/src/update/pnpm-global-install.mjs +591 -0
- package/src/update/pnpm-invocation.d.mts +43 -0
- package/src/update/pnpm-invocation.mjs +141 -0
- package/src/update/registry-integrity.d.mts +16 -0
- package/src/update/registry-integrity.mjs +37 -0
- package/src/update/transactional-install.d.mts +1 -1
- package/src/update/transactional-install.mjs +101 -7
- package/src/update/tray-update-plan.mjs +1 -1
- package/src/vision/plan.ts +13 -3
- package/src/vision/routed-describe.ts +51 -20
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/gui/dist/assets/index-BtyONQrZ.js +0 -115
|
@@ -58,7 +58,9 @@ function isCredentialRecord(value: unknown): value is CodexAccountCredentialReco
|
|
|
58
58
|
&& (value.replacedAt === undefined || typeof value.replacedAt === "number")
|
|
59
59
|
&& (value.lastCodexValidatedAt === undefined || typeof value.lastCodexValidatedAt === "number")
|
|
60
60
|
&& (value.lastCodexValidationStatus === undefined || value.lastCodexValidationStatus === "ok" || value.lastCodexValidationStatus === "failed")
|
|
61
|
-
&& (value.lastCodexValidationError === undefined || typeof value.lastCodexValidationError === "string")
|
|
61
|
+
&& (value.lastCodexValidationError === undefined || typeof value.lastCodexValidationError === "string")
|
|
62
|
+
&& (value.codexValidationPending === undefined || typeof value.codexValidationPending === "boolean")
|
|
63
|
+
&& (value.lastCodexValidationTerminal === undefined || typeof value.lastCodexValidationTerminal === "boolean");
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export function refreshGrantFingerprintForToken(refreshToken: string): string {
|
|
@@ -118,11 +120,21 @@ function persistCredentialMutation(store: CodexAccountStore): void {
|
|
|
118
120
|
advanceCodexCredentialMutationEpoch();
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Validation metadata that survives a credential write.
|
|
125
|
+
*
|
|
126
|
+
* `lastCodexValidationTerminal` is deliberately NOT in this list. Every credential write —
|
|
127
|
+
* re-login, the CAS refresh commit, same-grant alias propagation — rebuilds the record from this
|
|
128
|
+
* pick list, so leaving the marker out is what makes a successful refresh or a re-authentication
|
|
129
|
+
* erase a terminal verdict. Both events disprove "the grant was revoked", and a verdict that
|
|
130
|
+
* could only ever be set would brand an account dead forever on one spurious `invalid_grant`.
|
|
131
|
+
*/
|
|
121
132
|
function preservedValidationMetadata(record: CodexAccountCredentialRecord | undefined): Pick<
|
|
122
133
|
CodexAccountCredentialRecord,
|
|
123
|
-
"lastCodexValidatedAt" | "lastCodexValidationStatus" | "lastCodexValidationError"
|
|
134
|
+
"lastCodexValidatedAt" | "lastCodexValidationStatus" | "lastCodexValidationError" | "codexValidationPending"
|
|
124
135
|
> {
|
|
125
136
|
return {
|
|
137
|
+
...(record?.codexValidationPending === true ? { codexValidationPending: true } : {}),
|
|
126
138
|
...(record?.lastCodexValidatedAt !== undefined ? { lastCodexValidatedAt: record.lastCodexValidatedAt } : {}),
|
|
127
139
|
...(record?.lastCodexValidationStatus !== undefined ? { lastCodexValidationStatus: record.lastCodexValidationStatus } : {}),
|
|
128
140
|
...(record?.lastCodexValidationError !== undefined ? { lastCodexValidationError: record.lastCodexValidationError } : {}),
|
|
@@ -135,8 +147,12 @@ export function getCodexAccountCredential(id: string): CodexAccountCredentials |
|
|
|
135
147
|
return record.credential ?? null;
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
export function saveCodexAccountCredential(
|
|
139
|
-
|
|
150
|
+
export function saveCodexAccountCredential(
|
|
151
|
+
id: string,
|
|
152
|
+
cred: CodexAccountCredentials,
|
|
153
|
+
options: { validationPending?: boolean } = {},
|
|
154
|
+
): number {
|
|
155
|
+
return withCredentialMutationLockSync(() => {
|
|
140
156
|
const store = loadCodexAccountRecordStore();
|
|
141
157
|
const current = store[id];
|
|
142
158
|
const refreshGrantFingerprint = current?.credential?.refreshToken === cred.refreshToken
|
|
@@ -148,37 +164,84 @@ export function saveCodexAccountCredential(id: string, cred: CodexAccountCredent
|
|
|
148
164
|
refreshGrantFingerprint,
|
|
149
165
|
replacedAt: current ? Date.now() : undefined,
|
|
150
166
|
...preservedValidationMetadata(current),
|
|
167
|
+
...(options.validationPending ? {
|
|
168
|
+
codexValidationPending: true,
|
|
169
|
+
lastCodexValidatedAt: undefined,
|
|
170
|
+
lastCodexValidationStatus: undefined,
|
|
171
|
+
lastCodexValidationError: undefined,
|
|
172
|
+
} : {}),
|
|
151
173
|
};
|
|
152
174
|
persistCredentialMutation(store);
|
|
175
|
+
return store[id].generation;
|
|
153
176
|
});
|
|
154
177
|
}
|
|
155
178
|
|
|
156
|
-
export function markCodexAccountValidated(id: string, atMs: number = Date.now()): void {
|
|
179
|
+
export function markCodexAccountValidated(id: string, atMs: number = Date.now(), generation?: number): void {
|
|
157
180
|
withCredentialMutationLockSync(() => {
|
|
158
181
|
const store = loadCodexAccountRecordStore();
|
|
159
182
|
const current = store[id];
|
|
160
183
|
if (!current || current.deletedAt != null || !current.credential) return;
|
|
184
|
+
if (current.codexValidationPending && generation === undefined) return;
|
|
185
|
+
if (generation !== undefined && current.generation !== generation) return;
|
|
161
186
|
store[id] = {
|
|
162
187
|
...current,
|
|
163
188
|
lastCodexValidatedAt: atMs,
|
|
164
189
|
lastCodexValidationStatus: "ok",
|
|
165
190
|
lastCodexValidationError: undefined,
|
|
191
|
+
codexValidationPending: undefined,
|
|
192
|
+
// A completed validation is the direct refutation of a terminal verdict, and this
|
|
193
|
+
// spread would otherwise carry the old marker forward.
|
|
194
|
+
lastCodexValidationTerminal: undefined,
|
|
166
195
|
};
|
|
167
|
-
|
|
196
|
+
// Becoming routable invalidates credential-derived caches; a timestamp-only
|
|
197
|
+
// update on an already validated account preserves the existing epoch policy.
|
|
198
|
+
if (current.codexValidationPending) persistCredentialMutation(store);
|
|
199
|
+
else persist(store);
|
|
168
200
|
});
|
|
169
201
|
}
|
|
170
202
|
|
|
171
|
-
export
|
|
172
|
-
|
|
203
|
+
export interface MarkCodexAccountValidationFailedOptions {
|
|
204
|
+
/**
|
|
205
|
+
* Write only while the stored record is still at this generation.
|
|
206
|
+
*
|
|
207
|
+
* A validation attempt is not atomic with the store: an operator can re-authenticate the
|
|
208
|
+
* account, or another writer can commit a refresh, while a probe is still in flight. Without
|
|
209
|
+
* this fence the late failure lands on whatever credential happens to be there and brands a
|
|
210
|
+
* freshly installed one dead. Declining to write is the safe direction — the failure cannot be
|
|
211
|
+
* attributed to a credential the caller never observed.
|
|
212
|
+
*/
|
|
213
|
+
expectedGeneration?: number;
|
|
214
|
+
/** The grant itself is revoked or expired; only a re-login clears it. */
|
|
215
|
+
terminal?: boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Returns whether the verdict was actually persisted (false when the fence declined it). */
|
|
219
|
+
export function markCodexAccountValidationFailed(
|
|
220
|
+
id: string,
|
|
221
|
+
reason: string,
|
|
222
|
+
options: MarkCodexAccountValidationFailedOptions = {},
|
|
223
|
+
): boolean {
|
|
224
|
+
return withCredentialMutationLockSync(() => {
|
|
173
225
|
const store = loadCodexAccountRecordStore();
|
|
174
226
|
const current = store[id];
|
|
175
|
-
if (!current || current.deletedAt != null || !current.credential) return;
|
|
227
|
+
if (!current || current.deletedAt != null || !current.credential) return false;
|
|
228
|
+
// Deferred validation is settled only by a caller that names the generation it observed.
|
|
229
|
+
// An unfenced write must never resolve a pending account, whichever verdict it carries.
|
|
230
|
+
if (current.codexValidationPending && options.expectedGeneration === undefined) return false;
|
|
231
|
+
if (options.expectedGeneration !== undefined && current.generation !== options.expectedGeneration) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
176
234
|
store[id] = {
|
|
177
235
|
...current,
|
|
178
236
|
lastCodexValidationStatus: "failed",
|
|
179
237
|
lastCodexValidationError: reason,
|
|
238
|
+
// Only ever set here. A transient failure must not clear a terminal marker set earlier,
|
|
239
|
+
// and it must not invent one either, so the flag is written only when the caller proves
|
|
240
|
+
// the grant is dead.
|
|
241
|
+
...(options.terminal ? { lastCodexValidationTerminal: true } : {}),
|
|
180
242
|
};
|
|
181
243
|
persist(store);
|
|
244
|
+
return true;
|
|
182
245
|
});
|
|
183
246
|
}
|
|
184
247
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readCodexAccountRecord } from "./account-store";
|
|
2
2
|
import { isAccountNeedsReauth } from "./account-runtime-state";
|
|
3
3
|
import {
|
|
4
4
|
MAIN_CODEX_ACCOUNT_ID,
|
|
@@ -20,33 +20,70 @@ export interface CodexAccountUsabilityOptions {
|
|
|
20
20
|
modelEligibleAccountIds?: ReadonlySet<string>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Why an account was refused, in the order the checks run. This is the attribution half of
|
|
25
|
+
* selection: an operator whose model quietly disappeared needs to know that one account fell out
|
|
26
|
+
* and why, not merely that the pool got smaller (#4212).
|
|
27
|
+
*/
|
|
28
|
+
export type CodexAccountUnusableReason =
|
|
29
|
+
| "model_not_entitled"
|
|
30
|
+
| "main_hard_locked"
|
|
31
|
+
| "main_traffic_blocked"
|
|
32
|
+
| "legacy_pool_sentinel"
|
|
33
|
+
| "needs_reauth"
|
|
34
|
+
| "main_credential_unavailable"
|
|
35
|
+
| "not_in_pool"
|
|
36
|
+
| "missing_credential"
|
|
37
|
+
| "deleted"
|
|
38
|
+
| "validation_pending";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The single source of truth for both selection and its explanation. `isCodexAccountUsable()` is
|
|
42
|
+
* this function's boolean projection rather than a parallel copy of the same branches, so a reason
|
|
43
|
+
* can never claim an account is fine while routing drops it, or name a cause routing did not use.
|
|
44
|
+
*/
|
|
45
|
+
export function codexAccountUnusableReason(
|
|
24
46
|
config: OcxConfig,
|
|
25
47
|
accountId: string,
|
|
26
48
|
options: CodexAccountUsabilityOptions = {},
|
|
27
|
-
):
|
|
28
|
-
if (options.modelEligibleAccountIds && !options.modelEligibleAccountIds.has(accountId))
|
|
49
|
+
): CodexAccountUnusableReason | undefined {
|
|
50
|
+
if (options.modelEligibleAccountIds && !options.modelEligibleAccountIds.has(accountId)) {
|
|
51
|
+
return "model_not_entitled";
|
|
52
|
+
}
|
|
29
53
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
30
|
-
if (isMainAccountHardLocked(config)) return
|
|
54
|
+
if (isMainAccountHardLocked(config)) return "main_hard_locked";
|
|
31
55
|
// Startup recovery owns the physical auth/vault boundary. Never parse or select
|
|
32
56
|
// native __main__ while an encrypted switch journal is pending or inconclusive.
|
|
33
|
-
if (!options.nativeMainSelectionOnly && isNativeMainTrafficBlocked()) return
|
|
57
|
+
if (!options.nativeMainSelectionOnly && isNativeMainTrafficBlocked()) return "main_traffic_blocked";
|
|
34
58
|
// A legacy pool row with the sentinel makes an active `__main__` ambiguous.
|
|
35
59
|
// Fail closed until the authenticated compatibility-delete path removes it.
|
|
36
|
-
if (hasLegacyMainCodexPoolAccount(config.codexAccounts)) return
|
|
37
|
-
if (isAccountNeedsReauth(accountId) && !hasMainAccountRefreshGrant()) return
|
|
60
|
+
if (hasLegacyMainCodexPoolAccount(config.codexAccounts)) return "legacy_pool_sentinel";
|
|
61
|
+
if (isAccountNeedsReauth(accountId) && !hasMainAccountRefreshGrant()) return "needs_reauth";
|
|
38
62
|
// A selection-only caller owns the recovery/drain fence and will reject main
|
|
39
63
|
// before reservation or token materialization. Treat cached main as a routing
|
|
40
64
|
// candidate without touching the credential file so affinity is not rebound.
|
|
41
|
-
if (options.nativeMainSelectionOnly) return
|
|
65
|
+
if (options.nativeMainSelectionOnly) return undefined;
|
|
42
66
|
// Main account: a refresh grant is enough to route; materialization refreshes before I/O.
|
|
43
|
-
|
|
67
|
+
const mainLive = options.isMainAccountTokenLive
|
|
44
68
|
? options.isMainAccountTokenLive()
|
|
45
69
|
: isMainAccountCredentialUsable();
|
|
70
|
+
return mainLive ? undefined : "main_credential_unavailable";
|
|
46
71
|
}
|
|
47
72
|
const exists = (config.codexAccounts ?? [])
|
|
48
73
|
.some(account => isSelectableCodexPoolAccount(account) && account.id === accountId);
|
|
49
|
-
if (!exists) return
|
|
50
|
-
if (isAccountNeedsReauth(accountId)) return
|
|
51
|
-
|
|
74
|
+
if (!exists) return "not_in_pool";
|
|
75
|
+
if (isAccountNeedsReauth(accountId)) return "needs_reauth";
|
|
76
|
+
const record = readCodexAccountRecord(accountId);
|
|
77
|
+
if (!record?.credential) return "missing_credential";
|
|
78
|
+
if (record.deletedAt != null) return "deleted";
|
|
79
|
+
if (record.codexValidationPending) return "validation_pending";
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isCodexAccountUsable(
|
|
84
|
+
config: OcxConfig,
|
|
85
|
+
accountId: string,
|
|
86
|
+
options: CodexAccountUsabilityOptions = {},
|
|
87
|
+
): boolean {
|
|
88
|
+
return codexAccountUnusableReason(config, accountId, options) === undefined;
|
|
52
89
|
}
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isCodexAccountGenerationLive,
|
|
13
13
|
forceRefreshCodexPoolToken,
|
|
14
14
|
markCodexAccountValidated,
|
|
15
|
+
markCodexAccountValidationFailed,
|
|
15
16
|
readCodexAccountRecord,
|
|
16
17
|
saveCodexAccountCredential,
|
|
17
18
|
CodexCredentialGenerationConflictError,
|
|
@@ -114,8 +115,8 @@ export { clearMainAccountInfoCache } from "./main-account-cache";
|
|
|
114
115
|
import type { CodexQuotaRefreshOutcome } from "./quota-refresh-outcome";
|
|
115
116
|
import { getMainAccountHardLockStatus, type MainAccountHardLockStatus } from "./main-account-hard-lock";
|
|
116
117
|
import { observeMainReserveRevocation } from "./reserve-availability";
|
|
117
|
-
import {
|
|
118
|
-
import { codexWarmupFailureReason, warmCodexAccount } from "./warmup";
|
|
118
|
+
import { emailMaskingEnabled, projectEmail } from "../lib/privacy";
|
|
119
|
+
import { codexWarmupFailureReason, isCodexWarmupProvisioningFailure, warmCodexAccount } from "./warmup";
|
|
119
120
|
export { maskEmail } from "../lib/privacy";
|
|
120
121
|
import type { CodexAccount, CodexAccountCredentials, OcxConfig } from "../types";
|
|
121
122
|
import type { CatalogDisposition } from "./convergence-types";
|
|
@@ -192,6 +193,7 @@ interface CodexLoginStateRow {
|
|
|
192
193
|
code?: string;
|
|
193
194
|
needsReauth?: boolean;
|
|
194
195
|
catalogRefreshPending?: boolean;
|
|
196
|
+
validationPending?: boolean;
|
|
195
197
|
doneAt?: number;
|
|
196
198
|
}
|
|
197
199
|
const codexAuthLoginState = new Map<string, CodexLoginStateRow>();
|
|
@@ -362,20 +364,46 @@ function mainQuotaWithCarriedResetCredits(
|
|
|
362
364
|
};
|
|
363
365
|
}
|
|
364
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Why an account needs the operator. `missing_credential`, `refresh_failed`, and
|
|
369
|
+
* `quota_unauthorized` are the three causes this surface tells apart on its own. `unauthorized`
|
|
370
|
+
* and `forbidden` exist because the shared health projection may return them; today
|
|
371
|
+
* `projectCodexAccountHealth` only ever produces `refresh_failed`, so accepting the full union
|
|
372
|
+
* keeps this field correct if that projection widens rather than silently dropping a reason.
|
|
373
|
+
*/
|
|
374
|
+
export type CodexAccountReauthReason =
|
|
375
|
+
| "missing_credential"
|
|
376
|
+
| "refresh_failed"
|
|
377
|
+
| "quota_unauthorized"
|
|
378
|
+
| "unauthorized"
|
|
379
|
+
| "forbidden";
|
|
380
|
+
|
|
365
381
|
function poolAccountDto(
|
|
366
382
|
account: CodexAccount,
|
|
367
383
|
quotaResult: PoolQuotaResult,
|
|
368
384
|
hasCredential: boolean,
|
|
369
385
|
paused: boolean,
|
|
370
386
|
priority: number,
|
|
387
|
+
maskEmails: boolean,
|
|
371
388
|
): CodexAuthAccountDto {
|
|
372
389
|
const plan = codexPlanValue(account.plan);
|
|
373
390
|
const quota = quotaForPlan(quotaResult.quota, plan);
|
|
374
|
-
const
|
|
391
|
+
const runtimeReauth = isAccountNeedsReauth(account.id);
|
|
392
|
+
const needsReauth = !hasCredential || quotaResult.needsReauth || runtimeReauth;
|
|
375
393
|
const health = projectCodexAccountHealth({ accountId: account.id, needsReauth });
|
|
394
|
+
// `needsReauth` is an OR of three independent causes plus a persisted verdict resolved inside the
|
|
395
|
+
// health projection. Emitting only the boolean is what left #4212's reporter guessing which
|
|
396
|
+
// account took their model away and why, so name the cause they actually have to act on.
|
|
397
|
+
const reauthReason: CodexAccountReauthReason | undefined = !hasCredential
|
|
398
|
+
? "missing_credential"
|
|
399
|
+
: runtimeReauth
|
|
400
|
+
? "refresh_failed"
|
|
401
|
+
: quotaResult.needsReauth
|
|
402
|
+
? "quota_unauthorized"
|
|
403
|
+
: health.status === "reauth_required" ? health.reason : undefined;
|
|
376
404
|
return {
|
|
377
405
|
id: account.id,
|
|
378
|
-
email:
|
|
406
|
+
email: projectEmail(account.email, maskEmails) ?? account.email,
|
|
379
407
|
...(account.alias !== undefined ? { alias: account.alias } : {}),
|
|
380
408
|
...(plan !== undefined ? { plan } : {}),
|
|
381
409
|
logLabel: codexAccountLogLabel(account),
|
|
@@ -383,7 +411,8 @@ function poolAccountDto(
|
|
|
383
411
|
paused,
|
|
384
412
|
priority,
|
|
385
413
|
quota: quota ? { ...quota } : null,
|
|
386
|
-
needsReauth,
|
|
414
|
+
needsReauth: needsReauth || health.status === "reauth_required",
|
|
415
|
+
...(reauthReason !== undefined ? { reauthReason } : {}),
|
|
387
416
|
hasCredential,
|
|
388
417
|
...(quotaResult.quotaProbeSkipped ? { quotaProbeSkipped: true as const } : {}),
|
|
389
418
|
...oauthAccountHealthFields("codex", account.id, health),
|
|
@@ -585,7 +614,11 @@ async function verifyCodexAccountWarmup(
|
|
|
585
614
|
return {
|
|
586
615
|
ok: false,
|
|
587
616
|
response: jsonResponse({
|
|
588
|
-
|
|
617
|
+
// Every fallback model was refused for a provisioning reason, so telling the operator to
|
|
618
|
+
// reauthenticate sends them back through a login that already succeeded.
|
|
619
|
+
error: isCodexWarmupProvisioningFailure(err)
|
|
620
|
+
? "Codex account warmup failed. Verify account model access or provisioning and try again."
|
|
621
|
+
: "Codex account warmup failed. Reauthenticate the account and try again.",
|
|
589
622
|
code: "codex_warmup_failed",
|
|
590
623
|
reason,
|
|
591
624
|
accountId,
|
|
@@ -638,7 +671,7 @@ function saveRuntimeConfig(sourceConfig: OcxConfig, nextConfig: OcxConfig): void
|
|
|
638
671
|
|
|
639
672
|
interface StagedNewCodexAccountState {
|
|
640
673
|
credential: CodexAccountCredentials;
|
|
641
|
-
validatedAt
|
|
674
|
+
validatedAt?: number;
|
|
642
675
|
}
|
|
643
676
|
|
|
644
677
|
type PersistNewCodexAccountOutcome =
|
|
@@ -691,8 +724,10 @@ function persistNewCodexAccount(
|
|
|
691
724
|
}
|
|
692
725
|
|
|
693
726
|
try {
|
|
694
|
-
saveCodexAccountCredential(addedAccount.id, staged.credential
|
|
695
|
-
|
|
727
|
+
const generation = saveCodexAccountCredential(addedAccount.id, staged.credential, {
|
|
728
|
+
validationPending: staged.validatedAt === undefined,
|
|
729
|
+
});
|
|
730
|
+
if (staged.validatedAt !== undefined) markCodexAccountValidated(addedAccount.id, staged.validatedAt, generation);
|
|
696
731
|
clearAccountNeedsReauth(addedAccount.id);
|
|
697
732
|
} catch {
|
|
698
733
|
// Config is already durable. Return the failure outcome through the coordinator so its
|
|
@@ -1101,6 +1136,7 @@ interface PoolQuotaRefreshFlight {
|
|
|
1101
1136
|
superseded?: boolean;
|
|
1102
1137
|
startCredentialGeneration?: number;
|
|
1103
1138
|
resolvedCredentialGeneration?: number;
|
|
1139
|
+
validatePending?: boolean;
|
|
1104
1140
|
};
|
|
1105
1141
|
promise: Promise<PoolQuotaResult>;
|
|
1106
1142
|
}
|
|
@@ -1151,6 +1187,11 @@ export interface CodexAuthAccountDto {
|
|
|
1151
1187
|
priority: number;
|
|
1152
1188
|
quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
|
|
1153
1189
|
needsReauth?: boolean;
|
|
1190
|
+
/**
|
|
1191
|
+
* Which of the independent causes behind `needsReauth` fired. Present only when the account
|
|
1192
|
+
* needs the operator; `/api/oauth/accounts` already carries the same field name.
|
|
1193
|
+
*/
|
|
1194
|
+
reauthReason?: CodexAccountReauthReason;
|
|
1154
1195
|
hasCredential: boolean;
|
|
1155
1196
|
health: OAuthAccountHealth;
|
|
1156
1197
|
healthLabel: OAuthHealthLabel;
|
|
@@ -1480,11 +1521,12 @@ async function fetchFreshPoolAccountQuota(
|
|
|
1480
1521
|
}
|
|
1481
1522
|
}
|
|
1482
1523
|
|
|
1483
|
-
async function fetchPoolAccountQuota(
|
|
1524
|
+
export async function fetchPoolAccountQuota(
|
|
1484
1525
|
accountId: string,
|
|
1485
1526
|
forceRefresh = false,
|
|
1486
1527
|
configuredPlan?: string,
|
|
1487
1528
|
getValidToken: typeof getValidCodexToken = getValidCodexToken,
|
|
1529
|
+
validatePending = false,
|
|
1488
1530
|
afterDispatchSequence?: number,
|
|
1489
1531
|
): Promise<PoolQuotaResult> {
|
|
1490
1532
|
const existing = getAccountQuota(accountId);
|
|
@@ -1507,7 +1549,11 @@ async function fetchPoolAccountQuota(
|
|
|
1507
1549
|
&& (afterDispatchSequence === undefined || (flight.state.dispatchSequence ?? 0) > afterDispatchSequence)
|
|
1508
1550
|
&& generation !== undefined && isCodexAccountGenerationLive(accountId, generation);
|
|
1509
1551
|
});
|
|
1510
|
-
if (current)
|
|
1552
|
+
if (current) {
|
|
1553
|
+
// A manual refresh joining a passive read must not lose its validation intent.
|
|
1554
|
+
current.state.validatePending ||= validatePending;
|
|
1555
|
+
return current.promise;
|
|
1556
|
+
}
|
|
1511
1557
|
if (poolQuotaFlightCount() >= MAX_POOL_QUOTA_FLIGHTS) throw new PoolQuotaProbeBusyError();
|
|
1512
1558
|
|
|
1513
1559
|
// A post-reset request must not let an older same-account response overwrite its evidence.
|
|
@@ -1517,6 +1563,7 @@ async function fetchPoolAccountQuota(
|
|
|
1517
1563
|
}
|
|
1518
1564
|
const state: PoolQuotaRefreshFlight["state"] = {
|
|
1519
1565
|
startCredentialGeneration: record?.generation,
|
|
1566
|
+
validatePending,
|
|
1520
1567
|
};
|
|
1521
1568
|
const refresh = fetchFreshPoolAccountQuota(
|
|
1522
1569
|
accountId,
|
|
@@ -1528,18 +1575,54 @@ async function fetchPoolAccountQuota(
|
|
|
1528
1575
|
onDispatch: sequence => { state.dispatchSequence = sequence; },
|
|
1529
1576
|
mayPublish: () => state.superseded !== true,
|
|
1530
1577
|
},
|
|
1531
|
-
)
|
|
1578
|
+
).then(async result => {
|
|
1579
|
+
// A passive flight has consumed its validation decision. Remove it before
|
|
1580
|
+
// promise settlement queues other continuations, so a late explicit caller
|
|
1581
|
+
// starts fresh work instead of setting an intent nobody will read again.
|
|
1582
|
+
if (!state.validatePending) {
|
|
1583
|
+
releaseFlight();
|
|
1584
|
+
return result;
|
|
1585
|
+
}
|
|
1586
|
+
// Only an explicit account-list refresh finishes deferred registration. Passive quota
|
|
1587
|
+
// polls and startup priming remain read-only with respect to inference spending.
|
|
1588
|
+
const generation = result.freshCredentialGeneration;
|
|
1589
|
+
const record = state.validatePending ? readCodexAccountRecord(accountId) : null;
|
|
1590
|
+
if (record?.codexValidationPending && record.credential && record.deletedAt == null
|
|
1591
|
+
&& generation !== undefined && record.generation === generation
|
|
1592
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? configuredPlan)) {
|
|
1593
|
+
try {
|
|
1594
|
+
await warmCodexAccount({
|
|
1595
|
+
accessToken: record.credential.accessToken,
|
|
1596
|
+
chatgptAccountId: record.credential.chatgptAccountId,
|
|
1597
|
+
});
|
|
1598
|
+
markCodexAccountValidated(accountId, Date.now(), generation);
|
|
1599
|
+
clearAccountNeedsReauth(accountId, generation);
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
// Keep the durable restriction on any failed/partial inference response, even
|
|
1602
|
+
// when WHAM just reported headroom. No raw upstream text enters diagnostics.
|
|
1603
|
+
const reason = codexWarmupFailureReason(error);
|
|
1604
|
+
if (reason === "http_status:401" || reason === "http_status:403") {
|
|
1605
|
+
markCodexAccountValidationFailed(accountId, reason, { expectedGeneration: generation });
|
|
1606
|
+
markAccountNeedsReauth(accountId, captureConfigGeneration(), generation);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return result;
|
|
1611
|
+
});
|
|
1532
1612
|
const flight: PoolQuotaRefreshFlight = { state, promise: refresh };
|
|
1533
1613
|
const activeFlights = flights ?? new Set<PoolQuotaRefreshFlight>();
|
|
1534
1614
|
activeFlights.add(flight);
|
|
1535
1615
|
if (!flights) poolQuotaRefreshInFlight.set(accountId, activeFlights);
|
|
1536
|
-
|
|
1537
|
-
return await refresh;
|
|
1538
|
-
} finally {
|
|
1616
|
+
const releaseFlight = () => {
|
|
1539
1617
|
activeFlights.delete(flight);
|
|
1540
1618
|
if (activeFlights.size === 0 && poolQuotaRefreshInFlight.get(accountId) === activeFlights) {
|
|
1541
1619
|
poolQuotaRefreshInFlight.delete(accountId);
|
|
1542
1620
|
}
|
|
1621
|
+
};
|
|
1622
|
+
try {
|
|
1623
|
+
return await refresh;
|
|
1624
|
+
} finally {
|
|
1625
|
+
releaseFlight();
|
|
1543
1626
|
}
|
|
1544
1627
|
}
|
|
1545
1628
|
|
|
@@ -1594,8 +1677,10 @@ async function refreshAfterManualReset(
|
|
|
1594
1677
|
}
|
|
1595
1678
|
return { accessToken: auth.accessToken, chatgptAccountId: auth.chatgptAccountId, generation: auth.poolGeneration };
|
|
1596
1679
|
};
|
|
1680
|
+
// `validatePending` is false here: a manual reset settles cooldown, and finishing deferred
|
|
1681
|
+
// registration stays reserved for an explicit dashboard account-list refresh.
|
|
1597
1682
|
const result = await fetchPoolAccountQuota(accountId, true, account.plan, didReset ? resetToken : getValidCodexToken,
|
|
1598
|
-
didReset ? afterDispatchSequence : undefined);
|
|
1683
|
+
false, didReset ? afterDispatchSequence : undefined);
|
|
1599
1684
|
const record = readCodexAccountRecord(accountId);
|
|
1600
1685
|
const recovered = didReset && record?.credential?.chatgptAccountId === auth.chatgptAccountId
|
|
1601
1686
|
&& (result.quotaProbeAttempted?.dispatchSequence ?? 0) > afterDispatchSequence
|
|
@@ -1876,9 +1961,12 @@ export interface CodexAuthAccountsSnapshot {
|
|
|
1876
1961
|
export async function listCodexAuthAccountsSnapshot(
|
|
1877
1962
|
config: OcxConfig,
|
|
1878
1963
|
forceRefresh = false,
|
|
1964
|
+
options: { validatePending?: boolean } = {},
|
|
1879
1965
|
): Promise<CodexAuthAccountsSnapshot> {
|
|
1880
1966
|
const runtimeConfig = getRuntimeConfig(config);
|
|
1881
1967
|
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
1968
|
+
// One redaction decision for the whole snapshot, read once from the operator's config (#3859).
|
|
1969
|
+
const maskEmails = emailMaskingEnabled(runtimeConfig);
|
|
1882
1970
|
const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1);
|
|
1883
1971
|
const refreshedPool = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
1884
1972
|
const cred = getCodexAccountCredential(account.id);
|
|
@@ -1887,7 +1975,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1887
1975
|
quotaResult = { quota: null, needsReauth: true };
|
|
1888
1976
|
} else {
|
|
1889
1977
|
try {
|
|
1890
|
-
quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh, account.plan);
|
|
1978
|
+
quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh, account.plan, getValidCodexToken, options.validatePending === true);
|
|
1891
1979
|
} catch (error) {
|
|
1892
1980
|
if (!(error instanceof PoolQuotaProbeBusyError)) throw error;
|
|
1893
1981
|
quotaResult = {
|
|
@@ -1923,6 +2011,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1923
2011
|
false,
|
|
1924
2012
|
isCodexAccountPaused(runtimeConfig, accountId),
|
|
1925
2013
|
getCodexAccountPriority(runtimeConfig, accountId),
|
|
2014
|
+
maskEmails,
|
|
1926
2015
|
)];
|
|
1927
2016
|
}
|
|
1928
2017
|
const resultGeneration = quotaResult.credentialGeneration ?? quotaResult.freshCredentialGeneration;
|
|
@@ -1942,6 +2031,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1942
2031
|
true,
|
|
1943
2032
|
isCodexAccountPaused(runtimeConfig, accountId),
|
|
1944
2033
|
getCodexAccountPriority(runtimeConfig, accountId),
|
|
2034
|
+
maskEmails,
|
|
1945
2035
|
)];
|
|
1946
2036
|
});
|
|
1947
2037
|
const fetchedMainGeneration = mainResult.identityGeneration ?? captureMainAccountIdentityGeneration();
|
|
@@ -1950,15 +2040,23 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1950
2040
|
const hasMainCredential = mainSnapshotLive && mainResult.credentialChecked
|
|
1951
2041
|
? mainResult.hasCredential
|
|
1952
2042
|
: getMainAccountCredentialPresence() ?? false;
|
|
1953
|
-
const
|
|
1954
|
-
|
|
2043
|
+
const mainMissingCredential = mainSnapshotLive && mainResult.credentialChecked && !hasMainCredential;
|
|
2044
|
+
const mainNeedsReauth = mainMissingCredential || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
1955
2045
|
const mainHealth = projectCodexAccountHealth({
|
|
1956
2046
|
accountId: MAIN_CODEX_ACCOUNT_ID,
|
|
1957
2047
|
needsReauth: mainNeedsReauth,
|
|
1958
2048
|
});
|
|
2049
|
+
// The main row carries the same attribution as a pool row. Reaching this point without
|
|
2050
|
+
// `mainMissingCredential` means the runtime reauth flag is what set `mainNeedsReauth`, so the
|
|
2051
|
+
// cause is a refresh that did not complete.
|
|
2052
|
+
const mainReauthReason: CodexAccountReauthReason | undefined = mainMissingCredential
|
|
2053
|
+
? "missing_credential"
|
|
2054
|
+
: mainNeedsReauth
|
|
2055
|
+
? "refresh_failed"
|
|
2056
|
+
: mainHealth.status === "reauth_required" ? mainHealth.reason : undefined;
|
|
1959
2057
|
const main: CodexAuthAccountDto = {
|
|
1960
2058
|
id: MAIN_CODEX_ACCOUNT_ID,
|
|
1961
|
-
email:
|
|
2059
|
+
email: projectEmail(mainInfo.email, maskEmails) ?? "Codex App login",
|
|
1962
2060
|
plan: mainInfo.plan,
|
|
1963
2061
|
...(mainSnapshotLive && mainResult.quotaRefresh && mainResult.quotaRefreshGeneration !== undefined
|
|
1964
2062
|
&& isMainAccountIdentityGenerationLive(mainResult.quotaRefreshGeneration)
|
|
@@ -1970,6 +2068,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1970
2068
|
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
1971
2069
|
hasCredential: hasMainCredential,
|
|
1972
2070
|
needsReauth: mainNeedsReauth,
|
|
2071
|
+
...(mainReauthReason !== undefined ? { reauthReason: mainReauthReason } : {}),
|
|
1973
2072
|
quota: mainInfo.quota ? {
|
|
1974
2073
|
...quotaForPlan(mainQuotaWithCarriedResetCredits(mainInfo.quota), mainInfo.plan),
|
|
1975
2074
|
} : null,
|
|
@@ -2019,8 +2118,12 @@ export async function refreshCodexQuotaForActivation(config: OcxConfig, accountI
|
|
|
2019
2118
|
}
|
|
2020
2119
|
}
|
|
2021
2120
|
|
|
2022
|
-
export async function listCodexAuthAccounts(
|
|
2023
|
-
|
|
2121
|
+
export async function listCodexAuthAccounts(
|
|
2122
|
+
config: OcxConfig,
|
|
2123
|
+
forceRefresh = false,
|
|
2124
|
+
options: { validatePending?: boolean } = {},
|
|
2125
|
+
): Promise<CodexAuthAccountDto[]> {
|
|
2126
|
+
return (await listCodexAuthAccountsSnapshot(config, forceRefresh, options)).accounts;
|
|
2024
2127
|
}
|
|
2025
2128
|
|
|
2026
2129
|
interface PauseExhaustedResult {
|
|
@@ -2129,6 +2232,7 @@ export async function handleCodexAuthAPI(
|
|
|
2129
2232
|
url: URL,
|
|
2130
2233
|
config: OcxConfig,
|
|
2131
2234
|
convergeCodexCatalog?: CodexAuthCatalogConvergence,
|
|
2235
|
+
principal?: import("../server/management-auth").ManagementPrincipal,
|
|
2132
2236
|
): Promise<Response | null> {
|
|
2133
2237
|
|
|
2134
2238
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "GET") {
|
|
@@ -2136,6 +2240,14 @@ export async function handleCodexAuthAPI(
|
|
|
2136
2240
|
return jsonResponse({ accounts: await listCodexAuthAccounts(config, forceRefresh) });
|
|
2137
2241
|
}
|
|
2138
2242
|
|
|
2243
|
+
if (url.pathname === "/api/codex-auth/accounts/refresh" && req.method === "POST") {
|
|
2244
|
+
// Inference spends quota: only a dashboard session carries the consent
|
|
2245
|
+
// required by AGENTS_INSTALL.md. Raw-admin/CLI refreshes remain observational.
|
|
2246
|
+
return jsonResponse({ accounts: await listCodexAuthAccounts(config, true, {
|
|
2247
|
+
validatePending: principal === "gui-session",
|
|
2248
|
+
}) });
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2139
2251
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "POST") {
|
|
2140
2252
|
return manualImportDisabledResponse();
|
|
2141
2253
|
}
|
|
@@ -2313,6 +2425,9 @@ export async function handleCodexAuthAPI(
|
|
|
2313
2425
|
const exists = (runtimeConfig.codexAccounts ?? [])
|
|
2314
2426
|
.some(account => isSelectableCodexPoolAccount(account) && account.id === body.accountId);
|
|
2315
2427
|
if (!exists) return jsonResponse({ error: "Account not found" }, 400);
|
|
2428
|
+
if (readCodexAccountRecord(body.accountId)?.codexValidationPending) {
|
|
2429
|
+
return jsonResponse({ error: "Account validation is pending. Refresh quota after recovery to validate it." }, 409);
|
|
2430
|
+
}
|
|
2316
2431
|
}
|
|
2317
2432
|
runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
|
|
2318
2433
|
// "Use this account now" outranks selection order until the account is spent:
|
|
@@ -2757,7 +2872,12 @@ export async function handleCodexAuthAPI(
|
|
|
2757
2872
|
break;
|
|
2758
2873
|
}
|
|
2759
2874
|
|
|
2760
|
-
|
|
2875
|
+
// A successful authenticated WHAM read can prove quota is exhausted without
|
|
2876
|
+
// spending an inference request. Store the account, but defer inference validation
|
|
2877
|
+
// and keep it unavailable to routing. Unknown/failed usage reads retain the gate.
|
|
2878
|
+
const warmup = isCodexQuotaExhausted(quota, plan)
|
|
2879
|
+
? { ok: true as const, validatedAt: undefined }
|
|
2880
|
+
: await verifyCodexAccountWarmup(accountId, cred.access, oauthAccountId);
|
|
2761
2881
|
if (!warmup.ok) {
|
|
2762
2882
|
const body = await warmup.response.json().catch(() => ({})) as { error?: string; reason?: string };
|
|
2763
2883
|
setCodexLoginState(flowId, {
|
|
@@ -2797,11 +2917,13 @@ export async function handleCodexAuthAPI(
|
|
|
2797
2917
|
};
|
|
2798
2918
|
|
|
2799
2919
|
if (existingIdx >= 0) {
|
|
2800
|
-
saveCodexAccountCredential(accountId, credential
|
|
2920
|
+
const generation = saveCodexAccountCredential(accountId, credential, {
|
|
2921
|
+
validationPending: warmup.validatedAt === undefined,
|
|
2922
|
+
});
|
|
2801
2923
|
// A successful reauthentication replaces the credential generation. Do not let a
|
|
2802
2924
|
// failed optional WHAM probe make the replacement inherit quota from the old record.
|
|
2803
2925
|
if (reauth) clearAccountQuota(accountId);
|
|
2804
|
-
markCodexAccountValidated(accountId, warmup.validatedAt);
|
|
2926
|
+
if (warmup.validatedAt !== undefined) markCodexAccountValidated(accountId, warmup.validatedAt, generation);
|
|
2805
2927
|
clearAccountNeedsReauth(accountId);
|
|
2806
2928
|
if (quota) setAccountQuotaFromParsed(accountId, quota);
|
|
2807
2929
|
// Keep the pool id stable; refresh display metadata after a successful login/reauth.
|
|
@@ -2852,6 +2974,7 @@ export async function handleCodexAuthAPI(
|
|
|
2852
2974
|
status: "done",
|
|
2853
2975
|
accountId,
|
|
2854
2976
|
email,
|
|
2977
|
+
...(warmup.validatedAt === undefined ? { validationPending: true } : {}),
|
|
2855
2978
|
...(catalogRefreshPending ? { catalogRefreshPending: true } : {}),
|
|
2856
2979
|
doneAt: Date.now(),
|
|
2857
2980
|
});
|
|
@@ -2952,6 +3075,9 @@ export async function handleCodexAuthAPI(
|
|
|
2952
3075
|
if (url.pathname === "/api/codex-auth/login-status" && req.method === "GET") {
|
|
2953
3076
|
const flowId = url.searchParams.get("flowId");
|
|
2954
3077
|
const accountId = url.searchParams.get("accountId")?.trim();
|
|
3078
|
+
// Transient flow state carries the address of the account being added, so it follows the
|
|
3079
|
+
// same operator policy as the stored accounts it is about to become.
|
|
3080
|
+
const maskFlowEmails = emailMaskingEnabled(config);
|
|
2955
3081
|
// Reauth always has a pre-existing credential; never treat "credential exists" as success
|
|
2956
3082
|
// when the flow map entry is gone (would false-complete on lost/expired flow state).
|
|
2957
3083
|
const reauthStatus = url.searchParams.get("reauth") === "1";
|
|
@@ -2964,13 +3090,15 @@ export async function handleCodexAuthAPI(
|
|
|
2964
3090
|
&& !isAccountNeedsReauth(accountId)
|
|
2965
3091
|
&& getCodexAccountCredential(accountId)
|
|
2966
3092
|
) {
|
|
2967
|
-
return jsonResponse({ status: "done", accountId
|
|
3093
|
+
return jsonResponse({ status: "done", accountId,
|
|
3094
|
+
...(readCodexAccountRecord(accountId)?.codexValidationPending ? { validationPending: true } : {}),
|
|
3095
|
+
});
|
|
2968
3096
|
}
|
|
2969
|
-
return jsonResponse(st ? { ...st, email:
|
|
3097
|
+
return jsonResponse(st ? { ...st, email: projectEmail(st.email, maskFlowEmails) ?? undefined } : { status: "expired" });
|
|
2970
3098
|
}
|
|
2971
3099
|
// Legacy fallback: return latest pending flow
|
|
2972
3100
|
for (const [, st] of codexAuthLoginState) {
|
|
2973
|
-
if (st.status === "pending") return jsonResponse({ ...st, email:
|
|
3101
|
+
if (st.status === "pending") return jsonResponse({ ...st, email: projectEmail(st.email, maskFlowEmails) ?? undefined });
|
|
2974
3102
|
}
|
|
2975
3103
|
return jsonResponse({ status: "idle" });
|
|
2976
3104
|
}
|