docs: format GHOST_COCKPIT_SPEC.md

This commit is contained in:
Tyler Gillispie 2026-08-23 21:56:46 -07:00
parent fdcf9ded05
commit 0ebbcf38c7

View File

@ -2,30 +2,50 @@
## Overview ## 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. 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: In this state:
1. The WebSocket connection is gracefully terminated by the server with a specific control frame and status code.
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. 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. 3. A background or modal re-authentication process (like WebAuthn biometric
4. Upon successful re-authentication, the WebSocket connection is re-established, and the application resumes normal operation without any page reload. 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 ## Choreography
1. **Active Connection:** The client maintains an active WebSocket connection. The backend uses the `createWebSocketGuard` to monitor the session. 1. **Active Connection:** The client maintains an active WebSocket connection.
2. **Session Invalidation:** The Auth-Yes API Gateway or an admin invalidates the session. An `invalidate` event is propagated through the `AuthSdk`. The backend uses the `createWebSocketGuard` to monitor the session.
3. **Revocation Frame:** The WebSocket Guard intercepts the event and sends a JSON control frame: 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 ```json
{ "type": "AUTH_REVOKED", "reason": "SESSION_EXPIRED" } { "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"**. 4. **Connection Close:** Immediately after sending the frame, the server closes
5. **Client Freeze:** The client application receives the `AUTH_REVOKED` frame (or detects the 1008 close code) and transitions to the "Ghost Cockpit" state. the WebSocket with status code **1008 (Policy Violation)** and reason
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()`). **"Session Expired"**.
7. **Resumption:** Once a new session token is obtained, the client reconnects the WebSocket and the UI unfreezes. 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 ## Reference Client Implementation
Below is a complete, reference client-side TypeScript snippet demonstrating the `GhostCockpitClient` class that manages this lifecycle. Below is a complete, reference client-side TypeScript snippet demonstrating the
`GhostCockpitClient` class that manages this lifecycle.
```typescript ```typescript
/** /**
@ -102,11 +122,15 @@ export class GhostCockpitClient {
private async handleSessionExpired() { private async handleSessionExpired() {
if (this.isFrozen) return; // Already handling if (this.isFrozen) return; // Already handling
this.setFrozen(true); this.setFrozen(true);
console.log("[GhostCockpit] UI Frozen. Initiating background re-authentication..."); console.log(
"[GhostCockpit] UI Frozen. Initiating background re-authentication...",
);
try { try {
// 1. Fetch WebAuthn Challenge from Auth-Yes // 1. Fetch WebAuthn Challenge from Auth-Yes
const challengeRes = await fetch("/api/login/challenge", { method: "POST" }); const challengeRes = await fetch("/api/login/challenge", {
method: "POST",
});
if (!challengeRes.ok) throw new Error("Failed to get challenge"); if (!challengeRes.ok) throw new Error("Failed to get challenge");
const challengeData = await challengeRes.json(); const challengeData = await challengeRes.json();
@ -115,7 +139,7 @@ export class GhostCockpitClient {
// 2. Prompt user for Biometrics / Security Key via Web Authentication API // 2. Prompt user for Biometrics / Security Key via Web Authentication API
const credential = await navigator.credentials.get({ const credential = await navigator.credentials.get({
publicKey: challengeData.publicKeyRequestOptions publicKey: challengeData.publicKeyRequestOptions,
}); });
if (!credential) throw new Error("Credential not provided"); if (!credential) throw new Error("Credential not provided");
@ -124,15 +148,16 @@ export class GhostCockpitClient {
const verifyRes = await fetch("/api/login/verify", { const verifyRes = await fetch("/api/login/verify", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ credential }) // Mock serialization body: JSON.stringify({ credential }), // Mock serialization
}); });
if (!verifyRes.ok) throw new Error("Re-authentication failed"); if (!verifyRes.ok) throw new Error("Re-authentication failed");
console.log("[GhostCockpit] Re-authentication successful! Reconnecting..."); console.log(
"[GhostCockpit] Re-authentication successful! Reconnecting...",
);
// Reconnect with new session (implicitly via browser cookies or explicit headers) // Reconnect with new session (implicitly via browser cookies or explicit headers)
this.connect(); this.connect();
} catch (err) { } catch (err) {
console.error("[GhostCockpit] Re-authentication error:", err); console.error("[GhostCockpit] Re-authentication error:", err);
// Fallback: Redirect to full login page or show permanent error // Fallback: Redirect to full login page or show permanent error