codex-multi-auth 2.22.0 → 2.24.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.
Files changed (117) hide show
  1. package/.codex-plugin/plugin.json +9 -9
  2. package/LICENSE +21 -21
  3. package/README.md +159 -150
  4. package/assets/codex-multi-auth-icon.svg +39 -39
  5. package/assets/readme-hero.svg +31 -31
  6. package/config/README.md +31 -31
  7. package/config/codex-legacy.json +1021 -1021
  8. package/config/codex-modern.json +347 -347
  9. package/config/minimal-codex.json +15 -15
  10. package/config/schema/config.schema.json +8 -0
  11. package/dist/index.js +33 -10
  12. package/dist/lib/account-policy.d.ts +2 -0
  13. package/dist/lib/account-policy.js +3 -1
  14. package/dist/lib/accounts/rate-limits.d.ts +6 -0
  15. package/dist/lib/accounts/rate-limits.js +14 -1
  16. package/dist/lib/accounts.d.ts +6 -1
  17. package/dist/lib/accounts.js +33 -8
  18. package/dist/lib/api-route-store.d.ts +1 -0
  19. package/dist/lib/api-route-store.js +2 -1
  20. package/dist/lib/auth/account-access.js +2 -1
  21. package/dist/lib/auth/auth.js +3 -2
  22. package/dist/lib/auth/device-auth.js +2 -1
  23. package/dist/lib/auth/http.d.ts +1 -0
  24. package/dist/lib/auth/http.js +1 -0
  25. package/dist/lib/auth/token-utils.d.ts +5 -0
  26. package/dist/lib/auth/token-utils.js +10 -0
  27. package/dist/lib/codex-manager/account-manager-commands.js +1 -0
  28. package/dist/lib/codex-manager/commands/account.js +15 -0
  29. package/dist/lib/codex-manager/commands/locale.d.ts +12 -0
  30. package/dist/lib/codex-manager/commands/locale.js +25 -0
  31. package/dist/lib/codex-manager/commands/resets.js +18 -5
  32. package/dist/lib/codex-manager/commands/status.d.ts +4 -0
  33. package/dist/lib/codex-manager/commands/status.js +476 -5
  34. package/dist/lib/codex-manager/commands/workspace.d.ts +4 -2
  35. package/dist/lib/codex-manager/commands/workspace.js +33 -2
  36. package/dist/lib/codex-manager/help.js +5 -3
  37. package/dist/lib/codex-manager.js +112 -30
  38. package/dist/lib/config.js +3 -0
  39. package/dist/lib/constants.d.ts +2 -0
  40. package/dist/lib/constants.js +2 -0
  41. package/dist/lib/context-budget-response.js +8 -8
  42. package/dist/lib/context-overflow.js +10 -10
  43. package/dist/lib/locale.d.ts +7 -0
  44. package/dist/lib/locale.js +35 -0
  45. package/dist/lib/oauth-success.html +125 -125
  46. package/dist/lib/policy/runtime-policy-cache.js +1 -1
  47. package/dist/lib/policy/runtime-policy.d.ts +1 -0
  48. package/dist/lib/policy/runtime-policy.js +4 -0
  49. package/dist/lib/preemptive-quota-scheduler.js +12 -3
  50. package/dist/lib/prompts/codex-host-bridge.js +138 -138
  51. package/dist/lib/prompts/codex.js +79 -79
  52. package/dist/lib/quota-cache.d.ts +4 -0
  53. package/dist/lib/quota-probe.js +2 -1
  54. package/dist/lib/quota-readiness.d.ts +1 -1
  55. package/dist/lib/quota-readiness.js +9 -2
  56. package/dist/lib/request/proxy-fetch.d.ts +5 -0
  57. package/dist/lib/request/proxy-fetch.js +37 -0
  58. package/dist/lib/request/rate-limit-decision.js +1 -1
  59. package/dist/lib/request/url-rewriting.js +17 -0
  60. package/dist/lib/runtime/account-model-catalog.d.ts +2 -0
  61. package/dist/lib/runtime/account-model-catalog.js +7 -2
  62. package/dist/lib/runtime/account-reset-credits.d.ts +11 -2
  63. package/dist/lib/runtime/account-reset-credits.js +35 -12
  64. package/dist/lib/runtime/app-bind.d.ts +4 -1
  65. package/dist/lib/runtime/app-bind.js +63 -3
  66. package/dist/lib/runtime/automatic-account-checks.js +6 -5
  67. package/dist/lib/runtime/automatic-subscription-checks.js +4 -3
  68. package/dist/lib/runtime/model-discovery-status.js +2 -2
  69. package/dist/lib/runtime/reset-credit-routing.d.ts +11 -3
  70. package/dist/lib/runtime/reset-credit-routing.js +66 -21
  71. package/dist/lib/runtime/reset-credits.d.ts +31 -7
  72. package/dist/lib/runtime/reset-credits.js +135 -28
  73. package/dist/lib/runtime/responses-websocket.d.ts +30 -0
  74. package/dist/lib/runtime/responses-websocket.js +243 -29
  75. package/dist/lib/runtime/rotation-account-selection.d.ts +5 -4
  76. package/dist/lib/runtime/rotation-proxy-state.d.ts +3 -0
  77. package/dist/lib/runtime/rotation-server-types.d.ts +4 -1
  78. package/dist/lib/runtime/subscription-quota-order.d.ts +1 -0
  79. package/dist/lib/runtime/subscription-quota-order.js +7 -3
  80. package/dist/lib/runtime/workspace-model-scopes.d.ts +2 -0
  81. package/dist/lib/runtime/workspace-model-scopes.js +14 -4
  82. package/dist/lib/runtime-rotation-proxy.js +138 -38
  83. package/dist/lib/schemas.d.ts +5 -0
  84. package/dist/lib/schemas.js +1 -0
  85. package/dist/lib/storage/snapshot-merge.js +15 -4
  86. package/dist/lib/web-dashboard-html.d.ts +1 -0
  87. package/dist/lib/web-dashboard-html.js +66 -0
  88. package/dist/lib/web-dashboard.d.ts +36 -0
  89. package/dist/lib/web-dashboard.js +322 -0
  90. package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
  91. package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
  92. package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
  93. package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
  94. package/node_modules/@codex-ai/plugin/package.json +9 -9
  95. package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
  96. package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
  97. package/node_modules/@codex-ai/sdk/package.json +8 -8
  98. package/package.json +209 -207
  99. package/scripts/codex-app-launcher.js +653 -653
  100. package/scripts/codex-app-router.js +446 -402
  101. package/scripts/codex-bin-resolver.js +332 -332
  102. package/scripts/codex-multi-auth.js +65 -65
  103. package/scripts/codex-routing.js +63 -62
  104. package/scripts/codex.js +7143 -7006
  105. package/scripts/install-codex-auth-utils.js +213 -213
  106. package/scripts/mcodex.js +367 -367
  107. package/scripts/postinstall.js +114 -114
  108. package/scripts/preuninstall.js +265 -265
  109. package/skills/codex-auth-setup/SKILL.md +73 -73
  110. package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
  111. package/vendor/codex-ai-plugin/dist/index.js +2 -2
  112. package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
  113. package/vendor/codex-ai-plugin/dist/tool.js +29 -29
  114. package/vendor/codex-ai-plugin/package.json +9 -9
  115. package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
  116. package/vendor/codex-ai-sdk/dist/index.js +2 -2
  117. package/vendor/codex-ai-sdk/package.json +8 -8
