import { useEffect, useState } from 'react'; // Status returned by GET /api/identity. Private key is never present here — // it is shown exactly once at generation time (see GeneratedKeyPanel). interface IdentityStatus { hasIdentity: boolean; uid?: string; curve?: string; createdAt?: number; } interface GeneratedKey { uid: string; curve: string; createdAt: number; privateKeyHex: string; } const cardStyle: React.CSSProperties = { background: '#16213e', borderRadius: '8px', padding: '1.5rem', marginBottom: '1rem', }; const buttonStyle: React.CSSProperties = { padding: '0.5rem 1rem', background: '#0f3460', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', marginRight: '0.5rem', }; const dangerButtonStyle: React.CSSProperties = { ...buttonStyle, background: '#7f1d1d', }; const inputStyle: React.CSSProperties = { width: '100%', padding: '0.5rem', marginBottom: '0.5rem', background: '#1a1a2e', border: '1px solid #0f3460', borderRadius: '4px', color: '#fff', fontFamily: 'monospace', }; const monoStyle: React.CSSProperties = { fontFamily: 'monospace', fontSize: '0.85rem', wordBreak: 'break-all', background: '#0a0a18', border: '1px solid #0f3460', padding: '0.75rem', borderRadius: '4px', }; function formatCreatedAt(unixSec?: number): string { if (!unixSec) return ''; try { return new Date(unixSec * 1000).toISOString().replace('T', ' ').replace(/\..+$/, ' UTC'); } catch { return String(unixSec); } } // Read the JSON body of a non-2xx response if present, fall back to status text. async function errorMessage(res: Response, fallback: string): Promise { try { const j = await res.json(); if (j && (j.message || j.error)) return j.message ?? j.error; } catch { // not JSON } return `${fallback} (HTTP ${res.status})`; } function GeneratedKeyPanel({ data, onDismiss }: { data: GeneratedKey; onDismiss: () => void }) { const [copied, setCopied] = useState(false); const copy = async () => { try { await navigator.clipboard.writeText(data.privateKeyHex); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // Clipboard API blocked (non-HTTPS context, etc.) — fall back to selection. const sel = window.getSelection(); const range = document.createRange(); const el = document.getElementById('mm-privkey-box'); if (sel && el) { range.selectNodeContents(el); sel.removeAllRanges(); sel.addRange(range); } } }; return (
This private key is shown ONCE. Save it now in a password manager. It will never be displayed again — there is no recovery path. Losing it means losing access to every record signed under this identity.

uid (public):

{data.uid}

privateKeyHex (secret):

{data.privateKeyHex}
); } function NoIdentityPanel({ onGenerated, onImported, }: { onGenerated: (g: GeneratedKey) => void; onImported: () => void; }) { const [busy, setBusy] = useState(false); const [importing, setImporting] = useState(false); const [importHex, setImportHex] = useState(''); const [error, setError] = useState(null); const generate = async () => { setBusy(true); setError(null); try { const res = await fetch('/api/identity/generate', { method: 'POST' }); if (!res.ok) { setError(await errorMessage(res, 'Generate failed')); return; } const data = (await res.json()) as GeneratedKey; onGenerated(data); } catch (e) { setError(`Generate failed: ${e instanceof Error ? e.message : String(e)}`); } finally { setBusy(false); } }; const importKey = async (e: React.FormEvent) => { e.preventDefault(); setBusy(true); setError(null); try { const res = await fetch('/api/identity/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ privateKeyHex: importHex.trim() }), }); if (!res.ok) { setError(await errorMessage(res, 'Import failed')); return; } setImportHex(''); setImporting(false); onImported(); } catch (e) { setError(`Import failed: ${e instanceof Error ? e.message : String(e)}`); } finally { setBusy(false); } }; return (

No identity yet

The User Data Layer needs a signing key for your ratings, watch progress, and "liked" flags to be attributed to you across devices. Nothing happens automatically — generate one below, or import a key you already have.

{error && (

{error}

)}
{importing && (

64 hex chars (32-byte secp256k1 scalar). A "0x" prefix is fine.

setImportHex(e.target.value)} required autoFocus />
)}
); } function HasIdentityPanel({ status, onReplace }: { status: IdentityStatus; onReplace: () => void }) { const [confirming, setConfirming] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const remove = async () => { setBusy(true); setError(null); try { const res = await fetch('/api/identity?confirm=true', { method: 'DELETE' }); if (!res.ok && res.status !== 204) { setError(await errorMessage(res, 'Delete failed')); return; } onReplace(); } catch (e) { setError(`Delete failed: ${e instanceof Error ? e.message : String(e)}`); } finally { setBusy(false); setConfirming(false); } }; return (

Active identity

uid:

{status.uid}

Curve: {status.curve} · Created: {formatCreatedAt(status.createdAt)}

The private key lives at /meta-core/identity/identity.json on this volume and is never returned through the API after generation. Other services on this stack (e.g. meta-watch) sign records by calling POST /api/identity/sign.

{error && (

{error}

)} {!confirming ? ( ) : (

Replacing the identity deletes the current keypair from disk. Records signed by the current uid will keep verifying for anyone who has them cached, but this node will no longer be able to write under that uid. Make sure you have backed up the key first.

)}
); } export default function Identity() { const [status, setStatus] = useState(null); const [generated, setGenerated] = useState(null); const [loading, setLoading] = useState(true); const [fetchError, setFetchError] = useState(null); const refresh = async () => { setLoading(true); setFetchError(null); try { const res = await fetch('/api/identity'); if (!res.ok) { setFetchError(await errorMessage(res, 'Failed to load identity status')); return; } setStatus((await res.json()) as IdentityStatus); } catch (e) { setFetchError(`Failed to load identity status: ${e instanceof Error ? e.message : String(e)}`); } finally { setLoading(false); } }; useEffect(() => { refresh(); }, []); return (

Identity

Signing key for the User Data Layer (per-user ratings, progress, "liked" flags). One identity per node. The same key on another device makes that device count as "you".

{loading &&
Loading…
} {fetchError && (

{fetchError}

)} {!loading && !fetchError && generated && ( { setGenerated(null); refresh(); }} /> )} {!loading && !fetchError && !generated && status && !status.hasIdentity && ( setGenerated(g)} onImported={refresh} /> )} {!loading && !fetchError && !generated && status && status.hasIdentity && ( )}
); }