codex-multi-auth 2.1.13-beta.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +415 -415
- package/assets/codex-multi-auth-icon.svg +39 -39
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +29 -29
- package/config/codex-legacy.json +571 -571
- package/config/codex-modern.json +258 -258
- package/config/minimal-codex.json +15 -15
- package/config/schema/config.schema.json +20 -20
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +30 -1
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/org-override.d.ts +19 -0
- package/dist/lib/auth/org-override.d.ts.map +1 -0
- package/dist/lib/auth/org-override.js +23 -0
- package/dist/lib/auth/org-override.js.map +1 -0
- package/dist/lib/circuit-breaker.d.ts +6 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -1
- package/dist/lib/circuit-breaker.js +8 -0
- package/dist/lib/circuit-breaker.js.map +1 -1
- package/dist/lib/codex-manager/account-manager-commands.d.ts +14 -0
- package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -0
- package/dist/lib/codex-manager/account-manager-commands.js +43 -0
- package/dist/lib/codex-manager/account-manager-commands.js.map +1 -0
- package/dist/lib/codex-manager/commands/best.d.ts +1 -1
- package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/best.js +2 -1
- package/dist/lib/codex-manager/commands/best.js.map +1 -1
- package/dist/lib/codex-manager/commands/debug-bundle.d.ts +16 -0
- package/dist/lib/codex-manager/commands/debug-bundle.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/debug-bundle.js +80 -5
- package/dist/lib/codex-manager/commands/debug-bundle.js.map +1 -1
- 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 +2 -1
- package/dist/lib/codex-manager/commands/forecast.js.map +1 -1
- package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/report.js +2 -2
- package/dist/lib/codex-manager/commands/report.js.map +1 -1
- package/dist/lib/codex-manager/commands/status.d.ts +2 -0
- package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/status.js +84 -25
- package/dist/lib/codex-manager/commands/status.js.map +1 -1
- package/dist/lib/codex-manager/commands/switch.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/switch.js +7 -0
- package/dist/lib/codex-manager/commands/switch.js.map +1 -1
- package/dist/lib/codex-manager/experimental-settings-prompt.d.ts.map +1 -1
- package/dist/lib/codex-manager/experimental-settings-prompt.js +13 -3
- package/dist/lib/codex-manager/experimental-settings-prompt.js.map +1 -1
- package/dist/lib/codex-manager/forecast-report-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/forecast-report-commands.js +3 -3
- package/dist/lib/codex-manager/forecast-report-commands.js.map +1 -1
- package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/repair-commands.js +11 -1
- package/dist/lib/codex-manager/repair-commands.js.map +1 -1
- package/dist/lib/codex-manager/settings-hub/experimental.d.ts.map +1 -1
- package/dist/lib/codex-manager/settings-hub/experimental.js +8 -0
- package/dist/lib/codex-manager/settings-hub/experimental.js.map +1 -1
- package/dist/lib/codex-manager.d.ts +3 -0
- package/dist/lib/codex-manager.d.ts.map +1 -1
- package/dist/lib/codex-manager.js +53 -65
- package/dist/lib/codex-manager.js.map +1 -1
- package/dist/lib/config.d.ts +26 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +152 -5
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-overflow.d.ts +7 -2
- package/dist/lib/context-overflow.d.ts.map +1 -1
- package/dist/lib/context-overflow.js +65 -47
- package/dist/lib/context-overflow.js.map +1 -1
- package/dist/lib/errors.d.ts +16 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +23 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/local-bridge.d.ts +8 -0
- package/dist/lib/local-bridge.d.ts.map +1 -1
- package/dist/lib/local-bridge.js +81 -7
- package/dist/lib/local-bridge.js.map +1 -1
- package/dist/lib/logger.d.ts +9 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +104 -19
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/oauth-success.html +125 -125
- package/dist/lib/oc-chatgpt-orchestrator.d.ts +7 -1
- package/dist/lib/oc-chatgpt-orchestrator.d.ts.map +1 -1
- package/dist/lib/oc-chatgpt-orchestrator.js +68 -9
- package/dist/lib/oc-chatgpt-orchestrator.js.map +1 -1
- package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
- package/dist/lib/policy/runtime-policy.js +15 -1
- package/dist/lib/policy/runtime-policy.js.map +1 -1
- package/dist/lib/prompts/codex-host-bridge.js +138 -138
- package/dist/lib/prompts/codex.d.ts.map +1 -1
- package/dist/lib/prompts/codex.js +203 -108
- package/dist/lib/prompts/codex.js.map +1 -1
- package/dist/lib/prompts/fetch-utils.d.ts +72 -0
- package/dist/lib/prompts/fetch-utils.d.ts.map +1 -0
- package/dist/lib/prompts/fetch-utils.js +165 -0
- package/dist/lib/prompts/fetch-utils.js.map +1 -0
- package/dist/lib/prompts/host-codex-prompt.d.ts.map +1 -1
- package/dist/lib/prompts/host-codex-prompt.js +24 -10
- package/dist/lib/prompts/host-codex-prompt.js.map +1 -1
- package/dist/lib/quota-cache.d.ts.map +1 -1
- package/dist/lib/quota-cache.js +10 -1
- package/dist/lib/quota-cache.js.map +1 -1
- package/dist/lib/quota-probe.d.ts +21 -0
- package/dist/lib/quota-probe.d.ts.map +1 -1
- package/dist/lib/quota-probe.js +39 -0
- package/dist/lib/quota-probe.js.map +1 -1
- package/dist/lib/quota-readiness.d.ts +3 -1
- package/dist/lib/quota-readiness.d.ts.map +1 -1
- package/dist/lib/quota-readiness.js +16 -3
- package/dist/lib/quota-readiness.js.map +1 -1
- package/dist/lib/recovery/storage.d.ts +7 -0
- package/dist/lib/recovery/storage.d.ts.map +1 -1
- package/dist/lib/recovery/storage.js +178 -10
- package/dist/lib/recovery/storage.js.map +1 -1
- package/dist/lib/refresh-lease.d.ts +1 -1
- package/dist/lib/refresh-lease.d.ts.map +1 -1
- package/dist/lib/refresh-lease.js +25 -3
- package/dist/lib/refresh-lease.js.map +1 -1
- package/dist/lib/refresh-queue.d.ts.map +1 -1
- package/dist/lib/refresh-queue.js +31 -17
- package/dist/lib/refresh-queue.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +0 -5
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +36 -7
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/rotation.d.ts +12 -0
- package/dist/lib/rotation.d.ts.map +1 -1
- package/dist/lib/rotation.js +36 -0
- package/dist/lib/rotation.js.map +1 -1
- package/dist/lib/runtime/account-check-types.d.ts +1 -0
- package/dist/lib/runtime/account-check-types.d.ts.map +1 -1
- package/dist/lib/runtime/account-check-types.js +1 -0
- package/dist/lib/runtime/account-check-types.js.map +1 -1
- package/dist/lib/runtime/account-check.d.ts.map +1 -1
- package/dist/lib/runtime/account-check.js +15 -4
- package/dist/lib/runtime/account-check.js.map +1 -1
- package/dist/lib/runtime/runtime-observability.d.ts.map +1 -1
- package/dist/lib/runtime/runtime-observability.js +24 -2
- package/dist/lib/runtime/runtime-observability.js.map +1 -1
- package/dist/lib/runtime-rotation-proxy.d.ts +3 -0
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +296 -21
- package/dist/lib/runtime-rotation-proxy.js.map +1 -1
- package/dist/lib/schemas.d.ts +2 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +2 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/account-clear.d.ts.map +1 -1
- package/dist/lib/storage/account-clear.js +4 -2
- package/dist/lib/storage/account-clear.js.map +1 -1
- package/dist/lib/storage/flagged-storage-io.d.ts.map +1 -1
- package/dist/lib/storage/flagged-storage-io.js +4 -1
- package/dist/lib/storage/flagged-storage-io.js.map +1 -1
- package/dist/lib/storage/import-export.d.ts.map +1 -1
- package/dist/lib/storage/import-export.js +29 -9
- package/dist/lib/storage/import-export.js.map +1 -1
- package/dist/lib/storage/paths.d.ts.map +1 -1
- package/dist/lib/storage/paths.js +93 -0
- package/dist/lib/storage/paths.js.map +1 -1
- package/dist/lib/storage/storage-parser.d.ts.map +1 -1
- package/dist/lib/storage/storage-parser.js +7 -1
- package/dist/lib/storage/storage-parser.js.map +1 -1
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +13 -9
- package/dist/lib/storage.js.map +1 -1
- package/dist/lib/table-formatter.d.ts.map +1 -1
- package/dist/lib/table-formatter.js +20 -2
- package/dist/lib/table-formatter.js.map +1 -1
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +8 -2
- package/dist/lib/ui/auth-menu.js.map +1 -1
- package/dist/lib/ui/display-width.d.ts +27 -0
- package/dist/lib/ui/display-width.d.ts.map +1 -0
- package/dist/lib/ui/display-width.js +194 -0
- package/dist/lib/ui/display-width.js.map +1 -0
- package/dist/lib/ui/select.d.ts +23 -0
- package/dist/lib/ui/select.d.ts.map +1 -1
- package/dist/lib/ui/select.js +64 -16
- package/dist/lib/ui/select.js.map +1 -1
- package/dist/lib/ui/theme.d.ts +13 -0
- package/dist/lib/ui/theme.d.ts.map +1 -1
- package/dist/lib/ui/theme.js +34 -1
- package/dist/lib/ui/theme.js.map +1 -1
- 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 -0
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -0
- package/node_modules/@codex-ai/sdk/package.json +8 -0
- package/package.json +196 -193
- package/scripts/codex-app-launcher.js +653 -653
- package/scripts/codex-app-router.js +322 -322
- package/scripts/codex-bin-resolver.js +254 -254
- package/scripts/codex-multi-auth.js +59 -59
- package/scripts/codex-routing.js +59 -56
- package/scripts/codex.js +4782 -4275
- package/scripts/install-codex-auth-utils.js +206 -206
- package/scripts/mcodex +117 -0
- package/scripts/postinstall.js +343 -343
- package/scripts/preuninstall.js +254 -254
- 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
|
@@ -1,21 +1,74 @@
|
|
|
1
|
-
import { createHash, timingSafeEqual } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { AccountManager, extractAccountId, } from "./accounts.js";
|
|
5
5
|
import { getStoragePath } from "./storage.js";
|
|
6
|
-
import { getFetchTimeoutMs, getNetworkErrorCooldownMs, getRetryAllAccountsMaxRetries, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getStreamStallTimeoutMs, getTokenRefreshSkewMs, loadPluginConfig, } from "./config.js";
|
|
6
|
+
import { getFetchTimeoutMs, getNetworkErrorCooldownMs, getRetryAllAccountsMaxRetries, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getStreamStallTimeoutMs, getMinRotationIntervalMs, getTokenInvalidationCooldownMs, getTokenRefreshSkewMs, getPidOffsetEnabled, getRoutingMutexMode, loadPluginConfig, } from "./config.js";
|
|
7
7
|
import { CODEX_BASE_URL, HTTP_STATUS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
|
|
8
8
|
import { getModelFamily } from "./prompts/codex.js";
|
|
9
9
|
import { queuedRefresh } from "./refresh-queue.js";
|
|
10
10
|
import { mutateRuntimeObservabilitySnapshot, recordRuntimePoolExhaustion, recordRuntimeReload, recordRuntimeReset, } from "./runtime/runtime-observability.js";
|
|
11
11
|
import { createRuntimeUsageRecorder, evaluateRuntimePolicy, loadRuntimePolicyState, } from "./policy/runtime-policy.js";
|
|
12
12
|
import { isWorkspaceDisabledError } from "./request/fetch-helpers.js";
|
|
13
|
+
import { createLogger, maskString, runWithCorrelationId } from "./logger.js";
|
|
13
14
|
import { SessionAffinityStore } from "./session-affinity.js";
|
|
14
15
|
import { isRecord } from "./utils.js";
|
|
15
16
|
const DEFAULT_HOST = "127.0.0.1";
|
|
17
|
+
function isLoopbackHost(host) {
|
|
18
|
+
const normalized = host.trim().toLowerCase();
|
|
19
|
+
return (normalized === "127.0.0.1" ||
|
|
20
|
+
normalized === "localhost" ||
|
|
21
|
+
normalized === "::1" ||
|
|
22
|
+
normalized === "[::1]");
|
|
23
|
+
}
|
|
24
|
+
// IPv6 literals must be presented in two distinct forms and the proxy
|
|
25
|
+
// previously conflated them (runtime-proxy IPv6 bug). Node's
|
|
26
|
+
// net.Server.listen(port, host) requires the RAW literal ("::1"); a bracketed
|
|
27
|
+
// literal ("[::1]") makes the bind fail or behave wrong. Conversely a URL
|
|
28
|
+
// authority requires the BRACKETED literal ("[::1]") so "http://[::1]:port"
|
|
29
|
+
// parses unambiguously — the raw form yields the unparseable "http://::1:port".
|
|
30
|
+
// Normalize each form ONCE at startup so concurrent rotation paths never race
|
|
31
|
+
// on inconsistent host string representations.
|
|
32
|
+
function stripIpv6Brackets(host) {
|
|
33
|
+
const trimmed = host.trim();
|
|
34
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
35
|
+
return trimmed.slice(1, -1);
|
|
36
|
+
}
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
// Raw literal suitable for server.listen: "[::1]" -> "::1", others unchanged.
|
|
40
|
+
function toBindHost(host) {
|
|
41
|
+
return stripIpv6Brackets(host);
|
|
42
|
+
}
|
|
43
|
+
// URL authority host: IPv6 literals are bracketed ("::1" -> "[::1]") while
|
|
44
|
+
// IPv4 addresses and hostnames (no embedded colon) pass through unchanged.
|
|
45
|
+
function toUrlHost(host) {
|
|
46
|
+
const bare = stripIpv6Brackets(host);
|
|
47
|
+
return bare.includes(":") ? `[${bare}]` : bare;
|
|
48
|
+
}
|
|
49
|
+
// Structured logger for the default-on runtime proxy (errors-logging-01,
|
|
50
|
+
// runtime-proxy-04). Previously the 1900-LOC proxy had zero logger integration;
|
|
51
|
+
// failures surfaced only as a last-write-wins status.lastError string. Logs are
|
|
52
|
+
// level-gated and carry the per-request correlation id set in handleRequest.
|
|
53
|
+
const proxyLog = createLogger("runtime-proxy");
|
|
16
54
|
const DEFAULT_QUOTA_REMAINING_THRESHOLD = 10;
|
|
17
55
|
const DEFAULT_AUTH_FAILURE_COOLDOWN_MS = 30_000;
|
|
18
56
|
const DEFAULT_MAX_RUNTIME_ACCOUNT_ATTEMPTS = 4;
|
|
57
|
+
// Phrases observed in upstream 401 response bodies when OpenAI/Microsoft has
|
|
58
|
+
// explicitly revoked an OAuth token (as opposed to a generic expired-token 401
|
|
59
|
+
// that can be retried after a refresh). Matching is case-insensitive substring.
|
|
60
|
+
// If anti-abuse detection triggers different wording in production, add the new
|
|
61
|
+
// phrase here and record the source provider and date. See issue #495.
|
|
62
|
+
const TOKEN_INVALIDATION_PHRASES = [
|
|
63
|
+
"invalidated oauth token",
|
|
64
|
+
"authentication token has been invalidated",
|
|
65
|
+
"oauth token has been invalidated",
|
|
66
|
+
"token has been invalidated",
|
|
67
|
+
];
|
|
68
|
+
function isTokenInvalidationError(bodyText) {
|
|
69
|
+
const lower = bodyText.toLowerCase();
|
|
70
|
+
return TOKEN_INVALIDATION_PHRASES.some((phrase) => lower.includes(phrase));
|
|
71
|
+
}
|
|
19
72
|
const MAX_REQUEST_BODY_BYTES = 64 * 1024 * 1024;
|
|
20
73
|
const MAX_THREAD_GOAL_FALLBACKS = 512;
|
|
21
74
|
const HOP_BY_HOP_HEADERS = new Set([
|
|
@@ -90,6 +143,10 @@ function createOutboundHeaders(incoming, account, accessToken, accountId) {
|
|
|
90
143
|
}
|
|
91
144
|
headers.delete("host");
|
|
92
145
|
headers.delete("x-api-key");
|
|
146
|
+
// Never forward inbound client credentials upstream: a Cookie / proxy-auth
|
|
147
|
+
// header would ride along with the managed OAuth Bearer to OpenAI.
|
|
148
|
+
headers.delete("cookie");
|
|
149
|
+
headers.delete("proxy-authorization");
|
|
93
150
|
headers.set("authorization", `Bearer ${accessToken}`);
|
|
94
151
|
headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
|
|
95
152
|
headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
|
@@ -249,7 +306,12 @@ async function persistRuntimeActiveAccount(accountManager, account, family, isPi
|
|
|
249
306
|
return;
|
|
250
307
|
}
|
|
251
308
|
try {
|
|
252
|
-
|
|
309
|
+
// accounts-01/08: serialize the cursor mutation through the routing mutex
|
|
310
|
+
// (when routingMutex="enabled") because this commit spans an await
|
|
311
|
+
// (syncCodexCliActiveSelectionForIndex), which is the lost-update window the
|
|
312
|
+
// mutex exists to close. In legacy mode markSwitchedLocked runs inline, so
|
|
313
|
+
// behavior is unchanged by default.
|
|
314
|
+
await accountManager.markSwitchedLocked(account, "rotation", family);
|
|
253
315
|
accountManager.saveToDiskDebounced();
|
|
254
316
|
await accountManager.syncCodexCliActiveSelectionForIndex(account.index);
|
|
255
317
|
}
|
|
@@ -424,6 +486,21 @@ function buildUpstreamUrl(req, upstreamBaseUrl, upstreamPath) {
|
|
|
424
486
|
upstream.search = incomingUrl.search;
|
|
425
487
|
return upstream.toString();
|
|
426
488
|
}
|
|
489
|
+
// Monotonic auth-failure cooldown: only extend, never shorten. Two concurrent
|
|
490
|
+
// requests on the same account can race so that an invalidation path sets a
|
|
491
|
+
// long cooldown (5 min) and a subsequent generic 401 truncates it (30 s).
|
|
492
|
+
// Reading the live coolingDownUntil before writing prevents that race.
|
|
493
|
+
function applyMonotonicAuthCooldown(accountManager, account, cooldownMs) {
|
|
494
|
+
const existing = accountManager.getAccountByIndex(account.index)?.coolingDownUntil ?? 0;
|
|
495
|
+
// Intentionally Date.now(), not the proxy's injected now(): coolingDownUntil is
|
|
496
|
+
// written by markAccountCoolingDown via nowMs() (== Date.now()), so both sides of
|
|
497
|
+
// this comparison must live in the same real-wall-clock domain. Switching to the
|
|
498
|
+
// injected now() would mis-compare an injected-clock value against a real-clock
|
|
499
|
+
// deadline and silently defeat the monotonic guard.
|
|
500
|
+
if (Date.now() + cooldownMs > existing) {
|
|
501
|
+
accountManager.markAccountCoolingDown(account, cooldownMs, "auth-failure");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
427
504
|
function hasUsableAccessToken(account, now, skewMs) {
|
|
428
505
|
return (typeof account.access === "string" &&
|
|
429
506
|
account.access.trim().length > 0 &&
|
|
@@ -465,7 +542,7 @@ async function commitRefreshedAuthOnce(accountManager, account, auth) {
|
|
|
465
542
|
return pending;
|
|
466
543
|
}
|
|
467
544
|
async function ensureFreshAccessToken(params) {
|
|
468
|
-
const { accountManager, account, family, model, now, tokenRefreshSkewMs } = params;
|
|
545
|
+
const { accountManager, account, family, model, now, tokenRefreshSkewMs, tokenInvalidationCooldownMs } = params;
|
|
469
546
|
if (hasUsableAccessToken(account, now, tokenRefreshSkewMs)) {
|
|
470
547
|
return { ok: true, accessToken: account.access ?? "", account };
|
|
471
548
|
}
|
|
@@ -473,9 +550,14 @@ async function ensureFreshAccessToken(params) {
|
|
|
473
550
|
if (refreshResult.type === "failed") {
|
|
474
551
|
accountManager.recordFailure(account, family, model);
|
|
475
552
|
accountManager.incrementAuthFailures(account);
|
|
476
|
-
|
|
553
|
+
// If the refresh endpoint itself returns an explicit invalidation message
|
|
554
|
+
// (e.g. Microsoft/Outlook SSO revokes the refresh token server-side), apply
|
|
555
|
+
// the long cooldown and signal to the caller to stop rotating rather than
|
|
556
|
+
// presenting other accounts' tokens from the same IP.
|
|
557
|
+
const invalidated = isTokenInvalidationError(refreshResult.message ?? "");
|
|
558
|
+
applyMonotonicAuthCooldown(accountManager, account, invalidated ? tokenInvalidationCooldownMs : DEFAULT_AUTH_FAILURE_COOLDOWN_MS);
|
|
477
559
|
accountManager.saveToDiskDebounced();
|
|
478
|
-
return { ok: false, retryable: isTokenRefreshRetryable(refreshResult) };
|
|
560
|
+
return { ok: false, retryable: isTokenRefreshRetryable(refreshResult), invalidated };
|
|
479
561
|
}
|
|
480
562
|
const auth = {
|
|
481
563
|
type: "oauth",
|
|
@@ -548,14 +630,96 @@ function parseRetryAfterBodyMs(bodyText, now) {
|
|
|
548
630
|
}
|
|
549
631
|
return null;
|
|
550
632
|
}
|
|
551
|
-
async function readErrorBody(response) {
|
|
633
|
+
async function readErrorBody(response, timeoutMs, maxBytes = 1024 * 1024) {
|
|
634
|
+
// The outbound fetch's abort timer is cleared once headers arrive, so a
|
|
635
|
+
// stalled error body would otherwise hang this handler forever (the success
|
|
636
|
+
// path is per-chunk stall-bounded; the error path was not). Read the body via
|
|
637
|
+
// a reader, bound it by an idle timeout AND a size cap, and cancel the stream
|
|
638
|
+
// on timeout/overflow so the socket is released.
|
|
639
|
+
const body = response.body;
|
|
640
|
+
if (!body || typeof body.getReader !== "function") {
|
|
641
|
+
// Fallback for impls without a streamable body: race text() against a timer.
|
|
642
|
+
try {
|
|
643
|
+
return await withTimeout(response.text(), timeoutMs, () => undefined, "error body stalled");
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
return "";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const reader = body.getReader();
|
|
650
|
+
const chunks = [];
|
|
651
|
+
let total = 0;
|
|
552
652
|
try {
|
|
553
|
-
|
|
653
|
+
for (;;) {
|
|
654
|
+
let idleTimer;
|
|
655
|
+
const idle = new Promise((_resolve, reject) => {
|
|
656
|
+
idleTimer = setTimeout(() => reject(new Error("error body stalled")), Math.max(1, timeoutMs));
|
|
657
|
+
});
|
|
658
|
+
let result;
|
|
659
|
+
try {
|
|
660
|
+
result = await Promise.race([reader.read(), idle]);
|
|
661
|
+
}
|
|
662
|
+
finally {
|
|
663
|
+
if (idleTimer)
|
|
664
|
+
clearTimeout(idleTimer);
|
|
665
|
+
}
|
|
666
|
+
if (result.done)
|
|
667
|
+
break;
|
|
668
|
+
if (result.value) {
|
|
669
|
+
total += result.value.byteLength;
|
|
670
|
+
if (total > maxBytes)
|
|
671
|
+
break; // cap: enough for diagnostics, no OOM
|
|
672
|
+
chunks.push(result.value);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
// stalled or errored — fall through with whatever we have
|
|
678
|
+
}
|
|
679
|
+
finally {
|
|
680
|
+
await reader.cancel().catch(() => undefined);
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
554
684
|
}
|
|
555
685
|
catch {
|
|
556
686
|
return "";
|
|
557
687
|
}
|
|
558
688
|
}
|
|
689
|
+
const TOKEN_INVALIDATED_CODE = "token_invalidated";
|
|
690
|
+
const TOKEN_INVALIDATED_FALLBACK_MESSAGE = "OAuth token has been invalidated. Please re-login.";
|
|
691
|
+
// Both invalidation exit paths (refresh-failure and upstream-401) must hand the
|
|
692
|
+
// client the same machine-readable shape — { error: { message, code:
|
|
693
|
+
// "token_invalidated" } } — so a consumer keying off error.code behaves
|
|
694
|
+
// identically regardless of which vector fired. The upstream forwards a raw body
|
|
695
|
+
// with no guaranteed code, so we wrap it here while preserving its human-readable
|
|
696
|
+
// message when one is present.
|
|
697
|
+
export function buildTokenInvalidationBody(upstreamBodyText) {
|
|
698
|
+
let message = TOKEN_INVALIDATED_FALLBACK_MESSAGE;
|
|
699
|
+
const trimmed = upstreamBodyText.trim();
|
|
700
|
+
if (trimmed) {
|
|
701
|
+
try {
|
|
702
|
+
const parsed = JSON.parse(trimmed);
|
|
703
|
+
if (isRecord(parsed)) {
|
|
704
|
+
const direct = parsed.message;
|
|
705
|
+
if (typeof direct === "string" && direct.trim()) {
|
|
706
|
+
message = direct.trim();
|
|
707
|
+
}
|
|
708
|
+
else if (isRecord(parsed.error)) {
|
|
709
|
+
const nested = parsed.error.message;
|
|
710
|
+
if (typeof nested === "string" && nested.trim()) {
|
|
711
|
+
message = nested.trim();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
// Non-JSON upstream body (e.g. HTML error page): keep the stable fallback
|
|
718
|
+
// message rather than echoing markup back to the client.
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return JSON.stringify({ error: { message, code: TOKEN_INVALIDATED_CODE } });
|
|
722
|
+
}
|
|
559
723
|
function extractErrorCodeFromBody(bodyText) {
|
|
560
724
|
if (!bodyText.trim())
|
|
561
725
|
return null;
|
|
@@ -616,7 +780,7 @@ function getQuotaNearExhaustionWaitMs(headers, remainingThreshold, now) {
|
|
|
616
780
|
return candidates.length > 0 ? Math.max(...candidates) : 0;
|
|
617
781
|
}
|
|
618
782
|
export function chooseAccount(params) {
|
|
619
|
-
const { accountManager, sessionAffinityStore, sessionKey, family, model, attemptedIndexes, now, policy, pinnedIndex, skipReasons, } = params;
|
|
783
|
+
const { accountManager, sessionAffinityStore, sessionKey, family, model, attemptedIndexes, now, policy, pinnedIndex, skipReasons, stickyBoostByAccount, pidOffsetEnabled, } = params;
|
|
620
784
|
// Manual pin (from `codex-multi-auth switch <n>`) overrides every other
|
|
621
785
|
// selection signal. We do NOT call markSwitched here — the proxy must not
|
|
622
786
|
// clobber the pin set by the CLI. See issue #474.
|
|
@@ -662,7 +826,14 @@ export function chooseAccount(params) {
|
|
|
662
826
|
}
|
|
663
827
|
}
|
|
664
828
|
const selected = accountManager.getCurrentOrNextForFamilyHybrid(family, model, {
|
|
665
|
-
scoreBoostByAccount:
|
|
829
|
+
scoreBoostByAccount: {
|
|
830
|
+
...(policy?.scoreBoostByAccount ?? {}),
|
|
831
|
+
...(stickyBoostByAccount ?? {}),
|
|
832
|
+
},
|
|
833
|
+
// accounts-05: carry the PID-offset distribution into the default-on proxy
|
|
834
|
+
// path too (index.ts already does). Without it, parallel proxy processes can
|
|
835
|
+
// stampede the same account instead of spreading across the pool.
|
|
836
|
+
pidOffsetEnabled,
|
|
666
837
|
});
|
|
667
838
|
if (selected &&
|
|
668
839
|
!attemptedIndexes.has(selected.index) &&
|
|
@@ -816,8 +987,27 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
816
987
|
const pluginConfig = loadPluginConfig();
|
|
817
988
|
let activeAccountManager = options.accountManager ?? (await AccountManager.loadFromDisk());
|
|
818
989
|
const knownAccountManagers = new Set([activeAccountManager]);
|
|
990
|
+
// accounts-01/08: apply the configured routing-mutex mode so the proxy's
|
|
991
|
+
// async select->commit path (persistRuntimeActiveAccount) can serialize cursor
|
|
992
|
+
// mutations when routingMutex="enabled". Legacy mode keeps the inline fast path.
|
|
993
|
+
const routingMutexMode = getRoutingMutexMode(pluginConfig);
|
|
994
|
+
activeAccountManager.setRoutingMutexMode(routingMutexMode);
|
|
819
995
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
820
996
|
const host = options.host ?? DEFAULT_HOST;
|
|
997
|
+
// Defense in depth (runtime-proxy-01): the proxy presents managed OAuth tokens
|
|
998
|
+
// and must never be reachable off-box. It is loopback-only with NO opt-out —
|
|
999
|
+
// binding a non-loopback host would expose every managed account to the
|
|
1000
|
+
// network, so it is refused unconditionally.
|
|
1001
|
+
if (!isLoopbackHost(host)) {
|
|
1002
|
+
throw new Error(`Runtime rotation proxy refuses to bind non-loopback host "${host}". ` +
|
|
1003
|
+
"It forwards managed OAuth tokens and is loopback-only.");
|
|
1004
|
+
}
|
|
1005
|
+
// Normalize the validated host into its two representations exactly once so the
|
|
1006
|
+
// listen() bind and the emitted baseUrl can never disagree under concurrent
|
|
1007
|
+
// rotation: bindHost is the raw literal Node's listen() expects ("[::1]"->"::1"),
|
|
1008
|
+
// urlHost is the bracketed form a URL authority requires ("::1"->"[::1]").
|
|
1009
|
+
const bindHost = toBindHost(host);
|
|
1010
|
+
const urlHost = toUrlHost(host);
|
|
821
1011
|
const port = options.port ?? 0;
|
|
822
1012
|
const upstreamBaseUrl = options.upstreamBaseUrl ?? CODEX_BASE_URL;
|
|
823
1013
|
const clientApiKey = typeof options.clientApiKey === "string" &&
|
|
@@ -831,6 +1021,11 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
831
1021
|
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
832
1022
|
const networkErrorCooldownMs = getNetworkErrorCooldownMs(pluginConfig);
|
|
833
1023
|
const serverErrorCooldownMs = getServerErrorCooldownMs(pluginConfig);
|
|
1024
|
+
const tokenInvalidationCooldownMs = getTokenInvalidationCooldownMs(pluginConfig);
|
|
1025
|
+
const minRotationIntervalMs = getMinRotationIntervalMs(pluginConfig);
|
|
1026
|
+
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
|
|
1027
|
+
let lastGlobalAccountIndex = null;
|
|
1028
|
+
let lastGlobalSwitchAt = 0;
|
|
834
1029
|
const fetchTimeoutMs = options.fetchTimeoutMs ?? getFetchTimeoutMs(pluginConfig);
|
|
835
1030
|
const streamStallTimeoutMs = options.streamStallTimeoutMs ?? getStreamStallTimeoutMs(pluginConfig);
|
|
836
1031
|
const configuredMaxRetries = getRetryAllAccountsMaxRetries(pluginConfig);
|
|
@@ -877,6 +1072,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
877
1072
|
AccountManager.resetVolatileRuntimeState();
|
|
878
1073
|
recordRuntimeReset("pool-exhausted-no-account");
|
|
879
1074
|
const reloaded = await AccountManager.loadFromDisk();
|
|
1075
|
+
reloaded.setRoutingMutexMode(routingMutexMode);
|
|
880
1076
|
activeAccountManager = reloaded;
|
|
881
1077
|
knownAccountManagers.add(reloaded);
|
|
882
1078
|
lastStaleRuntimeReloadAt = Date.now();
|
|
@@ -893,6 +1089,13 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
893
1089
|
return staleRuntimeReloadPromise;
|
|
894
1090
|
};
|
|
895
1091
|
const handleRequest = async (req, res) => {
|
|
1092
|
+
// Per-request trace id (errors-logging-03): distinct from sessionKey, which
|
|
1093
|
+
// is shared across a thread's requests. Bound to this request's async context
|
|
1094
|
+
// so every proxyLog line and usage row can be correlated to one request.
|
|
1095
|
+
const traceId = randomUUID();
|
|
1096
|
+
return runWithCorrelationId(traceId, () => handleRequestInner(req, res, traceId));
|
|
1097
|
+
};
|
|
1098
|
+
const handleRequestInner = async (req, res, traceId) => {
|
|
896
1099
|
let usageRecorder = null;
|
|
897
1100
|
let accountManager = activeAccountManager;
|
|
898
1101
|
try {
|
|
@@ -952,7 +1155,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
952
1155
|
: "responses",
|
|
953
1156
|
model: context.model,
|
|
954
1157
|
projectKey,
|
|
955
|
-
requestId:
|
|
1158
|
+
requestId: traceId,
|
|
956
1159
|
startedAt: requestStartedAt,
|
|
957
1160
|
});
|
|
958
1161
|
if (policyError) {
|
|
@@ -1010,6 +1213,11 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1010
1213
|
}
|
|
1011
1214
|
while (attemptedIndexes.size < accountCount &&
|
|
1012
1215
|
transientAttempts < transientAttemptLimit) {
|
|
1216
|
+
const rotationStickyBoost = minRotationIntervalMs > 0 &&
|
|
1217
|
+
lastGlobalAccountIndex !== null &&
|
|
1218
|
+
now() - lastGlobalSwitchAt < minRotationIntervalMs
|
|
1219
|
+
? { [lastGlobalAccountIndex]: 1000 }
|
|
1220
|
+
: {};
|
|
1013
1221
|
const selected = chooseAccount({
|
|
1014
1222
|
accountManager,
|
|
1015
1223
|
sessionAffinityStore,
|
|
@@ -1021,6 +1229,8 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1021
1229
|
policy: policyDecision,
|
|
1022
1230
|
pinnedIndex,
|
|
1023
1231
|
skipReasons: accountSkipReasons,
|
|
1232
|
+
stickyBoostByAccount: rotationStickyBoost,
|
|
1233
|
+
pidOffsetEnabled,
|
|
1024
1234
|
});
|
|
1025
1235
|
if (!selected) {
|
|
1026
1236
|
if (!reloadedAfterNoAccount &&
|
|
@@ -1057,10 +1267,28 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1057
1267
|
model: context.model,
|
|
1058
1268
|
now: now(),
|
|
1059
1269
|
tokenRefreshSkewMs,
|
|
1270
|
+
tokenInvalidationCooldownMs,
|
|
1060
1271
|
});
|
|
1061
1272
|
if (!refreshed.ok) {
|
|
1062
1273
|
accountManager.refundToken(selected, context.family, context.model);
|
|
1063
1274
|
exhaustionReason = "auth-failure";
|
|
1275
|
+
if (refreshed.invalidated) {
|
|
1276
|
+
// Refresh endpoint explicitly revoked the token. Stop cascade:
|
|
1277
|
+
// return auth error to client instead of rotating to the next account.
|
|
1278
|
+
sessionAffinityStore?.forgetSession(context.sessionKey);
|
|
1279
|
+
res.writeHead(HTTP_STATUS.UNAUTHORIZED, { "content-type": "application/json" });
|
|
1280
|
+
// Route through the shared builder so both invalidation exit paths stay
|
|
1281
|
+
// in lockstep — empty input yields { error: { message: <fallback>,
|
|
1282
|
+
// code: "token_invalidated" } }.
|
|
1283
|
+
res.end(buildTokenInvalidationBody(""));
|
|
1284
|
+
await usageRecorder.record({
|
|
1285
|
+
outcome: "failure",
|
|
1286
|
+
statusCode: HTTP_STATUS.UNAUTHORIZED,
|
|
1287
|
+
errorCode: "token_invalidated",
|
|
1288
|
+
account: selected,
|
|
1289
|
+
});
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1064
1292
|
if (!refreshed.retryable)
|
|
1065
1293
|
continue;
|
|
1066
1294
|
transientAttempts += 1;
|
|
@@ -1112,7 +1340,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1112
1340
|
continue;
|
|
1113
1341
|
}
|
|
1114
1342
|
if (upstream.status === HTTP_STATUS.TOO_MANY_REQUESTS) {
|
|
1115
|
-
const bodyText = await readErrorBody(upstream);
|
|
1343
|
+
const bodyText = await readErrorBody(upstream, streamStallTimeoutMs);
|
|
1116
1344
|
const retryAfterMs = parseRetryAfterHeaderMs(upstream.headers, now()) ??
|
|
1117
1345
|
parseRetryAfterBodyMs(bodyText, now()) ??
|
|
1118
1346
|
60_000;
|
|
@@ -1129,7 +1357,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1129
1357
|
continue;
|
|
1130
1358
|
}
|
|
1131
1359
|
if (upstream.status === 402 || upstream.status === HTTP_STATUS.FORBIDDEN) {
|
|
1132
|
-
const bodyText = await readErrorBody(upstream);
|
|
1360
|
+
const bodyText = await readErrorBody(upstream, streamStallTimeoutMs);
|
|
1133
1361
|
const errorCode = extractErrorCodeFromBody(bodyText);
|
|
1134
1362
|
if (isWorkspaceDisabledError(upstream.status, errorCode, bodyText)) {
|
|
1135
1363
|
const accountWasEnabled = accountManager.getAccountByIndex(refreshed.account.index)?.enabled !==
|
|
@@ -1212,10 +1440,34 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1212
1440
|
return;
|
|
1213
1441
|
}
|
|
1214
1442
|
if (upstream.status === HTTP_STATUS.UNAUTHORIZED) {
|
|
1215
|
-
await readErrorBody(upstream);
|
|
1443
|
+
const bodyText = await readErrorBody(upstream, streamStallTimeoutMs);
|
|
1216
1444
|
accountManager.refundToken(refreshed.account, context.family, context.model);
|
|
1217
1445
|
accountManager.recordFailure(refreshed.account, context.family, context.model);
|
|
1218
|
-
|
|
1446
|
+
if (isTokenInvalidationError(bodyText)) {
|
|
1447
|
+
// The upstream explicitly revoked this OAuth token. Applying a long
|
|
1448
|
+
// cooldown prevents cascade invalidation: rapidly presenting each
|
|
1449
|
+
// account's token from the same IP triggers OpenAI's anti-abuse
|
|
1450
|
+
// detection and invalidates them in sequence. Return the 401 directly
|
|
1451
|
+
// rather than rotating so the client can prompt for re-login.
|
|
1452
|
+
applyMonotonicAuthCooldown(accountManager, refreshed.account, tokenInvalidationCooldownMs);
|
|
1453
|
+
sessionAffinityStore?.forgetSession(context.sessionKey);
|
|
1454
|
+
accountManager.saveToDiskDebounced();
|
|
1455
|
+
// Emit the same machine-readable shape as the refresh-failure path
|
|
1456
|
+
// (code: "token_invalidated") instead of forwarding the raw upstream
|
|
1457
|
+
// body, so the client contract is consistent across both vectors.
|
|
1458
|
+
const clientHeaders = responseHeadersForClient(upstream.headers);
|
|
1459
|
+
clientHeaders["content-type"] = "application/json";
|
|
1460
|
+
res.writeHead(upstream.status, clientHeaders);
|
|
1461
|
+
res.end(buildTokenInvalidationBody(bodyText));
|
|
1462
|
+
await usageRecorder.record({
|
|
1463
|
+
outcome: "failure",
|
|
1464
|
+
statusCode: upstream.status,
|
|
1465
|
+
errorCode: "token_invalidated",
|
|
1466
|
+
account: refreshed.account,
|
|
1467
|
+
});
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
applyMonotonicAuthCooldown(accountManager, refreshed.account, DEFAULT_AUTH_FAILURE_COOLDOWN_MS);
|
|
1219
1471
|
accountManager.saveToDiskDebounced();
|
|
1220
1472
|
exhaustionReason = "auth-failure";
|
|
1221
1473
|
transientAttempts += 1;
|
|
@@ -1225,7 +1477,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1225
1477
|
continue;
|
|
1226
1478
|
}
|
|
1227
1479
|
if (upstream.status >= 500) {
|
|
1228
|
-
await readErrorBody(upstream);
|
|
1480
|
+
await readErrorBody(upstream, streamStallTimeoutMs);
|
|
1229
1481
|
accountManager.refundToken(refreshed.account, context.family, context.model);
|
|
1230
1482
|
accountManager.recordFailure(refreshed.account, context.family, context.model);
|
|
1231
1483
|
accountManager.markAccountCoolingDown(refreshed.account, serverErrorCooldownMs, "server-error");
|
|
@@ -1266,6 +1518,10 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1266
1518
|
}
|
|
1267
1519
|
else {
|
|
1268
1520
|
sessionAffinityStore?.remember(context.sessionKey, refreshed.account.index, now());
|
|
1521
|
+
if (refreshed.account.index !== lastGlobalAccountIndex) {
|
|
1522
|
+
lastGlobalAccountIndex = refreshed.account.index;
|
|
1523
|
+
}
|
|
1524
|
+
lastGlobalSwitchAt = now();
|
|
1269
1525
|
}
|
|
1270
1526
|
await persistRuntimeActiveAccount(accountManager, refreshed.account, context.family, isPinned && refreshed.account.index === pinnedIndex);
|
|
1271
1527
|
const forwarded = await forwardStreamingResponse(upstream, res, status, () => {
|
|
@@ -1334,7 +1590,20 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1334
1590
|
}
|
|
1335
1591
|
}
|
|
1336
1592
|
catch (error) {
|
|
1337
|
-
|
|
1593
|
+
const rawErrorMessage = error instanceof Error ? error.message : String(error);
|
|
1594
|
+
// errors-logging-08: redact any email/token material that leaked into a
|
|
1595
|
+
// raw upstream or refresh error string before it reaches status consumers
|
|
1596
|
+
// or the structured log. maskString is a no-op for clean diagnostic text.
|
|
1597
|
+
const maskedErrorMessage = maskString(rawErrorMessage);
|
|
1598
|
+
status.lastError = maskedErrorMessage;
|
|
1599
|
+
// errors-logging-01: surface the failure through the structured logger
|
|
1600
|
+
// (redaction-safe) with the request trace id, instead of only stashing a
|
|
1601
|
+
// last-write-wins status string.
|
|
1602
|
+
proxyLog.error("runtime proxy request failed", {
|
|
1603
|
+
traceId,
|
|
1604
|
+
code: isRuntimeProxyHttpError(error) ? error.code : "codex_runtime_rotation_proxy_error",
|
|
1605
|
+
error: maskedErrorMessage,
|
|
1606
|
+
});
|
|
1338
1607
|
if (!res.headersSent) {
|
|
1339
1608
|
if (isRuntimeProxyHttpError(error)) {
|
|
1340
1609
|
await usageRecorder?.record({
|
|
@@ -1391,20 +1660,26 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1391
1660
|
};
|
|
1392
1661
|
server.once("error", onError);
|
|
1393
1662
|
server.once("listening", onListening);
|
|
1394
|
-
server.listen(port,
|
|
1663
|
+
server.listen(port, bindHost);
|
|
1395
1664
|
});
|
|
1396
1665
|
server.on("error", onPostStartupServerError);
|
|
1397
1666
|
const address = server.address();
|
|
1398
1667
|
const resolvedPort = typeof address === "object" && address ? address.port : port;
|
|
1399
1668
|
return {
|
|
1400
|
-
host,
|
|
1669
|
+
host: bindHost,
|
|
1401
1670
|
port: resolvedPort,
|
|
1402
|
-
baseUrl: `http://${
|
|
1671
|
+
baseUrl: `http://${urlHost}:${resolvedPort}`,
|
|
1403
1672
|
close: async () => {
|
|
1404
1673
|
await closeServer(server, sockets);
|
|
1405
1674
|
await activeAccountManager.flushPendingSave();
|
|
1406
1675
|
},
|
|
1407
|
-
getStatus: () => ({
|
|
1676
|
+
getStatus: () => ({
|
|
1677
|
+
...status,
|
|
1678
|
+
// Redact any email/token material that leaked into a raw upstream or
|
|
1679
|
+
// refresh error string before exposing it to status/report consumers
|
|
1680
|
+
// (errors-logging-08). maskString is a no-op for clean diagnostic text.
|
|
1681
|
+
lastError: status.lastError === null ? null : maskString(status.lastError),
|
|
1682
|
+
}),
|
|
1408
1683
|
};
|
|
1409
1684
|
}
|
|
1410
1685
|
async function closeServer(server, sockets) {
|