@@ -1,402 +1,446 @@
1
- #!/usr/bin/env node
2
-
3
- import {
4
- chmodSync,
5
- closeSync,
6
- fstatSync,
7
- ftruncateSync,
8
- mkdirSync,
9
- openSync,
10
- readFileSync,
11
- renameSync,
12
- rmSync,
13
- statSync,
14
- truncateSync,
15
- writeSync,
16
- writeFileSync,
17
- } from "node:fs";
18
- import { basename, dirname, join } from "node:path";
19
- import process from "node:process";
20
-
21
- const DEFAULT_MAX_LOG_BYTES = 1024 * 1024;
22
- const LOG_SIZE_CHECK_INTERVAL_MS = 60_000;
23
-
24
- function parsePort(value) {
25
- if (typeof value !== "string" && typeof value !== "number") return Number.NaN;
26
- const text = String(value).trim();
27
- if (!/^\d+$/.test(text)) return Number.NaN;
28
- const port = Number(text);
29
- return Number.isInteger(port) && port >= 0 && port <= 65535
30
- ? port
31
- : Number.NaN;
32
- }
33
-
34
- function parseArgs(argv) {
35
- const result = {
36
- host: "127.0.0.1",
37
- port: 0,
38
- statusPath: "",
39
- identityToken: "",
40
- statePath: "",
41
- logPath: "",
42
- maxLogBytes: DEFAULT_MAX_LOG_BYTES,
43
- };
44
- for (let index = 0; index < argv.length; index += 1) {
45
- const arg = argv[index];
46
- const next = argv[index + 1] ?? "";
47
- if (arg === "--host") {
48
- result.host = next;
49
- index += 1;
50
- continue;
51
- }
52
- if (arg === "--port") {
53
- result.port = parsePort(next);
54
- index += 1;
55
- continue;
56
- }
57
- if (arg === "--status") {
58
- result.statusPath = next;
59
- index += 1;
60
- continue;
61
- }
62
- if (arg === "--identity-token") {
63
- result.identityToken = next;
64
- index += 1;
65
- continue;
66
- }
67
- if (arg === "--state") {
68
- result.statePath = next;
69
- index += 1;
70
- continue;
71
- }
72
- if (arg === "--max-log-bytes") {
73
- const parsed = Number.parseInt(next, 10);
74
- result.maxLogBytes =
75
- Number.isFinite(parsed) && parsed > 0
76
- ? parsed
77
- : DEFAULT_MAX_LOG_BYTES;
78
- index += 1;
79
- continue;
80
- }
81
- if (arg === "--log") {
82
- result.logPath = next;
83
- index += 1;
84
- }
85
- }
86
- return result;
87
- }
88
-
89
- function readState(path) {
90
- if (!path) return null;
91
- try {
92
- return JSON.parse(readFileSync(path, "utf8"));
93
- } catch {
94
- return null;
95
- }
96
- }
97
-
98
- function readTrimmedString(record, key) {
99
- const value = record?.[key];
100
- return typeof value === "string" && value.trim().length > 0
101
- ? value.trim()
102
- : undefined;
103
- }
104
-
105
- function writeStatus(statusPath, payload) {
106
- if (!statusPath) return;
107
- const statusDir = dirname(statusPath);
108
- const tempPath = join(
109
- statusDir,
110
- [
111
- `.${basename(statusPath)}`,
112
- String(process.pid),
113
- String(Date.now()),
114
- "tmp",
115
- ].join("."),
116
- );
117
- let fd = null;
118
- try {
119
- mkdirSync(statusDir, { recursive: true });
120
- fd = openSync(tempPath, "w", 0o600);
121
- writeFileSync(fd, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
122
- closeSync(fd);
123
- fd = null;
124
- chmodSync(tempPath, 0o600);
125
- renameSync(tempPath, statusPath);
126
- chmodSync(statusPath, 0o600);
127
- } catch {
128
- // Status is best-effort. The router should keep serving if telemetry is locked.
129
- try {
130
- if (fd !== null) closeSync(fd);
131
- } catch {
132
- // Preserve the original status-write failure.
133
- }
134
- try {
135
- rmSync(tempPath, { force: true });
136
- } catch {
137
- // Preserve the original status-write failure.
138
- }
139
- }
140
- }
141
-
142
- // Status history is untrusted local JSON; retain only safe operational fields.
143
- function readDisconnect(value) {
144
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
145
- const { at, cause, stage, closeCode, routeKind, connectionAgeMs, eventCount } = value;
146
- if (typeof at !== "number" || !Number.isFinite(at) || at < 0 || at > 8.64e15) return null;
147
- if (!["close", "transport_error", "invalid_event", "buffer_limit", "send_error"].includes(cause)) return null;
148
- if (!["awaiting_first_event", "streaming"].includes(stage)) return null;
149
- if (routeKind !== undefined && !["oauth", "api", "zdr"].includes(routeKind)) return null;
150
- if (closeCode !== undefined && (!Number.isInteger(closeCode) || closeCode < 1000 || closeCode > 4999)) return null;
151
- for (const count of [connectionAgeMs, eventCount]) {
152
- if (count !== undefined && (!Number.isSafeInteger(count) || count < 0)) return null;
153
- }
154
- return {
155
- at, cause, stage,
156
- ...(closeCode !== undefined ? { closeCode } : {}),
157
- ...(routeKind !== undefined ? { routeKind } : {}),
158
- ...(connectionAgeMs !== undefined ? { connectionAgeMs } : {}),
159
- ...(eventCount !== undefined ? { eventCount } : {}),
160
- };
161
- }
162
-
163
- function createStatusPayload({
164
- state,
165
- proxyServer,
166
- error,
167
- stateRecord,
168
- startedAt,
169
- statusPath,
170
- routerScriptPath,
171
- identityToken,
172
- previousDisconnect = null,
173
- }) {
174
- const proxyStatus =
175
- typeof proxyServer?.getStatus === "function" ? proxyServer.getStatus() : {};
176
- const currentDisconnect = readDisconnect(proxyStatus.lastWebsocketDisconnect);
177
- // A current-process event is newer history even after wall-clock rollback.
178
- const lastDisconnect = currentDisconnect ?? previousDisconnect;
179
- const lastAccountIndex = proxyStatus.lastAccountIndex ?? null;
180
- const lastAccountLabel =
181
- typeof proxyStatus.lastAccountLabel === "string" &&
182
- !proxyStatus.lastAccountLabel.includes("@")
183
- ? proxyStatus.lastAccountLabel
184
- : typeof lastAccountIndex === "number"
185
- ? `Account ${lastAccountIndex + 1}`
186
- : null;
187
- return {
188
- version: 1,
189
- kind: "codex-app-runtime-rotation-router",
190
- state,
191
- pid: process.pid,
192
- startedAt,
193
- statusPath: statusPath || readTrimmedString(stateRecord, "statusPath") || null,
194
- identityToken:
195
- identityToken || readTrimmedString(stateRecord, "identityToken") || null,
196
- routerScriptPath:
197
- routerScriptPath ||
198
- readTrimmedString(stateRecord, "routerScriptPath") ||
199
- process.argv[1] ||
200
- null,
201
- updatedAt: Date.now(),
202
- baseUrl: proxyServer?.baseUrl ?? stateRecord?.baseUrl ?? null,
203
- schedulingStrategy: proxyStatus.schedulingStrategy,
204
- totalRequests: proxyStatus.totalRequests ?? 0,
205
- upstreamRequests: proxyStatus.upstreamRequests ?? 0,
206
- websocketConnections: proxyStatus.websocketConnections ?? 0,
207
- websocketUpstreamRequests: proxyStatus.websocketUpstreamRequests ?? 0,
208
- lastWebsocketDisconnect: lastDisconnect,
209
- streamQuotaUpdates: proxyStatus.streamQuotaUpdates ?? 0,
210
- lastStreamQuotaUpdateAt: proxyStatus.lastStreamQuotaUpdateAt ?? null,
211
- retries: proxyStatus.retries ?? 0,
212
- lunaReserveFallbacks: proxyStatus.lunaReserveFallbacks ?? 0,
213
- rotations: proxyStatus.rotations ?? 0,
214
- lastAccountIndex,
215
- lastAccountLabel,
216
- lastAccountId: proxyStatus.lastAccountId ?? null,
217
- lastRequestedWorkspaceId: proxyStatus.lastRequestedWorkspaceId ?? null,
218
- lastAccountUpdatedAt: proxyStatus.lastAccountUpdatedAt ?? null,
219
- lastError: error ? (error instanceof Error ? error.message : String(error)) : proxyStatus.lastError ?? null,
220
- };
221
- }
222
-
223
- function isLoopbackHost(host) {
224
- if (typeof host !== "string") return false;
225
- const normalized = host.trim().toLowerCase();
226
- const unbracketed =
227
- normalized.startsWith("[") && normalized.endsWith("]")
228
- ? normalized.slice(1, -1)
229
- : normalized;
230
- return (
231
- unbracketed === "127.0.0.1" ||
232
- unbracketed === "::1" ||
233
- unbracketed === "localhost"
234
- );
235
- }
236
-
237
- function truncateLogFdIfTooLarge(fd, maxBytes) {
238
- if (!Number.isFinite(maxBytes) || maxBytes <= 0) return;
239
- try {
240
- const stats = fstatSync(fd);
241
- if (!stats.isFile() || stats.size <= maxBytes) return;
242
- ftruncateSync(fd, 0);
243
- writeSync(
244
- fd,
245
- `codex-multi-auth app router log truncated after exceeding ${maxBytes} bytes\n`,
246
- );
247
- } catch {
248
- // stdout/stderr may be pipes or otherwise unavailable; logging must not fail startup.
249
- }
250
- }
251
-
252
- function truncateLogPathIfTooLarge(logPath, maxBytes) {
253
- if (!logPath || !Number.isFinite(maxBytes) || maxBytes <= 0) return false;
254
- try {
255
- const stats = statSync(logPath);
256
- if (!stats.isFile() || stats.size <= maxBytes) return false;
257
- truncateSync(logPath, 0);
258
- return true;
259
- } catch {
260
- // Log path may not exist yet or may be locked; fd-level checks can still work.
261
- return false;
262
- }
263
- }
264
-
265
- function writeLogTruncatedMarker(maxBytes) {
266
- try {
267
- writeSync(
268
- 1,
269
- `codex-multi-auth app router log truncated after exceeding ${maxBytes} bytes\n`,
270
- );
271
- } catch {
272
- // A closed stdout/stderr should not crash the router while enforcing log bounds.
273
- }
274
- }
275
-
276
- function installLogBounds(maxBytes, logPath) {
277
- if (truncateLogPathIfTooLarge(logPath, maxBytes)) {
278
- writeLogTruncatedMarker(maxBytes);
279
- }
280
- truncateLogFdIfTooLarge(1, maxBytes);
281
- truncateLogFdIfTooLarge(2, maxBytes);
282
- return setInterval(() => {
283
- if (truncateLogPathIfTooLarge(logPath, maxBytes)) {
284
- writeLogTruncatedMarker(maxBytes);
285
- }
286
- truncateLogFdIfTooLarge(1, maxBytes);
287
- truncateLogFdIfTooLarge(2, maxBytes);
288
- }, LOG_SIZE_CHECK_INTERVAL_MS);
289
- }
290
-
291
- async function main() {
292
- const routerStartedAt = Date.now();
293
- const args = parseArgs(process.argv.slice(2));
294
- installLogBounds(args.maxLogBytes, args.logPath).unref?.();
295
- const stateRecord = readState(args.statePath);
296
- let previousDisconnect = readDisconnect(readState(args.statusPath || stateRecord?.statusPath || "")?.lastWebsocketDisconnect);
297
- if (args.statePath && stateRecord === null) {
298
- const error = new Error(
299
- "Codex app runtime router state is unreadable; refusing to bind an ephemeral port.",
300
- );
301
- writeStatus(
302
- args.statusPath,
303
- createStatusPayload({
304
- previousDisconnect,
305
- state: "error",
306
- proxyServer: null,
307
- error,
308
- stateRecord: null,
309
- startedAt: routerStartedAt,
310
- statusPath: args.statusPath,
311
- routerScriptPath: process.argv[1],
312
- identityToken: args.identityToken,
313
- }),
314
- );
315
- throw error;
316
- }
317
- const host =
318
- typeof stateRecord?.host === "string" && stateRecord.host.trim().length > 0
319
- ? stateRecord.host.trim()
320
- : args.host;
321
- const statePort = parsePort(stateRecord?.port);
322
- const port = Number.isFinite(statePort) ? statePort : args.port;
323
- const clientApiKey = readTrimmedString(stateRecord, "clientApiKey");
324
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
325
- throw new Error(
326
- "A valid --port in the range 0-65535 is required for the Codex app runtime router.",
327
- );
328
- }
329
- if (!isLoopbackHost(host)) {
330
- throw new Error(
331
- "Codex app runtime router host must be loopback-only (127.0.0.1, ::1, or localhost).",
332
- );
333
- }
334
- if (!clientApiKey) {
335
- throw new Error(
336
- "Codex app runtime router state is missing its client token.",
337
- );
338
- }
339
-
340
- let proxyServer = null;
341
- const writeCurrentStatus = (state, error) => {
342
- const payload = createStatusPayload({
343
- previousDisconnect,
344
- state,
345
- proxyServer,
346
- error,
347
- stateRecord,
348
- startedAt: routerStartedAt,
349
- statusPath: args.statusPath || stateRecord?.statusPath || "",
350
- routerScriptPath: stateRecord?.routerScriptPath || process.argv[1],
351
- identityToken: args.identityToken || stateRecord?.identityToken || "",
352
- });
353
- previousDisconnect = payload.lastWebsocketDisconnect;
354
- writeStatus(args.statusPath || stateRecord?.statusPath || "", payload);
355
- };
356
-
357
- try {
358
- const proxyModule = await import("../dist/lib/runtime-rotation-proxy.js");
359
- proxyServer = await proxyModule.startRuntimeRotationProxy({
360
- nativeOpenai: stateRecord?.nativeOpenai === true,
361
- catalogAccount: stateRecord?.catalogAccount,
362
- host,
363
- port,
364
- clientApiKey,
365
- });
366
- writeCurrentStatus("running");
367
- const timer = setInterval(() => writeCurrentStatus("running"), 1000);
368
- let cleanupPromise = null;
369
- const cleanup = async (state) => {
370
- clearInterval(timer);
371
- try {
372
- await proxyServer?.close?.();
373
- } finally {
374
- writeCurrentStatus(state);
375
- }
376
- };
377
- const cleanupOnce = (state) => {
378
- cleanupPromise ??= cleanup(state);
379
- return cleanupPromise;
380
- };
381
- process.once("SIGINT", () => {
382
- void cleanupOnce("stopped").finally(() => process.exit(130));
383
- });
384
- process.once("SIGTERM", () => {
385
- void cleanupOnce("stopped").finally(() => process.exit(0));
386
- });
387
- process.once("SIGHUP", () => {
388
- void cleanupOnce("stopped").finally(() => process.exit(0));
389
- });
390
- await new Promise(() => undefined);
391
- } catch (error) {
392
- writeCurrentStatus("error", error);
393
- throw error;
394
- }
395
- }
396
-
397
- main().catch((error) => {
398
- console.error(
399
- `codex-multi-auth app router failed: ${error instanceof Error ? error.message : String(error)}`,
400
- );
401
- process.exitCode = 1;
402
- });
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ chmodSync,
5
+ closeSync,
6
+ fstatSync,
7
+ ftruncateSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readFileSync,
11
+ renameSync,
12
+ rmSync,
13
+ statSync,
14
+ truncateSync,
15
+ writeSync,
16
+ writeFileSync,
17
+ } from "node:fs";
18
+ import { basename, dirname, join } from "node:path";
19
+ import process from "node:process";
20
+
21
+ const DEFAULT_MAX_LOG_BYTES = 1024 * 1024;
22
+ const LOG_SIZE_CHECK_INTERVAL_MS = 60_000;
23
+
24
+ function parsePort(value) {
25
+ if (typeof value !== "string" && typeof value !== "number") return Number.NaN;
26
+ const text = String(value).trim();
27
+ if (!/^\d+$/.test(text)) return Number.NaN;
28
+ const port = Number(text);
29
+ return Number.isInteger(port) && port >= 0 && port <= 65535
30
+ ? port
31
+ : Number.NaN;
32
+ }
33
+
34
+ function parseArgs(argv) {
35
+ const result = {
36
+ host: "127.0.0.1",
37
+ port: 0,
38
+ statusPath: "",
39
+ identityToken: "",
40
+ statePath: "",
41
+ logPath: "",
42
+ maxLogBytes: DEFAULT_MAX_LOG_BYTES,
43
+ };
44
+ for (let index = 0; index < argv.length; index += 1) {
45
+ const arg = argv[index];
46
+ const next = argv[index + 1] ?? "";
47
+ if (arg === "--host") {
48
+ result.host = next;
49
+ index += 1;
50
+ continue;
51
+ }
52
+ if (arg === "--port") {
53
+ result.port = parsePort(next);
54
+ index += 1;
55
+ continue;
56
+ }
57
+ if (arg === "--status") {
58
+ result.statusPath = next;
59
+ index += 1;
60
+ continue;
61
+ }
62
+ if (arg === "--identity-token") {
63
+ result.identityToken = next;
64
+ index += 1;
65
+ continue;
66
+ }
67
+ if (arg === "--state") {
68
+ result.statePath = next;
69
+ index += 1;
70
+ continue;
71
+ }
72
+ if (arg === "--max-log-bytes") {
73
+ const parsed = Number.parseInt(next, 10);
74
+ result.maxLogBytes =
75
+ Number.isFinite(parsed) && parsed > 0
76
+ ? parsed
77
+ : DEFAULT_MAX_LOG_BYTES;
78
+ index += 1;
79
+ continue;
80
+ }
81
+ if (arg === "--log") {
82
+ result.logPath = next;
83
+ index += 1;
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+
89
+ function readState(path) {
90
+ if (!path) return null;
91
+ try {
92
+ return JSON.parse(readFileSync(path, "utf8"));
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ function readTrimmedString(record, key) {
99
+ const value = record?.[key];
100
+ return typeof value === "string" && value.trim().length > 0
101
+ ? value.trim()
102
+ : undefined;
103
+ }
104
+
105
+ function writeStatus(statusPath, payload) {
106
+ if (!statusPath) return;
107
+ const statusDir = dirname(statusPath);
108
+ const tempPath = join(
109
+ statusDir,
110
+ [
111
+ `.${basename(statusPath)}`,
112
+ String(process.pid),
113
+ String(Date.now()),
114
+ "tmp",
115
+ ].join("."),
116
+ );
117
+ let fd = null;
118
+ try {
119
+ mkdirSync(statusDir, { recursive: true });
120
+ fd = openSync(tempPath, "w", 0o600);
121
+ writeFileSync(fd, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
122
+ closeSync(fd);
123
+ fd = null;
124
+ chmodSync(tempPath, 0o600);
125
+ renameSync(tempPath, statusPath);
126
+ chmodSync(statusPath, 0o600);
127
+ } catch {
128
+ // Status is best-effort. The router should keep serving if telemetry is locked.
129
+ try {
130
+ if (fd !== null) closeSync(fd);
131
+ } catch {
132
+ // Preserve the original status-write failure.
133
+ }
134
+ try {
135
+ rmSync(tempPath, { force: true });
136
+ } catch {
137
+ // Preserve the original status-write failure.
138
+ }
139
+ }
140
+ }
141
+
142
+ // Status history is untrusted local JSON; retain only safe operational fields.
143
+ function readDisconnect(value) {
144
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
145
+ const { at, cause, stage, closeCode, routeKind, connectionAgeMs, eventCount } = value;
146
+ const transport = {};
147
+ for (const key of ["requestBytes", "inputItemCount"]) {
148
+ const field = value[key];
149
+ if (field !== undefined) {
150
+ if (typeof field !== "number" || !Number.isSafeInteger(field) || field < 0) return null;
151
+ transport[key] = field;
152
+ }
153
+ }
154
+ for (const key of ["isPrewarm", "hasPreviousResponse", "reusedConnection", "sendCompleted", "compressionNegotiated", "tcpEndReceived", "tcpCloseHadError"]) {
155
+ const field = value[key];
156
+ if (field !== undefined) {
157
+ if (typeof field !== "boolean") return null;
158
+ transport[key] = field;
159
+ }
160
+ }
161
+ if (value.transportErrorCode !== undefined) {
162
+ if (!["ECONNRESET", "EPIPE", "ETIMEDOUT", "OTHER"].includes(value.transportErrorCode)) return null;
163
+ transport.transportErrorCode = value.transportErrorCode;
164
+ }
165
+ if (typeof at !== "number" || !Number.isFinite(at) || at < 0 || at > 8.64e15) return null;
166
+ if (!["close", "transport_error", "invalid_event", "buffer_limit", "send_error"].includes(cause)) return null;
167
+ if (!["awaiting_first_event", "streaming"].includes(stage)) return null;
168
+ if (routeKind !== undefined && !["oauth", "api", "zdr"].includes(routeKind)) return null;
169
+ if (closeCode !== undefined && (!Number.isInteger(closeCode) || closeCode < 1000 || closeCode > 4999)) return null;
170
+ for (const count of [connectionAgeMs, eventCount]) {
171
+ if (count !== undefined && (!Number.isSafeInteger(count) || count < 0)) return null;
172
+ }
173
+ return {
174
+ at, cause, stage,
175
+ ...(closeCode !== undefined ? { closeCode } : {}),
176
+ ...(routeKind !== undefined ? { routeKind } : {}),
177
+ ...(connectionAgeMs !== undefined ? { connectionAgeMs } : {}),
178
+ ...(eventCount !== undefined ? { eventCount } : {}),
179
+ ...transport,
180
+ };
181
+ }
182
+
183
+ function readWebsocketCounter(value, maximum = Number.MAX_SAFE_INTEGER) {
184
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= maximum
185
+ ? value : null;
186
+ }
187
+
188
+ function readUpgradeRejection(value) {
189
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
190
+ const { at, reason, status, activeConnections } = value;
191
+ if (readWebsocketCounter(at, 8.64e15) === null || readWebsocketCounter(activeConnections, 64) === null) return null;
192
+ if (!((reason === "origin" && status === 403) ||
193
+ ((reason === "capacity" || reason === "repeated_disconnect") && status === 426))) return null;
194
+ return { at, reason, status, activeConnections };
195
+ }
196
+
197
+ function createStatusPayload({
198
+ state,
199
+ proxyServer,
200
+ error,
201
+ stateRecord,
202
+ startedAt,
203
+ statusPath,
204
+ routerScriptPath,
205
+ identityToken,
206
+ previousDisconnect = null,
207
+ previousUpgradeRejection = null,
208
+ }) {
209
+ const proxyStatus =
210
+ typeof proxyServer?.getStatus === "function" ? proxyServer.getStatus() : {};
211
+ const currentDisconnect = readDisconnect(proxyStatus.lastWebsocketDisconnect);
212
+ // A current-process event is newer history even after wall-clock rollback.
213
+ const lastDisconnect = currentDisconnect ?? previousDisconnect;
214
+ const lastUpgradeRejection = readUpgradeRejection(proxyStatus.lastWebsocketUpgradeRejection) ?? previousUpgradeRejection;
215
+ const lastAccountIndex = proxyStatus.lastAccountIndex ?? null;
216
+ const lastAccountLabel =
217
+ typeof proxyStatus.lastAccountLabel === "string" &&
218
+ !proxyStatus.lastAccountLabel.includes("@")
219
+ ? proxyStatus.lastAccountLabel
220
+ : typeof lastAccountIndex === "number"
221
+ ? `Account ${lastAccountIndex + 1}`
222
+ : null;
223
+ return {
224
+ version: 1,
225
+ kind: "codex-app-runtime-rotation-router",
226
+ state,
227
+ pid: process.pid,
228
+ startedAt,
229
+ statusPath: statusPath || readTrimmedString(stateRecord, "statusPath") || null,
230
+ identityToken:
231
+ identityToken || readTrimmedString(stateRecord, "identityToken") || null,
232
+ routerScriptPath:
233
+ routerScriptPath ||
234
+ readTrimmedString(stateRecord, "routerScriptPath") ||
235
+ process.argv[1] ||
236
+ null,
237
+ updatedAt: Date.now(),
238
+ baseUrl: proxyServer?.baseUrl ?? stateRecord?.baseUrl ?? null,
239
+ schedulingStrategy: proxyStatus.schedulingStrategy,
240
+ totalRequests: proxyStatus.totalRequests ?? 0,
241
+ upstreamRequests: proxyStatus.upstreamRequests ?? 0,
242
+ websocketConnections: proxyStatus.websocketConnections ?? 0,
243
+ websocketUpstreamRequests: proxyStatus.websocketUpstreamRequests ?? 0,
244
+ websocketActiveConnections: readWebsocketCounter(proxyStatus.websocketActiveConnections, 64) ?? 0,
245
+ websocketCapacityReclaims: readWebsocketCounter(proxyStatus.websocketCapacityReclaims) ?? 0,
246
+ lastWebsocketUpgradeRejection: lastUpgradeRejection,
247
+ lastWebsocketDisconnect: lastDisconnect,
248
+ streamQuotaUpdates: proxyStatus.streamQuotaUpdates ?? 0,
249
+ lastStreamQuotaUpdateAt: proxyStatus.lastStreamQuotaUpdateAt ?? null,
250
+ retries: proxyStatus.retries ?? 0,
251
+ lunaReserveFallbacks: proxyStatus.lunaReserveFallbacks ?? 0,
252
+ rotations: proxyStatus.rotations ?? 0,
253
+ lastAccountIndex,
254
+ lastAccountLabel,
255
+ lastAccountId: proxyStatus.lastAccountId ?? null,
256
+ lastRequestedWorkspaceId: proxyStatus.lastRequestedWorkspaceId ?? null,
257
+ lastAccountUpdatedAt: proxyStatus.lastAccountUpdatedAt ?? null,
258
+ lastError: error ? (error instanceof Error ? error.message : String(error)) : proxyStatus.lastError ?? null,
259
+ };
260
+ }
261
+
262
+ function isLoopbackHost(host) {
263
+ if (typeof host !== "string") return false;
264
+ const normalized = host.trim().toLowerCase();
265
+ const unbracketed =
266
+ normalized.startsWith("[") && normalized.endsWith("]")
267
+ ? normalized.slice(1, -1)
268
+ : normalized;
269
+ return (
270
+ unbracketed === "127.0.0.1" ||
271
+ unbracketed === "::1" ||
272
+ unbracketed === "localhost"
273
+ );
274
+ }
275
+
276
+ function truncateLogFdIfTooLarge(fd, maxBytes) {
277
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0) return;
278
+ try {
279
+ const stats = fstatSync(fd);
280
+ if (!stats.isFile() || stats.size <= maxBytes) return;
281
+ ftruncateSync(fd, 0);
282
+ writeSync(
283
+ fd,
284
+ `codex-multi-auth app router log truncated after exceeding ${maxBytes} bytes\n`,
285
+ );
286
+ } catch {
287
+ // stdout/stderr may be pipes or otherwise unavailable; logging must not fail startup.
288
+ }
289
+ }
290
+
291
+ function truncateLogPathIfTooLarge(logPath, maxBytes) {
292
+ if (!logPath || !Number.isFinite(maxBytes) || maxBytes <= 0) return false;
293
+ try {
294
+ const stats = statSync(logPath);
295
+ if (!stats.isFile() || stats.size <= maxBytes) return false;
296
+ truncateSync(logPath, 0);
297
+ return true;
298
+ } catch {
299
+ // Log path may not exist yet or may be locked; fd-level checks can still work.
300
+ return false;
301
+ }
302
+ }
303
+
304
+ function writeLogTruncatedMarker(maxBytes) {
305
+ try {
306
+ writeSync(
307
+ 1,
308
+ `codex-multi-auth app router log truncated after exceeding ${maxBytes} bytes\n`,
309
+ );
310
+ } catch {
311
+ // A closed stdout/stderr should not crash the router while enforcing log bounds.
312
+ }
313
+ }
314
+
315
+ function installLogBounds(maxBytes, logPath) {
316
+ if (truncateLogPathIfTooLarge(logPath, maxBytes)) {
317
+ writeLogTruncatedMarker(maxBytes);
318
+ }
319
+ truncateLogFdIfTooLarge(1, maxBytes);
320
+ truncateLogFdIfTooLarge(2, maxBytes);
321
+ return setInterval(() => {
322
+ if (truncateLogPathIfTooLarge(logPath, maxBytes)) {
323
+ writeLogTruncatedMarker(maxBytes);
324
+ }
325
+ truncateLogFdIfTooLarge(1, maxBytes);
326
+ truncateLogFdIfTooLarge(2, maxBytes);
327
+ }, LOG_SIZE_CHECK_INTERVAL_MS);
328
+ }
329
+
330
+ async function main() {
331
+ const routerStartedAt = Date.now();
332
+ const args = parseArgs(process.argv.slice(2));
333
+ installLogBounds(args.maxLogBytes, args.logPath).unref?.();
334
+ const stateRecord = readState(args.statePath);
335
+ const previousStatus = readState(args.statusPath || stateRecord?.statusPath || "");
336
+ let previousDisconnect = readDisconnect(previousStatus?.lastWebsocketDisconnect);
337
+ let previousUpgradeRejection = readUpgradeRejection(previousStatus?.lastWebsocketUpgradeRejection);
338
+ if (args.statePath && stateRecord === null) {
339
+ const error = new Error(
340
+ "Codex app runtime router state is unreadable; refusing to bind an ephemeral port.",
341
+ );
342
+ writeStatus(
343
+ args.statusPath,
344
+ createStatusPayload({
345
+ previousDisconnect,
346
+ previousUpgradeRejection,
347
+ state: "error",
348
+ proxyServer: null,
349
+ error,
350
+ stateRecord: null,
351
+ startedAt: routerStartedAt,
352
+ statusPath: args.statusPath,
353
+ routerScriptPath: process.argv[1],
354
+ identityToken: args.identityToken,
355
+ }),
356
+ );
357
+ throw error;
358
+ }
359
+ const host =
360
+ typeof stateRecord?.host === "string" && stateRecord.host.trim().length > 0
361
+ ? stateRecord.host.trim()
362
+ : args.host;
363
+ const statePort = parsePort(stateRecord?.port);
364
+ const port = Number.isFinite(statePort) ? statePort : args.port;
365
+ const clientApiKey = readTrimmedString(stateRecord, "clientApiKey");
366
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
367
+ throw new Error(
368
+ "A valid --port in the range 0-65535 is required for the Codex app runtime router.",
369
+ );
370
+ }
371
+ if (!isLoopbackHost(host)) {
372
+ throw new Error(
373
+ "Codex app runtime router host must be loopback-only (127.0.0.1, ::1, or localhost).",
374
+ );
375
+ }
376
+ if (!clientApiKey) {
377
+ throw new Error(
378
+ "Codex app runtime router state is missing its client token.",
379
+ );
380
+ }
381
+
382
+ let proxyServer = null;
383
+ const writeCurrentStatus = (state, error) => {
384
+ const payload = createStatusPayload({
385
+ previousDisconnect,
386
+ previousUpgradeRejection,
387
+ state,
388
+ proxyServer,
389
+ error,
390
+ stateRecord,
391
+ startedAt: routerStartedAt,
392
+ statusPath: args.statusPath || stateRecord?.statusPath || "",
393
+ routerScriptPath: stateRecord?.routerScriptPath || process.argv[1],
394
+ identityToken: args.identityToken || stateRecord?.identityToken || "",
395
+ });
396
+ previousDisconnect = payload.lastWebsocketDisconnect;
397
+ previousUpgradeRejection = payload.lastWebsocketUpgradeRejection;
398
+ writeStatus(args.statusPath || stateRecord?.statusPath || "", payload);
399
+ };
400
+
401
+ try {
402
+ const proxyModule = await import("../dist/lib/runtime-rotation-proxy.js");
403
+ proxyServer = await proxyModule.startRuntimeRotationProxy({
404
+ nativeOpenai: stateRecord?.nativeOpenai === true,
405
+ catalogAccount: stateRecord?.catalogAccount,
406
+ host,
407
+ port,
408
+ clientApiKey,
409
+ });
410
+ writeCurrentStatus("running");
411
+ const timer = setInterval(() => writeCurrentStatus("running"), 1000);
412
+ let cleanupPromise = null;
413
+ const cleanup = async (state) => {
414
+ clearInterval(timer);
415
+ try {
416
+ await proxyServer?.close?.();
417
+ } finally {
418
+ writeCurrentStatus(state);
419
+ }
420
+ };
421
+ const cleanupOnce = (state) => {
422
+ cleanupPromise ??= cleanup(state);
423
+ return cleanupPromise;
424
+ };
425
+ process.once("SIGINT", () => {
426
+ void cleanupOnce("stopped").finally(() => process.exit(130));
427
+ });
428
+ process.once("SIGTERM", () => {
429
+ void cleanupOnce("stopped").finally(() => process.exit(0));
430
+ });
431
+ process.once("SIGHUP", () => {
432
+ void cleanupOnce("stopped").finally(() => process.exit(0));
433
+ });
434
+ await new Promise(() => undefined);
435
+ } catch (error) {
436
+ writeCurrentStatus("error", error);
437
+ throw error;
438
+ }
439
+ }
440
+
441
+ main().catch((error) => {
442
+ console.error(
443
+ `codex-multi-auth app router failed: ${error instanceof Error ? error.message : String(error)}`,
444
+ );
445
+ process.exitCode = 1;
446
+ });