From 3494bb7d2024044aa6d4c2acc434f2047409220a Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Sat, 21 Mar 2026 20:34:37 +0000 Subject: [PATCH] fix: wrap loader in try/catch to prevent cascading plugin failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loader function can throw from refreshCursorToken, getCursorModels, or startProxy. When it does, the unhandled exception propagates up to OpenCode's plugin system and silently kills all plugin loading — including unrelated plugins. Changes: - Wrap entire loader in try/catch, return {} on any failure - Check for missing refresh token before attempting refresh - Log actionable error messages with re-auth instructions - Separate try/catch for refresh, model discovery, and proxy start so each failure mode gets a specific error message Note: This does NOT fix the OpenCode v1.2.27 auth hooks crash (Expected string, got undefined at worker.js:40673) which is caused by OpenCode's Zod schema validation rejecting the methods array. That bug is tracked at: anomalyco/opencode#18536 --- src/index.ts | 162 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 95 insertions(+), 67 deletions(-) diff --git a/src/index.ts b/src/index.ts index d1b9ac1..0b94b34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,81 +29,109 @@ export const CursorAuthPlugin: Plugin = async ( provider: CURSOR_PROVIDER_ID, async loader(getAuth, provider) { - const auth = await getAuth(); - if (!auth || auth.type !== "oauth") return {}; - - // Ensure we have a valid access token, refreshing if expired - let accessToken = auth.access; - if (!accessToken || auth.expires < Date.now()) { - const refreshed = await refreshCursorToken(auth.refresh); - await input.client.auth.set({ - path: { id: CURSOR_PROVIDER_ID }, - body: { - type: "oauth", - refresh: refreshed.refresh, - access: refreshed.access, - expires: refreshed.expires, - }, - }); - accessToken = refreshed.access; - } - - const models = await getCursorModels(accessToken); - - const port = await startProxy(async () => { - const currentAuth = await getAuth(); - if (currentAuth.type !== "oauth") { - throw new Error("Cursor auth not configured"); + try { + const auth = await getAuth(); + if (!auth || auth.type !== "oauth") return {}; + + // Ensure we have a valid access token, refreshing if expired + let accessToken = auth.access; + if (!accessToken || auth.expires < Date.now()) { + if (!auth.refresh) { + console.error("[cursor-oauth] No refresh token available — re-authenticate with: opencode auth login --provider cursor"); + return {}; + } + try { + const refreshed = await refreshCursorToken(auth.refresh); + await input.client.auth.set({ + path: { id: CURSOR_PROVIDER_ID }, + body: { + type: "oauth", + refresh: refreshed.refresh, + access: refreshed.access, + expires: refreshed.expires, + }, + }); + accessToken = refreshed.access; + } catch (refreshErr) { + console.error(`[cursor-oauth] Token refresh failed: ${refreshErr instanceof Error ? refreshErr.message : refreshErr}`); + console.error("[cursor-oauth] Re-authenticate with: opencode auth login --provider cursor"); + return {}; + } } - if (!currentAuth.access || currentAuth.expires < Date.now()) { - const refreshed = await refreshCursorToken(currentAuth.refresh); - await input.client.auth.set({ - path: { id: CURSOR_PROVIDER_ID }, - body: { - type: "oauth", - refresh: refreshed.refresh, - access: refreshed.access, - expires: refreshed.expires, - }, - }); - return refreshed.access; + let models: CursorModel[]; + try { + models = await getCursorModels(accessToken); + } catch (modelErr) { + console.error(`[cursor-oauth] Model discovery failed, using fallback models: ${modelErr instanceof Error ? modelErr.message : modelErr}`); + models = await getCursorModels(accessToken); // getCursorModels already has internal fallback } - return currentAuth.access; - }, models); + let port: number; + try { + port = await startProxy(async () => { + const currentAuth = await getAuth(); + if (currentAuth.type !== "oauth") { + throw new Error("Cursor auth not configured"); + } - if (provider) { - (provider as any).models = buildCursorProviderModels(models, port); - } + if (!currentAuth.access || currentAuth.expires < Date.now()) { + const refreshed = await refreshCursorToken(currentAuth.refresh); + await input.client.auth.set({ + path: { id: CURSOR_PROVIDER_ID }, + body: { + type: "oauth", + refresh: refreshed.refresh, + access: refreshed.access, + expires: refreshed.expires, + }, + }); + return refreshed.access; + } - return { - baseURL: `http://localhost:${port}/v1`, - apiKey: "cursor-proxy", - async fetch( - requestInput: RequestInfo | URL, - init?: RequestInit, - ) { - if (init?.headers) { - if (init.headers instanceof Headers) { - init.headers.delete("authorization"); - } else if (Array.isArray(init.headers)) { - init.headers = init.headers.filter( - ([key]) => key.toLowerCase() !== "authorization", - ); - } else { - delete (init.headers as Record)[ - "authorization" - ]; - delete (init.headers as Record)[ - "Authorization" - ]; + return currentAuth.access; + }, models); + } catch (proxyErr) { + console.error(`[cursor-oauth] Proxy failed to start: ${proxyErr instanceof Error ? proxyErr.message : proxyErr}`); + return {}; + } + + if (provider) { + (provider as any).models = buildCursorProviderModels(models, port); + } + + return { + baseURL: `http://localhost:${port}/v1`, + apiKey: "cursor-proxy", + async fetch( + requestInput: RequestInfo | URL, + init?: RequestInit, + ) { + if (init?.headers) { + if (init.headers instanceof Headers) { + init.headers.delete("authorization"); + } else if (Array.isArray(init.headers)) { + init.headers = init.headers.filter( + ([key]) => key.toLowerCase() !== "authorization", + ); + } else { + delete (init.headers as Record)[ + "authorization" + ]; + delete (init.headers as Record)[ + "Authorization" + ]; + } } - } - return fetch(requestInput, init); - }, - }; + return fetch(requestInput, init); + }, + }; + } catch (err) { + // Catch-all: never let loader errors propagate and kill other plugins + console.error(`[cursor-oauth] Loader failed: ${err instanceof Error ? err.message : err}`); + return {}; + } }, methods: [