import "./SecretsView.css"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Check, Copy, Eye, EyeOff, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react"; type ToastKind = "info" | "success" | "error"; type SecretScope = "project" | "global"; type SecretPolicy = "auto" | "prompt" | "deny"; interface SecretRecord { id: string; scope: SecretScope; key: string; description: string | null; accessPolicy: SecretPolicy; envExportable: boolean; envExportKey: string | null; lastReadAt: string | null; } interface SecretsViewProps { addToast?: (msg: string, kind?: ToastKind) => void; } const RESERVED_SYNC_PASSPHRASE_KEY = "__sync_passphrase__"; interface SecretFormState { key: string; value: string; description: string; scope: SecretScope; accessPolicy: SecretPolicy; envExportable: boolean; envExportKey: string; } const EMPTY_FORM: SecretFormState = { key: "", value: "", description: "", scope: "project", accessPolicy: "prompt", envExportable: false, envExportKey: "", }; const actionIconProps = { className: "secrets-action-icon", "aria-hidden": true, style: { width: "1em", height: "1em" }, } as const; const spinningActionIconProps = { ...actionIconProps, className: "secrets-action-icon spin", } as const; export const SecretsView = ({ addToast }: SecretsViewProps) => { const [secrets, setSecrets] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [formError, setFormError] = useState(null); const [editing, setEditing] = useState(null); const [showModal, setShowModal] = useState(false); const [showDeleteId, setShowDeleteId] = useState(null); const [form, setForm] = useState(EMPTY_FORM); const [showValue, setShowValue] = useState(false); const [revealedValues, setRevealedValues] = useState>({}); const [copiedId, setCopiedId] = useState(null); const [syncPassphraseConfigured, setSyncPassphraseConfigured] = useState(false); const [syncModalOpen, setSyncModalOpen] = useState(false); const [syncPassphrase, setSyncPassphrase] = useState(""); const [syncPassphraseConfirm, setSyncPassphraseConfirm] = useState(""); const [syncSaving, setSyncSaving] = useState(false); const revealTimersRef = useRef>>(new Map()); const copyTimersRef = useRef>>(new Map()); const request = useCallback(async (url: string, init?: RequestInit): Promise => { const response = await fetch(url, { ...init, headers: { "Content-Type": "application/json", ...(init?.headers ?? {}), }, }); if (!response.ok) { const payload = await response.json().catch(() => ({ error: "Request failed" })); throw new Error(String(payload?.error ?? "Request failed")); } if (response.status === 204) return undefined as T; return response.json() as Promise; }, []); const loadSecrets = useCallback(async () => { setLoading(true); setError(null); try { const data = await request<{ secrets: SecretRecord[] }>("/api/secrets"); setSecrets(data.secrets); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }, [request]); const loadSyncPassphraseStatus = useCallback(async () => { try { const data = await request<{ configured: boolean }>("/api/secrets/sync-passphrase"); setSyncPassphraseConfigured(Boolean(data.configured)); } catch (err) { addToast?.(`Failed to load sync passphrase status: ${err instanceof Error ? err.message : String(err)}`, "error"); } }, [addToast, request]); useEffect(() => { void loadSecrets(); void loadSyncPassphraseStatus(); return () => { revealTimersRef.current.forEach((timer) => clearTimeout(timer)); copyTimersRef.current.forEach((timer) => clearTimeout(timer)); }; }, [loadSecrets, loadSyncPassphraseStatus]); const closeSyncModal = () => { setSyncModalOpen(false); setSyncPassphrase(""); setSyncPassphraseConfirm(""); }; const saveSyncPassphrase = async (passphrase: string) => { await request<{ success: boolean }>("/api/secrets/sync-passphrase", { method: "PUT", body: JSON.stringify({ passphrase }), }); }; const submitSyncPassphrase = async () => { setSyncSaving(true); try { await saveSyncPassphrase(syncPassphrase); addToast?.(syncPassphraseConfigured ? "Sync passphrase rotated" : "Sync passphrase set", "success"); closeSyncModal(); await loadSyncPassphraseStatus(); } catch (err) { addToast?.(`Failed to save sync passphrase: ${err instanceof Error ? err.message : String(err)}`, "error"); } finally { setSyncSaving(false); } }; const clearSyncPassphraseHandler = async () => { const confirmed = window.confirm("Clear the cross-node sync passphrase? Existing sync pairs will stop working until you set a new passphrase."); if (!confirmed) return; try { await request<{ success: boolean }>("/api/secrets/sync-passphrase", { method: "DELETE" }); addToast?.("Sync passphrase cleared", "success"); await loadSyncPassphraseStatus(); } catch (err) { addToast?.(`Failed to clear sync passphrase: ${err instanceof Error ? err.message : String(err)}`, "error"); } }; const openCreate = () => { setEditing(null); setForm(EMPTY_FORM); setShowModal(true); setShowValue(false); setFormError(null); }; const openEdit = (secret: SecretRecord) => { setEditing(secret); setForm({ key: secret.key, value: "", description: secret.description ?? "", scope: secret.scope, accessPolicy: secret.accessPolicy, envExportable: secret.envExportable, envExportKey: secret.envExportKey ?? "", }); setShowModal(true); setShowValue(false); setFormError(null); }; const submit = async () => { setFormError(null); try { if (editing) { const body: Record = { key: form.key, description: form.description || null, accessPolicy: form.accessPolicy, envExportable: form.envExportable, envExportKey: form.envExportable ? (form.envExportKey || null) : null, }; if (form.value) body.value = form.value; await request(`/api/secrets/${editing.scope}/${editing.id}`, { method: "PATCH", body: JSON.stringify(body), }); } else { await request("/api/secrets", { method: "POST", body: JSON.stringify({ scope: form.scope, key: form.key, value: form.value, description: form.description || null, accessPolicy: form.accessPolicy, envExportable: form.envExportable, envExportKey: form.envExportable ? (form.envExportKey || null) : null, }), }); } setShowModal(false); setForm(EMPTY_FORM); await loadSecrets(); } catch (err) { setFormError(err instanceof Error ? err.message : String(err)); } }; const revealSecret = async (secret: SecretRecord) => { const data = await request<{ key: string; value: string }>(`/api/secrets/${secret.scope}/${secret.id}/reveal`, { method: "POST" }); setRevealedValues((current) => ({ ...current, [secret.id]: data.value })); addToast?.("Revealed", "success"); const timer = setTimeout(() => { setRevealedValues((current) => ({ ...current, [secret.id]: null })); }, 30000); const existing = revealTimersRef.current.get(secret.id); if (existing) clearTimeout(existing); revealTimersRef.current.set(secret.id, timer); }; const copySecret = async (secret: SecretRecord) => { const revealed = revealedValues[secret.id]; if (!revealed) return; await navigator.clipboard.writeText(revealed); setCopiedId(secret.id); addToast?.("Copied", "success"); const timer = setTimeout(() => { setCopiedId(null); setRevealedValues((current) => ({ ...current, [secret.id]: null })); }, 1500); const existing = copyTimersRef.current.get(secret.id); if (existing) clearTimeout(existing); copyTimersRef.current.set(secret.id, timer); }; const deleteSecret = async (secret: SecretRecord) => { await request(`/api/secrets/${secret.scope}/${secret.id}`, { method: "DELETE" }); setShowDeleteId(null); await loadSecrets(); }; const sortedSecrets = useMemo( () => [...secrets] .filter((secret) => !(secret.scope === "global" && secret.key === RESERVED_SYNC_PASSPHRASE_KEY)) .sort((a, b) => a.key.localeCompare(b.key)), [secrets], ); const syncPassphraseMatches = syncPassphrase.length > 0 && syncPassphrase === syncPassphraseConfirm; return (

