mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(providers/blackbox-web): BLACKBOX_WEB_VALIDATED_TOKEN env override (#2252)
Blackbox's `/api/chat` now rejects requests whose `validated` field
doesn't match the frontend `tk` token (exported from app.blackbox.ai's
Next.js bundle), returning HTTP 403 even when the session cookie is
valid and the subscription is active. The previous executor sent a
random UUID, which works only until Blackbox enforces the check.
This change:
- Adds `resolveBlackboxValidatedToken()` that returns
`BLACKBOX_WEB_VALIDATED_TOKEN` when set, otherwise falls back to the
legacy random UUID (no regression for users who already work).
- Detects 403 responses whose body indicates a token-specific failure
("invalid validated token", "validation token", etc.) and replaces
the generic "cookie expired" message with explicit guidance to set
BLACKBOX_WEB_VALIDATED_TOKEN. The cookie-expired path is preserved
for non-token 401/403.
- Documents the env var in `.env.example` and
`docs/reference/ENVIRONMENT.md` (env-doc-sync check passes).
Deliberately NOT included: runtime scraping of Blackbox's Next.js
chunks to auto-extract `tk`. That coupling to their bundle hash would
silently break on every frontend deploy — the env override is the
stable path for operators who have already resolved the token.
Reported by @kazimshah39 with detailed root-cause analysis.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -527,6 +527,14 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
|
||||
# QODER_CLI_WORKSPACE=
|
||||
# OMNIROUTE_QODER_WORKSPACE=
|
||||
|
||||
# ── Blackbox Web validated-token override (issue #2252) ──
|
||||
# Used by: open-sse/executors/blackbox-web.ts. Blackbox `/api/chat` rejects
|
||||
# requests whose `validated` field doesn't match the frontend `tk` token,
|
||||
# returning HTTP 403 even with a valid session cookie + active subscription.
|
||||
# Set this to the `tk` value exported from app.blackbox.ai's Next.js bundle
|
||||
# to bypass the random-UUID fallback. Leave empty to keep the legacy behavior.
|
||||
# BLACKBOX_WEB_VALIDATED_TOKEN=
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ⚠️ GOOGLE OAUTH (Antigravity, Gemini CLI) & OTHER PROVIDERS — REMOTE SERVERS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
- **fix(ui/claude-extra-usage):** clarify the toggle-success notification text to spell out the toggle→effect relationship ("Claude extra-usage blocking enabled/disabled" instead of the ambiguous "blocked/allowed"). (#2157)
|
||||
- **fix(providers/qoder):** disambiguate the "Local CLI runtime is not installed" error when a user pastes a Personal Access Token but the connection is in OAuth/CLI-flavored mode. The test route now surfaces a single actionable message ("switch this connection to API Key auth") instead of cascading CLI + 401 errors. (#2247)
|
||||
- **fix(dashboard/api-manager):** route custom OpenAI-/Anthropic-compatible provider IDs through `getProviderDisplayName` so the model grouping label shows `Compatible (openai)` instead of leaking the raw synthetic `openai-compatible-chat-<uuid>` value. (#2021)
|
||||
- **fix(providers/blackbox-web):** add `BLACKBOX_WEB_VALIDATED_TOKEN` env override and 403 token-error disambiguation. Blackbox `/api/chat` started rejecting requests whose `validated` field didn't match the frontend `tk` token, even with a valid cookie + active subscription. Operators with the real token can now set the env var; otherwise the previous random-UUID fallback still ships, and a 403 with a token-specific body now surfaces a one-line "set BLACKBOX_WEB_VALIDATED_TOKEN" hint instead of the generic "cookie expired" message. (#2252)
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -367,6 +367,7 @@ Built-in credentials for **localhost development**. For remote deployments, regi
|
||||
| `QODER_PERSONAL_ACCESS_TOKEN` | Qoder | Direct API key fallback (bypasses OAuth). |
|
||||
| `QODER_CLI_WORKSPACE` | Qoder | Workspace ID for Qoder CLI. |
|
||||
| `OMNIROUTE_QODER_WORKSPACE` | Qoder | Alias for `QODER_CLI_WORKSPACE`. |
|
||||
| `BLACKBOX_WEB_VALIDATED_TOKEN` | Blackbox Web | Frontend `tk` token to send as `validated` on `/api/chat`. Required when Blackbox enforces token matching; otherwise OmniRoute falls back to a random UUID. See issue #2252. |
|
||||
|
||||
> [!WARNING]
|
||||
> **Google OAuth** (Antigravity, Gemini CLI) credentials **only work on localhost**. For remote servers:
|
||||
|
||||
@@ -14,6 +14,45 @@ const BLACKBOX_USER_AGENT =
|
||||
|
||||
const SESSION_CACHE_TTL_MS = 5 * 60_000; // 5 minutes
|
||||
|
||||
/**
|
||||
* Resolve the `validated` token for Blackbox `/api/chat` requests.
|
||||
*
|
||||
* Blackbox's web frontend ships a real validation token (exported as `tk` from
|
||||
* its Next.js JavaScript chunks). If the value sent in `transformedBody.validated`
|
||||
* does not match that token, the upstream returns HTTP 403 even when the session
|
||||
* cookie and subscription are valid — see issue #2252.
|
||||
*
|
||||
* Resolution priority:
|
||||
* 1. `BLACKBOX_WEB_VALIDATED_TOKEN` env var (operator-supplied, preferred)
|
||||
* 2. Random UUID fallback (the original behavior; works only as long as
|
||||
* Blackbox doesn't enforce a specific frontend `tk` value)
|
||||
*
|
||||
* We do NOT scrape Blackbox's Next.js chunks at runtime to extract `tk` — that
|
||||
* coupling to their bundle hash is fragile and would silently break on every
|
||||
* frontend deploy. The env-var override gives operators who have figured out
|
||||
* the token a stable way to use it without patching code.
|
||||
*/
|
||||
export function resolveBlackboxValidatedToken(): string {
|
||||
const explicit = (process.env.BLACKBOX_WEB_VALIDATED_TOKEN || "").trim();
|
||||
if (explicit) return explicit;
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether a Blackbox 403 response body indicates that the `validated`
|
||||
* token is the problem (as opposed to a missing cookie or expired subscription).
|
||||
* Surfaces the BLACKBOX_WEB_VALIDATED_TOKEN env var as the next step.
|
||||
*/
|
||||
function isBlackboxValidatedTokenError(responseText: string): boolean {
|
||||
const lower = (responseText || "").toLowerCase();
|
||||
return (
|
||||
lower.includes("invalid validated token") ||
|
||||
lower.includes("invalid validated") ||
|
||||
lower.includes("validation token") ||
|
||||
lower.includes("invalid token")
|
||||
);
|
||||
}
|
||||
|
||||
type CachedSession = {
|
||||
sessionData: Record<string, unknown> | null;
|
||||
subscriptionCache: Record<string, unknown> | null;
|
||||
@@ -406,7 +445,9 @@ export class BlackboxWebExecutor extends BaseExecutor {
|
||||
mobileClient: false,
|
||||
userSelectedModel: model || null,
|
||||
userSelectedAgent: "VscodeAgent",
|
||||
validated: crypto.randomUUID(),
|
||||
// Issue #2252: prefer operator-supplied BLACKBOX_WEB_VALIDATED_TOKEN over
|
||||
// a random UUID — Blackbox's `/api/chat` rejects mismatched tokens with 403.
|
||||
validated: resolveBlackboxValidatedToken(),
|
||||
imageGenerationMode: false,
|
||||
imageGenMode: "autoMode",
|
||||
webSearchModePrompt: false,
|
||||
@@ -477,7 +518,15 @@ export class BlackboxWebExecutor extends BaseExecutor {
|
||||
if (!upstreamResponse.ok) {
|
||||
const status = upstreamResponse.status;
|
||||
let message = `Blackbox Web returned HTTP ${status}`;
|
||||
if (status === 401 || status === 403) {
|
||||
// Issue #2252: distinguish "wrong validated token" from "expired cookie"
|
||||
// when 403 carries a token-specific body — the fix is different in each case.
|
||||
const errorBody = await upstreamResponse.text().catch(() => "");
|
||||
if (status === 403 && isBlackboxValidatedTokenError(errorBody)) {
|
||||
message =
|
||||
"Blackbox Web rejected the request with an invalid `validated` token. " +
|
||||
"If you have a valid frontend token (the `tk` value from app.blackbox.ai's " +
|
||||
"Next.js bundle), set BLACKBOX_WEB_VALIDATED_TOKEN in your environment and restart.";
|
||||
} else if (status === 401 || status === 403) {
|
||||
message =
|
||||
"Blackbox Web auth failed — your app.blackbox.ai session cookie may be missing or expired.";
|
||||
} else if (status === 429) {
|
||||
|
||||
102
tests/unit/blackbox-web-validated-token.test.ts
Normal file
102
tests/unit/blackbox-web-validated-token.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Issue #2252 — BLACKBOX_WEB_VALIDATED_TOKEN env override + 403 detection.
|
||||
*
|
||||
* Blackbox's `/api/chat` rejects requests whose `validated` field doesn't
|
||||
* match the frontend `tk` token, even when the session cookie and
|
||||
* subscription are valid. These tests cover:
|
||||
*
|
||||
* 1. The new `resolveBlackboxValidatedToken()` helper:
|
||||
* - env var wins over the random-UUID fallback
|
||||
* - whitespace is trimmed
|
||||
* - empty/whitespace-only env falls back to randomUUID
|
||||
* - randomUUID fallback returns a well-formed UUID v4 shape
|
||||
*
|
||||
* 2. The 403 + token-specific error body should NOT be conflated with the
|
||||
* generic "expired cookie" message. We assert by string-search on the
|
||||
* executor source — the executor is hard to drive end-to-end in unit
|
||||
* tests because it opens a streaming fetch against app.blackbox.ai.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const { resolveBlackboxValidatedToken } = await import("../../open-sse/executors/blackbox-web.ts");
|
||||
|
||||
const EXECUTOR_FILE = path.resolve("open-sse/executors/blackbox-web.ts");
|
||||
const ORIGINAL_ENV = process.env.BLACKBOX_WEB_VALIDATED_TOKEN;
|
||||
|
||||
function restoreEnv() {
|
||||
if (ORIGINAL_ENV === undefined) {
|
||||
delete process.env.BLACKBOX_WEB_VALIDATED_TOKEN;
|
||||
} else {
|
||||
process.env.BLACKBOX_WEB_VALIDATED_TOKEN = ORIGINAL_ENV;
|
||||
}
|
||||
}
|
||||
|
||||
test.after(restoreEnv);
|
||||
|
||||
test("#2252 — BLACKBOX_WEB_VALIDATED_TOKEN env var wins over random UUID", () => {
|
||||
process.env.BLACKBOX_WEB_VALIDATED_TOKEN = "stub-frontend-token-12345";
|
||||
try {
|
||||
assert.equal(resolveBlackboxValidatedToken(), "stub-frontend-token-12345");
|
||||
} finally {
|
||||
restoreEnv();
|
||||
}
|
||||
});
|
||||
|
||||
test("#2252 — whitespace around the env value is trimmed", () => {
|
||||
process.env.BLACKBOX_WEB_VALIDATED_TOKEN = " stub-with-spaces ";
|
||||
try {
|
||||
assert.equal(resolveBlackboxValidatedToken(), "stub-with-spaces");
|
||||
} finally {
|
||||
restoreEnv();
|
||||
}
|
||||
});
|
||||
|
||||
test("#2252 — empty env falls back to random UUID", () => {
|
||||
delete process.env.BLACKBOX_WEB_VALIDATED_TOKEN;
|
||||
const token = resolveBlackboxValidatedToken();
|
||||
// crypto.randomUUID() returns RFC 4122 v4 — 8-4-4-4-12 hex
|
||||
assert.match(token, /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
||||
});
|
||||
|
||||
test("#2252 — whitespace-only env falls back to random UUID", () => {
|
||||
process.env.BLACKBOX_WEB_VALIDATED_TOKEN = " ";
|
||||
try {
|
||||
const token = resolveBlackboxValidatedToken();
|
||||
assert.match(
|
||||
token,
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
);
|
||||
} finally {
|
||||
restoreEnv();
|
||||
}
|
||||
});
|
||||
|
||||
test("#2252 — different random fallbacks return different tokens", () => {
|
||||
delete process.env.BLACKBOX_WEB_VALIDATED_TOKEN;
|
||||
const a = resolveBlackboxValidatedToken();
|
||||
const b = resolveBlackboxValidatedToken();
|
||||
assert.notEqual(a, b);
|
||||
});
|
||||
|
||||
test("#2252 — executor uses resolveBlackboxValidatedToken in transformedBody", () => {
|
||||
const source = fs.readFileSync(EXECUTOR_FILE, "utf8");
|
||||
// The literal crypto.randomUUID() was replaced
|
||||
assert.doesNotMatch(
|
||||
source.split("transformedBody = {")[1]?.split("};")[0] ?? "",
|
||||
/validated: crypto\.randomUUID\(\)/,
|
||||
"transformedBody must call resolveBlackboxValidatedToken() instead of crypto.randomUUID() directly"
|
||||
);
|
||||
assert.match(source, /validated: resolveBlackboxValidatedToken\(\)/);
|
||||
});
|
||||
|
||||
test("#2252 — 403 with token-specific body surfaces BLACKBOX_WEB_VALIDATED_TOKEN guidance", () => {
|
||||
const source = fs.readFileSync(EXECUTOR_FILE, "utf8");
|
||||
// The 403 disambiguation message must reference the env var so users know
|
||||
// exactly what to set.
|
||||
assert.match(source, /BLACKBOX_WEB_VALIDATED_TOKEN/);
|
||||
assert.match(source, /isBlackboxValidatedTokenError/);
|
||||
});
|
||||
Reference in New Issue
Block a user