diff --git a/src/post/post.ts b/src/post/post.ts index 3886e5c..90ae232 100644 --- a/src/post/post.ts +++ b/src/post/post.ts @@ -28,6 +28,11 @@ const VERDICTS = new Map([ ]); const PROFILE_TYPES = new Set(["CLAUDE", "CODEX", "OPENCODE"]); const FAILURE_REASONS = new Set(["SIGN_IN_FAILED", "USAGE_EXHAUSTED", "FAILED"]); +// Anchors written before the UPPER_SNAKE switch spell sides in lowercase. +const OLD_SIDES = new Map([ + ["new", "NEW"], + ["old", "OLD"], +]); const isProfileType = (value: unknown): value is ProfileFailure["type"] => PROFILE_TYPES.has(value); const isFailureReason = (value: unknown): value is FailureReason => FAILURE_REASONS.has(value); const failureComment = (failures: ProfileFailure[]): string => @@ -56,7 +61,7 @@ function parseAnchors(text: string, sourcePath: string): Anchor[] { const path = field(entry, "path"); const line = field(entry, "line"); const givenSide = field(entry, "side"); - const side = givenSide === undefined ? "NEW" : givenSide; + const side = givenSide === undefined ? "NEW" : (OLD_SIDES.get(givenSide) ?? givenSide); const body = field(entry, "body"); if (typeof path !== "string" || path === "") { throw new TypeError(`${at}.path: expected a repository path`); diff --git a/src/runtime/credentials.test.ts b/src/runtime/credentials.test.ts index ef9b43d..2508e5d 100644 --- a/src/runtime/credentials.test.ts +++ b/src/runtime/credentials.test.ts @@ -65,3 +65,14 @@ test("rejects malformed central credentials", async () => { ), ).rejects.toThrow("profiles[0].token"); }); + +test("reads lowercase profile types from older credentials", async () => { + const credentials = await parseCredentials( + JSON.stringify({ + "reviewer-token": ENCRYPTED, + profiles: [{ type: "codex" }, { type: "claude", token: ENCRYPTED }], + }), + MASTER_KEY, + ); + expect(credentials.profiles.map(({ type }) => type)).toStrictEqual(["CODEX", "CLAUDE"]); +}); diff --git a/src/runtime/credentials.ts b/src/runtime/credentials.ts index 3cf4fd0..3e9634a 100644 --- a/src/runtime/credentials.ts +++ b/src/runtime/credentials.ts @@ -78,6 +78,17 @@ export async function encryptCredential( return `v1:${encodeBase64Url(nonce)}:${encodeBase64Url(new Uint8Array(ciphertext))}`; } +// profiles.json files written before the UPPER_SNAKE switch spell types in lowercase. +const OLD_PROFILE_TYPES = new Map([ + ["claude", "CLAUDE"], + ["codex", "CODEX"], + ["opencode", "OPENCODE"], +]); + +function currentSpelling(type: unknown): unknown { + return OLD_PROFILE_TYPES.get(type) ?? type; +} + async function parseProfile(entry: unknown, index: number, masterKey: string): Promise { const at = `profiles[${index}]`; if (!isMapping(entry)) { @@ -88,7 +99,7 @@ async function parseProfile(entry: unknown, index: number, masterKey: string): P throw new Error(`${at}.${key}: unknown field`); } } - const type = field(entry, "type"); + const type = currentSpelling(field(entry, "type")); const model = field(entry, "model"); const token = field(entry, "token"); if (type !== "CLAUDE" && type !== "CODEX" && type !== "OPENCODE") {