export type ServerStatus = "pending" | "active" | "offline"; export type KeySource = "uploaded" | "generated"; export interface PackageUpdate { name: string; current_version?: string; new_version: string; } export interface Inventory { cpu: { model?: string; cores?: number; usage_pct: number; load1?: number }; memory: { total_bytes: number; used_bytes: number }; swap_total_bytes: number; swap_used_bytes: number; partitions?: { device: string; mountpoint: string; fstype?: string; total_bytes: number; used_bytes: number }[]; kernel?: string; metrics_at?: string; static_at?: string; } export interface Server { id: string; server_id: string; hostname: string; ip_address: string; os_info: string; status: ServerStatus; agent_version?: string; last_seen: string; created_at: string; available_updates?: PackageUpdate[]; updates_checked_at?: string; console_protocols?: string[]; inventory?: Inventory; } export type MonitorType = "http" | "tcp" | "icmp" | "tls"; export type MonitorStatus = "up" | "down" | "pending"; export interface MonitorTarget { url?: string; host?: string; port?: number; method?: string; expected_status?: number; keyword?: string; tls_warn_days?: number; insecure?: boolean; } export interface MonitorState { status: MonitorStatus; last_check_at?: string; latency_ms: number; message?: string; cert_expiry_at?: string; fails: number; } export interface Monitor { monitor_id: string; name: string; type: MonitorType; target: MonitorTarget; interval_sec: number; runner: string; // "server" or a server_id retries: number; enabled: boolean; channel_ids?: string[]; state: MonitorState; created_at: string; } export interface MonitorInput { name: string; type: MonitorType; target: MonitorTarget; interval_sec: number; runner: string; retries: number; enabled: boolean; channel_ids?: string[]; } export interface Incident { incident_id: string; monitor_id: string; started_at: string; resolved_at?: string; cause?: string; } export interface Rollup { monitor_id: string; period_start: string; checks: number; up_count: number; sum_latency: number; } export type ChannelType = "webhook" | "smtp" | "discord" | "slack" | "telegram"; export interface NotificationChannel { channel_id: string; name: string; type: ChannelType; config: Record; enabled: boolean; created_at: string; } export interface ChannelInput { name: string; type: ChannelType; config: Record; enabled: boolean; } export interface ConsoleConnectRequest { server_id: string; protocol: string; key_id?: string; rdp_username?: string; rdp_password?: string; ssh_username?: string; } export interface ConsoleConnectResponse { session_id: string; token: string; ws_path: string; } export interface Key { id: string; key_id: string; label: string; public_key: string; fingerprint: string; source: KeySource; generated_by_server_id?: string; has_private_key: boolean; has_passphrase?: boolean; created_at: string; assigned_count?: number; } export interface Assignment { id: string; key_id: string; server_id: string; assigned_at: string; revoked_at: string | null; } export interface AuditEvent { id: string; event_type: string; actor: string; server_id?: string; key_id?: string; details: string; created_at: string; } export interface AlertSettings { enabled: boolean; webhook_url: string; offline_threshold_minutes: number; } export interface EmailSettings { enabled: boolean; smtp_host: string; smtp_port: number; username: string; password: string; from_addr: string; to_addrs: string[]; use_tls: boolean; } export interface SecretsSettings { read_token_set: boolean; rotated_at?: string; } export interface Settings { alerts: AlertSettings; email: EmailSettings; secrets: SecretsSettings; workflow_log_retention_days?: number | null; } export interface SecretGroupSummary { group: string; key_count: number; updated_at: string; } export interface Secret { group: string; key: string; updated_at: string; } export interface NewServerResponse { server_id: string; pre_reg_token: string; install_command: string; install_command_ps: string; } export interface GenerateKeyOptions { label: string; key_type: "ed25519" | "rsa" | "ecdsa"; key_size?: number; passphrase?: string; comment?: string; } export interface KeyWithAssignments extends Key { assignments: (Assignment & { server: Server })[]; } export interface ServerWithKeys extends Server { keys: (Assignment & { key: Key })[]; } export interface InputParam { name: string; default: string; description: string; } export interface WorkflowStep { step_id: string; name: string; description: string; interpreter: "bash" | "powershell"; script: string; declared_outputs: string[]; declared_inputs: InputParam[]; secret_refs: string[]; source?: "user" | "default"; slug?: string; } export interface WorkflowStepRef { step_id?: string; inline?: WorkflowStep; order: number; on_failure: "stop" | "continue" | "retry"; max_retries: number; overrides?: { script?: string; secret_refs?: string[] }; inputs?: Record; } export interface Workflow { workflow_id: string; name: string; target_server_ids: string[]; steps: WorkflowStepRef[]; } export interface StepRun { order: number; name: string; status: string; attempts: number; exit_code: number; log_offset: number; output_env: Record; started_at?: string; finished_at?: string; } export interface ServerRun { server_id: string; hostname: string; status: string; run_env: Record; steps: StepRun[]; started_at?: string; finished_at?: string; } export interface WorkflowRun { run_id: string; workflow_id: string; name: string; status: string; triggered_by: string; started_at: string; finished_at?: string; server_runs: ServerRun[]; } export type Role = "owner" | "admin" | "member"; /** The session as returned by GET /auth/me mirrors auth.Session on the server. */ export interface SessionUser { user_id: string; instance_id: string; role: Role; email: string; name: string; } export interface Instance { instance_id: string; name: string; slug: string; created_at: string; } export interface MeResponse { user: SessionUser; instance: Instance | null; } export interface BootstrapStatus { needs_setup: boolean; instance_name?: string; } export interface BootstrapResponse { instance: Instance; slug: string; instance_id: string; } export interface InstanceUser { user_id: string; instance_id: string; email: string; role: Role; auth_source: "local" | "oidc"; created_at: string; last_login?: string; } export interface OrgUserInput { email: string; password: string; role: Role; } export interface OrgOIDCConfig { issuer?: string; client_id?: string; enabled: boolean; client_secret_set: boolean; updated_at?: string; } export interface OrgOIDCInput { issuer: string; client_id: string; client_secret: string; enabled: boolean; } class ApiError extends Error { constructor( public status: number, message: string, ) { super(message); this.name = "ApiError"; } } async function request(path: string, options?: RequestInit): Promise { const res = await fetch(`/api${path}`, { credentials: "include", headers: { "Content-Type": "application/json", ...options?.headers, }, ...options, }); if (!res.ok) { const text = await res.text().catch(() => ""); let message = text || res.statusText || `HTTP ${res.status}`; try { const body = JSON.parse(text); if (body?.error) message = body.error; } catch {} throw new ApiError(res.status, message); } if (res.status === 204) { return undefined as T; } return res.json(); } async function authRequest(path: string, options?: RequestInit): Promise { const res = await fetch(path, { credentials: "include", headers: { "Content-Type": "application/json", ...options?.headers }, ...options, }); if (!res.ok) { let message = `HTTP ${res.status}`; try { const body = await res.json(); if (body?.error) message = body.error; } catch {} throw new ApiError(res.status, message); } if (res.status === 204) { return undefined as T; } return res.json(); } export const auth = { bootstrapStatus(): Promise { return authRequest("/auth/bootstrap-status"); }, bootstrap(input: { instance_name: string; email: string; password: string }): Promise { return authRequest("/auth/bootstrap", { method: "POST", body: JSON.stringify(input), }); }, login(email: string, password: string): Promise<{ ok: boolean }> { return authRequest<{ ok: boolean }>("/auth/login", { method: "POST", body: JSON.stringify({ email, password }), }); }, logout(): Promise { return authRequest("/auth/logout", { method: "POST" }); }, me(): Promise { return authRequest("/auth/me"); }, /** The URL an admin must register with their OIDC provider. */ oidcRedirectUrl(): string { if (typeof window === "undefined") return "/auth/oidc/callback"; return `${window.location.origin}/auth/oidc/callback`; }, }; export const api = { listInstanceUsers(): Promise { return request("/instance/users"); }, createInstanceUser(input: OrgUserInput): Promise { return request("/instance/users", { method: "POST", body: JSON.stringify(input) }); }, updateInstanceUserRole(userId: string, role: Role): Promise<{ ok: boolean }> { return request<{ ok: boolean }>(`/instance/users/${userId}/role`, { method: "PUT", body: JSON.stringify({ role }), }); }, deleteInstanceUser(userId: string): Promise { return request(`/instance/users/${userId}`, { method: "DELETE" }); }, getInstanceOIDC(): Promise { return request("/instance/oidc"); }, saveInstanceOIDC(input: OrgOIDCInput): Promise<{ saved: boolean }> { return request<{ saved: boolean }>("/instance/oidc", { method: "PUT", body: JSON.stringify(input) }); }, listServers(): Promise { return request("/servers"); }, getServer(serverId: string): Promise { return request(`/servers/${serverId}`); }, createServer(): Promise { return request("/servers/new", { method: "POST" }); }, deleteServer(serverId: string): Promise { return request(`/servers/${serverId}`, { method: "DELETE" }); }, generateKeyForServer(serverId: string, opts: GenerateKeyOptions): Promise<{ command_id: string }> { return request<{ command_id: string }>(`/servers/${serverId}/generate-key`, { method: "POST", body: JSON.stringify(opts), }); }, getUpdateCommand(osInfo?: string): string { if (osInfo && osInfo.toLowerCase().includes("windows")) { return `irm "${window.location.origin}/update.ps1" | iex`; } return `curl -fsSL "${window.location.origin}/update" | bash`; }, listMonitors(): Promise { return request("/monitors"); }, getMonitor(monitorId: string): Promise { return request(`/monitors/${monitorId}`); }, createMonitor(input: MonitorInput): Promise { return request("/monitors", { method: "POST", body: JSON.stringify(input) }); }, updateMonitor(monitorId: string, input: Partial): Promise { return request(`/monitors/${monitorId}`, { method: "PUT", body: JSON.stringify(input) }); }, deleteMonitor(monitorId: string): Promise { return request(`/monitors/${monitorId}`, { method: "DELETE" }); }, getMonitorIncidents(monitorId: string): Promise { return request(`/monitors/${monitorId}/incidents`); }, getMonitorUptime(monitorId: string): Promise { return request(`/monitors/${monitorId}/uptime`); }, listChannels(): Promise { return request("/channels"); }, createChannel(input: ChannelInput): Promise { return request("/channels", { method: "POST", body: JSON.stringify(input) }); }, updateChannel(channelId: string, input: Partial): Promise { return request(`/channels/${channelId}`, { method: "PUT", body: JSON.stringify(input) }); }, deleteChannel(channelId: string): Promise { return request(`/channels/${channelId}`, { method: "DELETE" }); }, testChannel(channelId: string): Promise<{ status: string }> { return request<{ status: string }>(`/channels/${channelId}/test`, { method: "POST" }); }, getLatestAgentVersion(): Promise<{ version: string }> { return request<{ version: string }>("/agent/latest-version"); }, updateAgent(serverId: string): Promise<{ message: string; version: string }> { return request<{ message: string; version: string }>(`/servers/${serverId}/update-agent`, { method: "POST", }); }, applyUpdates(serverId: string): Promise<{ message: string }> { return request<{ message: string }>(`/servers/${serverId}/apply-updates`, { method: "POST", }); }, listAuditEvents(limit?: number): Promise { const qs = limit ? `?limit=${limit}` : ""; return request(`/audit${qs}`); }, getSettings(): Promise { return request("/settings"); }, saveSettings(settings: { alerts: AlertSettings; email: EmailSettings; workflow_log_retention_days?: number | null }): Promise<{ saved: boolean }> { return request<{ saved: boolean }>("/settings", { method: "PUT", body: JSON.stringify(settings), }); }, rotateSecretsToken(): Promise<{ token: string }> { return request<{ token: string }>("/settings/secrets-token", { method: "POST" }); }, listSecretGroups(): Promise { return request("/secrets"); }, createSecretGroup(group: string, values: Record): Promise<{ group: string }> { return request<{ group: string }>("/secrets", { method: "POST", body: JSON.stringify({ group, values }), }); }, getSecretGroup(group: string): Promise<{ group: string; secrets: Secret[] }> { return request<{ group: string; secrets: Secret[] }>(`/secrets/${encodeURIComponent(group)}`); }, putSecrets(group: string, values: Record): Promise<{ saved: boolean }> { return request<{ saved: boolean }>(`/secrets/${encodeURIComponent(group)}`, { method: "PUT", body: JSON.stringify(values), }); }, revealSecret(group: string, key: string): Promise<{ value: string }> { return request<{ value: string }>(`/secrets/${encodeURIComponent(group)}/reveal`, { method: "POST", body: JSON.stringify({ key }), }); }, deleteSecret(group: string, key: string): Promise { return request(`/secrets/${encodeURIComponent(group)}/${encodeURIComponent(key)}`, { method: "DELETE", }); }, deleteSecretGroup(group: string): Promise { return request(`/secrets/${encodeURIComponent(group)}`, { method: "DELETE" }); }, listKeys(): Promise { return request("/keys"); }, getKey(keyId: string): Promise { return request(`/keys/${keyId}`); }, uploadKey(label: string, public_key: string, private_key?: string, passphrase?: string): Promise { return request("/keys", { method: "POST", body: JSON.stringify({ label, public_key, private_key: private_key || undefined, passphrase: passphrase || undefined, }), }); }, getPrivateKey(keyId: string): Promise<{ private_key: string }> { return request<{ private_key: string }>(`/keys/${keyId}/private-key`); }, deleteKey(keyId: string): Promise { return request(`/keys/${keyId}`, { method: "DELETE" }); }, assignKey(keyId: string, serverId: string): Promise { return request(`/keys/${keyId}/assign`, { method: "POST", body: JSON.stringify({ server_id: serverId }), }); }, revokeKey(keyId: string, serverId: string): Promise { return request(`/keys/${keyId}/assign/${serverId}`, { method: "DELETE", }); }, connectConsole(body: ConsoleConnectRequest): Promise { return request("/console/connect", { method: "POST", body: JSON.stringify(body), }); }, listSteps(): Promise { return request("/steps"); }, createStep(s: Partial): Promise { return request("/steps", { method: "POST", body: JSON.stringify(s), }); }, updateStep(stepId: string, s: Partial): Promise { return request(`/steps/${stepId}`, { method: "PUT", body: JSON.stringify(s), }); }, deleteStep(stepId: string): Promise { return request(`/steps/${stepId}`, { method: "DELETE" }); }, exportStepUrl(stepId: string): string { return `/api/steps/${stepId}/export`; }, importStep(doc: unknown): Promise { return request("/steps/import", { method: "POST", body: JSON.stringify(doc), }); }, parseStep(doc: unknown): Promise { return request("/steps/parse", { method: "POST", body: JSON.stringify(doc), }); }, seedDefaults(): Promise<{ created: number; updated: number }> { return request<{ created: number; updated: number }>("/steps/seed-defaults", { method: "POST", }); }, stepUsage(): Promise> { return request>("/steps/usage"); }, listWorkflows(): Promise { return request("/workflows"); }, getWorkflow(workflowId: string): Promise { return request(`/workflows/${workflowId}`); }, createWorkflow(w: Partial): Promise { return request("/workflows", { method: "POST", body: JSON.stringify(w), }); }, updateWorkflow(workflowId: string, w: Partial): Promise { return request(`/workflows/${workflowId}`, { method: "PUT", body: JSON.stringify(w), }); }, deleteWorkflow(workflowId: string): Promise { return request(`/workflows/${workflowId}`, { method: "DELETE" }); }, runWorkflow(workflowId: string): Promise<{ run_id: string }> { return request<{ run_id: string }>(`/workflows/${workflowId}/run`, { method: "POST", }); }, listRuns(workflowId: string): Promise { return request(`/workflows/${workflowId}/runs`); }, getRun(runId: string): Promise { return request(`/runs/${runId}`); }, cancelRun(runId: string): Promise { return request(`/runs/${runId}/cancel`, { method: "POST" }); }, async getServerRunLog(runId: string, serverId: string): Promise { const res = await fetch(`/api/runs/${runId}/servers/${serverId}/logs`, { credentials: "include", }); if (!res.ok) throw new Error("no logs"); return res.text(); }, serverRunLogStreamUrl(runId: string, serverId: string): string { return `/api/runs/${runId}/servers/${serverId}/logs/stream`; }, };