codex-multi-auth 2.16.0 → 2.17.2
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/.codex-plugin/plugin.json +9 -9
- package/LICENSE +21 -21
- package/README.md +487 -463
- package/assets/codex-multi-auth-icon.svg +39 -39
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +31 -31
- package/config/codex-legacy.json +1221 -1221
- package/config/codex-modern.json +423 -423
- package/config/minimal-codex.json +15 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/lib/account-policy.d.ts +4 -0
- package/dist/lib/account-policy.d.ts.map +1 -1
- package/dist/lib/account-policy.js +8 -0
- package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
- package/dist/lib/accounts/rate-limits.js +2 -0
- package/dist/lib/accounts.d.ts +15 -3
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +231 -30
- package/dist/lib/api-route-store.d.ts +24 -0
- package/dist/lib/api-route-store.d.ts.map +1 -0
- package/dist/lib/api-route-store.js +145 -0
- package/dist/lib/auth/account-access.d.ts +115 -0
- package/dist/lib/auth/account-access.d.ts.map +1 -0
- package/dist/lib/auth/account-access.js +214 -0
- package/dist/lib/auth/token-utils.d.ts +54 -1
- package/dist/lib/auth/token-utils.d.ts.map +1 -1
- package/dist/lib/auth/token-utils.js +92 -7
- package/dist/lib/cli.d.ts +1 -1
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +4 -0
- package/dist/lib/codex-cli/state.d.ts +7 -0
- package/dist/lib/codex-cli/state.d.ts.map +1 -1
- package/dist/lib/codex-cli/state.js +18 -0
- package/dist/lib/codex-cli/writer.d.ts.map +1 -1
- package/dist/lib/codex-cli/writer.js +60 -8
- package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/account-manager-commands.js +1 -0
- package/dist/lib/codex-manager/account-pool-write.d.ts +3 -1
- package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
- package/dist/lib/codex-manager/account-pool-write.js +4 -0
- package/dist/lib/codex-manager/api-login-menu.d.ts +14 -0
- package/dist/lib/codex-manager/api-login-menu.d.ts.map +1 -0
- package/dist/lib/codex-manager/api-login-menu.js +224 -0
- package/dist/lib/codex-manager/commands/account.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/account.js +27 -0
- package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/best.js +6 -2
- package/dist/lib/codex-manager/commands/check.d.ts +7 -1
- package/dist/lib/codex-manager/commands/check.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/check.js +24 -3
- package/dist/lib/codex-manager/commands/forecast.d.ts +1 -1
- package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.js +7 -3
- package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/report.js +5 -1
- package/dist/lib/codex-manager/commands/resets.d.ts +2 -0
- package/dist/lib/codex-manager/commands/resets.d.ts.map +1 -0
- package/dist/lib/codex-manager/commands/resets.js +67 -0
- package/dist/lib/codex-manager/commands/rotation.d.ts +2 -2
- package/dist/lib/codex-manager/commands/rotation.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/rotation.js +63 -12
- package/dist/lib/codex-manager/commands/status.d.ts +12 -1
- package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/status.js +166 -22
- package/dist/lib/codex-manager/commands/uninstall.js +1 -1
- package/dist/lib/codex-manager/formatters/quota-formatters.d.ts.map +1 -1
- package/dist/lib/codex-manager/formatters/quota-formatters.js +3 -2
- package/dist/lib/codex-manager/health-check.d.ts +3 -0
- package/dist/lib/codex-manager/health-check.d.ts.map +1 -1
- package/dist/lib/codex-manager/health-check.js +57 -7
- package/dist/lib/codex-manager/help.d.ts.map +1 -1
- package/dist/lib/codex-manager/help.js +4 -2
- package/dist/lib/codex-manager/login-flow.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-flow.js +69 -7
- package/dist/lib/codex-manager/login-menu-data.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-menu-data.js +21 -8
- package/dist/lib/codex-manager/login-oauth.d.ts +3 -0
- package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-oauth.js +6 -1
- package/dist/lib/codex-manager/login-workspace-choice.d.ts +11 -0
- package/dist/lib/codex-manager/login-workspace-choice.d.ts.map +1 -0
- package/dist/lib/codex-manager/login-workspace-choice.js +37 -0
- package/dist/lib/codex-manager/persist-selected-account.d.ts.map +1 -1
- package/dist/lib/codex-manager/persist-selected-account.js +2 -1
- package/dist/lib/codex-manager/quota-cache-helpers.d.ts +7 -1
- package/dist/lib/codex-manager/quota-cache-helpers.d.ts.map +1 -1
- package/dist/lib/codex-manager/quota-cache-helpers.js +34 -19
- package/dist/lib/codex-manager/repair-commands.d.ts +13 -0
- package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/repair-commands.js +144 -9
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +25 -6
- package/dist/lib/concurrency.d.ts +3 -0
- package/dist/lib/concurrency.d.ts.map +1 -0
- package/dist/lib/concurrency.js +31 -0
- package/dist/lib/context-budget-response.js +8 -8
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/model-route-policy.d.ts +52 -0
- package/dist/lib/model-route-policy.d.ts.map +1 -0
- package/dist/lib/model-route-policy.js +159 -0
- package/dist/lib/oauth-success.html +125 -125
- package/dist/lib/policy/runtime-policy.d.ts +1 -0
- package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
- package/dist/lib/policy/runtime-policy.js +3 -0
- package/dist/lib/preemptive-quota-scheduler.d.ts +2 -0
- package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
- package/dist/lib/preemptive-quota-scheduler.js +4 -0
- package/dist/lib/prompts/codex-host-bridge.js +138 -138
- package/dist/lib/prompts/codex.js +79 -79
- package/dist/lib/quota-cache.d.ts +7 -3
- package/dist/lib/quota-cache.d.ts.map +1 -1
- package/dist/lib/quota-cache.js +91 -59
- package/dist/lib/quota-probe.d.ts +5 -0
- package/dist/lib/quota-probe.d.ts.map +1 -1
- package/dist/lib/quota-probe.js +26 -2
- package/dist/lib/quota-readiness.d.ts +7 -2
- package/dist/lib/quota-readiness.d.ts.map +1 -1
- package/dist/lib/quota-readiness.js +25 -4
- package/dist/lib/request/client-cancellation.d.ts +5 -0
- package/dist/lib/request/client-cancellation.d.ts.map +1 -0
- package/dist/lib/request/client-cancellation.js +7 -0
- package/dist/lib/request/response-outcome.d.ts +25 -0
- package/dist/lib/request/response-outcome.d.ts.map +1 -0
- package/dist/lib/request/response-outcome.js +49 -0
- package/dist/lib/request/stream-failover-runtime.d.ts +2 -1
- package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
- package/dist/lib/request/stream-failover-runtime.js +29 -10
- package/dist/lib/runtime/account-check.d.ts.map +1 -1
- package/dist/lib/runtime/account-check.js +2 -8
- package/dist/lib/runtime/account-model-catalog.d.ts +64 -0
- package/dist/lib/runtime/account-model-catalog.d.ts.map +1 -0
- package/dist/lib/runtime/account-model-catalog.js +154 -0
- package/dist/lib/runtime/account-pool.d.ts +3 -1
- package/dist/lib/runtime/account-pool.d.ts.map +1 -1
- package/dist/lib/runtime/account-pool.js +4 -0
- package/dist/lib/runtime/account-reset-credits.d.ts +42 -0
- package/dist/lib/runtime/account-reset-credits.d.ts.map +1 -0
- package/dist/lib/runtime/account-reset-credits.js +82 -0
- package/dist/lib/runtime/api-model-capabilities.d.ts +30 -0
- package/dist/lib/runtime/api-model-capabilities.d.ts.map +1 -0
- package/dist/lib/runtime/api-model-capabilities.js +434 -0
- package/dist/lib/runtime/api-model-runtime.d.ts +42 -0
- package/dist/lib/runtime/api-model-runtime.d.ts.map +1 -0
- package/dist/lib/runtime/api-model-runtime.js +358 -0
- package/dist/lib/runtime/app-bind.d.ts +19 -1
- package/dist/lib/runtime/app-bind.d.ts.map +1 -1
- package/dist/lib/runtime/app-bind.js +89 -11
- package/dist/lib/runtime/automatic-account-checks.d.ts +35 -0
- package/dist/lib/runtime/automatic-account-checks.d.ts.map +1 -0
- package/dist/lib/runtime/automatic-account-checks.js +140 -0
- package/dist/lib/runtime/automatic-subscription-checks.d.ts +7 -0
- package/dist/lib/runtime/automatic-subscription-checks.d.ts.map +1 -0
- package/dist/lib/runtime/automatic-subscription-checks.js +54 -0
- package/dist/lib/runtime/catalog-capabilities.d.ts +8 -0
- package/dist/lib/runtime/catalog-capabilities.d.ts.map +1 -0
- package/dist/lib/runtime/catalog-capabilities.js +61 -0
- package/dist/lib/runtime/inference-activity.d.ts +15 -0
- package/dist/lib/runtime/inference-activity.d.ts.map +1 -0
- package/dist/lib/runtime/inference-activity.js +95 -0
- package/dist/lib/runtime/model-discovery-status.d.ts +100 -0
- package/dist/lib/runtime/model-discovery-status.d.ts.map +1 -0
- package/dist/lib/runtime/model-discovery-status.js +387 -0
- package/dist/lib/runtime/native-account-storage.d.ts +11 -0
- package/dist/lib/runtime/native-account-storage.d.ts.map +1 -0
- package/dist/lib/runtime/native-account-storage.js +50 -0
- package/dist/lib/runtime/native-account-sync.d.ts +19 -0
- package/dist/lib/runtime/native-account-sync.d.ts.map +1 -0
- package/dist/lib/runtime/native-account-sync.js +62 -0
- package/dist/lib/runtime/native-binding-lock.d.ts +3 -0
- package/dist/lib/runtime/native-binding-lock.d.ts.map +1 -0
- package/dist/lib/runtime/native-binding-lock.js +5 -0
- package/dist/lib/runtime/native-client-auth.d.ts +3 -0
- package/dist/lib/runtime/native-client-auth.d.ts.map +1 -0
- package/dist/lib/runtime/native-client-auth.js +42 -0
- package/dist/lib/runtime/native-provider-config.d.ts +4 -0
- package/dist/lib/runtime/native-provider-config.d.ts.map +1 -0
- package/dist/lib/runtime/native-provider-config.js +52 -0
- package/dist/lib/runtime/native-rate-limits.d.ts +18 -0
- package/dist/lib/runtime/native-rate-limits.d.ts.map +1 -0
- package/dist/lib/runtime/native-rate-limits.js +135 -0
- package/dist/lib/runtime/reset-credit-routing.d.ts +41 -0
- package/dist/lib/runtime/reset-credit-routing.d.ts.map +1 -0
- package/dist/lib/runtime/reset-credit-routing.js +66 -0
- package/dist/lib/runtime/reset-credits.d.ts +101 -0
- package/dist/lib/runtime/reset-credits.d.ts.map +1 -0
- package/dist/lib/runtime/reset-credits.js +162 -0
- package/dist/lib/runtime/response-output-history.d.ts +17 -0
- package/dist/lib/runtime/response-output-history.d.ts.map +1 -0
- package/dist/lib/runtime/response-output-history.js +85 -0
- package/dist/lib/runtime/responses-websocket.d.ts +31 -0
- package/dist/lib/runtime/responses-websocket.d.ts.map +1 -0
- package/dist/lib/runtime/responses-websocket.js +636 -0
- package/dist/lib/runtime/resume-picker.d.ts +41 -0
- package/dist/lib/runtime/resume-picker.d.ts.map +1 -0
- package/dist/lib/runtime/resume-picker.js +169 -0
- package/dist/lib/runtime/rotation-account-selection.d.ts +6 -0
- package/dist/lib/runtime/rotation-account-selection.d.ts.map +1 -1
- package/dist/lib/runtime/rotation-account-selection.js +63 -0
- package/dist/lib/runtime/rotation-proxy-state.d.ts +33 -0
- package/dist/lib/runtime/rotation-proxy-state.d.ts.map +1 -1
- package/dist/lib/runtime/rotation-server-types.d.ts +18 -0
- package/dist/lib/runtime/rotation-server-types.d.ts.map +1 -1
- package/dist/lib/runtime/runtime-capability-failures.d.ts +14 -0
- package/dist/lib/runtime/runtime-capability-failures.d.ts.map +1 -0
- package/dist/lib/runtime/runtime-capability-failures.js +76 -0
- package/dist/lib/runtime/runtime-observability.d.ts +7 -0
- package/dist/lib/runtime/runtime-observability.d.ts.map +1 -1
- package/dist/lib/runtime/runtime-observability.js +42 -2
- package/dist/lib/runtime/subscription-first-use.d.ts +10 -0
- package/dist/lib/runtime/subscription-first-use.d.ts.map +1 -0
- package/dist/lib/runtime/subscription-first-use.js +64 -0
- package/dist/lib/runtime/subscription-quota-event.d.ts +6 -0
- package/dist/lib/runtime/subscription-quota-event.d.ts.map +1 -0
- package/dist/lib/runtime/subscription-quota-event.js +31 -0
- package/dist/lib/runtime/subscription-quota-order.d.ts +17 -0
- package/dist/lib/runtime/subscription-quota-order.d.ts.map +1 -0
- package/dist/lib/runtime/subscription-quota-order.js +31 -0
- package/dist/lib/runtime/workspace-model-scopes.d.ts +15 -0
- package/dist/lib/runtime/workspace-model-scopes.d.ts.map +1 -0
- package/dist/lib/runtime/workspace-model-scopes.js +34 -0
- package/dist/lib/runtime-constants.d.ts +3 -0
- package/dist/lib/runtime-constants.d.ts.map +1 -1
- package/dist/lib/runtime-constants.js +5 -0
- package/dist/lib/runtime-paths.d.ts.map +1 -1
- package/dist/lib/runtime-paths.js +4 -1
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +852 -42
- package/dist/lib/schemas.d.ts +20 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +3 -0
- package/dist/lib/storage/credential-sidecars.d.ts +13 -0
- package/dist/lib/storage/credential-sidecars.d.ts.map +1 -0
- package/dist/lib/storage/credential-sidecars.js +52 -0
- package/dist/lib/storage/file-lock.d.ts +9 -0
- package/dist/lib/storage/file-lock.d.ts.map +1 -0
- package/dist/lib/storage/file-lock.js +186 -0
- package/dist/lib/storage/flagged-storage.d.ts.map +1 -1
- package/dist/lib/storage/flagged-storage.js +12 -0
- package/dist/lib/storage/path-state.d.ts +2 -0
- package/dist/lib/storage/path-state.d.ts.map +1 -1
- package/dist/lib/storage/path-state.js +9 -0
- package/dist/lib/storage/paths.js +4 -4
- package/dist/lib/storage/pending-auth.d.ts +18 -0
- package/dist/lib/storage/pending-auth.d.ts.map +1 -0
- package/dist/lib/storage/pending-auth.js +155 -0
- package/dist/lib/storage/public-types.d.ts +13 -0
- package/dist/lib/storage/public-types.d.ts.map +1 -1
- package/dist/lib/storage/record-identity.d.ts +16 -0
- package/dist/lib/storage/record-identity.d.ts.map +1 -0
- package/dist/lib/storage/record-identity.js +16 -0
- package/dist/lib/storage/snapshot-merge.d.ts +16 -0
- package/dist/lib/storage/snapshot-merge.d.ts.map +1 -0
- package/dist/lib/storage/snapshot-merge.js +206 -0
- package/dist/lib/storage/transactions.d.ts.map +1 -1
- package/dist/lib/storage/transactions.js +5 -4
- package/dist/lib/storage.d.ts +6 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +77 -7
- package/dist/lib/ui/auth-menu-builder.d.ts +2 -0
- package/dist/lib/ui/auth-menu-builder.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu-builder.js +1 -0
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +1 -0
- package/dist/lib/ui/check-progress.d.ts +8 -0
- package/dist/lib/ui/check-progress.d.ts.map +1 -0
- package/dist/lib/ui/check-progress.js +30 -0
- package/dist/lib/ui/format.d.ts +3 -1
- package/dist/lib/ui/format.d.ts.map +1 -1
- package/dist/lib/ui/format.js +8 -0
- package/dist/lib/ui/ui-copy.d.ts +2 -2
- package/dist/lib/ui/ui-copy.js +2 -2
- package/dist/lib/usage/pricing.js +2 -2
- package/dist/lib/usage/usage-extraction.d.ts +3 -1
- package/dist/lib/usage/usage-extraction.d.ts.map +1 -1
- package/dist/lib/usage/usage-extraction.js +46 -9
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
- package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
- package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
- package/node_modules/@codex-ai/plugin/package.json +9 -9
- package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
- package/node_modules/@codex-ai/sdk/package.json +8 -8
- package/package.json +200 -197
- package/scripts/codex-app-launcher.js +653 -653
- package/scripts/codex-app-router.js +373 -366
- package/scripts/codex-bin-resolver.js +313 -281
- package/scripts/codex-multi-auth.js +59 -59
- package/scripts/codex-routing.js +62 -61
- package/scripts/codex.js +6642 -6564
- package/scripts/install-codex-auth-utils.js +213 -213
- package/scripts/mcodex.js +367 -367
- package/scripts/postinstall.js +114 -114
- package/scripts/preuninstall.js +265 -265
- package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
- package/vendor/codex-ai-plugin/dist/index.js +2 -2
- package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
- package/vendor/codex-ai-plugin/dist/tool.js +29 -29
- package/vendor/codex-ai-plugin/package.json +9 -9
- package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
- package/vendor/codex-ai-sdk/dist/index.js +2 -2
- package/vendor/codex-ai-sdk/package.json +8 -8
package/dist/lib/accounts.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mergeAccountCooldown, mergeAccountSnapshot, mergeAccountRuntimeObservations } from "./storage/snapshot-merge.js";
|
|
2
|
+
import { deriveAccountRecordId, resolveAccountRecordId } from "./storage/record-identity.js";
|
|
3
|
+
export { resolveAccountRecordId } from "./storage/record-identity.js";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
2
5
|
import { saveAccountsWithRetry } from "./storage/save-retry.js";
|
|
3
6
|
import { createLogger } from "./logger.js";
|
|
4
|
-
import { getStoragePath, loadAccounts, readPinAndGenFromDisk, saveAccounts, findMatchingAccountIndex, withAccountStorageTransaction, } from "./storage.js";
|
|
7
|
+
import { getStoragePath, loadAccounts, readPinAndGenFromDisk, saveAccounts, findMatchingAccountIndex, withAccountStorageTransaction, ACCOUNT_STORAGE_UNREADABLE, recordPendingAuth, } from "./storage.js";
|
|
5
8
|
import { MODEL_FAMILIES } from "./request/helpers/model-map.js";
|
|
6
9
|
import { getHealthTracker, getTokenTracker, resetTrackers, selectHybridAccount, } from "./rotation.js";
|
|
7
10
|
import { withRoutingMutex, } from "./routing-mutex.js";
|
|
@@ -17,7 +20,7 @@ import { getStoragePathState, runWithStoragePathState, } from "./storage/path-st
|
|
|
17
20
|
export { extractAccountId, extractAccountEmail, getAccountIdCandidates, selectBestAccountCandidate, resolveRuntimeRequestIdentity, shouldUpdateAccountIdFromToken, resolveRequestAccountId, sanitizeEmail, } from "./auth/token-utils.js";
|
|
18
21
|
export { parseRateLimitReason, getQuotaKey, clampNonNegativeInt, clearExpiredRateLimits, clearAllRateLimits, isRateLimitedForQuotaKey, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
|
|
19
22
|
export { lookupCodexCliTokensByEmail, isCodexCliSyncEnabled, } from "./codex-cli/state.js";
|
|
20
|
-
import { extractAccountId, extractAccountEmail, shouldUpdateAccountIdFromToken, sanitizeEmail, } from "./auth/token-utils.js";
|
|
23
|
+
import { codexCliAccountIdFor, extractAccountId, extractAccountEmail, shouldUpdateAccountIdFromToken, sanitizeEmail, } from "./auth/token-utils.js";
|
|
21
24
|
import { clampNonNegativeInt, getQuotaKey, clearExpiredRateLimits, clearAllRateLimits, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
|
|
22
25
|
const log = createLogger("accounts");
|
|
23
26
|
let nextRuntimeCircuitKeyId = 0;
|
|
@@ -28,19 +31,6 @@ function getAccountCircuitKey(account) {
|
|
|
28
31
|
}
|
|
29
32
|
return account.circuitKeyId;
|
|
30
33
|
}
|
|
31
|
-
function deriveAccountRecordId(account) {
|
|
32
|
-
const seed = [
|
|
33
|
-
account.addedAt,
|
|
34
|
-
account.accountId?.trim() ?? "",
|
|
35
|
-
account.email?.trim().toLowerCase() ?? "",
|
|
36
|
-
account.refreshToken.trim(),
|
|
37
|
-
].join("\u0000");
|
|
38
|
-
return `record:${createHash("sha256").update(seed).digest("hex")}`;
|
|
39
|
-
}
|
|
40
|
-
function resolveAccountRecordId(account) {
|
|
41
|
-
const stored = account.recordId?.trim();
|
|
42
|
-
return stored || deriveAccountRecordId(account);
|
|
43
|
-
}
|
|
44
34
|
export function getRuntimeTrackerKey(account) {
|
|
45
35
|
if (account._runtimeTrackerKey !== undefined) {
|
|
46
36
|
return account._runtimeTrackerKey;
|
|
@@ -147,7 +137,11 @@ export const MAX_DEBOUNCED_SAVE_RETRIES = 5;
|
|
|
147
137
|
/** Ceiling for the debounced-save retry backoff. */
|
|
148
138
|
export const MAX_SAVE_RETRY_DELAY_MS = 8_000;
|
|
149
139
|
export class AccountManager {
|
|
140
|
+
persistenceBaseline = null;
|
|
150
141
|
accounts = [];
|
|
142
|
+
persistedWorkspaceSelections = new WeakMap();
|
|
143
|
+
persistedWorkspaces = new WeakMap();
|
|
144
|
+
hadPersistedStorage = false;
|
|
151
145
|
cursorByFamily = initFamilyState(0);
|
|
152
146
|
currentAccountIndexByFamily = initFamilyState(-1);
|
|
153
147
|
lastToastAccountIndex = -1;
|
|
@@ -272,6 +266,7 @@ export class AccountManager {
|
|
|
272
266
|
}
|
|
273
267
|
constructor(authFallback, stored) {
|
|
274
268
|
this.storagePathState = { ...getStoragePathState() };
|
|
269
|
+
this.hadPersistedStorage = existsSync(this.resolveSelectionStoragePath());
|
|
275
270
|
const fallbackAccountId = extractAccountId(authFallback?.access)?.trim() || undefined;
|
|
276
271
|
const fallbackAccountEmail = sanitizeEmail(extractAccountEmail(authFallback?.access));
|
|
277
272
|
// Hydrate pin/gen from the stored snapshot. Validation (range, integer)
|
|
@@ -333,7 +328,7 @@ export class AccountManager {
|
|
|
333
328
|
: account.refreshToken;
|
|
334
329
|
return {
|
|
335
330
|
index,
|
|
336
|
-
recordId: resolveAccountRecordId(
|
|
331
|
+
recordId: resolveAccountRecordId(account),
|
|
337
332
|
accountId: matchesFallback
|
|
338
333
|
? (fallbackAccountId ?? account.accountId)
|
|
339
334
|
: account.accountId,
|
|
@@ -360,6 +355,7 @@ export class AccountManager {
|
|
|
360
355
|
authInvalidationErrorCode: account.authInvalidationErrorCode,
|
|
361
356
|
workspaces: account.workspaces,
|
|
362
357
|
currentWorkspaceIndex: account.currentWorkspaceIndex,
|
|
358
|
+
codexCliMirror: account.codexCliMirror,
|
|
363
359
|
};
|
|
364
360
|
})
|
|
365
361
|
.filter((account) => account !== null);
|
|
@@ -396,6 +392,8 @@ export class AccountManager {
|
|
|
396
392
|
this.cursorByFamily[family] = nextIndex;
|
|
397
393
|
}
|
|
398
394
|
}
|
|
395
|
+
this.persistenceBaseline = stored ? structuredClone(stored) : null;
|
|
396
|
+
this.rememberWorkspaceSelections();
|
|
399
397
|
return;
|
|
400
398
|
}
|
|
401
399
|
if (authFallback) {
|
|
@@ -427,6 +425,64 @@ export class AccountManager {
|
|
|
427
425
|
this.cursorByFamily[family] = 0;
|
|
428
426
|
}
|
|
429
427
|
}
|
|
428
|
+
this.persistenceBaseline = stored ? structuredClone(stored) : null;
|
|
429
|
+
this.rememberWorkspaceSelections();
|
|
430
|
+
}
|
|
431
|
+
/** Reconcile external workspace policy by ID, preserving health changes not superseded on disk. */
|
|
432
|
+
syncWorkspaceSelections(current) {
|
|
433
|
+
if (!current)
|
|
434
|
+
return false;
|
|
435
|
+
let changed = false;
|
|
436
|
+
for (const account of this.accounts) {
|
|
437
|
+
const matches = current.accounts.filter(disk => disk.recordId && disk.recordId === account.recordId);
|
|
438
|
+
const key = getAccountIdentityKey(account);
|
|
439
|
+
const candidates = matches.length ? matches : current.accounts.filter(disk => key && getAccountIdentityKey(disk) === key);
|
|
440
|
+
if (candidates.length !== 1)
|
|
441
|
+
continue;
|
|
442
|
+
const disk = candidates[0];
|
|
443
|
+
if (!disk)
|
|
444
|
+
continue;
|
|
445
|
+
const localSelectedId = account.workspaces?.[account.currentWorkspaceIndex ?? 0]?.id;
|
|
446
|
+
const baseline = this.persistedWorkspaces.get(account);
|
|
447
|
+
if (JSON.stringify(disk.workspaces) !== JSON.stringify(baseline)) {
|
|
448
|
+
account.workspaces = disk.workspaces?.map(workspace => {
|
|
449
|
+
const previous = baseline?.find(item => item.id === workspace.id);
|
|
450
|
+
const local = account.workspaces?.find(item => item.id === workspace.id);
|
|
451
|
+
return previous && local && workspace.enabled === previous.enabled && workspace.disabledAt === previous.disabledAt
|
|
452
|
+
? { ...workspace, enabled: local.enabled, disabledAt: local.disabledAt }
|
|
453
|
+
: { ...workspace };
|
|
454
|
+
});
|
|
455
|
+
this.persistedWorkspaces.set(account, structuredClone(disk.workspaces));
|
|
456
|
+
changed = true;
|
|
457
|
+
}
|
|
458
|
+
const selectedId = disk.workspaces?.[disk.currentWorkspaceIndex ?? 0]?.id;
|
|
459
|
+
const preferredId = selectedId !== this.persistedWorkspaceSelections.get(account) ? selectedId : localSelectedId;
|
|
460
|
+
let index = account.workspaces?.findIndex(workspace => workspace.id === preferredId) ?? -1;
|
|
461
|
+
if (index < 0)
|
|
462
|
+
index = account.workspaces?.findIndex(workspace => workspace.id === selectedId) ?? -1;
|
|
463
|
+
if (index < 0)
|
|
464
|
+
index = 0;
|
|
465
|
+
if (account.workspaces?.length && account.currentWorkspaceIndex !== index) {
|
|
466
|
+
account.currentWorkspaceIndex = index;
|
|
467
|
+
changed = true;
|
|
468
|
+
}
|
|
469
|
+
this.persistedWorkspaceSelections.set(account, selectedId);
|
|
470
|
+
}
|
|
471
|
+
return changed;
|
|
472
|
+
}
|
|
473
|
+
rememberWorkspaceSelections(snapshot) {
|
|
474
|
+
this.accounts.forEach(account => {
|
|
475
|
+
// recordId first; the identity key only when defined and unique, since
|
|
476
|
+
// duplicate identities (or two missing ones) would pick another row's baseline.
|
|
477
|
+
const rows = snapshot?.accounts ?? [];
|
|
478
|
+
const byRecord = account.recordId ? rows.filter(row => row.recordId === account.recordId) : [];
|
|
479
|
+
const key = getAccountIdentityKey(account);
|
|
480
|
+
const matches = byRecord.length ? byRecord : key ? rows.filter(row => getAccountIdentityKey(row) === key) : [];
|
|
481
|
+
const saved = (matches.length === 1 ? matches[0] : undefined) ?? account;
|
|
482
|
+
this.persistedWorkspaceSelections.set(account, saved.workspaces?.[saved.currentWorkspaceIndex ?? 0]?.id);
|
|
483
|
+
this.persistedWorkspaces.set(account, structuredClone(saved.workspaces));
|
|
484
|
+
});
|
|
485
|
+
this.hadPersistedStorage ||= existsSync(this.resolveSelectionStoragePath());
|
|
430
486
|
}
|
|
431
487
|
getAccountCount() {
|
|
432
488
|
return this.accounts.length;
|
|
@@ -615,7 +671,7 @@ export class AccountManager {
|
|
|
615
671
|
if (!account)
|
|
616
672
|
return;
|
|
617
673
|
await setCodexCliActiveSelection({
|
|
618
|
-
accountId: account.
|
|
674
|
+
accountId: codexCliAccountIdFor(account, account.access),
|
|
619
675
|
email: account.email,
|
|
620
676
|
accessToken: account.access,
|
|
621
677
|
refreshToken: account.refreshToken,
|
|
@@ -1281,6 +1337,7 @@ export class AccountManager {
|
|
|
1281
1337
|
: undefined,
|
|
1282
1338
|
workspaces: account.workspaces,
|
|
1283
1339
|
currentWorkspaceIndex: account.currentWorkspaceIndex,
|
|
1340
|
+
codexCliMirror: account.codexCliMirror,
|
|
1284
1341
|
};
|
|
1285
1342
|
}),
|
|
1286
1343
|
activeIndex,
|
|
@@ -1299,13 +1356,16 @@ export class AccountManager {
|
|
|
1299
1356
|
const nextEmail = sanitizeEmail(extractAccountEmail(auth.access));
|
|
1300
1357
|
try {
|
|
1301
1358
|
return await withAccountStorageTransaction(async (_current, persist) => {
|
|
1359
|
+
if (!_current && this.hadPersistedStorage)
|
|
1360
|
+
throw Object.assign(new Error("Account storage was removed; reload before saving."), { code: "ESTALE" });
|
|
1361
|
+
this.syncWorkspaceSelections(_current);
|
|
1302
1362
|
// Snapshot the live in-memory pool under the storage lock so refresh
|
|
1303
1363
|
// persistence merges against the latest account state. Reconcile the
|
|
1304
1364
|
// selection first for the same reason `saveToDisk` does: this write
|
|
1305
1365
|
// carries `pinnedAccountIndex`/`affinityGeneration` too, so a stale
|
|
1306
1366
|
// pair here would clobber a `switch` that landed since the last save.
|
|
1307
1367
|
this.reconcileSelectionFromDisk();
|
|
1308
|
-
const nextStorage = structuredClone(this.buildStorageSnapshot());
|
|
1368
|
+
const nextStorage = structuredClone(this.reconcileTokensFromDisk(this.buildStorageSnapshot(), _current));
|
|
1309
1369
|
const storageIndex = findAccountIndexByIdentity(nextStorage.accounts, source, auth);
|
|
1310
1370
|
if (storageIndex === undefined) {
|
|
1311
1371
|
log.warn("Unable to resolve refreshed account for persistence", {
|
|
@@ -1354,7 +1414,8 @@ export class AccountManager {
|
|
|
1354
1414
|
this.clearAccountCooldown(liveAccount);
|
|
1355
1415
|
this.clearAuthFailures(liveAccount);
|
|
1356
1416
|
try {
|
|
1357
|
-
await
|
|
1417
|
+
await this.persistSnapshot(_current, nextStorage, persist, storedAccount);
|
|
1418
|
+
this.rememberWorkspaceSelections(nextStorage);
|
|
1358
1419
|
}
|
|
1359
1420
|
catch (error) {
|
|
1360
1421
|
liveAccount.access = previousLiveAccountState.access;
|
|
@@ -1399,7 +1460,8 @@ export class AccountManager {
|
|
|
1399
1460
|
}
|
|
1400
1461
|
return liveAccount;
|
|
1401
1462
|
}
|
|
1402
|
-
await
|
|
1463
|
+
await this.persistSnapshot(_current, nextStorage, persist, storedAccount);
|
|
1464
|
+
this.rememberWorkspaceSelections(nextStorage);
|
|
1403
1465
|
log.warn("Unable to resolve refreshed live account after persistence", {
|
|
1404
1466
|
sourceIndex: source.index,
|
|
1405
1467
|
});
|
|
@@ -1407,6 +1469,52 @@ export class AccountManager {
|
|
|
1407
1469
|
});
|
|
1408
1470
|
}
|
|
1409
1471
|
catch (error) {
|
|
1472
|
+
// The refresh already rotated the token upstream; the old one is spent.
|
|
1473
|
+
// A locked or unreadable accounts file must not discard the only valid
|
|
1474
|
+
// credential, so keep it live and let the retrying debounced save
|
|
1475
|
+
// persist it once the file can be read.
|
|
1476
|
+
// A write that still fails after the storage retries (e.g. a Windows lock)
|
|
1477
|
+
// spends the old token just the same, so it is rescued like an unreadable file.
|
|
1478
|
+
const live = [ACCOUNT_STORAGE_UNREADABLE, "ELOCKED", "EACCES"].includes(error.code ?? "") || isRetryableAuthPersistenceError(error)
|
|
1479
|
+
? this.getAccountByIdentity(source, auth)
|
|
1480
|
+
: null;
|
|
1481
|
+
if (live) {
|
|
1482
|
+
const priorRefreshToken = live.refreshToken;
|
|
1483
|
+
this.updateFromAuth(live, auth);
|
|
1484
|
+
live.enabled = true;
|
|
1485
|
+
if (live.cooldownReason === "auth-failure")
|
|
1486
|
+
this.clearAccountCooldown(live);
|
|
1487
|
+
this.clearAuthFailures(live);
|
|
1488
|
+
// The debounced save needs this process to survive and the file to
|
|
1489
|
+
// unlock. Journal the rotated credential beside the pool so the next
|
|
1490
|
+
// load (any process) applies it even if neither happens.
|
|
1491
|
+
try {
|
|
1492
|
+
await runWithStoragePathState(this.storagePathState, () => recordPendingAuth(getStoragePath(), {
|
|
1493
|
+
priorRefreshToken,
|
|
1494
|
+
recordId: live.recordId,
|
|
1495
|
+
refreshToken: auth.refresh,
|
|
1496
|
+
accessToken: auth.access,
|
|
1497
|
+
expiresAt: auth.expires,
|
|
1498
|
+
at: nowMs(),
|
|
1499
|
+
}));
|
|
1500
|
+
// Loads now see the journaled token, so it is the persisted state
|
|
1501
|
+
// that later merges (and a further rotation) compare against.
|
|
1502
|
+
const baselineRow = this.persistenceBaseline?.accounts.find(row => row.refreshToken === priorRefreshToken);
|
|
1503
|
+
if (baselineRow)
|
|
1504
|
+
Object.assign(baselineRow, { refreshToken: auth.refresh, accessToken: auth.access, expiresAt: auth.expires });
|
|
1505
|
+
log.warn("Account storage could not be read or written; rotated credential journaled until it can be saved", {
|
|
1506
|
+
sourceIndex: source.index,
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
catch (journalError) {
|
|
1510
|
+
log.error("Account storage could not be read or written and the rotated credential could not be journaled; it is only in memory. Keep this process running until the accounts file is readable, or the account will need a re-login.", {
|
|
1511
|
+
sourceIndex: source.index,
|
|
1512
|
+
code: typeof journalError.code === "string" && /^[A-Z_]{1,40}$/.test(journalError.code ?? "") ? journalError.code : "INVALID_PENDING_AUTH",
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
this.saveToDiskDebounced();
|
|
1516
|
+
return live;
|
|
1517
|
+
}
|
|
1410
1518
|
throw new CodexAuthError(ERROR_MESSAGES.TOKEN_REFRESH_FAILED, {
|
|
1411
1519
|
retryable: isRetryableAuthPersistenceError(error),
|
|
1412
1520
|
cause: error,
|
|
@@ -1671,14 +1779,93 @@ export class AccountManager {
|
|
|
1671
1779
|
}
|
|
1672
1780
|
return account;
|
|
1673
1781
|
}
|
|
1674
|
-
async
|
|
1782
|
+
async persistSnapshot(current, proposed, persist, refreshed, runtimeOnConflict = false) {
|
|
1783
|
+
// Preserve initial/missing-store creation. An intentionally cleared store has
|
|
1784
|
+
// distinct metadata and must still win over this manager's stale inventory.
|
|
1785
|
+
let merged;
|
|
1786
|
+
let rescuedBaseline;
|
|
1787
|
+
try {
|
|
1788
|
+
merged = current ? mergeAccountSnapshot(this.persistenceBaseline, current, proposed) : proposed;
|
|
1789
|
+
}
|
|
1790
|
+
catch (error) {
|
|
1791
|
+
if (runtimeOnConflict && error.code === "ESTALE" && current && this.persistenceBaseline) {
|
|
1792
|
+
await persist(mergeAccountRuntimeObservations(this.persistenceBaseline, current, proposed));
|
|
1793
|
+
log.warn("Saved runtime observations; conflicting account edits remain pending and require reload or reconciliation.");
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (error.code !== "ESTALE" || !refreshed?.recordId || !current)
|
|
1797
|
+
throw error;
|
|
1798
|
+
// A rotated credential must not be discarded because an unrelated user edit
|
|
1799
|
+
// conflicts. Persist only its auth delta under this same transaction lock.
|
|
1800
|
+
const matches = (row) => resolveAccountRecordId(row) === refreshed.recordId;
|
|
1801
|
+
const prior = this.persistenceBaseline?.accounts.find(matches);
|
|
1802
|
+
const index = current.accounts.findIndex(matches);
|
|
1803
|
+
const disk = current.accounts[index];
|
|
1804
|
+
if (!prior || !disk || disk.refreshToken !== prior.refreshToken || disk.enabled === false || disk.authInvalidatedAt)
|
|
1805
|
+
throw error;
|
|
1806
|
+
merged = structuredClone(current);
|
|
1807
|
+
const row = merged.accounts[index];
|
|
1808
|
+
if (!row || !this.persistenceBaseline)
|
|
1809
|
+
throw error;
|
|
1810
|
+
rescuedBaseline = structuredClone(this.persistenceBaseline);
|
|
1811
|
+
const baselineRow = rescuedBaseline.accounts.find(matches);
|
|
1812
|
+
if (!baselineRow)
|
|
1813
|
+
throw error;
|
|
1814
|
+
const cooldown = mergeAccountCooldown(prior, disk, refreshed);
|
|
1815
|
+
// Persist the stable identity before rotating a legacy row's identity material.
|
|
1816
|
+
row.recordId = refreshed.recordId;
|
|
1817
|
+
baselineRow.recordId = refreshed.recordId;
|
|
1818
|
+
// Only authentication was committed. Pending additions, removals and user
|
|
1819
|
+
// edits must still differ from their old baseline on the next save. The
|
|
1820
|
+
// refresh can also move the identity and clears auth blockers; a restart
|
|
1821
|
+
// must not bring the old identity or cooldown back.
|
|
1822
|
+
for (const field of ["accessToken", "refreshToken", "expiresAt", "accountId", "accountIdSource", "email", "authInvalidatedAt", "authInvalidationErrorCode", "coolingDownUntil", "cooldownReason"]) {
|
|
1823
|
+
const value = refreshed[field];
|
|
1824
|
+
if (value === undefined) {
|
|
1825
|
+
delete row[field];
|
|
1826
|
+
delete baselineRow[field];
|
|
1827
|
+
}
|
|
1828
|
+
else {
|
|
1829
|
+
Object.assign(row, { [field]: value });
|
|
1830
|
+
Object.assign(baselineRow, { [field]: value });
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
Object.assign(row, cooldown);
|
|
1834
|
+
}
|
|
1835
|
+
await persist(merged);
|
|
1836
|
+
if (refreshed) {
|
|
1837
|
+
const saved = merged.accounts.find(row => row.recordId === refreshed.recordId);
|
|
1838
|
+
const live = this.accounts.find(row => row.recordId === refreshed.recordId);
|
|
1839
|
+
// A successful refresh must retain blockers learned by another request,
|
|
1840
|
+
// including an observation made while the storage write was pending.
|
|
1841
|
+
if (saved && live)
|
|
1842
|
+
Object.assign(live, mergeAccountCooldown(refreshed, saved, live));
|
|
1843
|
+
}
|
|
1844
|
+
// Keep the baseline in this manager's inventory/intent space. Adopting added
|
|
1845
|
+
// disk records here would misread their absence in the next save as deletion.
|
|
1846
|
+
this.persistenceBaseline = rescuedBaseline ?? structuredClone(proposed);
|
|
1847
|
+
if (refreshed) {
|
|
1848
|
+
const saved = merged.accounts.find(row => row.recordId === refreshed.recordId);
|
|
1849
|
+
const baseline = this.persistenceBaseline.accounts.find(row => row.recordId === refreshed.recordId);
|
|
1850
|
+
// The adopted blocker is now known persisted state, so a later explicit
|
|
1851
|
+
// clear must compare against it rather than the pre-refresh clear.
|
|
1852
|
+
if (saved && baseline)
|
|
1853
|
+
Object.assign(baseline, { coolingDownUntil: saved.coolingDownUntil, cooldownReason: saved.cooldownReason });
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
async saveToDisk(runtimeOnConflict = false) {
|
|
1675
1857
|
await runWithStoragePathState(this.storagePathState, async () => {
|
|
1676
1858
|
await withAccountStorageTransaction(async (current, persist) => {
|
|
1859
|
+
if (!current && this.hadPersistedStorage)
|
|
1860
|
+
throw Object.assign(new Error("Account storage was removed; reload before saving."), { code: "ESTALE" });
|
|
1677
1861
|
// Reconcile against the disk state loaded under the storage lock so a
|
|
1678
1862
|
// routine save does not clobber a token another process just rotated
|
|
1679
1863
|
// (stress audit H3) or a pin the CLI just wrote (#474).
|
|
1680
1864
|
this.reconcileSelectionFromDisk();
|
|
1681
|
-
|
|
1865
|
+
this.syncWorkspaceSelections(current);
|
|
1866
|
+
const snapshot = this.reconcileTokensFromDisk(this.buildStorageSnapshot(), current);
|
|
1867
|
+
await this.persistSnapshot(current, snapshot, persist, undefined, runtimeOnConflict);
|
|
1868
|
+
this.rememberWorkspaceSelections(snapshot);
|
|
1682
1869
|
});
|
|
1683
1870
|
});
|
|
1684
1871
|
}
|
|
@@ -1693,7 +1880,7 @@ export class AccountManager {
|
|
|
1693
1880
|
if (this.pendingSave) {
|
|
1694
1881
|
await this.pendingSave;
|
|
1695
1882
|
}
|
|
1696
|
-
this.pendingSave = this.saveToDisk().finally(() => {
|
|
1883
|
+
this.pendingSave = this.saveToDisk(true).finally(() => {
|
|
1697
1884
|
this.pendingSave = null;
|
|
1698
1885
|
});
|
|
1699
1886
|
await this.pendingSave;
|
|
@@ -1735,7 +1922,7 @@ export class AccountManager {
|
|
|
1735
1922
|
if (this.saveDebounceTimer) {
|
|
1736
1923
|
clearTimeout(this.saveDebounceTimer);
|
|
1737
1924
|
this.saveDebounceTimer = null;
|
|
1738
|
-
await this.saveToDisk();
|
|
1925
|
+
await this.saveToDisk(true);
|
|
1739
1926
|
}
|
|
1740
1927
|
if (this.pendingSave) {
|
|
1741
1928
|
await this.pendingSave;
|
|
@@ -1754,10 +1941,15 @@ export class AccountManager {
|
|
|
1754
1941
|
}
|
|
1755
1942
|
const resetIndex = account.workspaces.findIndex((workspace) => workspace.isDefault === true);
|
|
1756
1943
|
for (const workspace of account.workspaces) {
|
|
1757
|
-
workspace.
|
|
1758
|
-
|
|
1944
|
+
// Only undo runtime health disablement, never an explicit workspace exclusion.
|
|
1945
|
+
if (workspace.disabledAt !== undefined) {
|
|
1946
|
+
workspace.enabled = true;
|
|
1947
|
+
delete workspace.disabledAt;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if (!account.workspaces[account.currentWorkspaceIndex ?? -1]) {
|
|
1951
|
+
account.currentWorkspaceIndex = resetIndex >= 0 ? resetIndex : 0;
|
|
1759
1952
|
}
|
|
1760
|
-
account.currentWorkspaceIndex = resetIndex >= 0 ? resetIndex : 0;
|
|
1761
1953
|
}
|
|
1762
1954
|
getCurrentWorkspace(account) {
|
|
1763
1955
|
if (!account.workspaces || account.workspaces.length === 0) {
|
|
@@ -1766,6 +1958,15 @@ export class AccountManager {
|
|
|
1766
1958
|
const idx = account.currentWorkspaceIndex ?? 0;
|
|
1767
1959
|
return account.workspaces[idx] ?? null;
|
|
1768
1960
|
}
|
|
1961
|
+
/** Disable a specific request workspace without changing the interactive selection. */
|
|
1962
|
+
disableWorkspace(account, workspaceId) {
|
|
1963
|
+
const workspace = account.workspaces?.find(item => item.id === workspaceId);
|
|
1964
|
+
if (!workspace || workspace.enabled === false)
|
|
1965
|
+
return false;
|
|
1966
|
+
workspace.enabled = false;
|
|
1967
|
+
workspace.disabledAt = nowMs();
|
|
1968
|
+
return true;
|
|
1969
|
+
}
|
|
1769
1970
|
disableCurrentWorkspace(account, expectedWorkspaceId) {
|
|
1770
1971
|
if (!account.workspaces || account.workspaces.length === 0) {
|
|
1771
1972
|
return false;
|
|
@@ -1873,7 +2074,7 @@ export function formatAccountLabel(account, index) {
|
|
|
1873
2074
|
* render these (e.g. only when more than one workspace exists) and supply the
|
|
1874
2075
|
* leading indent.
|
|
1875
2076
|
*/
|
|
1876
|
-
export function formatWorkspaceLines(account, indent = " ") {
|
|
2077
|
+
export function formatWorkspaceLines(account, indent = " ", selectionLabel = "active") {
|
|
1877
2078
|
const workspaces = account?.workspaces;
|
|
1878
2079
|
if (!workspaces || workspaces.length === 0)
|
|
1879
2080
|
return [];
|
|
@@ -1885,7 +2086,7 @@ export function formatWorkspaceLines(account, indent = " ") {
|
|
|
1885
2086
|
const idSuffix = id.length > 6 ? id.slice(-6) : id;
|
|
1886
2087
|
const tags = [];
|
|
1887
2088
|
if (isActive)
|
|
1888
|
-
tags.push(
|
|
2089
|
+
tags.push(selectionLabel);
|
|
1889
2090
|
if (workspace.enabled === false)
|
|
1890
2091
|
tags.push("disabled");
|
|
1891
2092
|
const tagLabel = tags.length > 0 ? ` (${tags.join(", ")})` : "";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const routeSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
label: z.ZodString;
|
|
5
|
+
kind: z.ZodEnum<{
|
|
6
|
+
api: "api";
|
|
7
|
+
zdr: "zdr";
|
|
8
|
+
}>;
|
|
9
|
+
apiKey: z.ZodString;
|
|
10
|
+
enabled: z.ZodBoolean;
|
|
11
|
+
priority: z.ZodNumber;
|
|
12
|
+
visibleModels: z.ZodArray<z.ZodString>;
|
|
13
|
+
knownModels: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
14
|
+
probeCapabilities: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
+
accessPrograms: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
16
|
+
modelAccessPrograms: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export type ApiRouteCredential = z.infer<typeof routeSchema>;
|
|
19
|
+
export declare function getApiRoutesPath(): string;
|
|
20
|
+
export declare function loadApiRoutes(path?: string): Promise<ApiRouteCredential[]>;
|
|
21
|
+
export declare function saveApiRoutes(routes: ApiRouteCredential[], path?: string, expected?: ApiRouteCredential[]): Promise<void>;
|
|
22
|
+
export declare function updateApiModelDiscovery(route: ApiRouteCredential, models: string[], path?: string): Promise<ApiRouteCredential>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=api-route-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-route-store.d.ts","sourceRoot":"","sources":["../../lib/api-route-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;iBAsBf,CAAC;AAKH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAC7D,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAWD,wBAAsB,aAAa,CAClC,IAAI,SAAqB,GACvB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAwB/B;AACD,wBAAsB,aAAa,CAClC,MAAM,EAAE,kBAAkB,EAAE,EAC5B,IAAI,SAAqB,EACzB,QAAQ,CAAC,EAAE,kBAAkB,EAAE,GAC7B,OAAO,CAAC,IAAI,CAAC,CAgCf;AAKD,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,SAAqB,GACvB,OAAO,CAAC,kBAAkB,CAAC,CAS7B"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getCodexMultiAuthDir } from "./runtime-paths.js";
|
|
5
|
+
import { tempPathFor } from "./temp-path.js";
|
|
6
|
+
import { withRetry } from "./fs-retry.js";
|
|
7
|
+
import { withStorageLock } from "./storage/transactions.js";
|
|
8
|
+
import { withFileTransactionLock } from "./storage/file-lock.js";
|
|
9
|
+
const modelId = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(200)
|
|
13
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
14
|
+
const programId = z.string().min(1).max(80).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
|
|
15
|
+
const accessPrograms = z.record(programId, z.array(programId).max(32))
|
|
16
|
+
.refine(value => Object.keys(value).length <= 16);
|
|
17
|
+
const routeSchema = z.object({
|
|
18
|
+
id: z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(80)
|
|
22
|
+
.regex(/^[A-Za-z0-9_-]+$/),
|
|
23
|
+
label: z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1)
|
|
26
|
+
.max(80)
|
|
27
|
+
.regex(/^[^\x00-\x1f\x7f]+$/),
|
|
28
|
+
kind: z.enum(["api", "zdr"]),
|
|
29
|
+
apiKey: z.string().min(1).max(1024).regex(/^\S+$/),
|
|
30
|
+
enabled: z.boolean(),
|
|
31
|
+
priority: z.number().int().min(0).max(9),
|
|
32
|
+
visibleModels: z.array(modelId).max(1000),
|
|
33
|
+
knownModels: z.array(modelId).max(10000).optional(),
|
|
34
|
+
probeCapabilities: z.boolean().optional(),
|
|
35
|
+
/** Administrator-confirmed credential entitlements absent from API model discovery. */
|
|
36
|
+
accessPrograms: accessPrograms.optional(),
|
|
37
|
+
modelAccessPrograms: z.record(modelId, accessPrograms)
|
|
38
|
+
.refine(value => Object.keys(value).length <= 1000).optional(),
|
|
39
|
+
});
|
|
40
|
+
const schema = z.object({
|
|
41
|
+
version: z.literal(1),
|
|
42
|
+
routes: z.array(routeSchema).max(100),
|
|
43
|
+
});
|
|
44
|
+
export function getApiRoutesPath() {
|
|
45
|
+
return join(getCodexMultiAuthDir(), "api-routes.json");
|
|
46
|
+
}
|
|
47
|
+
function validate(value) {
|
|
48
|
+
const parsed = schema.safeParse(value);
|
|
49
|
+
if (!parsed.success ||
|
|
50
|
+
new Set(parsed.data.routes.map((r) => r.id)).size !==
|
|
51
|
+
parsed.data.routes.length)
|
|
52
|
+
throw new Error("Invalid API route configuration");
|
|
53
|
+
return parsed.data.routes;
|
|
54
|
+
}
|
|
55
|
+
export async function loadApiRoutes(path = getApiRoutesPath()) {
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = await withRetry(async () => {
|
|
59
|
+
const handle = await fs.open(path, "r");
|
|
60
|
+
try {
|
|
61
|
+
const buffer = Buffer.alloc(1024 * 1024 + 1);
|
|
62
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
63
|
+
if (bytesRead > 1024 * 1024)
|
|
64
|
+
throw new Error("Invalid API route configuration");
|
|
65
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await handle.close();
|
|
69
|
+
}
|
|
70
|
+
}, { maxAttempts: 6, backoffMs: 25 });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.code === "ENOENT")
|
|
74
|
+
return [];
|
|
75
|
+
throw new Error("Unable to read API route configuration");
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return validate(JSON.parse(raw));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new Error("Invalid API route configuration");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function saveApiRoutes(routes, path = getApiRoutesPath(), expected) {
|
|
85
|
+
const validated = validate({ version: 1, routes });
|
|
86
|
+
const serialized = JSON.stringify({ version: 1, routes: validated }) + "\n";
|
|
87
|
+
if (Buffer.byteLength(serialized) > 1024 * 1024)
|
|
88
|
+
throw new Error("API route configuration is too large");
|
|
89
|
+
await withStorageLock(() => withFileTransactionLock(path, async () => {
|
|
90
|
+
await fs.mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
91
|
+
const temp = tempPathFor(path);
|
|
92
|
+
try {
|
|
93
|
+
if (expected &&
|
|
94
|
+
JSON.stringify(await loadApiRoutes(path)) !==
|
|
95
|
+
JSON.stringify(validate({ version: 1, routes: expected }))) {
|
|
96
|
+
throw new Error("API route configuration changed; reopen the menu before saving.");
|
|
97
|
+
}
|
|
98
|
+
await fs.writeFile(temp, serialized, { mode: 0o600, flag: "wx" });
|
|
99
|
+
await withRetry(() => fs.rename(temp, path), {
|
|
100
|
+
maxAttempts: 6,
|
|
101
|
+
backoffMs: 25,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await withRetry(() => fs.unlink(temp), {
|
|
106
|
+
maxAttempts: 6,
|
|
107
|
+
backoffMs: 25,
|
|
108
|
+
}).catch((error) => {
|
|
109
|
+
if (error.code !== "ENOENT")
|
|
110
|
+
throw error;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
/** Persist a discovery baseline and new GPT text IDs without undoing menu edits.
|
|
116
|
+
* Callers supply IDs already filtered for the Responses endpoint family. */
|
|
117
|
+
let discoveryUpdates = Promise.resolve();
|
|
118
|
+
export function updateApiModelDiscovery(route, models, path = getApiRoutesPath()) {
|
|
119
|
+
const result = discoveryUpdates.then(() => updateApiModelDiscoverySerial(route, models, path));
|
|
120
|
+
discoveryUpdates = result.then(() => undefined, () => undefined);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
async function updateApiModelDiscoverySerial(route, models, path = getApiRoutesPath()) {
|
|
124
|
+
const routes = await loadApiRoutes(path);
|
|
125
|
+
const current = routes.find((r) => r.id === route.id && r.apiKey === route.apiKey && r.kind === route.kind);
|
|
126
|
+
if (!current)
|
|
127
|
+
return { ...route, enabled: false, visibleModels: [] };
|
|
128
|
+
if (!current.enabled)
|
|
129
|
+
return current;
|
|
130
|
+
const known = new Set(current.knownModels ?? models);
|
|
131
|
+
const visible = new Set(current.visibleModels);
|
|
132
|
+
for (const id of models) {
|
|
133
|
+
if (!known.has(id) && /^gpt-/i.test(id))
|
|
134
|
+
visible.add(id);
|
|
135
|
+
known.add(id);
|
|
136
|
+
}
|
|
137
|
+
const updated = {
|
|
138
|
+
...current,
|
|
139
|
+
knownModels: [...known],
|
|
140
|
+
visibleModels: [...visible],
|
|
141
|
+
};
|
|
142
|
+
if (JSON.stringify(updated) !== JSON.stringify(current))
|
|
143
|
+
await saveApiRoutes(routes.map((r) => (r.id === current.id ? updated : r)), path, routes);
|
|
144
|
+
return updated;
|
|
145
|
+
}
|