diff --git a/open-sse/services/qoderCli.ts b/open-sse/services/qoderCli.ts index 5b596ed795..09c052097b 100644 --- a/open-sse/services/qoderCli.ts +++ b/open-sse/services/qoderCli.ts @@ -505,6 +505,22 @@ export async function validateQoderCliPat({ // Treat 5xx as valid bypass to prevent false negatives from legacy Qoder APIs (issue #1391) if (res.status >= 500) { + const isCosyAppError = + /"success"\s*:\s*false/.test(errorDetail) && + (/"msgCode"\s*:\s*500/.test(errorDetail) || /internal\s*server\s*error/i.test(errorDetail)); + + if (isCosyAppError) { + return { + valid: false, + error: + `Authentication failed (HTTP ${res.status}). The Qoder Cosy server returned an Internal Server Error. ` + + "This typically indicates that your Personal Access Token is invalid, expired, or not authorized. " + + "Please check your token at https://qoder.com/account/integrations." + + (errorDetail ? ` Server response: ${errorDetail}` : ""), + unsupported: false, + }; + } + return { valid: true, error: `Validation endpoint returned HTTP ${res.status}${errorDetail ? `: ${errorDetail}` : ""}, treating PAT as valid`, diff --git a/tests/unit/qoder-cli.test.ts b/tests/unit/qoder-cli.test.ts index 310a2bda83..c4a45cb01c 100644 --- a/tests/unit/qoder-cli.test.ts +++ b/tests/unit/qoder-cli.test.ts @@ -364,3 +364,41 @@ test("validateQoderCliPat treats 5xx HTTP failures as valid bypass", async () => globalThis.fetch = originalFetch; } }); + +test("validateQoderCliPat rejects 500 HTTP failures if response is Cosy app-level auth error", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url) => { + if (String(url).includes("/ping")) return new Response("pong", { status: 200 }); + return new Response( + ' { "success" : false, "traceId": "a4e5de61929400b9243b4f6e49756906", "msgCode" : 500 } ', + { status: 500 } + ); + }; + + try { + const result = await qoderCli.validateQoderCliPat({ apiKey: "invalid-pat" }); + assert.equal(result.valid, false); + assert.match(result.error!, /Authentication failed \(HTTP 500\)/); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("validateQoderCliPat rejects 500 HTTP failures using regex for Internal Server Error with whitespace", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url) => { + if (String(url).includes("/ping")) return new Response("pong", { status: 200 }); + return new Response( + ' { "success" : false, "error": "Internal Server Error" } ', + { status: 500 } + ); + }; + + try { + const result = await qoderCli.validateQoderCliPat({ apiKey: "invalid-pat" }); + assert.equal(result.valid, false); + assert.match(result.error!, /Authentication failed \(HTTP 500\)/); + } finally { + globalThis.fetch = originalFetch; + } +});