codex-multi-auth 2.21.0 → 2.22.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/.codex-plugin/plugin.json +1 -1
- package/README.md +5 -4
- package/dist/lib/accounts.d.ts +5 -0
- package/dist/lib/accounts.js +33 -4
- package/dist/lib/api-route-store.d.ts +6 -0
- package/dist/lib/api-route-store.js +98 -30
- package/dist/lib/codex-manager/api-login-menu.d.ts +1 -1
- package/dist/lib/codex-manager/api-login-menu.js +4 -6
- package/dist/lib/codex-manager/commands/status.d.ts +1 -1
- package/dist/lib/codex-manager/commands/status.js +26 -9
- package/dist/lib/codex-manager.js +7 -1
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +1 -0
- package/dist/lib/model-route-policy.d.ts +1 -0
- package/dist/lib/model-route-policy.js +2 -1
- package/dist/lib/runtime/account-model-catalog.d.ts +1 -0
- package/dist/lib/runtime/api-model-capabilities.d.ts +11 -2
- package/dist/lib/runtime/api-model-capabilities.js +155 -24
- package/dist/lib/runtime/app-bind.d.ts +4 -0
- package/dist/lib/runtime/app-bind.js +32 -1
- package/dist/lib/runtime/automatic-api-checks.d.ts +15 -0
- package/dist/lib/runtime/automatic-api-checks.js +188 -0
- package/dist/lib/runtime/model-discovery-status.d.ts +11 -1
- package/dist/lib/runtime/model-discovery-status.js +64 -49
- package/dist/lib/runtime/persisted-api-capabilities.d.ts +20 -0
- package/dist/lib/runtime/persisted-api-capabilities.js +387 -0
- package/dist/lib/runtime/responses-websocket.d.ts +18 -5
- package/dist/lib/runtime/responses-websocket.js +33 -11
- package/dist/lib/runtime/rotation-account-selection.d.ts +20 -1
- package/dist/lib/runtime/rotation-account-selection.js +94 -4
- package/dist/lib/runtime/rotation-proxy-state.d.ts +26 -0
- package/dist/lib/runtime/rotation-proxy-state.js +376 -4
- package/dist/lib/runtime/rotation-server-types.d.ts +3 -0
- package/dist/lib/runtime/rotation-token-refresh.d.ts +23 -1
- package/dist/lib/runtime/rotation-token-refresh.js +218 -27
- package/dist/lib/runtime-rotation-proxy.js +571 -96
- package/dist/lib/storage/credential-sidecars.js +34 -2
- package/dist/lib/storage/file-lock.d.ts +2 -0
- package/dist/lib/storage/file-lock.js +44 -30
- package/dist/lib/usage/ledger.js +1 -0
- package/dist/lib/usage/redaction.js +1 -0
- package/dist/lib/usage/types.d.ts +1 -1
- package/package.json +1 -1
- package/scripts/codex-app-router.js +42 -14
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { getApiRoutesPath } from "../api-route-store.js";
|
|
4
5
|
import { withRetry } from "../fs-retry.js";
|
|
@@ -13,18 +14,49 @@ const retry = { maxAttempts: 6, backoffMs: 25 };
|
|
|
13
14
|
*/
|
|
14
15
|
export async function clearCredentialSidecars() {
|
|
15
16
|
const routes = getApiRoutesPath();
|
|
16
|
-
// Same locks as saveApiRoutes, so a concurrent menu save cannot resurrect keys.
|
|
17
|
-
await withStorageLock(() => withFileTransactionLock(routes, () => withRetry(() => fs.rm(routes, { force: true }), retry)));
|
|
18
17
|
const dir = getCodexMultiAuthDir();
|
|
18
|
+
// Same locks as saveApiRoutes, so a concurrent menu save cannot resurrect keys.
|
|
19
|
+
await withStorageLock(() => withFileTransactionLock(routes, async () => {
|
|
20
|
+
await withRetry(() => fs.rm(routes, { force: true }), retry);
|
|
21
|
+
const leases = join(dir, "api-probe-locks");
|
|
22
|
+
try {
|
|
23
|
+
const stat = await fs.lstat(leases);
|
|
24
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
25
|
+
throw Error("Invalid API probe lease directory; manual repair required");
|
|
26
|
+
// Detach while publication is excluded; never recursively delete a
|
|
27
|
+
// canonical directory that a replacement owner can populate.
|
|
28
|
+
await withRetry(() => fs.rename(leases, join(dir, `api-probe-locks.wiped-${randomUUID()}`)), retry);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code !== "ENOENT")
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
19
35
|
// Each writer read-modify-writes under its file lock; deleting under the same
|
|
20
36
|
// lock stops an in-flight refresh or redemption renaming its old state (and a
|
|
21
37
|
// last-resort policy) back after the reset.
|
|
22
38
|
for (const path of [
|
|
23
39
|
join(dir, "reset-credits.json"),
|
|
24
40
|
join(dir, "api-capability-probes.json"),
|
|
41
|
+
join(dir, "automatic-api-checks.json"),
|
|
25
42
|
]) {
|
|
26
43
|
await withFileTransactionLock(path, () => withRetry(() => fs.rm(path, { force: true }), retry));
|
|
27
44
|
}
|
|
45
|
+
// Also finish deletion after an interrupted earlier wipe. These unique paths
|
|
46
|
+
// can contain old holders/candidates, but never a new canonical lease.
|
|
47
|
+
let leftovers;
|
|
48
|
+
try {
|
|
49
|
+
leftovers = await fs.readdir(dir);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code !== "ENOENT")
|
|
53
|
+
throw error;
|
|
54
|
+
leftovers = [];
|
|
55
|
+
}
|
|
56
|
+
for (const entry of leftovers) {
|
|
57
|
+
if (/^api-probe-locks\.wiped-[a-f0-9-]{36}$/.test(entry))
|
|
58
|
+
await withRetry(() => fs.rm(join(dir, entry), { recursive: true, force: true }), retry);
|
|
59
|
+
}
|
|
28
60
|
// Per-account timestamps only; their writers take no lock and hold no secrets.
|
|
29
61
|
const activity = join(dir, "inference-activity");
|
|
30
62
|
await withRetry(() => fs.rm(activity, { recursive: true, force: true }), retry);
|
|
@@ -145,21 +145,32 @@ async function recoverDeadCandidates(lock) {
|
|
|
145
145
|
* A reused PID fails closed (bounded contention) rather than risking two writers.
|
|
146
146
|
*/
|
|
147
147
|
export async function withFileTransactionLock(path, action, options = {}) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const key = join(await fs.realpath(dirname(absolute)), absolute.slice(dirname(absolute).length + 1));
|
|
152
|
-
if (leases.getStore()?.get(key)?.active)
|
|
153
|
-
return action();
|
|
154
|
-
const lock = `${key}.write-lock`;
|
|
155
|
-
await recoverDeadCandidates(lock);
|
|
156
|
-
const candidate = await fs.mkdtemp(`${lock}.candidate-`);
|
|
148
|
+
options.signal?.throwIfAborted();
|
|
149
|
+
const guard = options.guardAcquisition ?? (async (run) => run());
|
|
150
|
+
let key = "", lock = "", candidate;
|
|
157
151
|
const owner = `${host}.${process.pid}.${randomUUID()}`;
|
|
158
|
-
let published = false;
|
|
152
|
+
let published = false, reentrant = false;
|
|
159
153
|
try {
|
|
160
|
-
await
|
|
161
|
-
|
|
162
|
-
|
|
154
|
+
await guard(async () => {
|
|
155
|
+
options.signal?.throwIfAborted();
|
|
156
|
+
const absolute = resolve(path);
|
|
157
|
+
await fs.mkdir(dirname(absolute), { recursive: true, mode: 0o700 });
|
|
158
|
+
key = join(await fs.realpath(dirname(absolute)), absolute.slice(dirname(absolute).length + 1));
|
|
159
|
+
options.signal?.throwIfAborted();
|
|
160
|
+
if (leases.getStore()?.get(key)?.active) {
|
|
161
|
+
reentrant = true;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
lock = `${key}.write-lock`;
|
|
165
|
+
await recoverDeadCandidates(lock);
|
|
166
|
+
candidate = await fs.mkdtemp(`${lock}.candidate-`);
|
|
167
|
+
await fs.chmod(candidate, 0o700);
|
|
168
|
+
await fs.writeFile(join(candidate, owner), "", { flag: "wx", mode: 0o600 });
|
|
169
|
+
});
|
|
170
|
+
if (reentrant)
|
|
171
|
+
return action();
|
|
172
|
+
await withRetry(() => guard(async () => {
|
|
173
|
+
options.signal?.throwIfAborted();
|
|
163
174
|
try {
|
|
164
175
|
await fs.rename(candidate, lock);
|
|
165
176
|
published = true;
|
|
@@ -179,12 +190,13 @@ export async function withFileTransactionLock(path, action, options = {}) {
|
|
|
179
190
|
await recoverDeadOwner(lock);
|
|
180
191
|
throw Object.assign(Error("Storage is busy with another live writer; retry shortly."), { code: "ELOCKED" });
|
|
181
192
|
}
|
|
182
|
-
}, {
|
|
193
|
+
}), {
|
|
183
194
|
maxAttempts: lockMaxAttemptsForWaitMs(options.waitMs ?? 10000),
|
|
184
195
|
backoffMs: lockRetryBaseDelayMs,
|
|
185
196
|
jitterMs: LOCK_RETRY_JITTER_MS,
|
|
186
197
|
retryableCodes: ["ELOCKED", "EBUSY", "EPERM", "EACCES", "EAGAIN"],
|
|
187
198
|
});
|
|
199
|
+
options.signal?.throwIfAborted();
|
|
188
200
|
const lease = { active: true };
|
|
189
201
|
const context = new Map(leases.getStore());
|
|
190
202
|
context.set(key, lease);
|
|
@@ -197,22 +209,24 @@ export async function withFileTransactionLock(path, action, options = {}) {
|
|
|
197
209
|
}
|
|
198
210
|
finally {
|
|
199
211
|
const directory = published ? lock : candidate;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
if (directory) {
|
|
213
|
+
try {
|
|
214
|
+
await withRetry(async () => {
|
|
215
|
+
try {
|
|
216
|
+
await fs.unlink(join(directory, owner));
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (code(error) !== "ENOENT")
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
await removeEmpty(directory);
|
|
223
|
+
}, { maxAttempts: 6, backoffMs: 25 });
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
abandonedOwners.add(owner);
|
|
227
|
+
retryAbandonedRelease(directory, owner);
|
|
228
|
+
logWarn("Storage lock cleanup deferred", { code: code(error) ?? "unknown" });
|
|
229
|
+
}
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
}
|
package/dist/lib/usage/ledger.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type UsageLedgerSource = "runtime-proxy" | "plugin-host" | "local-bridge" | "cli" | "unknown";
|
|
2
|
-
export type UsageLedgerOperation = "responses" | "images" | "models" | "thread-goal" | "auth-refresh" | "diagnostic" | "unknown";
|
|
2
|
+
export type UsageLedgerOperation = "responses" | "images" | "models" | "thread-goal" | "transcribe" | "auth-refresh" | "diagnostic" | "unknown";
|
|
3
3
|
export type UsageLedgerOutcome = "success" | "failure" | "blocked" | "cancelled";
|
|
4
4
|
/**
|
|
5
5
|
* The service tier a response was actually billed at.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.22.0",
|
|
4
4
|
"description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -139,6 +139,27 @@ function writeStatus(statusPath, payload) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
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
|
+
|
|
142
163
|
function createStatusPayload({
|
|
143
164
|
state,
|
|
144
165
|
proxyServer,
|
|
@@ -148,9 +169,13 @@ function createStatusPayload({
|
|
|
148
169
|
statusPath,
|
|
149
170
|
routerScriptPath,
|
|
150
171
|
identityToken,
|
|
172
|
+
previousDisconnect = null,
|
|
151
173
|
}) {
|
|
152
174
|
const proxyStatus =
|
|
153
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;
|
|
154
179
|
const lastAccountIndex = proxyStatus.lastAccountIndex ?? null;
|
|
155
180
|
const lastAccountLabel =
|
|
156
181
|
typeof proxyStatus.lastAccountLabel === "string" &&
|
|
@@ -175,10 +200,12 @@ function createStatusPayload({
|
|
|
175
200
|
null,
|
|
176
201
|
updatedAt: Date.now(),
|
|
177
202
|
baseUrl: proxyServer?.baseUrl ?? stateRecord?.baseUrl ?? null,
|
|
203
|
+
schedulingStrategy: proxyStatus.schedulingStrategy,
|
|
178
204
|
totalRequests: proxyStatus.totalRequests ?? 0,
|
|
179
205
|
upstreamRequests: proxyStatus.upstreamRequests ?? 0,
|
|
180
206
|
websocketConnections: proxyStatus.websocketConnections ?? 0,
|
|
181
207
|
websocketUpstreamRequests: proxyStatus.websocketUpstreamRequests ?? 0,
|
|
208
|
+
lastWebsocketDisconnect: lastDisconnect,
|
|
182
209
|
streamQuotaUpdates: proxyStatus.streamQuotaUpdates ?? 0,
|
|
183
210
|
lastStreamQuotaUpdateAt: proxyStatus.lastStreamQuotaUpdateAt ?? null,
|
|
184
211
|
retries: proxyStatus.retries ?? 0,
|
|
@@ -266,6 +293,7 @@ async function main() {
|
|
|
266
293
|
const args = parseArgs(process.argv.slice(2));
|
|
267
294
|
installLogBounds(args.maxLogBytes, args.logPath).unref?.();
|
|
268
295
|
const stateRecord = readState(args.statePath);
|
|
296
|
+
let previousDisconnect = readDisconnect(readState(args.statusPath || stateRecord?.statusPath || "")?.lastWebsocketDisconnect);
|
|
269
297
|
if (args.statePath && stateRecord === null) {
|
|
270
298
|
const error = new Error(
|
|
271
299
|
"Codex app runtime router state is unreadable; refusing to bind an ephemeral port.",
|
|
@@ -273,6 +301,7 @@ async function main() {
|
|
|
273
301
|
writeStatus(
|
|
274
302
|
args.statusPath,
|
|
275
303
|
createStatusPayload({
|
|
304
|
+
previousDisconnect,
|
|
276
305
|
state: "error",
|
|
277
306
|
proxyServer: null,
|
|
278
307
|
error,
|
|
@@ -310,20 +339,19 @@ async function main() {
|
|
|
310
339
|
|
|
311
340
|
let proxyServer = null;
|
|
312
341
|
const writeCurrentStatus = (state, error) => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
);
|
|
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);
|
|
327
355
|
};
|
|
328
356
|
|
|
329
357
|
try {
|