import { getKnownEnvironmentHttpBaseUrl } from "@t3tools/client-runtime"; import type { AuthEnvironmentScope, EnvironmentId, ExecutionEnvironmentDescriptor, PersistedSavedEnvironmentRecord, ServerConfig, } from "@t3tools/contracts"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { create } from "zustand"; import { ensureLocalApi } from "../../localApi"; import { getPrimaryKnownEnvironment } from "../primary"; export interface SavedEnvironmentRecord { readonly environmentId: EnvironmentId; readonly label: string; readonly wsBaseUrl: string; readonly httpBaseUrl: string; readonly createdAt: string; readonly lastConnectedAt: string | null; readonly desktopSsh?: PersistedSavedEnvironmentRecord["desktopSsh"]; readonly relayManaged?: PersistedSavedEnvironmentRecord["relayManaged"]; } export const SavedEnvironmentCredential = Schema.Union([ Schema.Struct({ version: Schema.Literal(1), method: Schema.Literal("bearer"), token: Schema.String, }), Schema.Struct({ version: Schema.Literal(1), method: Schema.Literal("dpop"), accessToken: Schema.String, }), ]); export type SavedEnvironmentCredential = typeof SavedEnvironmentCredential.Type; const SavedEnvironmentCredentialJson = Schema.fromJsonString(SavedEnvironmentCredential); const decodeSavedEnvironmentCredentialJson = Schema.decodeUnknownOption( SavedEnvironmentCredentialJson, ); const encodeSavedEnvironmentCredentialJson = Schema.encodeSync(SavedEnvironmentCredentialJson); interface SavedEnvironmentRegistryState { readonly byId: Record; } interface SavedEnvironmentRegistryStore extends SavedEnvironmentRegistryState { readonly upsert: (record: SavedEnvironmentRecord) => void; readonly remove: (environmentId: EnvironmentId) => void; readonly markConnected: (environmentId: EnvironmentId, connectedAt: string) => void; readonly rename: (environmentId: EnvironmentId, label: string) => void; readonly reset: () => void; } let savedEnvironmentRegistryHydrated = false; let savedEnvironmentRegistryHydrationPromise: Promise | null = null; export function toPersistedSavedEnvironmentRecord( record: SavedEnvironmentRecord, ): PersistedSavedEnvironmentRecord { return { environmentId: record.environmentId, label: record.label, httpBaseUrl: record.httpBaseUrl, wsBaseUrl: record.wsBaseUrl, createdAt: record.createdAt, lastConnectedAt: record.lastConnectedAt, ...(record.desktopSsh ? { desktopSsh: record.desktopSsh } : {}), ...(record.relayManaged ? { relayManaged: record.relayManaged } : {}), }; } function valuesOfSavedEnvironmentRegistry( byId: Record, ): ReadonlyArray { return Object.values(byId) as ReadonlyArray; } function persistSavedEnvironmentRegistryState( byId: Record, ): void { try { void ensureLocalApi() .persistence.setSavedEnvironmentRegistry( valuesOfSavedEnvironmentRegistry(byId).map((record) => toPersistedSavedEnvironmentRecord(record), ), ) .catch((error) => { console.error("[SAVED_ENVIRONMENTS] persist failed", error); }); } catch (error) { console.error("[SAVED_ENVIRONMENTS] persist failed", error); } } function replaceSavedEnvironmentRegistryState( records: ReadonlyArray, ): void { const currentById = useSavedEnvironmentRegistryStore.getState().byId; const hydratedById = Object.fromEntries(records.map((record) => [record.environmentId, record])); useSavedEnvironmentRegistryStore.setState({ byId: { ...hydratedById, ...currentById, }, }); } async function hydrateSavedEnvironmentRegistry(): Promise { if (savedEnvironmentRegistryHydrated) { return; } if (savedEnvironmentRegistryHydrationPromise) { return savedEnvironmentRegistryHydrationPromise; } const nextHydration = (async () => { try { const persistedRecords = await ensureLocalApi().persistence.getSavedEnvironmentRegistry(); replaceSavedEnvironmentRegistryState(persistedRecords); } catch (error) { console.error("[SAVED_ENVIRONMENTS] hydrate failed", error); } finally { savedEnvironmentRegistryHydrated = true; } })(); const hydrationPromise = nextHydration.finally(() => { if (savedEnvironmentRegistryHydrationPromise === hydrationPromise) { savedEnvironmentRegistryHydrationPromise = null; } }); savedEnvironmentRegistryHydrationPromise = hydrationPromise; return savedEnvironmentRegistryHydrationPromise; } export const useSavedEnvironmentRegistryStore = create()((set) => ({ byId: {}, upsert: (record) => set((state) => { const byId = { ...state.byId, [record.environmentId]: record, }; persistSavedEnvironmentRegistryState(byId); return { byId }; }), remove: (environmentId) => set((state) => { const { [environmentId]: _removed, ...remaining } = state.byId; persistSavedEnvironmentRegistryState(remaining); return { byId: remaining, }; }), markConnected: (environmentId, connectedAt) => set((state) => { const existing = state.byId[environmentId]; if (!existing) { return state; } const byId = { ...state.byId, [environmentId]: { ...existing, lastConnectedAt: connectedAt, }, }; persistSavedEnvironmentRegistryState(byId); return { byId }; }), rename: (environmentId, label) => set((state) => { const existing = state.byId[environmentId]; const nextLabel = label.trim(); if (!existing || nextLabel.length === 0 || existing.label === nextLabel) { return state; } const byId = { ...state.byId, [environmentId]: { ...existing, label: nextLabel, }, }; persistSavedEnvironmentRegistryState(byId); return { byId }; }), reset: () => { persistSavedEnvironmentRegistryState({}); set({ byId: {}, }); }, })); export function hasSavedEnvironmentRegistryHydrated(): boolean { return savedEnvironmentRegistryHydrated; } export function waitForSavedEnvironmentRegistryHydration(): Promise { if (hasSavedEnvironmentRegistryHydrated()) { return Promise.resolve(); } return hydrateSavedEnvironmentRegistry(); } export function listSavedEnvironmentRecords(): ReadonlyArray { return Object.values(useSavedEnvironmentRegistryStore.getState().byId).toSorted((left, right) => left.label.localeCompare(right.label), ); } export function getSavedEnvironmentRecord( environmentId: EnvironmentId, ): SavedEnvironmentRecord | null { return useSavedEnvironmentRegistryStore.getState().byId[environmentId] ?? null; } export function getEnvironmentHttpBaseUrl(environmentId: EnvironmentId): string | null { const primaryEnvironment = getPrimaryKnownEnvironment(); if (primaryEnvironment?.environmentId === environmentId) { return getKnownEnvironmentHttpBaseUrl(primaryEnvironment); } return getSavedEnvironmentRecord(environmentId)?.httpBaseUrl ?? null; } export function resolveEnvironmentHttpUrl(input: { readonly environmentId: EnvironmentId; readonly pathname: string; readonly searchParams?: Record; }): string { const httpBaseUrl = getEnvironmentHttpBaseUrl(input.environmentId); if (!httpBaseUrl) { throw new Error(`Unable to resolve HTTP base URL for environment ${input.environmentId}.`); } const url = new URL(httpBaseUrl); url.pathname = input.pathname; if (input.searchParams) { url.search = new URLSearchParams(input.searchParams).toString(); } return url.toString(); } export function resetSavedEnvironmentRegistryStoreForTests() { savedEnvironmentRegistryHydrated = false; savedEnvironmentRegistryHydrationPromise = null; useSavedEnvironmentRegistryStore.setState({ byId: {} }); } export async function persistSavedEnvironmentRecord(record: SavedEnvironmentRecord): Promise { const byId = { ...useSavedEnvironmentRegistryStore.getState().byId, [record.environmentId]: record, }; await ensureLocalApi().persistence.setSavedEnvironmentRegistry( valuesOfSavedEnvironmentRegistry(byId).map((entry) => toPersistedSavedEnvironmentRecord(entry)), ); } export async function readSavedEnvironmentBearerToken( environmentId: EnvironmentId, ): Promise { return ensureLocalApi().persistence.getSavedEnvironmentSecret(environmentId); } export async function readSavedEnvironmentCredential( environmentId: EnvironmentId, ): Promise { const secret = await ensureLocalApi().persistence.getSavedEnvironmentSecret(environmentId); if (!secret) { return null; } const decoded = decodeSavedEnvironmentCredentialJson(secret); if (Option.isSome(decoded)) { return decoded.value; } // Legacy bearer secrets were stored directly as strings. return { version: 1, method: "bearer", token: secret }; } export async function writeSavedEnvironmentCredential( environmentId: EnvironmentId, credential: SavedEnvironmentCredential, ): Promise { return ensureLocalApi().persistence.setSavedEnvironmentSecret( environmentId, encodeSavedEnvironmentCredentialJson(credential), ); } export async function writeSavedEnvironmentBearerToken( environmentId: EnvironmentId, bearerToken: string, ): Promise { return ensureLocalApi().persistence.setSavedEnvironmentSecret(environmentId, bearerToken); } export async function removeSavedEnvironmentBearerToken( environmentId: EnvironmentId, ): Promise { await ensureLocalApi().persistence.removeSavedEnvironmentSecret(environmentId); } export type SavedEnvironmentConnectionState = "connecting" | "connected" | "disconnected" | "error"; export type SavedEnvironmentAuthState = "authenticated" | "requires-auth" | "unknown"; export interface SavedEnvironmentRuntimeState { readonly connectionState: SavedEnvironmentConnectionState; readonly authState: SavedEnvironmentAuthState; readonly lastError: string | null; readonly lastErrorAt: string | null; readonly scopes: ReadonlyArray | null; readonly descriptor: ExecutionEnvironmentDescriptor | null; readonly serverConfig: ServerConfig | null; readonly connectedAt: string | null; readonly disconnectedAt: string | null; } interface SavedEnvironmentRuntimeStoreState { readonly byId: Record; readonly ensure: (environmentId: EnvironmentId) => void; readonly patch: ( environmentId: EnvironmentId, patch: Partial, ) => void; readonly clear: (environmentId: EnvironmentId) => void; readonly reset: () => void; } const DEFAULT_SAVED_ENVIRONMENT_RUNTIME_STATE: SavedEnvironmentRuntimeState = Object.freeze({ connectionState: "disconnected", authState: "unknown", lastError: null, lastErrorAt: null, scopes: null, descriptor: null, serverConfig: null, connectedAt: null, disconnectedAt: null, }); function createDefaultSavedEnvironmentRuntimeState(): SavedEnvironmentRuntimeState { return { ...DEFAULT_SAVED_ENVIRONMENT_RUNTIME_STATE, }; } export const useSavedEnvironmentRuntimeStore = create()( (set) => ({ byId: {}, ensure: (environmentId) => set((state) => { if (state.byId[environmentId]) { return state; } return { byId: { ...state.byId, [environmentId]: createDefaultSavedEnvironmentRuntimeState(), }, }; }), patch: (environmentId, patch) => set((state) => ({ byId: { ...state.byId, [environmentId]: { ...(state.byId[environmentId] ?? createDefaultSavedEnvironmentRuntimeState()), ...patch, }, }, })), clear: (environmentId) => set((state) => { const { [environmentId]: _removed, ...remaining } = state.byId; return { byId: remaining, }; }), reset: () => set({ byId: {}, }), }), ); export function getSavedEnvironmentRuntimeState( environmentId: EnvironmentId, ): SavedEnvironmentRuntimeState { return ( useSavedEnvironmentRuntimeStore.getState().byId[environmentId] ?? DEFAULT_SAVED_ENVIRONMENT_RUNTIME_STATE ); } export function resetSavedEnvironmentRuntimeStoreForTests() { useSavedEnvironmentRuntimeStore.getState().reset(); }