Skip to content

Commit c0b51aa

Browse files
committed
fix: keep deactivated workspaces out of restore flow
1 parent de8615b commit c0b51aa

File tree

3 files changed

+97
-23
lines changed

3 files changed

+97
-23
lines changed

index.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3071,6 +3071,9 @@ while (attempted.size < Math.max(1, accountCount)) {
30713071
lastError = new Error("Codex response did not include quota headers");
30723072
} catch (error) {
30733073
lastError = error instanceof Error ? error : new Error(String(error));
3074+
if (lastError.message === "deactivated_workspace") {
3075+
throw lastError;
3076+
}
30743077
}
30753078
}
30763079

@@ -3291,27 +3294,27 @@ while (attempted.size < Math.max(1, accountCount)) {
32913294
console.log(
32923295
`[${i + 1}/${total}] ${label}: ${formatCodexQuotaLine(snapshot)}`,
32933296
);
3294-
} catch (error) {
3295-
errors += 1;
3296-
const message = error instanceof Error ? error.message : String(error);
3297-
if (message.includes("deactivated_workspace")) {
3298-
const flaggedRecord: FlaggedAccountMetadataV1 = {
3299-
...account,
3300-
flaggedAt: Date.now(),
3301-
flaggedReason: "workspace-deactivated",
3302-
lastError: message,
3303-
};
3304-
flaggedUpdates.set(
3305-
getWorkspaceIdentityKey(flaggedRecord),
3306-
flaggedRecord,
3307-
);
3308-
removeFromActive.add(getWorkspaceIdentityKey(account));
3309-
flaggedChanged = true;
3310-
}
3311-
console.log(
3312-
`[${i + 1}/${total}] ${label}: ERROR (${message.slice(0, 160)})`,
3297+
} catch (error) {
3298+
errors += 1;
3299+
const message = error instanceof Error ? error.message : String(error);
3300+
if (message === "deactivated_workspace") {
3301+
const flaggedRecord: FlaggedAccountMetadataV1 = {
3302+
...account,
3303+
flaggedAt: Date.now(),
3304+
flaggedReason: "workspace-deactivated",
3305+
lastError: message,
3306+
};
3307+
flaggedUpdates.set(
3308+
getWorkspaceIdentityKey(flaggedRecord),
3309+
flaggedRecord,
33133310
);
3311+
removeFromActive.add(getWorkspaceIdentityKey(account));
3312+
flaggedChanged = true;
33143313
}
3314+
console.log(
3315+
`[${i + 1}/${total}] ${label}: ERROR (${message.slice(0, 160)})`,
3316+
);
3317+
}
33153318
} catch (error) {
33163319
errors += 1;
33173320
const message = error instanceof Error ? error.message : String(error);
@@ -3369,6 +3372,13 @@ while (attempted.size < Math.max(1, accountCount)) {
33693372
const flagged = flaggedStorage.accounts[i];
33703373
if (!flagged) continue;
33713374
const label = flagged.email ?? flagged.accountLabel ?? `Flagged ${i + 1}`;
3375+
if (flagged.flaggedReason === "workspace-deactivated") {
3376+
console.log(
3377+
`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: STILL FLAGGED (workspace deactivated)`,
3378+
);
3379+
remaining.push(flagged);
3380+
continue;
3381+
}
33723382
try {
33733383
const cached = await lookupCodexCliTokensByEmail(flagged.email);
33743384
const now = Date.now();

lib/storage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,9 @@ function normalizeFlaggedStorage(data: unknown): FlaggedAccountStorageV1 {
923923

924924
const normalizeFlaggedIdentityPart = (value: unknown): string | undefined =>
925925
typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
926+
// Flagged storage must keep sibling workspace entries separate when they share an
927+
// organization but have different accountIds, so this key is more specific than
928+
// the normal account identity collapse used in active storage.
926929
const getFlaggedIdentityKey = (account: {
927930
organizationId?: string;
928931
accountId?: string;

test/index.test.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,21 @@ vi.mock("../lib/request/rate-limit-backoff.js", () => ({
186186
refreshAndUpdateToken: vi.fn(async (auth: unknown) => auth),
187187
createCodexHeaders: vi.fn(() => new Headers()),
188188
handleErrorResponse: vi.fn(async (response: Response) => ({ response })),
189-
isDeactivatedWorkspaceError: vi.fn((errorBody: unknown, status?: number) =>
190-
status === 402 &&
191-
(errorBody as { error?: { code?: string } })?.error?.code === "deactivated_workspace",
192-
),
189+
isDeactivatedWorkspaceError: vi.fn((errorBody: unknown, status?: number) => {
190+
if (status !== 402 || !errorBody || typeof errorBody !== "object") return false;
191+
const body = errorBody as {
192+
code?: unknown;
193+
detail?: { code?: unknown } | undefined;
194+
error?: { code?: unknown; type?: unknown } | undefined;
195+
};
196+
const code =
197+
(typeof body.code === "string" && body.code) ||
198+
(typeof body.detail?.code === "string" && body.detail.code) ||
199+
(typeof body.error?.code === "string" && body.error.code) ||
200+
(typeof body.error?.type === "string" && body.error.type) ||
201+
undefined;
202+
return code === "deactivated_workspace";
203+
}),
193204
getUnsupportedCodexModelInfo: vi.fn(() => ({ isUnsupported: false })),
194205
resolveUnsupportedCodexFallbackModel: vi.fn(() => undefined),
195206
shouldFallbackToGpt52OnUnsupportedGpt53: vi.fn(() => false),
@@ -3498,6 +3509,56 @@ describe("OpenAIOAuthPlugin persistAccountPool", () => {
34983509
});
34993510
});
35003511

3512+
it("keeps workspace-deactivated flagged entries out of verify-flagged restore", async () => {
3513+
const cliModule = await import("../lib/cli.js");
3514+
const storageModule = await import("../lib/storage.js");
3515+
const refreshQueueModule = await import("../lib/refresh-queue.js");
3516+
3517+
mockFlaggedStorage.accounts = [
3518+
{
3519+
refreshToken: "flagged-refresh-dead",
3520+
organizationId: "org-dead",
3521+
accountId: "workspace-dead",
3522+
accountIdSource: "manual",
3523+
accountLabel: "Dead Workspace",
3524+
email: "dead@example.com",
3525+
flaggedAt: Date.now() - 500,
3526+
flaggedReason: "workspace-deactivated",
3527+
lastError: "deactivated_workspace",
3528+
addedAt: Date.now() - 500,
3529+
lastUsed: Date.now() - 500,
3530+
},
3531+
];
3532+
3533+
vi.mocked(cliModule.promptLoginMode)
3534+
.mockResolvedValueOnce({ mode: "verify-flagged" })
3535+
.mockResolvedValueOnce({ mode: "cancel" });
3536+
3537+
const mockClient = createMockClient();
3538+
const { OpenAIOAuthPlugin } = await import("../index.js");
3539+
const plugin = (await OpenAIOAuthPlugin({
3540+
client: mockClient,
3541+
} as never)) as unknown as PluginType;
3542+
const autoMethod = plugin.auth.methods[0] as unknown as {
3543+
authorize: (inputs?: Record<string, string>) => Promise<{ instructions: string }>;
3544+
};
3545+
3546+
const authResult = await autoMethod.authorize();
3547+
expect(authResult.instructions).toBe("Authentication cancelled");
3548+
3549+
expect(vi.mocked(refreshQueueModule.queuedRefresh)).not.toHaveBeenCalled();
3550+
expect(mockStorage.accounts).toHaveLength(0);
3551+
expect(vi.mocked(storageModule.saveFlaggedAccounts)).toHaveBeenCalledWith({
3552+
version: 1,
3553+
accounts: expect.arrayContaining([
3554+
expect.objectContaining({
3555+
accountId: "workspace-dead",
3556+
flaggedReason: "workspace-deactivated",
3557+
}),
3558+
]),
3559+
});
3560+
});
3561+
35013562
it("removes only the token-invalid org-scoped workspace during deep-check cleanup", async () => {
35023563
const cliModule = await import("../lib/cli.js");
35033564
const refreshQueueModule = await import("../lib/refresh-queue.js");

0 commit comments

Comments
 (0)