import { EveBrowserProviderHost, type EveBrowserProviderTransport, type EveCommandIntent, type EveProviderAdvertisement, type EveProviderSurfaceAdvertisement, type EveSurfaceDocument, } from "@gamecult/eve-browser-lowering"; import { heimdallAccessBrowserAdapter, resumeHeimdallAccess } from "./heimdall-access-adapter"; import "@gamecult/eve-browser-lowering/styles.css"; import "./style.css"; class GhostlightEveTransport implements EveBrowserProviderTransport { private provider: EveProviderAdvertisement | undefined; private sourceVersion = 0; private authenticatedSurface = false; async providerAdvertisement(): Promise { this.provider = await fetchJson("api/eve/provider"); return this.provider; } async surface(surface: EveProviderSurfaceAdvertisement): Promise { const surfaceId = surface.surfaceId || "ghostlight.play"; const document = await fetchJson(`api/eve/surfaces/${encodeURIComponent(surfaceId)}`); this.sourceVersion = Math.max(0, Math.trunc(document.version || 0)); this.authenticatedSurface = !containsAnonymousAccessGate(document); return document; } hasAuthenticatedSurface(): boolean { return this.authenticatedSurface; } async submitCommand(intent: EveCommandIntent): Promise { const result = await fetchJson("api/eve/commands", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(intent), }); return result; } async completeAuthentication(handle: string): Promise { if (!this.provider) await this.providerAdvertisement(); return await this.submitCommand({ schema: "gamecult.eve.command_invocation.v1", providerId: this.provider?.providerId || "gamecult.ghostlight.dungeon", surfaceId: "ghostlight.play", operation: { operationId: "heimdall.auth.complete", schemaId: "heimdall.auth_complete_command.v1", idempotencyKey: crypto.randomUUID(), routeHint: { sourceVersion: this.sourceVersion }, }, payload: { handle }, issuedAt: new Date().toISOString(), clientId: "ghostlight.browser", commandBoundary: "ghostlight.eve.commands", receiptSchema: "gamecult.eve.command_result.v1", }); } } async function fetchJson(input: RequestInfo | URL, init?: RequestInit): Promise { const response = await fetch(input, { cache: "no-store", credentials: "same-origin", ...init }); const body = await response.json().catch(() => undefined); if (!response.ok) { const message = body && typeof body.message === "string" ? body.message : `Ghostlight refused the request (${response.status}).`; throw new Error(message); } return body as T; } function containsAnonymousAccessGate(value: unknown): boolean { if (!value || typeof value !== "object") return false; if (Array.isArray(value)) return value.some(containsAnonymousAccessGate); const node = value as Record; if (node.kind === "heimdall.access_gate") { const props = node.props && typeof node.props === "object" && !Array.isArray(node.props) ? node.props as Record : {}; return props.state === "anonymous"; } return Object.values(node).some(containsAnonymousAccessGate); } function requiredElement(id: string): T { const element = document.getElementById(id); if (!(element instanceof HTMLElement)) throw new Error(`Missing browser host #${id}.`); return element as T; } const surfaceHost = requiredElement("surface"); const status = requiredElement("status"); const api = new GhostlightEveTransport(); const host = new EveBrowserProviderHost(surfaceHost, api, { body: document.body, clientId: "ghostlight.browser", pollMs: 0, requestedSurfaceId: "ghostlight.play", source: "Ghostlight", statusElement: status, pluginAdapters: [heimdallAccessBrowserAdapter], }); window.addEventListener("unhandledrejection", event => { event.preventDefault(); status.textContent = event.reason instanceof Error ? event.reason.message : String(event.reason); }); await host.start(); await resumeHeimdallAccess({ complete: async handle => { const result = await api.completeAuthentication(handle); if (result.pluginPayload?.pluginId === heimdallAccessBrowserAdapter.pluginId) { await heimdallAccessBrowserAdapter.consumeCommandResult?.(result.pluginPayload); } await host.refresh(); }, }, { appSlug: "ghostlight" }); if (api.hasAuthenticatedSurface()) { const events = new EventSource("api/eve/events"); events.addEventListener("revision", () => void host.refresh()); events.addEventListener("error", () => { status.textContent = "Live revision notices are unavailable; your authoritative surface remains safe to refresh."; }); }