# Ghost Cockpit Protocol Specification ## Overview The Ghost Cockpit Protocol provides a seamless experience for real-time applications connected via WebSockets to Auth-Yes secured services. When a user's session expires or is revoked, instead of abruptly disconnecting and redirecting the user to a login page (losing unsaved state or telemetry data), the application enters a "frozen" or "ghost" state. In this state: 1. The WebSocket connection is gracefully terminated by the server with a specific control frame and status code. 2. The UI freezes real-time updates and input, keeping the current state intact. 3. A background or modal re-authentication process (like WebAuthn biometric prompt) is triggered. 4. Upon successful re-authentication, the WebSocket connection is re-established, and the application resumes normal operation without any page reload. ## Choreography 1. **Active Connection:** The client maintains an active WebSocket connection. The backend uses the `createWebSocketGuard` to monitor the session. 2. **Session Invalidation:** The Auth-Yes API Gateway or an admin invalidates the session. An `invalidate` event is propagated through the `AuthSdk`. 3. **Revocation Frame:** The WebSocket Guard intercepts the event and sends a JSON control frame: ```json { "type": "AUTH_REVOKED", "reason": "SESSION_EXPIRED" } ``` 4. **Connection Close:** Immediately after sending the frame, the server closes the WebSocket with status code **1008 (Policy Violation)** and reason **"Session Expired"**. 5. **Client Freeze:** The client application receives the `AUTH_REVOKED` frame (or detects the 1008 close code) and transitions to the "Ghost Cockpit" state. 6. **Re-authentication:** The client calls Auth-Yes endpoints (e.g., `/api/login/challenge` and `/api/login/verify`) using the Web Authentication API (`navigator.credentials.get()`). 7. **Resumption:** Once a new session token is obtained, the client reconnects the WebSocket and the UI unfreezes. ## Reference Client Implementation Below is a complete, reference client-side TypeScript snippet demonstrating the `GhostCockpitClient` class that manages this lifecycle. ```typescript /** * GhostCockpitClient manages a resilient WebSocket connection that supports * the Ghost Cockpit Protocol for non-destructive re-authentication. */ export class GhostCockpitClient { private ws: WebSocket | null = null; private url: string; private isFrozen: boolean = false; // Application callbacks public onMessage?: (data: any) => void; public onStateChange?: (frozen: boolean) => void; constructor(url: string) { this.url = url; } /** * Connects to the WebSocket server. */ public connect(): void { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log("[GhostCockpit] Connected."); if (this.isFrozen) { this.setFrozen(false); } }; this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === "AUTH_REVOKED" && data.reason === "SESSION_EXPIRED") { console.warn("[GhostCockpit] Received AUTH_REVOKED frame."); this.handleSessionExpired(); return; } } catch (e) { // Not JSON, normal message } if (this.onMessage && !this.isFrozen) { this.onMessage(event.data); } }; this.ws.onclose = (event) => { console.log(`[GhostCockpit] Disconnected (code: ${event.code}).`); if (event.code === 1008 && event.reason === "Session Expired") { this.handleSessionExpired(); } else if (!this.isFrozen) { // Attempt normal reconnection logic here (omitted for brevity) setTimeout(() => this.connect(), 5000); } }; } /** * Updates the frozen state and triggers the UI callback. */ private setFrozen(frozen: boolean) { this.isFrozen = frozen; if (this.onStateChange) { this.onStateChange(frozen); } } /** * Triggers the freeze state and initiates background WebAuthn re-auth. */ private async handleSessionExpired() { if (this.isFrozen) return; // Already handling this.setFrozen(true); console.log( "[GhostCockpit] UI Frozen. Initiating background re-authentication...", ); try { // 1. Fetch WebAuthn Challenge from Auth-Yes const challengeRes = await fetch("/api/login/challenge", { method: "POST", }); if (!challengeRes.ok) throw new Error("Failed to get challenge"); const challengeData = await challengeRes.json(); // Convert server challenge/ids to Uint8Array (omitted util details) // e.g., decode base64url to Uint8Array // 2. Prompt user for Biometrics / Security Key via Web Authentication API const credential = await navigator.credentials.get({ publicKey: challengeData.publicKeyRequestOptions, }); if (!credential) throw new Error("Credential not provided"); // 3. Verify credential with Auth-Yes // (Serialize credential response before sending) const verifyRes = await fetch("/api/login/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ credential }), // Mock serialization }); if (!verifyRes.ok) throw new Error("Re-authentication failed"); console.log( "[GhostCockpit] Re-authentication successful! Reconnecting...", ); // Reconnect with new session (implicitly via browser cookies or explicit headers) this.connect(); } catch (err) { console.error("[GhostCockpit] Re-authentication error:", err); // Fallback: Redirect to full login page or show permanent error window.location.href = "/login"; } } } ```