@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906
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 +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-VGeQEZ_v.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +54 -24
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/identity.ts +8 -2
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +259 -29
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +47 -13
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +49 -9
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +15 -4
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/observe.ts +3 -1
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +272 -32
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +44 -5
- package/src/codex/catalog/metadata.ts +149 -14
- package/src/codex/catalog/native-models.ts +116 -4
- package/src/codex/catalog/parsing.ts +122 -8
- package/src/codex/catalog/provider-fetch.ts +154 -23
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +99 -34
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +474 -178
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/account-quota-rank.ts +40 -1
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +140 -29
- package/src/oauth/log.ts +3 -0
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/muse-subscription-usage.ts +95 -0
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +625 -98
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +122 -28
- package/src/server/live.ts +18 -4
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +45 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +510 -57
- package/src/server/responses/empty-completion-guard.ts +4 -0
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/request.ts +8 -0
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-B2YjLA-i.css +0 -1
- package/gui/dist/assets/index-aPup8CKb.js +0 -112
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import {
|
|
3
3
|
atomicWriteFile,
|
|
4
|
+
deleteConfigTopLevelKey,
|
|
4
5
|
getConfigPath,
|
|
5
6
|
saveConfigPreservingClaudeCode,
|
|
6
7
|
withConfigMutationLockSync,
|
|
@@ -12,9 +13,10 @@ import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
|
12
13
|
import { clearAccountQuota } from "./quota";
|
|
13
14
|
import { clearCodexUpstreamHealthForAccount, clearThreadAccountMapForAccount } from "./routing";
|
|
14
15
|
import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
|
|
15
|
-
import { clearMainAccountCredentialPresence, clearMainAccountInfoCache } from "./main-account-cache";
|
|
16
|
+
import { clearMainAccountCredentialPresence, clearMainAccountInfoCache, observeMainQuotaIdentity } from "./main-account-cache";
|
|
16
17
|
import { forgetCodexAccountPause } from "./account-pause";
|
|
17
18
|
import { clearCodexAccountPin, forgetCodexAccountPriority } from "./account-priority";
|
|
19
|
+
import { forgetCodexQuotaAutoRefreshAccount } from "./quota-auto-refresh-state";
|
|
18
20
|
import { codexAccountNamespaceEntries, codexAccountPickerEnabled } from "./account-namespaces";
|
|
19
21
|
import type { OcxConfig } from "../types";
|
|
20
22
|
|
|
@@ -64,9 +66,13 @@ export function reconcileMainCodexAccountRuntimeState(): boolean {
|
|
|
64
66
|
if (currentAccountId === null) return false;
|
|
65
67
|
const previousAccountId = observedMainChatgptAccountId;
|
|
66
68
|
observedMainChatgptAccountId = currentAccountId;
|
|
67
|
-
if (previousAccountId === undefined || previousAccountId === currentAccountId)
|
|
69
|
+
if (previousAccountId === undefined || previousAccountId === currentAccountId) {
|
|
70
|
+
observeMainQuotaIdentity(currentAccountId);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
68
73
|
|
|
69
74
|
purgeMainCodexAccountRuntimeState();
|
|
75
|
+
observeMainQuotaIdentity(currentAccountId);
|
|
70
76
|
return true;
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -79,11 +85,15 @@ export function applyConfirmedMainCodexAccountTransition(
|
|
|
79
85
|
toAccountId: string,
|
|
80
86
|
): boolean {
|
|
81
87
|
if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) {
|
|
82
|
-
if (toAccountId)
|
|
88
|
+
if (toAccountId) {
|
|
89
|
+
observedMainChatgptAccountId = toAccountId;
|
|
90
|
+
observeMainQuotaIdentity(toAccountId);
|
|
91
|
+
}
|
|
83
92
|
return false;
|
|
84
93
|
}
|
|
85
94
|
observedMainChatgptAccountId = toAccountId;
|
|
86
95
|
purgeMainCodexAccountRuntimeState();
|
|
96
|
+
observeMainQuotaIdentity(toAccountId);
|
|
87
97
|
return true;
|
|
88
98
|
}
|
|
89
99
|
|
|
@@ -135,6 +145,12 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
|
|
|
135
145
|
.filter(account => account.isMain || account.id !== accountId);
|
|
136
146
|
forgetCodexAccountPause(runtimeConfig, accountId);
|
|
137
147
|
forgetCodexAccountPriority(runtimeConfig, accountId);
|
|
148
|
+
if (runtimeConfig.codexQuotaAutoRefresh?.[accountId]) {
|
|
149
|
+
const retained = { ...runtimeConfig.codexQuotaAutoRefresh };
|
|
150
|
+
delete retained[accountId];
|
|
151
|
+
if (Object.keys(retained).length > 0) runtimeConfig.codexQuotaAutoRefresh = retained;
|
|
152
|
+
else deleteConfigTopLevelKey(runtimeConfig, "codexQuotaAutoRefresh");
|
|
153
|
+
}
|
|
138
154
|
clearCodexAccountPin(runtimeConfig, accountId);
|
|
139
155
|
if (runtimeConfig.activeCodexAccountId === accountId) runtimeConfig.activeCodexAccountId = undefined;
|
|
140
156
|
|
|
@@ -167,6 +183,7 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
|
|
|
167
183
|
return hadVisiblePickerBinding;
|
|
168
184
|
});
|
|
169
185
|
|
|
186
|
+
forgetCodexQuotaAutoRefreshAccount(accountId);
|
|
170
187
|
if (cleanupFailed) throw new CodexAccountDeleteCleanupError();
|
|
171
188
|
return pickerVisibilityChanged;
|
|
172
189
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount } from "./account-id";
|
|
10
10
|
import type { OcxConfig } from "../types";
|
|
11
11
|
import { isNativeMainTrafficBlocked } from "./native-profile-startup";
|
|
12
|
+
import { isMainAccountHardLocked } from "./main-account-hard-lock";
|
|
12
13
|
|
|
13
14
|
export interface CodexAccountUsabilityOptions {
|
|
14
15
|
/** Route using cached runtime state only; the caller must reject selected main before auth. */
|
|
@@ -26,6 +27,7 @@ export function isCodexAccountUsable(
|
|
|
26
27
|
): boolean {
|
|
27
28
|
if (options.modelEligibleAccountIds && !options.modelEligibleAccountIds.has(accountId)) return false;
|
|
28
29
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
30
|
+
if (isMainAccountHardLocked(config)) return false;
|
|
29
31
|
// Startup recovery owns the physical auth/vault boundary. Never parse or select
|
|
30
32
|
// native __main__ while an encrypted switch journal is pending or inconclusive.
|
|
31
33
|
if (!options.nativeMainSelectionOnly && isNativeMainTrafficBlocked()) return false;
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
isCompleteCodexQuotaRecoverySnapshot,
|
|
70
70
|
isCodexQuotaExhausted,
|
|
71
71
|
listAccountQuotas,
|
|
72
|
+
parseMainPolicyUsageQuota,
|
|
72
73
|
parseUsageQuota,
|
|
73
74
|
setAccountQuotaFromParsed,
|
|
74
75
|
updateAccountQuota,
|
|
@@ -84,7 +85,10 @@ export {
|
|
|
84
85
|
updateAccountQuota,
|
|
85
86
|
} from "./quota";
|
|
86
87
|
import { extractAccountId } from "../oauth/chatgpt";
|
|
87
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
getMainAccountPlan, getValidMainAccountToken, isMainAccountTokenVerifiablyLive,
|
|
90
|
+
MainAccountTokenRefreshError, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan,
|
|
91
|
+
} from "./main-account";
|
|
88
92
|
import { captureConfigGeneration, registerStateSweepAfterTick } from "../lib/state-store-sweeper";
|
|
89
93
|
import { reconcileLiveStateStores } from "../lib/state-store-registrations";
|
|
90
94
|
import {
|
|
@@ -92,12 +96,17 @@ import {
|
|
|
92
96
|
clearMainAccountInfoCache,
|
|
93
97
|
getMainAccountCredentialPresence,
|
|
94
98
|
getMainAccountInfoCache,
|
|
99
|
+
getMainQuotaCredentialGeneration,
|
|
95
100
|
isMainAccountIdentityGenerationLive,
|
|
101
|
+
matchesMainQuotaCredential,
|
|
102
|
+
observeMainQuotaCredential,
|
|
96
103
|
setMainAccountCredentialPresence,
|
|
97
104
|
setMainAccountInfoCache,
|
|
98
105
|
type MainAccountInfo,
|
|
99
106
|
} from "./main-account-cache";
|
|
100
107
|
export { clearMainAccountInfoCache } from "./main-account-cache";
|
|
108
|
+
import { getMainAccountHardLockStatus, type MainAccountHardLockStatus } from "./main-account-hard-lock";
|
|
109
|
+
import { observeMainReserveRevocation } from "./reserve-availability";
|
|
101
110
|
import { maskEmail } from "../lib/privacy";
|
|
102
111
|
import { codexWarmupFailureReason, warmCodexAccount } from "./warmup";
|
|
103
112
|
export { maskEmail } from "../lib/privacy";
|
|
@@ -120,6 +129,12 @@ import {
|
|
|
120
129
|
isValidCodexAccountId,
|
|
121
130
|
} from "./account-id";
|
|
122
131
|
import { codexAccountIdNamespaceCollisionError } from "./account-namespace-match";
|
|
132
|
+
import {
|
|
133
|
+
markManualResetCreditOperationAmbiguous,
|
|
134
|
+
openManualResetCreditOperation,
|
|
135
|
+
settleManualResetCreditOperation,
|
|
136
|
+
} from "./reset-credit-operation-ledger";
|
|
137
|
+
import { isCodexResetCreditOperationId } from "./reset-credit-recovery";
|
|
123
138
|
import { ResourceAdmissionError, type AdmissionLease } from "../lib/admission";
|
|
124
139
|
import { tryAcquireNativeMainProfileClaim } from "./native-main-admission";
|
|
125
140
|
import { withNativeMainSharedClaim } from "./native-main-claim";
|
|
@@ -274,6 +289,69 @@ function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAc
|
|
|
274
289
|
} as T;
|
|
275
290
|
}
|
|
276
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Last reset-credit count this process parsed for the main account, tagged with the
|
|
294
|
+
* physical ChatGPT account it was read from.
|
|
295
|
+
*
|
|
296
|
+
* It is deliberately memory-only. The quota store is keyed by the stable `__main__`
|
|
297
|
+
* ALIAS, and `~/.codex/auth.json` can be swapped for another account while the proxy is
|
|
298
|
+
* not running — `reconcileMainCodexAccountRuntimeState` only purges alias-keyed state
|
|
299
|
+
* when it observes the id CHANGE, and its first observation after a restart has nothing
|
|
300
|
+
* to compare against. A disk-hydrated `__main__` entry can therefore belong to the
|
|
301
|
+
* previous login, so filling the DTO from it would show one account's tickets on
|
|
302
|
+
* another's card. Pool accounts have no such hole because their store key IS the account
|
|
303
|
+
* id. Binding the value to `requestAccountId` keeps the fill honest: after a restart the
|
|
304
|
+
* badge simply waits for the first usage response that carries the summary.
|
|
305
|
+
*/
|
|
306
|
+
let mainResetCreditsProvenance: { accountId: string; credits: number } | null = null;
|
|
307
|
+
|
|
308
|
+
function rememberMainResetCredits(accountId: string | null, credits: number | undefined): void {
|
|
309
|
+
if (accountId === null || credits === undefined) return;
|
|
310
|
+
mainResetCreditsProvenance = { accountId, credits };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Forget the remembered count when the physical main identity is no longer the same. */
|
|
314
|
+
function mainResetCreditsForCurrentIdentity(): number | undefined {
|
|
315
|
+
if (!mainResetCreditsProvenance) return undefined;
|
|
316
|
+
const currentAccountId = getMainChatgptAccountId();
|
|
317
|
+
if (currentAccountId === null) return undefined;
|
|
318
|
+
if (currentAccountId !== mainResetCreditsProvenance.accountId) {
|
|
319
|
+
mainResetCreditsProvenance = null;
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
return mainResetCreditsProvenance.credits;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The main account is the only account whose DTO quota comes from the raw WHAM parse
|
|
327
|
+
* result instead of the merged store: `poolAccountDto` serializes what
|
|
328
|
+
* `commitPoolQuotaResponse` read back out of `getAccountQuota()`, while the main DTO
|
|
329
|
+
* spreads `mainInfo.quota` directly. `/wham/usage` carries `rate_limit_reset_credits`
|
|
330
|
+
* only intermittently, and the store exists to bridge that gap
|
|
331
|
+
* (`setAccountQuotaFromParsed` carries an existing `resetCredits` forward when the new
|
|
332
|
+
* snapshot omits it), so the main card lost its ticket badge on every response that
|
|
333
|
+
* happened to omit the summary while pool cards kept theirs.
|
|
334
|
+
*
|
|
335
|
+
* Only `resetCredits` is carried, deliberately, and only from an identity-tagged
|
|
336
|
+
* in-process observation rather than the alias-keyed store. The window fields have
|
|
337
|
+
* *clearing* semantics — a monthly-only snapshot must drop a stale weekly value (#382) —
|
|
338
|
+
* so reinstating the whole stored object would resurrect a window the parse meant to
|
|
339
|
+
* clear whenever the store write was refused by generation gating. A freshly parsed value
|
|
340
|
+
* always wins, including `0`: zero is defined, so it never takes the fill branch.
|
|
341
|
+
*/
|
|
342
|
+
function mainQuotaWithCarriedResetCredits(
|
|
343
|
+
parsed: Omit<StoredAccountQuota, "updatedAt">,
|
|
344
|
+
): StoredAccountQuota {
|
|
345
|
+
const carried = parsed.resetCredits === undefined
|
|
346
|
+
? mainResetCreditsForCurrentIdentity()
|
|
347
|
+
: undefined;
|
|
348
|
+
return {
|
|
349
|
+
...parsed,
|
|
350
|
+
...(carried !== undefined ? { resetCredits: carried } : {}),
|
|
351
|
+
updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
277
355
|
function poolAccountDto(
|
|
278
356
|
account: CodexAccount,
|
|
279
357
|
quotaResult: PoolQuotaResult,
|
|
@@ -749,6 +827,7 @@ async function fetchMainAccountInfoAttempt(
|
|
|
749
827
|
retriesRemaining: number,
|
|
750
828
|
existingNativeMainLease?: AdmissionLease,
|
|
751
829
|
nativeMainSharedClaimHeld = false,
|
|
830
|
+
explicitRefresh: boolean = forceRefresh,
|
|
752
831
|
): Promise<MainAccountInfoFetchResult> {
|
|
753
832
|
const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
|
|
754
833
|
if (!nativeMainLease) {
|
|
@@ -761,7 +840,7 @@ async function fetchMainAccountInfoAttempt(
|
|
|
761
840
|
}
|
|
762
841
|
try {
|
|
763
842
|
const operation = async () => ({
|
|
764
|
-
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease),
|
|
843
|
+
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh),
|
|
765
844
|
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
766
845
|
});
|
|
767
846
|
if (nativeMainSharedClaimHeld) return await operation();
|
|
@@ -815,6 +894,12 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
815
894
|
if (!forceRefresh && cached && Date.now() - cached.ts < MAIN_CACHE_TTL) {
|
|
816
895
|
return { info: cached, credentialChecked: true, hasCredential: true };
|
|
817
896
|
}
|
|
897
|
+
// Bind quota to the owned credential and the account actually selected by WHAM's header.
|
|
898
|
+
// A conflicting legacy token/account tuple is not evidence for the new policy.
|
|
899
|
+
const mainQuotaWriter = requestAccountId === tokens.account_id
|
|
900
|
+
? observeMainQuotaCredential(tokens.access_token, tokens.account_id)
|
|
901
|
+
: undefined;
|
|
902
|
+
const mainQuotaCredentialGeneration = getMainQuotaCredentialGeneration();
|
|
818
903
|
try {
|
|
819
904
|
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
820
905
|
headers: { Authorization: `Bearer ${tokens.access_token}`, "ChatGPT-Account-Id": tokens.account_id },
|
|
@@ -833,9 +918,20 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
833
918
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
834
919
|
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
835
920
|
if (retried) return retried;
|
|
921
|
+
// A delayed response from a replaced bearer cannot revoke a newer Reserve grant,
|
|
922
|
+
// even in the same workspace or after an A→B→A credential transition.
|
|
923
|
+
if (mainQuotaCredentialGeneration === getMainQuotaCredentialGeneration()
|
|
924
|
+
&& matchesMainQuotaCredential(tokens.access_token, tokens.account_id)) {
|
|
925
|
+
observeMainReserveRevocation(data, mainQuotaWriter);
|
|
926
|
+
}
|
|
836
927
|
const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
|
|
837
|
-
const
|
|
928
|
+
const usage = { ...data, ...(plan ? { plan_type: plan } : {}) };
|
|
929
|
+
const quota = parseUsageQuota(usage);
|
|
930
|
+
const policyQuota = parseMainPolicyUsageQuota(usage);
|
|
838
931
|
const freshResetCredits = quota?.resetCredits;
|
|
932
|
+
// Tag the count with the identity it was read from, so a later response that omits the
|
|
933
|
+
// summary can restore the badge without ever crossing an account boundary.
|
|
934
|
+
rememberMainResetCredits(requestAccountId, freshResetCredits);
|
|
839
935
|
const result = {
|
|
840
936
|
email: data.email ?? null,
|
|
841
937
|
plan,
|
|
@@ -857,7 +953,7 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
857
953
|
// score and auto-switch the main account exactly like a pool account (Option A).
|
|
858
954
|
setMainAccountPlan(result.plan);
|
|
859
955
|
if (result.quota) {
|
|
860
|
-
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration);
|
|
956
|
+
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration, mainQuotaWriter, policyQuota);
|
|
861
957
|
}
|
|
862
958
|
return {
|
|
863
959
|
info: result,
|
|
@@ -965,6 +1061,7 @@ export interface CodexAuthAccountDto {
|
|
|
965
1061
|
healthSummary: string;
|
|
966
1062
|
healthAction?: string;
|
|
967
1063
|
quotaProbeSkipped?: true;
|
|
1064
|
+
mainAccountHardLock?: MainAccountHardLockStatus;
|
|
968
1065
|
}
|
|
969
1066
|
|
|
970
1067
|
interface FreshPoolPlanUpdate {
|
|
@@ -1353,10 +1450,9 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
|
|
|
1353
1450
|
}
|
|
1354
1451
|
try {
|
|
1355
1452
|
const result = await fetchPoolAccountQuota(claim.accountId, true, account.plan);
|
|
1356
|
-
// Defence in depth:
|
|
1357
|
-
//
|
|
1358
|
-
|
|
1359
|
-
const recovered = claim.scope !== "spark"
|
|
1453
|
+
// Defence in depth: independent scopes are already excluded at the claim site.
|
|
1454
|
+
// Generic WHAM must never clear Spark or Reserve even if claim selection changes.
|
|
1455
|
+
const recovered = (claim.scope === undefined || claim.scope === "shared")
|
|
1360
1456
|
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
1361
1457
|
settleCodexQuotaRecoveryProbe(claim, recovered, {
|
|
1362
1458
|
credentialGeneration: result.freshCredentialGeneration,
|
|
@@ -1371,10 +1467,50 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
|
|
|
1371
1467
|
return cooldownRecoveryInFlight;
|
|
1372
1468
|
}
|
|
1373
1469
|
|
|
1470
|
+
let mainHardLockRecoveryInFlight: Promise<void> | null = null;
|
|
1471
|
+
|
|
1472
|
+
/** Metadata-only recovery on the existing sweep; failures retain the observed policy block. */
|
|
1473
|
+
export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise<void> {
|
|
1474
|
+
if (mainHardLockRecoveryInFlight) return mainHardLockRecoveryInFlight;
|
|
1475
|
+
if (getMainAccountHardLockStatus(config).state !== "blocked"
|
|
1476
|
+
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
1477
|
+
const lease = tryAcquireNativeMainProfileClaim();
|
|
1478
|
+
if (!lease) return;
|
|
1479
|
+
mainHardLockRecoveryInFlight = (async () => {
|
|
1480
|
+
reconcileMainCodexAccountRuntimeState();
|
|
1481
|
+
if (getMainAccountHardLockStatus(config).state !== "blocked"
|
|
1482
|
+
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
1483
|
+
const identityGeneration = captureMainAccountIdentityGeneration();
|
|
1484
|
+
const writerGeneration = captureConfigGeneration();
|
|
1485
|
+
try {
|
|
1486
|
+
// Refresh can require an exclusive credential claim: never hold WHAM's shared
|
|
1487
|
+
// claim while obtaining a valid token. The runtime lease spans both operations.
|
|
1488
|
+
if (!await getValidMainAccountToken({ preserveReauth: true })) return;
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth"
|
|
1491
|
+
&& isMainAccountIdentityGenerationLive(identityGeneration)) {
|
|
1492
|
+
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
1493
|
+
}
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
1497
|
+
await fetchMainAccountInfoAttempt(true, 1, lease, false, false);
|
|
1498
|
+
})().catch(() => {
|
|
1499
|
+
// Best-effort background metadata read; no cooldown/pause or policy clearing on failure.
|
|
1500
|
+
}).finally(() => {
|
|
1501
|
+
lease.release();
|
|
1502
|
+
mainHardLockRecoveryInFlight = null;
|
|
1503
|
+
});
|
|
1504
|
+
return mainHardLockRecoveryInFlight;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1374
1507
|
export function registerCodexCooldownRecoveryProbeWorker(config: OcxConfig): void {
|
|
1375
1508
|
registerStateSweepAfterTick({
|
|
1376
1509
|
name: "codex-cooldown-recovery",
|
|
1377
|
-
afterTick: () => {
|
|
1510
|
+
afterTick: () => {
|
|
1511
|
+
void runCodexCooldownRecoveryProbes(config);
|
|
1512
|
+
void runMainAccountHardLockRecovery(config);
|
|
1513
|
+
},
|
|
1378
1514
|
});
|
|
1379
1515
|
}
|
|
1380
1516
|
|
|
@@ -1636,14 +1772,12 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1636
1772
|
logLabel: "main",
|
|
1637
1773
|
isMain: true,
|
|
1638
1774
|
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
1775
|
+
mainAccountHardLock: getMainAccountHardLockStatus(runtimeConfig),
|
|
1639
1776
|
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
1640
1777
|
hasCredential: hasMainCredential,
|
|
1641
1778
|
needsReauth: mainNeedsReauth,
|
|
1642
1779
|
quota: mainInfo.quota ? {
|
|
1643
|
-
...quotaForPlan(
|
|
1644
|
-
...mainInfo.quota,
|
|
1645
|
-
updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
|
|
1646
|
-
}, mainInfo.plan),
|
|
1780
|
+
...quotaForPlan(mainQuotaWithCarriedResetCredits(mainInfo.quota), mainInfo.plan),
|
|
1647
1781
|
} : null,
|
|
1648
1782
|
...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
|
|
1649
1783
|
};
|
|
@@ -2105,31 +2239,107 @@ export async function handleCodexAuthAPI(
|
|
|
2105
2239
|
}
|
|
2106
2240
|
|
|
2107
2241
|
if (url.pathname === "/api/codex-auth/reset-credits/consume" && req.method === "POST") {
|
|
2108
|
-
const body = (await req.json().catch(() => ({}))) as {
|
|
2242
|
+
const body = (await req.json().catch(() => ({}))) as {
|
|
2243
|
+
accountId?: string;
|
|
2244
|
+
operationId?: unknown;
|
|
2245
|
+
};
|
|
2109
2246
|
if (!body.accountId) return jsonResponse({ error: "accountId required" }, 400);
|
|
2110
2247
|
const accountId = body.accountId;
|
|
2248
|
+
// Optional caller-owned idempotency identity (#3375 axis D). Absent => legacy
|
|
2249
|
+
// behavior: a fresh random redeem_request_id and no durable ledger row.
|
|
2250
|
+
// The ledger throws TypeError on a malformed id, so the format check has to
|
|
2251
|
+
// happen here rather than at the call site, or it surfaces as a 500.
|
|
2252
|
+
const hasOperationId = body.operationId !== undefined;
|
|
2253
|
+
if (hasOperationId && !isCodexResetCreditOperationId(body.operationId)) {
|
|
2254
|
+
return jsonResponse({ error: "Invalid operationId format" }, 400);
|
|
2255
|
+
}
|
|
2256
|
+
const requestedOperationId = hasOperationId ? body.operationId as string : undefined;
|
|
2111
2257
|
|
|
2112
2258
|
try {
|
|
2113
2259
|
const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2260
|
+
// The ledger keys manual operations by the *physical* ChatGPT account, which is
|
|
2261
|
+
// only known after the auth wrapper resolves credentials. Open here, not earlier.
|
|
2262
|
+
const identity = requestedOperationId === undefined
|
|
2263
|
+
? undefined
|
|
2264
|
+
: {
|
|
2265
|
+
accountId,
|
|
2266
|
+
chatgptAccountId: auth.chatgptAccountId,
|
|
2267
|
+
operationId: requestedOperationId,
|
|
2268
|
+
} as const;
|
|
2269
|
+
let idempotencyKey: string;
|
|
2270
|
+
if (identity) {
|
|
2271
|
+
const opened = openManualResetCreditOperation(identity);
|
|
2272
|
+
if (opened.kind === "terminal") {
|
|
2273
|
+
// Durably settled already: replay the recorded outcome instead of
|
|
2274
|
+
// trusting upstream idempotency for an irreversible spend. No
|
|
2275
|
+
// `remaining` — that field is only reported from a freshly parsed
|
|
2276
|
+
// available_count, and a replay has none.
|
|
2277
|
+
return jsonResponse({ code: opened.code, replayed: true });
|
|
2278
|
+
}
|
|
2279
|
+
if (opened.kind === "identity-mismatch") {
|
|
2280
|
+
return jsonResponse({
|
|
2281
|
+
error: "operation_id_owned_by_another_account",
|
|
2282
|
+
code: "identity_mismatch",
|
|
2283
|
+
}, 409);
|
|
2284
|
+
}
|
|
2285
|
+
if (opened.kind !== "execute") {
|
|
2286
|
+
// capacity | unavailable -> fail closed. Falling back to a random id
|
|
2287
|
+
// would silently reintroduce the double-spend this identity prevents.
|
|
2288
|
+
const response = jsonResponse({
|
|
2289
|
+
error: opened.kind === "capacity"
|
|
2290
|
+
? "reset_credit_ledger_capacity"
|
|
2291
|
+
: "reset_credit_ledger_unavailable",
|
|
2292
|
+
code: opened.kind,
|
|
2293
|
+
}, 503);
|
|
2294
|
+
response.headers.set("Retry-After", "1");
|
|
2295
|
+
return response;
|
|
2296
|
+
}
|
|
2297
|
+
// Canonical id, which an alias join may map to an earlier caller id.
|
|
2298
|
+
idempotencyKey = opened.operationId;
|
|
2299
|
+
} else {
|
|
2300
|
+
idempotencyKey = crypto.randomUUID();
|
|
2301
|
+
}
|
|
2302
|
+
let resp: Response;
|
|
2303
|
+
try {
|
|
2304
|
+
resp = await fetch(
|
|
2305
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
2306
|
+
{
|
|
2307
|
+
method: "POST",
|
|
2308
|
+
headers: {
|
|
2309
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
2310
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
2311
|
+
"Content-Type": "application/json",
|
|
2312
|
+
},
|
|
2313
|
+
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
2314
|
+
signal: AbortSignal.timeout(10_000),
|
|
2123
2315
|
},
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2316
|
+
);
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
// Dispatch outcome unknown: the credit may or may not have been spent.
|
|
2319
|
+
// Mark ambiguous so a replay of this same id is never treated as new.
|
|
2320
|
+
if (identity) markManualResetCreditOperationAmbiguous(identity);
|
|
2321
|
+
throw error;
|
|
2322
|
+
}
|
|
2128
2323
|
if (!resp.ok) {
|
|
2129
2324
|
await resp.body?.cancel().catch(() => {});
|
|
2325
|
+
if (identity) markManualResetCreditOperationAmbiguous(identity);
|
|
2130
2326
|
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
2131
2327
|
}
|
|
2132
2328
|
const result = safeResetCreditConsumeDto(await resp.json());
|
|
2329
|
+
if (identity) {
|
|
2330
|
+
// Narrow explicitly rather than casting: `safeResetCreditConsumeDto`
|
|
2331
|
+
// normalizes anything unrecognized to "unknown", and settling that
|
|
2332
|
+
// would come back as a mismatch and leave the row pending anyway.
|
|
2333
|
+
// Settlement failure never downgrades the user-visible outcome: the
|
|
2334
|
+
// spend already happened upstream, and reporting failure would invite
|
|
2335
|
+
// a manual retry -- the exact double-spend this unit removes.
|
|
2336
|
+
if (result.code === "reset" || result.code === "already_redeemed"
|
|
2337
|
+
|| result.code === "nothing_to_reset" || result.code === "no_credit") {
|
|
2338
|
+
settleManualResetCreditOperation(identity, result.code);
|
|
2339
|
+
} else {
|
|
2340
|
+
markManualResetCreditOperationAmbiguous(identity);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2133
2343
|
// After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
|
|
2134
2344
|
// and return remaining only when that refresh freshly parsed available_count.
|
|
2135
2345
|
// Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
|
|
@@ -2167,7 +2377,15 @@ export async function handleCodexAuthAPI(
|
|
|
2167
2377
|
}
|
|
2168
2378
|
|
|
2169
2379
|
if (url.pathname === "/api/codex-auth/login" && req.method === "POST") {
|
|
2170
|
-
const body = (await req.json().catch(() => ({}))) as {
|
|
2380
|
+
const body = (await req.json().catch(() => ({}))) as {
|
|
2381
|
+
id?: string;
|
|
2382
|
+
reauth?: boolean;
|
|
2383
|
+
openBrowser?: unknown;
|
|
2384
|
+
device?: unknown;
|
|
2385
|
+
};
|
|
2386
|
+
// Device mode: no local browser, no loopback listener. The only way to add
|
|
2387
|
+
// an account to a headless hub (#3366).
|
|
2388
|
+
const useDeviceFlow = body.device === true;
|
|
2171
2389
|
const requestedAccountId = body.id?.trim();
|
|
2172
2390
|
const reauth = body.reauth === true;
|
|
2173
2391
|
if (requestedAccountId && !isValidCodexAccountId(requestedAccountId)) {
|
|
@@ -2197,13 +2415,20 @@ export async function handleCodexAuthAPI(
|
|
|
2197
2415
|
codexAuthLoginState.set(flowId, loginOwner);
|
|
2198
2416
|
try {
|
|
2199
2417
|
const { startLoginFlow, getLoginStatus, publicOAuthAuthenticationErrorMessage } = await import("../oauth");
|
|
2200
|
-
const result = await startLoginFlow("chatgpt", {
|
|
2418
|
+
const result = await startLoginFlow("chatgpt", {
|
|
2419
|
+
forceLogin: true,
|
|
2420
|
+
...(useDeviceFlow ? { flow: "device" as const } : {}),
|
|
2421
|
+
});
|
|
2201
2422
|
|
|
2202
2423
|
// Open the browser server-side (same pattern as /api/oauth/login in management-api.ts).
|
|
2203
2424
|
// The GUI's window.open is popup-blocked because it runs after an await, not a direct click.
|
|
2204
2425
|
// Both login routes share one resolver so this surface cannot drift from the other.
|
|
2205
2426
|
const { shouldOpenBrowserForLogin } = await import("../oauth/open-browser-choice");
|
|
2206
|
-
|
|
2427
|
+
// A device flow's URL is a verification page the user opens on ANOTHER
|
|
2428
|
+
// machine. Opening it on the hub host is useless at best, and on a
|
|
2429
|
+
// headless host it fails. `deviceCode` is the same signal the generic
|
|
2430
|
+
// OAuth login route uses to make this decision.
|
|
2431
|
+
if (result.url && !result.deviceCode && shouldOpenBrowserForLogin(body.openBrowser, runtimeConfig)) {
|
|
2207
2432
|
const { openUrl } = await import("../lib/open-url");
|
|
2208
2433
|
openUrl(result.url);
|
|
2209
2434
|
}
|
|
@@ -2211,7 +2436,14 @@ export async function handleCodexAuthAPI(
|
|
|
2211
2436
|
(async () => {
|
|
2212
2437
|
try {
|
|
2213
2438
|
let completed = false;
|
|
2214
|
-
|
|
2439
|
+
// The device grant lives 15 minutes and the whole point is that the
|
|
2440
|
+
// user walks to another device to enter the code. A 5-minute server
|
|
2441
|
+
// budget would kill the flow at minute five while the grant is still
|
|
2442
|
+
// valid. The extra 30 attempts past 450 are settlement margin: a user
|
|
2443
|
+
// who authorizes in the final seconds still needs the token exchange
|
|
2444
|
+
// and credential write to land before this loop gives up.
|
|
2445
|
+
const pollAttempts = useDeviceFlow ? 480 : 150;
|
|
2446
|
+
for (let i = 0; i < pollAttempts; i++) {
|
|
2215
2447
|
await new Promise(r => setTimeout(r, 2000));
|
|
2216
2448
|
const st = getLoginStatus("chatgpt");
|
|
2217
2449
|
if (st.done && st.loggedIn) {
|
|
@@ -2437,7 +2669,15 @@ export async function handleCodexAuthAPI(
|
|
|
2437
2669
|
})();
|
|
2438
2670
|
|
|
2439
2671
|
setCodexLoginState(flowId, { status: "pending" });
|
|
2440
|
-
return jsonResponse({
|
|
2672
|
+
return jsonResponse({
|
|
2673
|
+
ok: true,
|
|
2674
|
+
flowId,
|
|
2675
|
+
url: result.url,
|
|
2676
|
+
instructions: result.instructions,
|
|
2677
|
+
// Dropped before #3366: every device-code surface renders this field,
|
|
2678
|
+
// so withholding it left the GUI and CLI with no code to show.
|
|
2679
|
+
...(result.deviceCode ? { deviceCode: result.deviceCode } : {}),
|
|
2680
|
+
});
|
|
2441
2681
|
} catch (e) {
|
|
2442
2682
|
if (codexAuthLoginState.get(flowId) === loginOwner) codexAuthLoginState.delete(flowId);
|
|
2443
2683
|
const msg = e instanceof Error ? e.message : String(e);
|