@bitkyc08/opencodex 2.48.0-preview.20260908 → 2.49.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 +8 -5
- package/SPONSORS.md +1 -1
- package/assets/sponsors/orcarouter.png +0 -0
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/index-BoBRSehJ.css +1 -0
- package/gui/dist/assets/index-BtyONQrZ.js +115 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/packycode.svg +19 -0
- package/gui/dist/provider-icons/qoder.svg +5 -0
- package/package.json +5 -3
- package/src/adapters/anthropic.ts +31 -16
- package/src/adapters/codebuddy/adapter.ts +85 -0
- package/src/adapters/codebuddy/profiles.ts +52 -0
- package/src/adapters/coding-agent/profile.ts +100 -0
- package/src/adapters/coding-agent/protocol.ts +463 -0
- package/src/adapters/coding-agent/turn.ts +353 -0
- package/src/adapters/google.ts +15 -11
- package/src/adapters/mimo-free.ts +3 -0
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses.ts +18 -11
- package/src/adapters/qoder/adapter.ts +70 -0
- package/src/adapters/qoder/live-models.ts +89 -0
- package/src/adapters/qoder/profiles.ts +36 -0
- package/src/adapters/registry.ts +12 -0
- package/src/adapters/responses-tool-schema.ts +113 -8
- package/src/cli/dispatch.ts +2 -0
- package/src/cli/doctor.ts +40 -0
- package/src/cli/effort.ts +24 -8
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +24 -1
- package/src/cli/provider-runtime.ts +2 -1
- package/src/cli/registry.ts +2 -2
- package/src/cli/system-command.ts +10 -3
- package/src/cli/usage-report.ts +9 -5
- package/src/codex/account-lifecycle.ts +35 -2
- package/src/codex/auth-api.ts +6 -3
- package/src/codex/auth-collision.ts +12 -2
- package/src/codex/auth-context.ts +75 -7
- package/src/codex/catalog/provider-fetch.ts +73 -9
- package/src/codex/inject.ts +127 -30
- package/src/codex/internal/catalog-writer.ts +3 -0
- package/src/codex/journal.ts +61 -12
- package/src/codex/model-cache.ts +11 -4
- package/src/codex/native-profile-startup.ts +72 -5
- package/src/codex/native-profile-store.ts +2 -2
- package/src/codex/ocx-compaction-history.ts +226 -0
- package/src/codex/project-config-warnings.ts +3 -1
- package/src/codex/quota.ts +18 -8
- package/src/codex/reserve-availability.ts +21 -5
- package/src/codex/runtime.ts +45 -1
- package/src/codex/sync.ts +5 -0
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +126 -74
- package/src/images/loop.ts +1 -0
- package/src/images/xai-video-client.ts +2 -0
- package/src/integrations/registry.ts +1 -0
- package/src/lib/process-control.ts +52 -8
- package/src/lib/upstream-retry.ts +1 -0
- package/src/oauth/chatgpt.ts +83 -0
- package/src/oauth/xai.ts +151 -8
- package/src/providers/api-key-selection-capture.ts +10 -0
- package/src/providers/api-key-selection.ts +2 -7
- package/src/providers/caller-authorization.ts +36 -0
- package/src/providers/codebuddy-models.ts +184 -0
- package/src/providers/derive.ts +5 -0
- package/src/providers/free-directory.ts +26 -2
- package/src/providers/openai-sidecar.ts +35 -11
- package/src/providers/qoder-models.ts +25 -0
- package/src/providers/registry.ts +139 -0
- package/src/responses/compaction.ts +4 -0
- package/src/responses/task-input.ts +21 -1
- package/src/router.ts +1 -1
- package/src/server/chat-completions.ts +26 -9
- package/src/server/claude-messages.ts +12 -2
- package/src/server/images.ts +1 -0
- package/src/server/management/account-selection-stream.ts +13 -4
- package/src/server/management/config-routes.ts +24 -5
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-routes.ts +24 -0
- package/src/server/management-api.ts +14 -0
- package/src/server/port-reclaim.ts +11 -26
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/collaboration.ts +4 -9
- package/src/server/responses/core.ts +141 -19
- package/src/server/responses/fetch-helpers.ts +18 -7
- package/src/server/responses/policy-fallback.ts +12 -0
- package/src/service.ts +36 -2
- package/src/storage/cleanup.ts +77 -45
- package/src/types/config.ts +7 -1
- package/src/usage/log.ts +24 -0
- package/src/vision/anthropic-describe.ts +1 -0
- package/src/web-search/anthropic-executor.ts +1 -0
- package/src/web-search/loop.ts +1 -0
- package/src/web-search/progress-stream.ts +4 -0
- package/gui/dist/assets/index-B6EFoatP.js +0 -115
- package/gui/dist/assets/index-D5SiRo8X.css +0 -1
package/src/codex/journal.ts
CHANGED
|
@@ -62,12 +62,40 @@ export interface RestoreJournalResult {
|
|
|
62
62
|
configChanged: boolean;
|
|
63
63
|
profileChanged: boolean;
|
|
64
64
|
complete: boolean;
|
|
65
|
+
/** A changed artifact has no recorded injected hash, so snapshot ownership is unknown. */
|
|
66
|
+
unverified: boolean;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
function sha256(content: string | null): string | null {
|
|
68
70
|
return content === null ? null : createHash("sha256").update(content).digest("hex");
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
function compareJournalState(journal: Journal, config: string | null, profile: string | null) {
|
|
74
|
+
const originalConfig = Buffer.from(journal.originalConfig, "base64").toString("utf-8");
|
|
75
|
+
const originalProfile = journal.originalProfile === null
|
|
76
|
+
? null
|
|
77
|
+
: Buffer.from(journal.originalProfile, "base64").toString("utf-8");
|
|
78
|
+
const configAlreadyOriginal = config === originalConfig;
|
|
79
|
+
const profileAlreadyOriginal = profile === originalProfile;
|
|
80
|
+
const configHashKnown = typeof journal.injectedConfigHash === "string" && journal.injectedConfigHash.length > 0;
|
|
81
|
+
const profileHashKnown = journal.injectedProfileHash !== undefined;
|
|
82
|
+
return {
|
|
83
|
+
originalConfig,
|
|
84
|
+
originalProfile,
|
|
85
|
+
configAlreadyOriginal,
|
|
86
|
+
profileAlreadyOriginal,
|
|
87
|
+
configUnchanged: configAlreadyOriginal || (configHashKnown && sha256(config) === journal.injectedConfigHash),
|
|
88
|
+
profileUnchanged: profileAlreadyOriginal || (profileHashKnown && sha256(profile) === (journal.injectedProfileHash ?? null)),
|
|
89
|
+
unverified: (!configHashKnown && !configAlreadyOriginal) || (!profileHashKnown && !profileAlreadyOriginal),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Read-only check of the pre-injection snapshot input, never the newly injected bytes. */
|
|
94
|
+
export function hasUnverifiedJournalBaseline(config: string | null, profile: string | null): boolean {
|
|
95
|
+
const journal = readJournal(false);
|
|
96
|
+
return journal !== null && compareJournalState(journal, config, profile).unverified;
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
export interface WriteJournalOptions {
|
|
72
100
|
/**
|
|
73
101
|
* The caller's verdict on the config it is about to transform: false when
|
|
@@ -114,7 +142,7 @@ export function writeJournal(options: WriteJournalOptions = {}): void {
|
|
|
114
142
|
const journal: Journal = {
|
|
115
143
|
version: 1,
|
|
116
144
|
originalConfig: Buffer.from(config).toString("base64"),
|
|
117
|
-
originalProfile: profile ? Buffer.from(profile).toString("base64") : null,
|
|
145
|
+
originalProfile: profile !== null ? Buffer.from(profile).toString("base64") : null,
|
|
118
146
|
pid: process.pid,
|
|
119
147
|
owner: options.owner?.kind === "client"
|
|
120
148
|
? { kind: "client", apiKeyId: options.owner.apiKeyId }
|
|
@@ -208,22 +236,34 @@ export function journalOwner(): JournalOwner | null {
|
|
|
208
236
|
export function restoreJournalState(): RestoreJournalResult {
|
|
209
237
|
const journal = readJournal();
|
|
210
238
|
if (!journal) {
|
|
211
|
-
return { configRestored: false, profileRestored: false, configChanged: false, profileChanged: false, complete: false };
|
|
239
|
+
return { configRestored: false, profileRestored: false, configChanged: false, profileChanged: false, complete: false, unverified: false };
|
|
212
240
|
}
|
|
213
|
-
const currentConfig = existsSync(CODEX_CONFIG_PATH) ? readFileSync(CODEX_CONFIG_PATH, "utf-8") :
|
|
241
|
+
const currentConfig = existsSync(CODEX_CONFIG_PATH) ? readFileSync(CODEX_CONFIG_PATH, "utf-8") : null;
|
|
214
242
|
const currentProfile = existsSync(CODEX_PROFILE_PATH) ? readFileSync(CODEX_PROFILE_PATH, "utf-8") : null;
|
|
215
|
-
const
|
|
216
|
-
const
|
|
243
|
+
const comparison = compareJournalState(journal, currentConfig, currentProfile);
|
|
244
|
+
const { configUnchanged, profileUnchanged } = comparison;
|
|
245
|
+
// A legacy record or interruption before markJournalInjectedState is not proof that
|
|
246
|
+
// later bytes belong to OpenCodex. Keep the whole pair and its recovery evidence intact.
|
|
247
|
+
if (comparison.unverified) {
|
|
248
|
+
return {
|
|
249
|
+
configRestored: comparison.configAlreadyOriginal,
|
|
250
|
+
profileRestored: comparison.profileAlreadyOriginal,
|
|
251
|
+
configChanged: !configUnchanged,
|
|
252
|
+
profileChanged: !profileUnchanged,
|
|
253
|
+
complete: false,
|
|
254
|
+
unverified: true,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
217
257
|
|
|
218
|
-
let configRestored =
|
|
219
|
-
let profileRestored =
|
|
220
|
-
if (configUnchanged) {
|
|
221
|
-
atomicWriteFile(CODEX_CONFIG_PATH,
|
|
258
|
+
let configRestored = comparison.configAlreadyOriginal;
|
|
259
|
+
let profileRestored = comparison.profileAlreadyOriginal;
|
|
260
|
+
if (configUnchanged && !configRestored) {
|
|
261
|
+
atomicWriteFile(CODEX_CONFIG_PATH, comparison.originalConfig);
|
|
222
262
|
configRestored = true;
|
|
223
263
|
}
|
|
224
|
-
if (profileUnchanged) {
|
|
225
|
-
if (
|
|
226
|
-
atomicWriteFile(CODEX_PROFILE_PATH,
|
|
264
|
+
if (profileUnchanged && !profileRestored) {
|
|
265
|
+
if (comparison.originalProfile !== null) {
|
|
266
|
+
atomicWriteFile(CODEX_PROFILE_PATH, comparison.originalProfile);
|
|
227
267
|
profileRestored = true;
|
|
228
268
|
} else if (existsSync(CODEX_PROFILE_PATH)) {
|
|
229
269
|
// "There was no profile before, so remove the one we generated." Claiming success
|
|
@@ -249,6 +289,7 @@ export function restoreJournalState(): RestoreJournalResult {
|
|
|
249
289
|
configChanged: !configUnchanged,
|
|
250
290
|
profileChanged: !profileUnchanged,
|
|
251
291
|
complete,
|
|
292
|
+
unverified: false,
|
|
252
293
|
};
|
|
253
294
|
}
|
|
254
295
|
|
|
@@ -267,6 +308,10 @@ export function reconcileJournal(options: ReconcileJournalOptions = {}): boolean
|
|
|
267
308
|
if (owner?.kind === "client") {
|
|
268
309
|
if (options.activeClientApiKeyId === owner.apiKeyId) return false;
|
|
269
310
|
const restored = restoreJournalState();
|
|
311
|
+
if (restored.unverified) {
|
|
312
|
+
console.error("⚠️ Codex journal recovery was not verified; current configuration files and the journal were preserved.");
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
270
315
|
if (!restored.configRestored && !restored.profileRestored) return false;
|
|
271
316
|
console.error(`⚠️ Uncommitted or mismatched client routing (${owner.apiKeyId}) was restored from the Codex journal.`);
|
|
272
317
|
return true;
|
|
@@ -281,6 +326,10 @@ export function reconcileJournal(options: ReconcileJournalOptions = {}): boolean
|
|
|
281
326
|
}
|
|
282
327
|
}
|
|
283
328
|
const restored = restoreJournalState();
|
|
329
|
+
if (restored.unverified) {
|
|
330
|
+
console.error("⚠️ Codex journal recovery was not verified; current configuration files and the journal were preserved.");
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
284
333
|
if (!restored.configRestored && !restored.profileRestored) return false;
|
|
285
334
|
console.error(`⚠️ Previous session (PID ${pid}) did not shut down cleanly. Codex state restored from journal.`);
|
|
286
335
|
return true;
|
package/src/codex/model-cache.ts
CHANGED
|
@@ -18,6 +18,8 @@ interface CacheEntry {
|
|
|
18
18
|
models: CatalogModel[];
|
|
19
19
|
fetchedAt: number;
|
|
20
20
|
sizeBytes: number;
|
|
21
|
+
/** Irreversible credential/account identity for entitlement-sensitive catalogs. */
|
|
22
|
+
authorityIdentity?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export type ProviderModelDiscoveryFailureReason =
|
|
@@ -149,15 +151,19 @@ export function isModelsFetchCoolingDown(provider: string, cooldownMs = MODELS_F
|
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
/** Fresh cached models for a provider, or null when absent/stale (caller should re-fetch). */
|
|
152
|
-
export function getFreshCached(provider: string, ttlMs: number, now = Date.now()): CatalogModel[] | null {
|
|
154
|
+
export function getFreshCached(provider: string, ttlMs: number, now = Date.now(), authorityIdentity?: string): CatalogModel[] | null {
|
|
153
155
|
const entry = cache.get(provider);
|
|
154
156
|
if (!entry) return null;
|
|
157
|
+
if (authorityIdentity !== undefined && entry.authorityIdentity !== authorityIdentity) return null;
|
|
155
158
|
return now - entry.fetchedAt < ttlMs ? entry.models : null;
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
/** Last-known-good models regardless of age — the fallback when a live fetch fails. */
|
|
159
|
-
export function getStaleCached(provider: string): CatalogModel[] | null {
|
|
160
|
-
|
|
162
|
+
export function getStaleCached(provider: string, authorityIdentity?: string): CatalogModel[] | null {
|
|
163
|
+
const entry = cache.get(provider);
|
|
164
|
+
if (!entry) return null;
|
|
165
|
+
if (authorityIdentity !== undefined && entry.authorityIdentity !== authorityIdentity) return null;
|
|
166
|
+
return entry.models;
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
/** Capture the cache generation before an asynchronous provider discovery starts. */
|
|
@@ -181,12 +187,13 @@ export function setCached(
|
|
|
181
187
|
models: CatalogModel[],
|
|
182
188
|
now = Date.now(),
|
|
183
189
|
generation?: string,
|
|
190
|
+
authorityIdentity?: string,
|
|
184
191
|
): boolean {
|
|
185
192
|
if (generation !== undefined && !isModelCacheGenerationCurrent(provider, generation)) return false;
|
|
186
193
|
deleteCachedProvider(provider);
|
|
187
194
|
const sizeBytes = modelCacheEncoder.encode(provider).byteLength
|
|
188
195
|
+ modelCacheEncoder.encode(JSON.stringify(models)).byteLength;
|
|
189
|
-
cache.set(provider, { models, fetchedAt: now, sizeBytes });
|
|
196
|
+
cache.set(provider, { models, fetchedAt: now, sizeBytes, ...(authorityIdentity ? { authorityIdentity } : {}) });
|
|
190
197
|
cacheBytes += sizeBytes;
|
|
191
198
|
if (oldestCachedAt === null || now < oldestCachedAt) {
|
|
192
199
|
oldestCachedProvider = provider;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { NativeProfileManager } from "./native-profile-manager";
|
|
2
|
+
import { loadConfig } from "../config";
|
|
3
|
+
import { initializeMainAccountPolicyBinding } from "./account-lifecycle";
|
|
2
4
|
import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
3
5
|
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
4
6
|
import {
|
|
@@ -73,6 +75,7 @@ interface StartupEntry {
|
|
|
73
75
|
owner: NativeMainOwnerReference;
|
|
74
76
|
unsubscribe: () => void;
|
|
75
77
|
recoveryStarted: boolean;
|
|
78
|
+
policyBindingPending: boolean;
|
|
76
79
|
settled: Promise<NativeMainStartupGateSnapshot>;
|
|
77
80
|
resolveAcquisition?: (value: NativeMainStartupGateSnapshot) => void;
|
|
78
81
|
deps: NativeMainStartupGateDeps;
|
|
@@ -172,17 +175,21 @@ async function runOwnedStageSweep(entry: StartupEntry): Promise<boolean> {
|
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
function scheduleStageSweep(entry: StartupEntry): void {
|
|
175
|
-
if (entry.sweepStopping || entry.sweepTimer ||
|
|
178
|
+
if (entry.sweepStopping || entry.sweepTimer || entry.sweepInFlight || entry.policyBindingPending
|
|
179
|
+
|| startupEntries.get(entry.homeId) !== entry) return;
|
|
176
180
|
const intervalMs = Math.max(10, entry.deps.stageSweepIntervalMs ?? NATIVE_STAGE_SWEEP_INTERVAL_MS);
|
|
177
181
|
entry.sweepTimer = setTimeout(() => {
|
|
178
182
|
entry.sweepTimer = undefined;
|
|
179
183
|
if (entry.sweepStopping || startupEntries.get(entry.homeId) !== entry) return;
|
|
184
|
+
const sweepEpoch = entry.epoch;
|
|
180
185
|
entry.sweepInFlight = (async () => {
|
|
181
186
|
const safe = await runOwnedStageSweep(entry);
|
|
182
|
-
if (entry.sweepStopping || startupEntries.get(entry.homeId) !== entry
|
|
187
|
+
if (entry.sweepStopping || startupEntries.get(entry.homeId) !== entry
|
|
188
|
+
|| entry.epoch !== sweepEpoch || entry.policyBindingPending) return;
|
|
183
189
|
if (!safe) snapshot = { status: "blocked", homeId: entry.homeId, reason: "stage-cleanup-required" };
|
|
184
190
|
else if (snapshot.homeId === entry.homeId && snapshot.status === "blocked" && snapshot.reason === "stage-cleanup-required") {
|
|
185
|
-
|
|
191
|
+
if (loadConfig().codexMainAccountHardLock === true) rearmOwnedMainPolicyBinding(entry);
|
|
192
|
+
else snapshot = ready(entry.homeId);
|
|
186
193
|
}
|
|
187
194
|
})().finally(() => {
|
|
188
195
|
entry.sweepInFlight = undefined;
|
|
@@ -218,8 +225,29 @@ function convergeOwnedStartup(entry: StartupEntry): void {
|
|
|
218
225
|
));
|
|
219
226
|
const stageSweepSafe = recoveryState === "none" ? await runOwnedStageSweep(entry) : false;
|
|
220
227
|
if (startupEntries.get(entry.homeId) === entry && entry.epoch === currentEpoch && recoveryState === "none" && stageSweepSafe) {
|
|
221
|
-
|
|
222
|
-
|
|
228
|
+
if (loadConfig().codexMainAccountHardLock === true) {
|
|
229
|
+
await withNativeMainOwnerOperation(entry.manager.context, () => withNativeMainExclusiveClaim(
|
|
230
|
+
entry.manager.context,
|
|
231
|
+
async () => {
|
|
232
|
+
if (startupEntries.get(entry.homeId) !== entry || entry.epoch !== currentEpoch) return;
|
|
233
|
+
if (probe(entry.manager.context) !== "none") {
|
|
234
|
+
snapshot = { status: "blocked", homeId: entry.homeId, reason: "manual-recovery" };
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// The HMAC is deliberately not persisted. Bind only the pinned owned home,
|
|
238
|
+
// after recovery/cleanup, and before caller-owned admission can observe ready.
|
|
239
|
+
if (loadConfig().codexMainAccountHardLock === true) {
|
|
240
|
+
initializeMainAccountPolicyBinding(entry.manager.context.authPath);
|
|
241
|
+
}
|
|
242
|
+
clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
243
|
+
snapshot = ready(entry.homeId);
|
|
244
|
+
},
|
|
245
|
+
{ waitMs: 10_000 },
|
|
246
|
+
));
|
|
247
|
+
} else {
|
|
248
|
+
clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
249
|
+
snapshot = ready(entry.homeId);
|
|
250
|
+
}
|
|
223
251
|
} else if (startupEntries.get(entry.homeId) === entry && entry.epoch === currentEpoch && recoveryState === "none") {
|
|
224
252
|
snapshot = { status: "blocked", homeId: entry.homeId, reason: "stage-cleanup-required" };
|
|
225
253
|
} else if (startupEntries.get(entry.homeId) === entry && entry.epoch === currentEpoch) {
|
|
@@ -230,12 +258,35 @@ function convergeOwnedStartup(entry: StartupEntry): void {
|
|
|
230
258
|
snapshot = { status: "blocked", homeId: entry.homeId, reason: "manual-recovery" };
|
|
231
259
|
}
|
|
232
260
|
}
|
|
261
|
+
entry.policyBindingPending = false;
|
|
233
262
|
if (startupEntries.get(entry.homeId) === entry && entry.epoch === currentEpoch) scheduleStageSweep(entry);
|
|
234
263
|
return snapshot;
|
|
235
264
|
})();
|
|
236
265
|
if (acquisitionWaiter) void entry.settled.then(acquisitionWaiter);
|
|
237
266
|
}
|
|
238
267
|
|
|
268
|
+
/** Join an active startup, or rearm its held owner before publishing another ready transition. */
|
|
269
|
+
function rearmOwnedMainPolicyBinding(entry: StartupEntry): boolean {
|
|
270
|
+
if (entry.sweepStopping || startupEntries.get(entry.homeId) !== entry) return false;
|
|
271
|
+
const owner = entry.owner.snapshot();
|
|
272
|
+
if (entry.policyBindingPending && (owner.status === "held" || owner.status === "acquiring")) {
|
|
273
|
+
snapshot = { status: "blocked", homeId: entry.homeId, reason: "recovery-pending" };
|
|
274
|
+
settled = entry.settled;
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (owner.status !== "held") {
|
|
278
|
+
snapshot = { status: "blocked", homeId: entry.homeId, reason: ownerBlockedReason(owner) };
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
if (entry.sweepTimer) clearTimeout(entry.sweepTimer);
|
|
282
|
+
entry.sweepTimer = undefined;
|
|
283
|
+
entry.epoch = ++epoch;
|
|
284
|
+
entry.policyBindingPending = true;
|
|
285
|
+
entry.recoveryStarted = false;
|
|
286
|
+
convergeOwnedStartup(entry);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
239
290
|
function observeOwner(entry: StartupEntry, owner: NativeMainOwnerSnapshot): void {
|
|
240
291
|
if (startupEntries.get(entry.homeId) !== entry) return;
|
|
241
292
|
if (owner.status === "acquiring") {
|
|
@@ -283,6 +334,7 @@ export function startNativeMainStartupLifecycle(
|
|
|
283
334
|
owner,
|
|
284
335
|
unsubscribe: () => {},
|
|
285
336
|
recoveryStarted: false,
|
|
337
|
+
policyBindingPending: true,
|
|
286
338
|
settled: acquisition,
|
|
287
339
|
resolveAcquisition,
|
|
288
340
|
deps,
|
|
@@ -291,6 +343,12 @@ export function startNativeMainStartupLifecycle(
|
|
|
291
343
|
};
|
|
292
344
|
startupEntries.set(homeId, entry);
|
|
293
345
|
entry.unsubscribe = owner.subscribe(ownerState => observeOwner(entry!, ownerState));
|
|
346
|
+
} else if (!entry.policyBindingPending
|
|
347
|
+
&& snapshot.status === "ready" && snapshot.homeId === homeId
|
|
348
|
+
&& loadConfig().codexMainAccountHardLock === true) {
|
|
349
|
+
// A new same-process listener can enable protection or follow a credential replacement.
|
|
350
|
+
// Re-read its pinned home through the held owner before admitting caller-owned main.
|
|
351
|
+
rearmOwnedMainPolicyBinding(entry);
|
|
294
352
|
}
|
|
295
353
|
entry.refs += 1;
|
|
296
354
|
let released = false;
|
|
@@ -602,6 +660,8 @@ export function blockNativeMainRecovery(
|
|
|
602
660
|
|
|
603
661
|
export function completeNativeMainRecovery(homeId: string): boolean {
|
|
604
662
|
if (snapshot.status !== "blocked" || snapshot.homeId !== homeId) return false;
|
|
663
|
+
const entry = startupEntries.get(homeId);
|
|
664
|
+
if (entry && loadConfig().codexMainAccountHardLock === true) return rearmOwnedMainPolicyBinding(entry);
|
|
605
665
|
epoch += 1;
|
|
606
666
|
clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
607
667
|
snapshot = ready(homeId);
|
|
@@ -615,6 +675,13 @@ export function nativeMainStartupGateSnapshot(): NativeMainStartupGateSnapshot {
|
|
|
615
675
|
return { ...snapshot };
|
|
616
676
|
}
|
|
617
677
|
|
|
678
|
+
/** Read-only: caller-owned credentials must not trigger physical-main ownership reprobes. */
|
|
679
|
+
export function isMainAccountPolicyBindingPending(): boolean {
|
|
680
|
+
const current = nativeMainStartupGateSnapshot();
|
|
681
|
+
return current.status === "blocked" && current.reason === "recovery-pending"
|
|
682
|
+
&& current.homeId !== null && startupEntries.get(current.homeId)?.policyBindingPending === true;
|
|
683
|
+
}
|
|
684
|
+
|
|
618
685
|
export function waitForNativeMainStartupGate(): Promise<NativeMainStartupGateSnapshot> {
|
|
619
686
|
const reason = activeServiceOwnershipBlockReason();
|
|
620
687
|
if (reason) return Promise.resolve(serviceOwnershipSnapshot(reason));
|
|
@@ -41,7 +41,7 @@ const KEYRING_SERVICE = "opencodex.native-main-profile.v1";
|
|
|
41
41
|
const SHARED_METADATA_DIR = ".opencodex-native-main-profiles";
|
|
42
42
|
const INSTANCE_STAGING_DIR = "native-main-profile-staging";
|
|
43
43
|
const LEGACY_METADATA_DIR = "native-main-profiles";
|
|
44
|
-
const MAX_AUTH_BYTES = 4 * 1024 * 1024;
|
|
44
|
+
export const MAX_AUTH_BYTES = 4 * 1024 * 1024;
|
|
45
45
|
export const MAX_NATIVE_PROFILE_METADATA_BYTES = 4 * 1024 * 1024;
|
|
46
46
|
export const MAX_NATIVE_PROFILE_JOURNAL_BYTES = 17 * 1024 * 1024;
|
|
47
47
|
export const MAX_NATIVE_PROFILES = 32;
|
|
@@ -376,7 +376,7 @@ export function resolveNativeProfileContext(options: { codexHome?: string; confi
|
|
|
376
376
|
};
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
function readBounded(path: string, limit: number, testSeam?: BoundedReadTestSeam): Buffer {
|
|
379
|
+
export function readBounded(path: string, limit: number, testSeam?: BoundedReadTestSeam): Buffer {
|
|
380
380
|
let fd: number | undefined;
|
|
381
381
|
let failed = false;
|
|
382
382
|
try {
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
existsSync,
|
|
7
|
+
fsyncSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
truncateSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
import { Database } from "bun:sqlite";
|
|
20
|
+
|
|
21
|
+
import { getConfigDir } from "../config";
|
|
22
|
+
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
23
|
+
import { renameAtomicFile } from "../lib/windows-atomic-replace";
|
|
24
|
+
import {
|
|
25
|
+
decodeCompactionSummary,
|
|
26
|
+
isCompactionItemType,
|
|
27
|
+
SUMMARY_PREFIX,
|
|
28
|
+
} from "../responses/compaction";
|
|
29
|
+
import { resolveCodexHomeDir } from "./home";
|
|
30
|
+
import { resolveCodexStateDbPath } from "./paths";
|
|
31
|
+
|
|
32
|
+
export interface OcxCompactionRewriteResult {
|
|
33
|
+
content: string;
|
|
34
|
+
replaced: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface OcxCompactionHistoryRecoveryResult {
|
|
38
|
+
rolloutPath: string;
|
|
39
|
+
backupPath: string | null;
|
|
40
|
+
replaced: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface OcxCompactionHistoryRecoveryOptions {
|
|
44
|
+
threadId: string;
|
|
45
|
+
codexHome?: string;
|
|
46
|
+
stateDbPath?: string;
|
|
47
|
+
backupRoot?: string;
|
|
48
|
+
now?: () => Date;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const THREAD_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
52
|
+
|
|
53
|
+
function digest(content: string | Buffer): string {
|
|
54
|
+
return createHash("sha256").update(content).digest("hex");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pathInside(root: string, candidate: string): boolean {
|
|
58
|
+
const rel = relative(root, candidate);
|
|
59
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveOwnedRolloutPath(codexHome: string, rawPath: string): string {
|
|
63
|
+
const candidate = resolve(isAbsolute(rawPath) ? rawPath : join(codexHome, rawPath));
|
|
64
|
+
const roots = [join(codexHome, "sessions"), join(codexHome, "archived_sessions")]
|
|
65
|
+
.filter(existsSync)
|
|
66
|
+
.map(root => realpathSync.native(root));
|
|
67
|
+
const entry = lstatSync(candidate);
|
|
68
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
69
|
+
throw new Error("the referenced rollout is not a regular file");
|
|
70
|
+
}
|
|
71
|
+
const canonical = realpathSync.native(candidate);
|
|
72
|
+
if (!roots.some(root => pathInside(root, canonical))) {
|
|
73
|
+
throw new Error("the referenced rollout is outside Codex session storage");
|
|
74
|
+
}
|
|
75
|
+
return canonical;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writePrivateFile(path: string, content: string): void {
|
|
79
|
+
const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
80
|
+
try {
|
|
81
|
+
if (process.platform !== "win32") chmodSync(path, 0o600);
|
|
82
|
+
else hardenSecretPath(path, { required: true, timeoutMemoKey: path });
|
|
83
|
+
writeFileSync(fd, content, "utf8");
|
|
84
|
+
fsyncSync(fd);
|
|
85
|
+
} finally {
|
|
86
|
+
closeSync(fd);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function safeRemovePrivateFile(path: string): void {
|
|
91
|
+
try { truncateSync(path, 0); } catch { /* best effort before unlink */ }
|
|
92
|
+
try { unlinkSync(path); } catch { /* caller reports the original failure */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
96
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function lowerCompactionItem(item: unknown): { item: unknown; changed: boolean } {
|
|
100
|
+
if (!isRecord(item) || !isCompactionItemType(item.type)) {
|
|
101
|
+
return { item, changed: false };
|
|
102
|
+
}
|
|
103
|
+
if (typeof item.encrypted_content !== "string") {
|
|
104
|
+
return { item, changed: false };
|
|
105
|
+
}
|
|
106
|
+
const summary = decodeCompactionSummary(item.encrypted_content);
|
|
107
|
+
if (summary === null) return { item, changed: false };
|
|
108
|
+
return {
|
|
109
|
+
item: {
|
|
110
|
+
type: "message",
|
|
111
|
+
role: "user",
|
|
112
|
+
content: [{ type: "input_text", text: `${SUMMARY_PREFIX}\n${summary}` }],
|
|
113
|
+
},
|
|
114
|
+
changed: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function rewriteJsonlLine(line: string): { line: string; replaced: number } {
|
|
119
|
+
let record: unknown;
|
|
120
|
+
try {
|
|
121
|
+
record = JSON.parse(line);
|
|
122
|
+
} catch {
|
|
123
|
+
return { line, replaced: 0 };
|
|
124
|
+
}
|
|
125
|
+
if (!isRecord(record) || record.type !== "compacted" || !isRecord(record.payload)) {
|
|
126
|
+
return { line, replaced: 0 };
|
|
127
|
+
}
|
|
128
|
+
const history = record.payload.replacement_history;
|
|
129
|
+
if (!Array.isArray(history)) return { line, replaced: 0 };
|
|
130
|
+
|
|
131
|
+
let replaced = 0;
|
|
132
|
+
const replacementHistory = history.map(item => {
|
|
133
|
+
const lowered = lowerCompactionItem(item);
|
|
134
|
+
if (lowered.changed) replaced += 1;
|
|
135
|
+
return lowered.item;
|
|
136
|
+
});
|
|
137
|
+
if (replaced === 0) return { line, replaced: 0 };
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
line: JSON.stringify({
|
|
141
|
+
...record,
|
|
142
|
+
payload: { ...record.payload, replacement_history: replacementHistory },
|
|
143
|
+
}),
|
|
144
|
+
replaced,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convert OpenCodeX-owned `ocx1:` compaction items into ordinary replayable user messages.
|
|
150
|
+
*
|
|
151
|
+
* Only the authoritative `compacted.payload.replacement_history` snapshot is changed. Earlier
|
|
152
|
+
* response-item events are historical output and are deliberately preserved byte-for-byte.
|
|
153
|
+
* Native opaque compactions are also untouched because OpenCodeX cannot decode them safely.
|
|
154
|
+
*/
|
|
155
|
+
export function rewriteOcxCompactionsForNativeReplay(content: string): OcxCompactionRewriteResult {
|
|
156
|
+
const parts = content.split(/(\r?\n)/);
|
|
157
|
+
let replaced = 0;
|
|
158
|
+
for (let index = 0; index < parts.length; index += 2) {
|
|
159
|
+
const line = parts[index];
|
|
160
|
+
if (!line) continue;
|
|
161
|
+
const rewritten = rewriteJsonlLine(line);
|
|
162
|
+
if (rewritten.replaced === 0) continue;
|
|
163
|
+
parts[index] = rewritten.line;
|
|
164
|
+
replaced += rewritten.replaced;
|
|
165
|
+
}
|
|
166
|
+
return replaced === 0
|
|
167
|
+
? { content, replaced: 0 }
|
|
168
|
+
: { content: parts.join(""), replaced };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Repair one explicitly selected Codex rollout for direct native replay.
|
|
173
|
+
*
|
|
174
|
+
* The original bytes are copied to an owner-private backup before the rollout is atomically
|
|
175
|
+
* replaced. A last-moment digest check refuses a concurrent Codex append instead of losing it.
|
|
176
|
+
*/
|
|
177
|
+
export function recoverOcxCompactionHistory(
|
|
178
|
+
options: OcxCompactionHistoryRecoveryOptions,
|
|
179
|
+
): OcxCompactionHistoryRecoveryResult {
|
|
180
|
+
if (!THREAD_ID_RE.test(options.threadId)) throw new Error("thread id must be a UUID");
|
|
181
|
+
const codexHome = realpathSync.native(options.codexHome ?? resolveCodexHomeDir());
|
|
182
|
+
const stateDbPath = options.stateDbPath ?? resolveCodexStateDbPath({ codexHome });
|
|
183
|
+
if (!existsSync(stateDbPath)) throw new Error("Codex state database was not found");
|
|
184
|
+
|
|
185
|
+
const db = new Database(stateDbPath, { readonly: true });
|
|
186
|
+
let rawRolloutPath: string | undefined;
|
|
187
|
+
try {
|
|
188
|
+
db.exec("PRAGMA busy_timeout = 1000");
|
|
189
|
+
rawRolloutPath = db.query<{ rollout_path: string }, [string]>(
|
|
190
|
+
"SELECT rollout_path FROM threads WHERE id = ? LIMIT 1",
|
|
191
|
+
).get(options.threadId)?.rollout_path;
|
|
192
|
+
} finally {
|
|
193
|
+
db.close();
|
|
194
|
+
}
|
|
195
|
+
if (!rawRolloutPath) throw new Error("thread was not found in the Codex state database");
|
|
196
|
+
|
|
197
|
+
const rolloutPath = resolveOwnedRolloutPath(codexHome, rawRolloutPath);
|
|
198
|
+
const originalBytes = readFileSync(rolloutPath);
|
|
199
|
+
const original = originalBytes.toString("utf8");
|
|
200
|
+
if (!Buffer.from(original, "utf8").equals(originalBytes)) {
|
|
201
|
+
throw new Error("the rollout is not valid UTF-8 and cannot be repaired safely");
|
|
202
|
+
}
|
|
203
|
+
const rewritten = rewriteOcxCompactionsForNativeReplay(original);
|
|
204
|
+
if (rewritten.replaced === 0) {
|
|
205
|
+
return { rolloutPath, backupPath: null, replaced: 0 };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const stamp = (options.now ?? (() => new Date()))().toISOString().replace(/[:.]/g, "-");
|
|
209
|
+
const backupDir = resolve(options.backupRoot ?? join(getConfigDir(), "history-recovery-backups", options.threadId));
|
|
210
|
+
mkdirSync(backupDir, { recursive: true, mode: 0o700 });
|
|
211
|
+
const backupPath = join(backupDir, `${basename(rolloutPath)}.${stamp}.bak`);
|
|
212
|
+
writePrivateFile(backupPath, original);
|
|
213
|
+
|
|
214
|
+
const tempPath = `${rolloutPath}.ocx-repair-${process.pid}-${crypto.randomUUID()}.tmp`;
|
|
215
|
+
try {
|
|
216
|
+
writePrivateFile(tempPath, rewritten.content);
|
|
217
|
+
if (digest(readFileSync(rolloutPath)) !== digest(originalBytes)) {
|
|
218
|
+
throw new Error("the rollout changed while it was being repaired; close Codex and retry");
|
|
219
|
+
}
|
|
220
|
+
renameAtomicFile(tempPath, rolloutPath);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
safeRemovePrivateFile(tempPath);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
return { rolloutPath, backupPath, replaced: rewritten.replaced };
|
|
226
|
+
}
|
|
@@ -69,7 +69,9 @@ function multilineCloseIndex(
|
|
|
69
69
|
backslashes += 1;
|
|
70
70
|
}
|
|
71
71
|
if (backslashes % 2 === 0) break;
|
|
72
|
-
|
|
72
|
+
// An escaped quote can overlap the real terminator (backslash plus four quotes).
|
|
73
|
+
// Keep overlapping candidates instead of skipping the entire rejected delimiter.
|
|
74
|
+
index = line.indexOf(delimiter, index + 1);
|
|
73
75
|
}
|
|
74
76
|
return index;
|
|
75
77
|
}
|
package/src/codex/quota.ts
CHANGED
|
@@ -336,6 +336,9 @@ function mergeAccountQuota(
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
if (snapshotHasCustom(quota)) next.customWindows = quota.customWindows;
|
|
339
|
+
// Ordinary response headers omit model-specific windows reported by WHAM.
|
|
340
|
+
// Absence is a partial update; an explicit list (including []) still replaces it.
|
|
341
|
+
else if (existing?.customWindows !== undefined) next.customWindows = existing.customWindows;
|
|
339
342
|
|
|
340
343
|
if (quota.resetCredits !== undefined) next.resetCredits = quota.resetCredits;
|
|
341
344
|
else if (existing?.resetCredits !== undefined) next.resetCredits = existing.resetCredits;
|
|
@@ -795,6 +798,10 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
795
798
|
});
|
|
796
799
|
const sparkWindows = [spark?.rate_limit?.primary_window, spark?.rate_limit?.secondary_window]
|
|
797
800
|
.filter((window): window is WhamUsageWindow => !!window);
|
|
801
|
+
const sparkShort = sparkWindows.find(window => {
|
|
802
|
+
const percent = normalizeUsagePercent(window.used_percent);
|
|
803
|
+
return percent !== undefined && isExplicitShortWindow(window);
|
|
804
|
+
});
|
|
798
805
|
const sparkWeekly = sparkWindows.find(window => {
|
|
799
806
|
const percent = normalizeUsagePercent(window.used_percent);
|
|
800
807
|
const seconds = window.limit_window_seconds;
|
|
@@ -803,16 +810,19 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
803
810
|
&& !isExplicitMonthlyWindow(window)
|
|
804
811
|
&& (seconds === undefined || seconds >= WEEKLY_WINDOW_MIN_SECONDS);
|
|
805
812
|
});
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
+
const sparkCustomWindows: Array<{ label: string; percent: number; resetAt?: number }> = [];
|
|
814
|
+
for (const [label, window] of [
|
|
815
|
+
["GPT-5.3-Codex-Spark 5h", sparkShort],
|
|
816
|
+
["GPT-5.3-Codex-Spark Weekly", sparkWeekly],
|
|
817
|
+
] as const) {
|
|
818
|
+
const percent = normalizeUsagePercent(window?.used_percent);
|
|
819
|
+
if (percent === undefined) continue;
|
|
820
|
+
const sparkWindow: { label: string; percent: number; resetAt?: number } = { label, percent };
|
|
821
|
+
const resetAt = normalizeResetAt(window?.reset_at);
|
|
813
822
|
if (resetAt !== undefined) sparkWindow.resetAt = resetAt;
|
|
814
|
-
|
|
823
|
+
sparkCustomWindows.push(sparkWindow);
|
|
815
824
|
}
|
|
825
|
+
if (sparkCustomWindows.length > 0) quota.customWindows = sparkCustomWindows;
|
|
816
826
|
if (resetCredits !== undefined) quota.resetCredits = resetCredits;
|
|
817
827
|
|
|
818
828
|
return hasKnownQuotaValue(quota) || resetCredits !== undefined ? quota : null;
|
|
@@ -44,14 +44,30 @@ function owned(token: Token, writer: MainQuotaWriter): boolean {
|
|
|
44
44
|
function record(value: unknown): value is Record<string, unknown> {
|
|
45
45
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The ChatGPT per-user identity carried by a native credential, plus whether the token's own two
|
|
49
|
+
* encodings of it disagree. Precedence stays on the RAW claims, so an empty or non-string
|
|
50
|
+
* `chatgpt_user_id` still blocks the `user_id` fallback exactly as before; `conflict` is a
|
|
51
|
+
* separate observation for callers that must fail closed on an ambiguous identity.
|
|
52
|
+
*/
|
|
53
|
+
export function nativeUserIdClaims(token: string): { userId: string | undefined; conflict: boolean } {
|
|
54
|
+
const none = { userId: undefined, conflict: false };
|
|
48
55
|
try {
|
|
49
56
|
const payload: unknown = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
50
57
|
const auth = record(payload) ? payload["https://api.openai.com/auth"] : undefined;
|
|
51
|
-
if (!record(auth)) return;
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
if (!record(auth)) return none;
|
|
59
|
+
const named = (value: unknown): string | undefined =>
|
|
60
|
+
typeof value === "string" && value.length > 0 ? value : undefined;
|
|
61
|
+
const primary = named(auth.chatgpt_user_id);
|
|
62
|
+
const secondary = named(auth.user_id);
|
|
63
|
+
return {
|
|
64
|
+
userId: named(auth.chatgpt_user_id ?? auth.user_id),
|
|
65
|
+
conflict: primary !== undefined && secondary !== undefined && primary !== secondary,
|
|
66
|
+
};
|
|
67
|
+
} catch { return none; }
|
|
68
|
+
}
|
|
69
|
+
function userId(token: string): string | undefined {
|
|
70
|
+
return nativeUserIdClaims(token).userId;
|
|
55
71
|
}
|
|
56
72
|
function identityMatches(data: WhamUsageResponse, token: Token): boolean {
|
|
57
73
|
if (data.account_id != null && data.account_id !== token.chatgptAccountId) return false;
|