fix(cortex-ui): strip whitespace from token; drop credentials:'include'

Live e2e test caught a paste-inserted whitespace in URL token param —
copy-paste from terminal had inserted %20%20%20 into middle of the
64-char hex token, which passed URL parsing but failed byte-level
auth::tokens_match on the daemon → 403.

Two fixes:

1. `sanitize_token()` strips ALL whitespace (spaces, tabs, newlines,
   zero-width) from token before use, applied on both URL-param and
   localStorage read paths. Defensive even against future Setup-form
   paste mishaps — Setup input itself could also be whitespace-dirty.

2. `credentials: 'include'` → `credentials: 'omit'`. Bearer auth rides
   on an explicit header; we don't need cookies. `include` triggers
   browser quirks (Safari especially) around credentialed cross-origin
   fetches that can strip or mismangle Authorization on redirects.

3. Error message now includes response body preview — `"403 Forbidden
   — {\"error\":{\"code\":\"forbidden\",\"message\":\"bearer token
   rejected\"}}"` — so the next failing setup surfaces root-cause.

Tests unchanged (10 passing). Rebuild hash: index-7ZqAoBoM.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Parfii-bot 2026-04-24 03:14:09 +08:00
parent 6672ae48e7
commit 39f95f7e04
2 changed files with 22 additions and 5 deletions

View file

@ -13,9 +13,16 @@ async function api<T>(
Authorization: `Bearer ${c.token}`,
'Content-Type': 'application/json',
},
credentials: 'include',
// Bearer auth via explicit header; no cookies needed. credentials:'omit'
// is the safe cross-origin default and avoids Safari quirks around
// credentials:'include' stripping Authorization on some redirects.
credentials: 'omit',
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
if (!res.ok) {
let body = '';
try { body = (await res.text()).slice(0, 200); } catch { /* ignore */ }
throw new Error(`${res.status} ${res.statusText}${body ? `${body}` : ''}`);
}
return res.json() as Promise<T>;
}

View file

@ -7,12 +7,22 @@ const DEFAULT_DAEMON = 'http://localhost:9797';
const KEY_DAEMON = 'kei-cortex-daemon';
const KEY_TOKEN = 'kei-cortex-token';
/** Strip ALL whitespace (spaces, tabs, newlines, zero-width) from a token. */
function sanitize_token(raw: string): string {
return raw.replace(/\s+/g, '');
}
export function load_config(): CortexConfig | null {
const params = new URLSearchParams(window.location.search);
const override = params.get('daemon');
const url_daemon = params.get('daemon')?.trim();
const url_token_raw = params.get('token');
const url_token = url_token_raw ? sanitize_token(url_token_raw) : null;
if (url_daemon) localStorage.setItem(KEY_DAEMON, url_daemon);
if (url_token) localStorage.setItem(KEY_TOKEN, url_token);
const daemon_url =
override ?? localStorage.getItem(KEY_DAEMON) ?? DEFAULT_DAEMON;
const token = params.get('token') ?? localStorage.getItem(KEY_TOKEN) ?? '';
url_daemon ?? localStorage.getItem(KEY_DAEMON) ?? DEFAULT_DAEMON;
const stored_token = localStorage.getItem(KEY_TOKEN);
const token = url_token ?? (stored_token ? sanitize_token(stored_token) : '');
if (!token) return null;
return { daemon_url, token };
}