Add full shell, replaceable Omics adapter and bilingual interaction

Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
This commit is contained in:
Codex
2026-09-13 14:26:39 +02:00
parent 2d1b714ebe
commit d8a29bfbdd
207 changed files with 8570 additions and 2164 deletions
+35 -7
View File
@@ -399,11 +399,11 @@ test("a stale source error cannot clear or mutate the next user's live session",
expect(useSessionStore.getState().transcript).toEqual([{ role: "assistant", text: "B-data" }]);
});
test("a native current-source error probes /me once and clears auth on a guarded 401", async () => {
test.each([401, 403])("a native current-source error coalesces access probes and clears auth on a guarded %s", async (status) => {
let probes = 0;
server.use(http.get("/api/me", () => {
probes += 1;
return new HttpResponse(null, { status: 401 });
return new HttpResponse(null, { status });
}));
setAuthState({
issuer: "local", subject: "user-a", roles: ["user"], permissions: ["session.use"], isAdmin: false,
@@ -422,7 +422,7 @@ test("a native current-source error probes /me once and clears auth on a guarded
expect((await import("../auth/authState")).getAuthState()).toBeNull();
});
test("a native error keeps current auth connected after a 200 /me probe and does not storm", async () => {
test("a successful access probe does not claim the stream is connected and a later error rechecks access", async () => {
let probes = 0;
const user = {
issuer: "local", subject: "user-a", roles: ["user"] as const, permissions: ["session.use"], isAdmin: false,
@@ -430,18 +430,26 @@ test("a native error keeps current auth connected after a 200 /me probe and does
};
server.use(http.get("/api/me", () => {
probes += 1;
return HttpResponse.json(user);
return HttpResponse.json({ ...user, csrfToken: "b".repeat(43) });
}));
setAuthState(user);
const result = renderHook(() => useSessionStream("s1", 0, 0, true, getAuthGeneration()));
const source = FakeEventSource.instances[0];
act(() => source.onerror?.(new Event("error")));
await waitFor(() => expect(result.result.current.connected).toBe(true));
act(() => source.onerror?.(new Event("error")));
act(() => {
source.onerror?.(new Event("error"));
source.onerror?.(new Event("error"));
});
await waitFor(() => expect(getAuthState()?.csrfToken).toBe("b".repeat(43)));
expect(probes).toBe(1);
expect(result.result.current.connected).toBe(false);
expect(getAuthState()).toMatchObject({ subject: "user-a" });
act(() => source.onopen?.());
expect(result.result.current.connected).toBe(true);
server.use(http.get("/api/me", () => new HttpResponse(null, { status: 403 })));
act(() => source.onerror?.(new Event("error")));
await waitFor(() => expect(getAuthState()).toBeNull());
});
test("a stale source does not probe and a user-B login during an A probe cannot mutate B", async () => {
@@ -478,3 +486,23 @@ test("a stale source does not probe and a user-B login during an A probe cannot
expect(getAuthState()).toMatchObject({ subject: "user-b" });
expect(sourceB.closed).toBe(false);
});
test("opening after an outage revalidates access even when the outage probe could not reach the server", async () => {
let probes = 0;
const user = {
issuer: "local", subject: "user-a", roles: ["user"] as const, permissions: ["session.use"], isAdmin: false,
csrfToken: "a".repeat(43), session: null,
};
server.use(http.get("/api/me", () => {
probes += 1;
return probes === 1 ? HttpResponse.error() : new HttpResponse(null, { status: 403 });
}));
setAuthState(user);
renderHook(() => useSessionStream("s1", 0, 0, true, getAuthGeneration()));
const source = FakeEventSource.instances[0];
act(() => source.onerror?.(new Event("error")));
await waitFor(() => expect(probes).toBe(1));
act(() => source.onopen?.());
await waitFor(() => expect(getAuthState()).toBeNull());
expect(probes).toBe(2);
});
+16 -14
View File
@@ -7,7 +7,7 @@ import {
getAuthGeneration,
isAuthGenerationCurrent,
} from "../auth/authState";
import { getMe } from "../api/auth";
import { checkAccess } from "../auth/checkAccess";
import { captureAuthOperation, isAuthOperationCurrent } from "../auth/authOperation";
const STREAM_UPDATE_INTERVAL_MS = 100;
@@ -120,28 +120,30 @@ export function useSessionStream(
const sourceAuthGeneration = effectiveAuthGeneration;
const es = new EventSource(joinBackendPath(BASE, `/sessions/${sessionId}/events${query}`));
const identity = { source: es, sessionId, cursorResetEpoch };
let authProbeStarted = false;
let authProbe: Promise<void> | null = null;
const isCurrentSource = () => activeSource.current === identity
&& isAuthGenerationCurrent(sourceAuthGeneration);
const probeAuth = async () => {
if (authProbeStarted || !isCurrentSource()) return;
authProbeStarted = true;
const probeAuth = (): Promise<void> => {
if (authProbe) return authProbe;
if (!isCurrentSource()) return Promise.resolve();
const operation = captureAuthOperation({ sessionId, disposalEpoch: 0 });
if (!operation) return;
try {
await getMe();
if (!isCurrentSource() || !isAuthOperationCurrent(operation, { sessionId, disposalEpoch: 0 })) return;
setConnected(true);
} catch {
if (!isCurrentSource() || !isAuthOperationCurrent(operation, { sessionId, disposalEpoch: 0 })) return;
setConnected(false);
}
if (!operation) return Promise.resolve();
authProbe = checkAccess().then(() => {}, () => {
if (isCurrentSource() && isAuthOperationCurrent(operation, { sessionId, disposalEpoch: 0 })) {
setConnected(false);
}
}).finally(() => { authProbe = null; });
return authProbe;
};
const coalescer = createStreamEventCoalescer(applyEvent, STREAM_UPDATE_INTERVAL_MS, isCurrentSource);
activeSource.current = identity;
es.onopen = () => {
if (!isCurrentSource()) { es.close(); return; }
setConnected(true);
// An outage probe may still be failing as the stream reopens. Read /me
// after it settles so a successful reconnect always refreshes identity.
const pending = authProbe ?? Promise.resolve();
void pending.then(() => probeAuth());
};
es.onerror = () => {
if (!isCurrentSource()) { es.close(); return; }