@bitkyc08/opencodex 2.8.0 → 2.9.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/README.md +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +126 -10
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +143 -17
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/init.ts +129 -102
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +14 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +60 -14
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +179 -13
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +97 -25
- package/src/types.ts +39 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
2
|
+
|
|
1
3
|
const reauthAccounts = new Set<string>();
|
|
4
|
+
let lastReconciledGeneration = 0;
|
|
5
|
+
let liveAccountIds = new Set<string>();
|
|
2
6
|
|
|
3
|
-
export function markAccountNeedsReauth(id: string): void {
|
|
7
|
+
export function markAccountNeedsReauth(id: string, writerGeneration = captureConfigGeneration()): void {
|
|
8
|
+
if (writerGeneration < lastReconciledGeneration && !liveAccountIds.has(id)) return;
|
|
4
9
|
reauthAccounts.add(id);
|
|
5
10
|
}
|
|
6
11
|
|
|
12
|
+
export function reconcileCodexReauthState(context: GenerationContext): number {
|
|
13
|
+
if (context.generation <= lastReconciledGeneration) return 0;
|
|
14
|
+
let removed = 0;
|
|
15
|
+
for (const id of reauthAccounts) {
|
|
16
|
+
if (context.codexAccountIds.has(id)) continue;
|
|
17
|
+
reauthAccounts.delete(id);
|
|
18
|
+
removed += 1;
|
|
19
|
+
}
|
|
20
|
+
liveAccountIds = new Set(context.codexAccountIds);
|
|
21
|
+
lastReconciledGeneration = context.generation;
|
|
22
|
+
return removed;
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
export function isAccountNeedsReauth(id: string): boolean {
|
|
8
26
|
return reauthAccounts.has(id);
|
|
9
27
|
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { closeSync, existsSync, readFileSync, mkdirSync, openSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ConfigMutationLockError,
|
|
6
|
+
getConfigDir,
|
|
7
|
+
atomicWriteFile,
|
|
8
|
+
backupInvalidConfig,
|
|
9
|
+
hardenConfigDir,
|
|
10
|
+
hardenExistingSecret,
|
|
11
|
+
withConfigMutationLockSync,
|
|
12
|
+
} from "../config";
|
|
5
13
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
6
14
|
import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
|
|
7
15
|
|
|
@@ -121,44 +129,50 @@ export function getCodexAccountCredential(id: string): CodexAccountCredentials |
|
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
export function saveCodexAccountCredential(id: string, cred: CodexAccountCredentials): void {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
withCredentialMutationLockSync(() => {
|
|
133
|
+
const store = loadCodexAccountRecordStore();
|
|
134
|
+
const current = store[id];
|
|
135
|
+
const refreshGrantFingerprint = current?.credential?.refreshToken === cred.refreshToken
|
|
136
|
+
? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
|
|
137
|
+
: refreshGrantFingerprintForToken(cred.refreshToken);
|
|
138
|
+
store[id] = {
|
|
139
|
+
credential: cred,
|
|
140
|
+
generation: (current?.generation ?? 0) + 1,
|
|
141
|
+
refreshGrantFingerprint,
|
|
142
|
+
replacedAt: current ? Date.now() : undefined,
|
|
143
|
+
...preservedValidationMetadata(current),
|
|
144
|
+
};
|
|
145
|
+
persist(store);
|
|
146
|
+
});
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
export function markCodexAccountValidated(id: string, atMs: number = Date.now()): void {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
withCredentialMutationLockSync(() => {
|
|
151
|
+
const store = loadCodexAccountRecordStore();
|
|
152
|
+
const current = store[id];
|
|
153
|
+
if (!current || current.deletedAt != null || !current.credential) return;
|
|
154
|
+
store[id] = {
|
|
155
|
+
...current,
|
|
156
|
+
lastCodexValidatedAt: atMs,
|
|
157
|
+
lastCodexValidationStatus: "ok",
|
|
158
|
+
lastCodexValidationError: undefined,
|
|
159
|
+
};
|
|
160
|
+
persist(store);
|
|
161
|
+
});
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
export function markCodexAccountValidationFailed(id: string, reason: string): void {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
165
|
+
withCredentialMutationLockSync(() => {
|
|
166
|
+
const store = loadCodexAccountRecordStore();
|
|
167
|
+
const current = store[id];
|
|
168
|
+
if (!current || current.deletedAt != null || !current.credential) return;
|
|
169
|
+
store[id] = {
|
|
170
|
+
...current,
|
|
171
|
+
lastCodexValidationStatus: "failed",
|
|
172
|
+
lastCodexValidationError: reason,
|
|
173
|
+
};
|
|
174
|
+
persist(store);
|
|
175
|
+
});
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
export function removeCodexAccountCredential(id: string): void {
|
|
@@ -183,32 +197,36 @@ export function saveCodexAccountCredentialIfGeneration(
|
|
|
183
197
|
generation: number,
|
|
184
198
|
cred: CodexAccountCredentials,
|
|
185
199
|
): boolean {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
return withCredentialMutationLockSync(() => {
|
|
201
|
+
const store = loadCodexAccountRecordStore();
|
|
202
|
+
const current = store[id];
|
|
203
|
+
if (!current || current.generation !== generation || current.deletedAt != null || !current.credential) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const refreshGrantFingerprint = current.credential.refreshToken === cred.refreshToken
|
|
207
|
+
? current.refreshGrantFingerprint ?? refreshGrantFingerprintForToken(cred.refreshToken)
|
|
208
|
+
: refreshGrantFingerprintForToken(cred.refreshToken);
|
|
209
|
+
store[id] = {
|
|
210
|
+
credential: cred,
|
|
211
|
+
generation: generation + 1,
|
|
212
|
+
refreshGrantFingerprint,
|
|
213
|
+
replacedAt: current.replacedAt,
|
|
214
|
+
...preservedValidationMetadata(current),
|
|
215
|
+
};
|
|
216
|
+
persist(store);
|
|
217
|
+
return true;
|
|
218
|
+
});
|
|
203
219
|
}
|
|
204
220
|
|
|
205
221
|
export function tombstoneCodexAccount(id: string): number {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
return withCredentialMutationLockSync(() => {
|
|
223
|
+
const store = loadCodexAccountRecordStore();
|
|
224
|
+
const current = store[id];
|
|
225
|
+
const generation = (current?.generation ?? 0) + 1;
|
|
226
|
+
store[id] = { generation, deletedAt: Date.now() };
|
|
227
|
+
persist(store);
|
|
228
|
+
return generation;
|
|
229
|
+
});
|
|
212
230
|
}
|
|
213
231
|
|
|
214
232
|
const CHATGPT_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
@@ -237,17 +255,66 @@ export class CodexCredentialRefreshLockTimeoutError extends Error {
|
|
|
237
255
|
}
|
|
238
256
|
}
|
|
239
257
|
|
|
258
|
+
export class CodexCredentialRefreshBusyError extends Error {
|
|
259
|
+
readonly code = "CODEX_REFRESH_BUSY";
|
|
260
|
+
readonly retryable = true;
|
|
261
|
+
|
|
262
|
+
constructor() {
|
|
263
|
+
super("Codex credential refresh capacity reached");
|
|
264
|
+
this.name = "CodexCredentialRefreshBusyError";
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export class CodexCredentialRefreshStaleError extends Error {
|
|
269
|
+
readonly code = "CODEX_REFRESH_STALE";
|
|
270
|
+
readonly retryable = true;
|
|
271
|
+
|
|
272
|
+
constructor() {
|
|
273
|
+
super("Codex credential refresh owner became stale");
|
|
274
|
+
this.name = "CodexCredentialRefreshStaleError";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Credential writers share the config mutation coordinator; contention is transient, not reauth. */
|
|
279
|
+
function withCredentialMutationLockSync<T>(fn: () => T): T {
|
|
280
|
+
try {
|
|
281
|
+
return withConfigMutationLockSync(fn);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (error instanceof ConfigMutationLockError) throw new CodexCredentialRefreshLockTimeoutError();
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
240
288
|
type CodexTokenResult = { accessToken: string; chatgptAccountId: string; generation: number };
|
|
241
289
|
type CodexRefreshResult = CodexTokenResult & { credential?: CodexAccountCredentials };
|
|
242
|
-
const
|
|
290
|
+
const MAX_CODEX_REFRESH_FLIGHTS = 32;
|
|
291
|
+
const CODEX_REFRESH_FLIGHT_STALE_MS = 120_000;
|
|
292
|
+
interface RefreshFlight {
|
|
293
|
+
promise: Promise<CodexRefreshResult>;
|
|
294
|
+
startedAt: number;
|
|
295
|
+
abort: AbortController;
|
|
296
|
+
}
|
|
297
|
+
const refreshLocks = new Map<string, RefreshFlight>();
|
|
243
298
|
|
|
244
299
|
function codexRefreshLockPath(lockKey: string): string {
|
|
245
300
|
const digest = createHash("sha256").update(lockKey).digest("hex").slice(0, 32);
|
|
246
301
|
return join(getConfigDir(), `codex-refresh-${digest}.lock`);
|
|
247
302
|
}
|
|
248
303
|
|
|
249
|
-
function sleep(ms: number): Promise<void> {
|
|
250
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
304
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
305
|
+
if (!signal) return new Promise(resolve => setTimeout(resolve, ms));
|
|
306
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const timer = setTimeout(() => {
|
|
309
|
+
signal.removeEventListener("abort", onAbort);
|
|
310
|
+
resolve();
|
|
311
|
+
}, ms);
|
|
312
|
+
const onAbort = () => {
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
reject(signal.reason);
|
|
315
|
+
};
|
|
316
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
317
|
+
});
|
|
251
318
|
}
|
|
252
319
|
|
|
253
320
|
function errCode(err: unknown): string | undefined {
|
|
@@ -264,7 +331,7 @@ function isRefreshLockStale(path: string): boolean {
|
|
|
264
331
|
}
|
|
265
332
|
}
|
|
266
333
|
|
|
267
|
-
async function withCodexRefreshFileLock<T>(lockKey: string, fn: () => Promise<T>): Promise<T> {
|
|
334
|
+
async function withCodexRefreshFileLock<T>(lockKey: string, signal: AbortSignal, fn: () => Promise<T>): Promise<T> {
|
|
268
335
|
hardenConfigDir();
|
|
269
336
|
const dir = getConfigDir();
|
|
270
337
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -273,6 +340,7 @@ async function withCodexRefreshFileLock<T>(lockKey: string, fn: () => Promise<T>
|
|
|
273
340
|
const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
|
|
274
341
|
let fd: number | null = null;
|
|
275
342
|
while (fd == null) {
|
|
343
|
+
if (signal.aborted) throw signal.reason;
|
|
276
344
|
try {
|
|
277
345
|
fd = openSync(path, "wx", 0o600);
|
|
278
346
|
writeFileSync(fd, JSON.stringify({ acquiredAt: Date.now(), pid: process.pid }) + "\n");
|
|
@@ -288,7 +356,7 @@ async function withCodexRefreshFileLock<T>(lockKey: string, fn: () => Promise<T>
|
|
|
288
356
|
continue;
|
|
289
357
|
}
|
|
290
358
|
if (Date.now() >= deadline) throw new CodexCredentialRefreshLockTimeoutError();
|
|
291
|
-
await sleep(REFRESH_LOCK_POLL_MS);
|
|
359
|
+
await sleep(REFRESH_LOCK_POLL_MS, signal);
|
|
292
360
|
}
|
|
293
361
|
}
|
|
294
362
|
|
|
@@ -331,28 +399,39 @@ export async function getValidCodexToken(id: string): Promise<CodexTokenResult>
|
|
|
331
399
|
|
|
332
400
|
const existing = refreshLocks.get(refreshGrantFingerprint);
|
|
333
401
|
if (existing) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
402
|
+
if (Date.now() - existing.startedAt > CODEX_REFRESH_FLIGHT_STALE_MS) {
|
|
403
|
+
existing.abort.abort(new CodexCredentialRefreshStaleError());
|
|
404
|
+
if (refreshLocks.get(refreshGrantFingerprint) === existing) refreshLocks.delete(refreshGrantFingerprint);
|
|
405
|
+
} else {
|
|
406
|
+
const refreshed = await existing.promise;
|
|
407
|
+
const current = readCodexAccountRecord(id);
|
|
408
|
+
const currentCred = current?.deletedAt == null ? current?.credential : undefined;
|
|
409
|
+
if (
|
|
410
|
+
current &&
|
|
411
|
+
currentCred &&
|
|
412
|
+
refreshed.credential &&
|
|
413
|
+
recordGrantFingerprint(current) === refreshGrantFingerprint
|
|
414
|
+
) {
|
|
415
|
+
if (!saveCodexAccountCredentialIfGeneration(id, current.generation, refreshed.credential)) {
|
|
416
|
+
throw new CodexCredentialGenerationConflictError();
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
accessToken: refreshed.credential.accessToken,
|
|
420
|
+
chatgptAccountId: refreshed.credential.chatgptAccountId,
|
|
421
|
+
generation: current.generation + 1,
|
|
422
|
+
};
|
|
345
423
|
}
|
|
346
|
-
return
|
|
347
|
-
accessToken: refreshed.credential.accessToken,
|
|
348
|
-
chatgptAccountId: refreshed.credential.chatgptAccountId,
|
|
349
|
-
generation: current.generation + 1,
|
|
350
|
-
};
|
|
424
|
+
return getValidCodexToken(id);
|
|
351
425
|
}
|
|
352
|
-
return getValidCodexToken(id);
|
|
353
426
|
}
|
|
354
427
|
|
|
355
|
-
|
|
428
|
+
if (refreshLocks.size >= MAX_CODEX_REFRESH_FLIGHTS) throw new CodexCredentialRefreshBusyError();
|
|
429
|
+
|
|
430
|
+
const abort = new AbortController();
|
|
431
|
+
const signal = AbortSignal.any([abort.signal, AbortSignal.timeout(30_000)]);
|
|
432
|
+
let flight!: RefreshFlight;
|
|
433
|
+
const refreshPromise = withCodexRefreshFileLock(refreshGrantFingerprint, signal, async (): Promise<CodexRefreshResult> => {
|
|
434
|
+
const current = readCodexAccountRecord(id);
|
|
356
435
|
const lockedRecord = readCodexAccountRecord(id);
|
|
357
436
|
const lockedCred = lockedRecord?.deletedAt == null ? lockedRecord?.credential : undefined;
|
|
358
437
|
if (!lockedRecord || !lockedCred) throw new CodexCredentialGenerationConflictError();
|
|
@@ -397,7 +476,7 @@ export async function getValidCodexToken(id: string): Promise<CodexTokenResult>
|
|
|
397
476
|
client_id: CHATGPT_CLIENT_ID,
|
|
398
477
|
refresh_token: lockedCred.refreshToken,
|
|
399
478
|
}).toString(),
|
|
400
|
-
signal
|
|
479
|
+
signal,
|
|
401
480
|
});
|
|
402
481
|
if (!res.ok) {
|
|
403
482
|
const errText = await res.text().catch(() => "");
|
|
@@ -424,10 +503,11 @@ export async function getValidCodexToken(id: string): Promise<CodexTokenResult>
|
|
|
424
503
|
}
|
|
425
504
|
return { accessToken: updated.accessToken, chatgptAccountId: updated.chatgptAccountId, generation: startGeneration + 1, credential: updated };
|
|
426
505
|
}).finally(() => {
|
|
427
|
-
refreshLocks.delete(refreshGrantFingerprint);
|
|
506
|
+
if (refreshLocks.get(refreshGrantFingerprint) === flight) refreshLocks.delete(refreshGrantFingerprint);
|
|
428
507
|
});
|
|
429
508
|
|
|
430
|
-
|
|
509
|
+
flight = { promise: refreshPromise, startedAt: Date.now(), abort };
|
|
510
|
+
refreshLocks.set(refreshGrantFingerprint, flight);
|
|
431
511
|
const result = await refreshPromise;
|
|
432
512
|
return {
|
|
433
513
|
accessToken: result.accessToken,
|