feat(frontend): client web read-only pairing + snapshot état (#13)
Lot F2 du chantier server/client mode : client web read-only complétant le premier incrément livrable — pairing, liste des projets, ouverture et snapshot de l'état, sans PTY. - frontend/src/adapters/http/webSession.ts : session web (pairing/cookie). - frontend/src/features/web : PairingScreen, WebWorkspace, WebApp, index. - Câblage main.tsx et adaptations httpInvoker.ts / index.ts (cas 401). - Tests : webSession.test.ts, WebApp.test.tsx, cas 401 dans httpInvoker.test.ts. Validé : frontend 736 tests verts, build vert, garde no-direct-invoke verte, contrat B4↔F2 aligné, desktop non régressé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -30,6 +30,8 @@ export type FetchLike = (
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
signal?: AbortSignal;
|
||||
/** Cookie policy — F2 uses `same-origin` so the session cookie rides along. */
|
||||
credentials?: "same-origin" | "include" | "omit";
|
||||
},
|
||||
) => Promise<{
|
||||
ok: boolean;
|
||||
@ -47,6 +49,12 @@ export interface HttpInvokerConfig {
|
||||
token?: string;
|
||||
/** Injected fetch (defaults to global `fetch`). */
|
||||
fetchImpl?: FetchLike;
|
||||
/**
|
||||
* Called when the backend answers `401` (the session cookie is missing or
|
||||
* expired). F2 wires this to the web session so the app routes back to the
|
||||
* pairing screen. The `GatewayError` is still thrown to the caller.
|
||||
*/
|
||||
onUnauthorized?: () => void;
|
||||
}
|
||||
|
||||
/** Builds a {@link GatewayError} from an arbitrary thrown/parsed value. */
|
||||
@ -71,10 +79,12 @@ export class HttpInvoker {
|
||||
private readonly baseUrl: string;
|
||||
private readonly token?: string;
|
||||
private readonly fetchImpl: FetchLike;
|
||||
private readonly onUnauthorized?: () => void;
|
||||
|
||||
constructor(config: HttpInvokerConfig) {
|
||||
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
||||
this.token = config.token;
|
||||
this.onUnauthorized = config.onUnauthorized;
|
||||
// `globalThis.fetch` exists in the browser (and jsdom); the cast narrows it
|
||||
// to the minimal shape used here.
|
||||
this.fetchImpl =
|
||||
@ -98,6 +108,9 @@ export class HttpInvoker {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ command, args }),
|
||||
// Same-origin so the HttpOnly session cookie is sent automatically
|
||||
// (ticket #13: no secret in the URL/headers from JS).
|
||||
credentials: "same-origin",
|
||||
});
|
||||
} catch (networkError) {
|
||||
const err: GatewayError = {
|
||||
@ -108,6 +121,9 @@ export class HttpInvoker {
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
// A 401 means the session cookie is missing/expired: signal the app to
|
||||
// route back to pairing (the error is still thrown to the caller).
|
||||
if (res.status === 401) this.onUnauthorized?.();
|
||||
// Preserve the backend `ErrorDto` when present; fall back to the status.
|
||||
let parsed: unknown;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user