Merge branch 'fix/v1439-jobs' into release/v1.43.9

This commit is contained in:
Marc-André Bombeck committed 2026-10-21 01:17:39 +02:00
commit 897fb10c5b
2 files changed
+76 -4

No files matched your search

+22 -4
View File
@@ -14,6 +14,7 @@ import {
decryptCodexCreds,
encryptAdminCodexCreds,
decryptAdminCodexCreds,
CodexRefreshError,
type CodexCreds,
} from "./codex-oauth";
import { isPublicUrl } from "@/lib/validations/notifications";
@@ -287,16 +288,33 @@ async function runCodexRefresh(
// A sign-in replaced the token while the issuer refused the old one.
return { fresh: now.creds };
}
if (!isDeadCodexSignIn(error)) return { failed: error };
// `refresh_token_reused` is a dead grant here and only here: under the
// lock, with the row still holding the very token just spent, no peer
// of ours rotated it, so the issuer is refusing the whole family (a
// revoked sign-in answers every refresh so). Outside the lock it is
// the lost race it usually is (see DEAD_REFRESH_TOKEN_CODES).
const refused =
oauthErrorOf(error) === "refresh_token_reused"
? new CodexRefreshError(401, "refresh_token_reused")
: error;
if (!isDeadCodexSignIn(refused)) return { failed: error };
return {
refused: error,
refused,
parked: (await store.park(tx, row.refreshEncrypted)) === 1,
};
}
if ((await store.writeRotated(tx, row.refreshEncrypted, fresh)) === 0) {
// A sign-in replaced the row meanwhile; it wins over this refresh.
// A sign-in replaced the row meanwhile; it wins over this refresh. A
// row with no usable credential left was disconnected meanwhile: the
// person took Codex away, so nothing may be sent with it any more.
const now = await store.read(tx);
if (now?.creds) return { fresh: now.creds };
if (now?.status === "connected" && now.creds) {
return { fresh: now.creds };
}
return {
refused: codexReconnectRequired("disconnected during refresh"),
parked: false,
};
}
return { fresh };
},
@@ -319,3 +319,57 @@ describe("two refreshes of one Codex token", () => {
expect(parkSignals("codex")).toHaveLength(0);
});
});
describe("a Codex refresh under the lock", () => {
it("parks a sign-in whose every refresh the issuer answers as reused", async () => {
const user = await seedCodexUser();
// The issuer revoked the token family: no peer rotated anything, yet each
// refresh comes back reused.
vi.mocked(refreshDeviceTokens).mockRejectedValue(
new CodexRefreshError(400, "refresh_token_reused"),
);
const chain = await resolveProviderChain(user.id);
expect(chain.map((p) => p.providerType)).not.toContain("codex");
expect(
(
await prisma.user.findUniqueOrThrow({
where: { id: user.id },
select: { codexConnectionStatus: true },
})
).codexConnectionStatus,
).toBe("expired");
expect(parkSignals("codex")).toHaveLength(1);
expect(parkSignals("codex")[0][0].meta?.oauth_error).toBe(
"refresh_token_reused",
);
});
it("does not use Codex once more when the person disconnected it meanwhile", async () => {
const user = await seedCodexUser();
vi.mocked(refreshDeviceTokens).mockImplementation(async () => {
await prisma.user.update({
where: { id: user.id },
data: {
codexAccessTokenEncrypted: null,
codexRefreshTokenEncrypted: null,
codexConnectionStatus: "disconnected",
},
});
return creds("rotated", 10 * HOUR_MS);
});
const chain = await resolveProviderChain(user.id);
expect(chain.map((p) => p.providerType)).not.toContain("codex");
expect(
(
await prisma.user.findUniqueOrThrow({
where: { id: user.id },
select: { codexConnectionStatus: true },
})
).codexConnectionStatus,
).toBe("disconnected");
});
});