Secrets

Cross-Node Sync Passphrase

{syncPassphraseConfigured ? : null}

Shared passphrase used to wrap cross-node secret bundles. Both nodes in a sync pair must share the same value. Stored locally only; never transmitted.

{error ?
{error}
: null} {loading ?
Loading…
: null} {!loading && sortedSecrets.length === 0 ?
No secrets found.
: null}
{sortedSecrets.map((secret) => { const revealed = revealedValues[secret.id]; return (
{secret.key}
{secret.scope} {secret.accessPolicy} {secret.envExportable ? env_exportable : null}
{revealed ?
{revealed}
: null}
{secret.lastReadAt ? new Date(secret.lastReadAt).toLocaleString() : "Never read"}
{showDeleteId === secret.id ? (
) : null}
); })}
{syncModalOpen ? (

{syncPassphraseConfigured ? "Rotate sync passphrase" : "Set sync passphrase"}

setSyncPassphrase(e.target.value)} />
setSyncPassphraseConfirm(e.target.value)} />
{!syncPassphraseMatches && syncPassphraseConfirm.length > 0 ?
Passphrases must match.
: null}
) : null} {showModal ? (

{editing ? "Edit secret" : "Add secret"}

setForm((c) => ({ ...c, key: e.target.value }))} />
setForm((c) => ({ ...c, value: e.target.value }))} />