From 59a8b8db358c05d5bf2c929ff0e27dd213dd9f2e Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Mon, 9 Mar 2026 10:47:16 -0300 Subject: [PATCH] fix: cursor decompression fallback + codex token refresh + password setup bypass (#250, #251, #256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(cursor): add zlib inflate/inflateRaw fallback for GZIP_ALT/GZIP_BOTH frames (#250) - GZIP_ALT (0x02) and GZIP_BOTH (0x03) frames may use zlib deflate instead of gzip - Now tries gunzipSync → inflateSync → inflateRawSync with verbose error logging fix(codex): override refreshCredentials to enable automatic token refresh on 401 (#251) - CodexExecutor was inheriting base class null return, blocking 401 recovery - Now calls refreshCodexToken() from tokenRefresh.ts on 401 upstream response - Handles rotating token errors and surfaces re-auth warning clearly fix(auth): allow dashboard access when no password is set after onboarding skip (#256) - isAuthRequired() was blocking /dashboard/settings when setupComplete=true but no password - Removed setupComplete guard: if there's no password, auth cannot be required - Configure Password button on login page now navigates correctly to security settings --- open-sse/executors/codex.ts | 26 ++++++++++++++++++++++ open-sse/executors/cursor.ts | 43 ++++++++++++++++++++++++------------ src/shared/utils/apiAuth.ts | 14 ++++++++---- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 13de1d9547..04ba86bc79 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -1,6 +1,7 @@ import { BaseExecutor } from "./base.ts"; import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.ts"; import { PROVIDERS } from "../config/constants.ts"; +import { refreshCodexToken } from "../services/tokenRefresh.ts"; // Ordered list of effort levels from lowest to highest const EFFORT_ORDER = ["none", "low", "medium", "high", "xhigh"] as const; @@ -62,6 +63,31 @@ export class CodexExecutor extends BaseExecutor { return headers; } + /** + * Refresh Codex OAuth credentials when a 401 is received. + * OpenAI uses rotating (one-time-use) refresh tokens — if the token was already + * consumed by a concurrent refresh, this returns null to signal re-auth is needed. + * + * Fixes #251: After a server restart/upgrade, previously cached access tokens may + * have expired or become invalid. chatCore.ts calls this on 401; previously the + * base class returned null causing the request to fail instead of refreshing. + */ + async refreshCredentials(credentials, log) { + if (!credentials?.refreshToken) { + log?.warn?.("TOKEN_REFRESH", "Codex: no refresh token available, re-authentication required"); + return null; + } + const result = await refreshCodexToken(credentials.refreshToken, log); + if (!result || result.error) { + log?.warn?.( + "TOKEN_REFRESH", + `Codex: token refresh failed${result?.error ? ` (${result.error})` : ""} — re-authentication required` + ); + return null; + } + return result; + } + /** * Transform request before sending - inject default instructions if missing */ diff --git a/open-sse/executors/cursor.ts b/open-sse/executors/cursor.ts index 97b05d4e28..cad6ba982d 100644 --- a/open-sse/executors/cursor.ts +++ b/open-sse/executors/cursor.ts @@ -76,22 +76,37 @@ function decompressPayload(payload, flags) { flags === COMPRESS_FLAG.GZIP_ALT || flags === COMPRESS_FLAG.GZIP_BOTH ) { + // Primary: try gzip decompression (standard gzip header 0x1f 0x8b) try { return zlib.gunzipSync(payload); - } catch (err) { - console.log( - `[DECOMPRESS ERROR] flags=${flags}, payloadSize=${payload.length}, error=${err.message}` - ); - console.log(`[DECOMPRESS ERROR] First 50 bytes (hex):`, payload.slice(0, 50).toString("hex")); - console.log( - `[DECOMPRESS ERROR] First 50 bytes (utf8):`, - payload - .slice(0, 50) - .toString("utf8") - .replace(/[^\x20-\x7E]/g, ".") - ); - // Try to use payload as-is if decompression fails - return payload; + } catch (gzipErr) { + // Fallback: GZIP_ALT (0x02) and GZIP_BOTH (0x03) frames sometimes use + // raw zlib deflate format instead of gzip wrapping (#250) + try { + return zlib.inflateSync(payload); + } catch (deflateErr) { + // Last resort: try raw deflate (no zlib header) + try { + return zlib.inflateRawSync(payload); + } catch (rawErr) { + console.log( + `[DECOMPRESS ERROR] flags=${flags}, payloadSize=${payload.length}, gzip=${gzipErr.message}, deflate=${deflateErr.message}, raw=${rawErr.message}` + ); + console.log( + `[DECOMPRESS ERROR] First 50 bytes (hex):`, + payload.slice(0, 50).toString("hex") + ); + console.log( + `[DECOMPRESS ERROR] First 50 bytes (utf8):`, + payload + .slice(0, 50) + .toString("utf8") + .replace(/[^\x20-\x7E]/g, ".") + ); + // Try to use payload as-is if all decompression methods fail + return payload; + } + } } } return payload; diff --git a/src/shared/utils/apiAuth.ts b/src/shared/utils/apiAuth.ts index 55ab0b091d..9a6535cea0 100644 --- a/src/shared/utils/apiAuth.ts +++ b/src/shared/utils/apiAuth.ts @@ -134,10 +134,16 @@ export async function isAuthRequired(): Promise { try { const settings = await getSettings(); if (settings.requireLogin === false) return false; - // Only skip auth for fresh installs (not yet onboarded) with no password. - // Once setupComplete is true, always require auth — prevents bypass if password row is lost (#151) - if (!settings.setupComplete && !settings.password && !process.env.INITIAL_PASSWORD) - return false; + // Allow access with no password set — there's nothing to authenticate against. + // This covers two cases: + // 1. Fresh installs (setupComplete=false) — first-run, no password yet + // 2. setupComplete=true but password was skipped during onboarding (#256) + // The user needs unauthenticated access to /dashboard/settings to set a password. + // Note: this is safe because Bearer API key auth is still checked in verifyAuth(). + // The security concern from #151 (password row lost after being set) is handled by the + // hasPassword flag — if a password WAS set and then somehow lost, the user can use the + // reset-password CLI tool (bin/reset-password.mjs). + if (!settings.password && !process.env.INITIAL_PASSWORD) return false; return true; } catch { // On error, require auth (secure by default)