authenticate the chat socket the same way every request is authenticated
Reported after the revert: the app loads, the old layout is back, history lists — and the socket never reaches connected. The two doors disagreed. `createClient` accepts a token from seven places: window.officerBearerToken, two body datasets, an `?officerToken=` query param, PERTENTO_EDITOR_AUTH_TOKEN, localStorage and sessionStorage. The chat socket url read exactly one of them, `localStorage.BEARER_TOKEN`, so a token held anywhere else authenticated every HTTP request and left the WebSocket with a bare `?token=`. That failure is silent and reads as a dead server: verified here, an empty token closes with 1002 "Expected 101 status code", and the hook's retry loop repeats it forever. Nothing logs a missing credential, so the app looks fine in every way except the one that matters. Resolution is now one exported function, `resolveBearerToken`, used by both. The point is that it cannot be re-spelled: this bug is the second spelling drifting from the first. Predates the tabs work and survived reverting it, which is the evidence it was never a panes bug. Not fixed here, same shape, left alone deliberately: Terminal, Desktop, AudioStreamPlayer, the pipeline and task runners, JobDetail and EmailList all build socket or fetch urls from `localStorage.BEARER_TOKEN` directly and will fail identically for the same user. Typecheck clean. 600 pass, 2 fail — cliamp and pty, unchanged and unrelated. Not verified in a browser; Andre has the only client that reproduces it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export { useGlobal } from './useGlobal';
|
export { useGlobal } from './useGlobal';
|
||||||
export { usePanelChannel } from './usePanelChannel';
|
export { usePanelChannel } from './usePanelChannel';
|
||||||
export { useClient, createClient } from './useClient';
|
export { useClient, createClient, resolveBearerToken } from './useClient';
|
||||||
export {
|
export {
|
||||||
useConnections,
|
useConnections,
|
||||||
listConnections,
|
listConnections,
|
||||||
|
|||||||
@@ -8,8 +8,17 @@ let theToken: string | null = null;
|
|||||||
* caller. Naming both is how a panel talks to ANOTHER Officer without any global being switched, which
|
* caller. Naming both is how a panel talks to ANOTHER Officer without any global being switched, which
|
||||||
* is what lets two panes hold two live conversations on two machines (see `connections.ts`).
|
* is what lets two panes hold two live conversations on two machines (see `connections.ts`).
|
||||||
*/
|
*/
|
||||||
export const createClient = (baseUrl: string = '/api', token?: string | null) => {
|
/**
|
||||||
const lsToken =
|
* The session token for THIS origin, from every place one is allowed to live.
|
||||||
|
*
|
||||||
|
* Exported because it must not be re-spelled anywhere. A caller that reads only
|
||||||
|
* `localStorage.BEARER_TOKEN` — which the chat socket url did — authenticates for HTTP and fails for
|
||||||
|
* WebSockets the moment the token is held anywhere else: an embedded host setting
|
||||||
|
* `window.officerBearerToken`, a `?officerToken=` link, or sessionStorage. The app then loads, renders
|
||||||
|
* and lists history perfectly while the socket is refused with a 1002 and retries forever, which reads
|
||||||
|
* as a dead server rather than a missing credential.
|
||||||
|
*/
|
||||||
|
export const resolveBearerToken = (): string | null =>
|
||||||
window.officerBearerToken ||
|
window.officerBearerToken ||
|
||||||
document.body.dataset['officerBearerToken'] ||
|
document.body.dataset['officerBearerToken'] ||
|
||||||
document.body.dataset['bearerToken'] ||
|
document.body.dataset['bearerToken'] ||
|
||||||
@@ -18,6 +27,9 @@ export const createClient = (baseUrl: string = '/api', token?: string | null) =>
|
|||||||
localStorage.getItem('BEARER_TOKEN') ||
|
localStorage.getItem('BEARER_TOKEN') ||
|
||||||
sessionStorage.getItem('BEARER_TOKEN');
|
sessionStorage.getItem('BEARER_TOKEN');
|
||||||
|
|
||||||
|
export const createClient = (baseUrl: string = '/api', token?: string | null) => {
|
||||||
|
const lsToken = resolveBearerToken();
|
||||||
|
|
||||||
theToken = lsToken;
|
theToken = lsToken;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useChatWebSocket } from 'hooks/useChatWebSocket';
|
import { useChatWebSocket } from 'hooks/useChatWebSocket';
|
||||||
import { useServerClient, chatSocketUrl } from 'hooks/useServerClient';
|
import { useServerClient, chatSocketUrl } from 'hooks/useServerClient';
|
||||||
|
import { resolveBearerToken } from 'hooks/useClient';
|
||||||
import { useSettings } from 'state/useSettings';
|
import { useSettings } from 'state/useSettings';
|
||||||
import type { ChatMessage, ServerMessage, TaskInfo, Message } from '../apps/Chat/types';
|
import type { ChatMessage, ServerMessage, TaskInfo, Message } from '../apps/Chat/types';
|
||||||
import { spliceRunningTasks } from '../apps/Chat/running-tasks';
|
import { spliceRunningTasks } from '../apps/Chat/running-tasks';
|
||||||
@@ -179,7 +180,10 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
|
|||||||
|
|
||||||
// Null when a named server is unknown, which opens no socket rather than dialling this origin under
|
// Null when a named server is unknown, which opens no socket rather than dialling this origin under
|
||||||
// another server's name — the failure that would put one machine's turn in another's pane.
|
// another server's name — the failure that would put one machine's turn in another's pane.
|
||||||
const wsUrl = chatSocketUrl(serverId, localStorage.getItem('BEARER_TOKEN'));
|
// The SAME resolution the HTTP client uses. Reading localStorage directly here meant a token held
|
||||||
|
// anywhere else authenticated every request and left the socket with `?token=`, refused 1002 and
|
||||||
|
// retrying forever — an app that loads and lists history but never connects.
|
||||||
|
const wsUrl = chatSocketUrl(serverId, resolveBearerToken());
|
||||||
|
|
||||||
function flushStreaming() {
|
function flushStreaming() {
|
||||||
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
||||||
|
|||||||
Reference in New Issue
Block a user