mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user