Merge pull request #257 from diegosouzapw/fix/issue-256-password-setup-bypass

fix: cursor decompression fallback + codex token refresh + password setup bypass (#250, #251, #256)
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-03-09 10:48:04 -03:00
committed by GitHub
3 changed files with 65 additions and 18 deletions

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -134,10 +134,16 @@ export async function isAuthRequired(): Promise<boolean> {
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)