import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Plus, Upload, X, AlertTriangle, Search, Target, } from '../icons'; import api from '../utils/api'; import { useEscapeKey } from '../hooks/useEscapeKey'; import { useFocusTrap } from '../hooks/useFocusTrap'; import CanaryTokenDrawer from './CanaryTokenDrawer'; import type { CanaryTokenRow } from './CanaryTokenDrawer'; import { KNOWN_GENERATORS, KIND_OPTIONS, STATE_COLOR, type BlobRow, type DeckyOption, type TopologyOption, type Scope, type GeneratorName, } from './CanaryTokens/types'; import { extractError, fmt, fmtBytes } from './CanaryTokens/helpers'; import { INPUT_STYLE, BTN_PRIMARY, BTN_GHOST, Field, Stat } from './CanaryTokens/ui'; // ─── CREATE MODAL ────────────────────────────────────────────────────────── interface CreateModalProps { blobs: BlobRow[]; deckies: DeckyOption[]; topologies: TopologyOption[]; onClose: () => void; onCreated: (token: CanaryTokenRow) => void; } const CreateModal: React.FC = ({ blobs, deckies, topologies, onClose, onCreated }) => { const panelRef = useRef(null); useEscapeKey(onClose, true); useFocusTrap(panelRef, true); const [scope, setScope] = useState('fleet'); const [topologyId, setTopologyId] = useState(topologies[0]?.id ?? ''); const [topoDeckies, setTopoDeckies] = useState([]); const [topoLoading, setTopoLoading] = useState(false); // When scope flips to topology (or topology selection changes) we // hydrate the chosen topology's decky list — different shape than the // /deckies endpoint, so the picker must repopulate. useEffect(() => { if (scope !== 'topology' || !topologyId) { setTopoDeckies([]); return; } let cancelled = false; setTopoLoading(true); api.get(`/topologies/${encodeURIComponent(topologyId)}`) .then((res) => { if (cancelled) return; const list: DeckyOption[] = (res.data?.deckies ?? []).map( (d: { name: string; ip?: string }) => ({ name: d.name, ip: d.ip }), ); setTopoDeckies(list); }) .catch(() => { if (!cancelled) setTopoDeckies([]); }) .finally(() => { if (!cancelled) setTopoLoading(false); }); return () => { cancelled = true; }; }, [scope, topologyId]); const activeDeckies = scope === 'topology' ? topoDeckies : deckies; const [decky, setDecky] = useState(deckies[0]?.name ?? ''); // Reset the decky selection when the active list changes — otherwise // a fleet decky name lingers as a stale value when the user flips to // a topology that doesn't have that decky. useEffect(() => { if (activeDeckies.length === 0) { setDecky(''); } else if (!activeDeckies.some((d) => d.name === decky)) { setDecky(activeDeckies[0].name); } }, [activeDeckies]); // eslint-disable-line react-hooks/exhaustive-deps const [kind, setKind] = useState<'http' | 'dns' | 'aws_passive'>('http'); const [path, setPath] = useState('/home/admin/.aws/credentials'); const [source, setSource] = useState<'generator' | 'blob'>('generator'); const [generator, setGenerator] = useState('aws_creds'); const [blobUuid, setBlobUuid] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const handleSubmit = async () => { setError(null); if (scope === 'topology' && !topologyId) return setError('Pick a topology.'); if (!decky.trim()) return setError('Pick a decky.'); if (!path.trim().startsWith('/')) return setError('placement_path must be absolute.'); if (source === 'blob' && !blobUuid) return setError('Pick a blob or switch to Generator.'); setSubmitting(true); try { const body: Record = { decky_name: decky.trim(), kind, placement_path: path.trim(), }; if (scope === 'topology') body.topology_id = topologyId; if (source === 'generator') body.generator = generator; else body.blob_uuid = blobUuid; const res = await api.post('/canary/tokens', body); onCreated(res.data); } catch (err) { setError(extractError(err, 'Create failed.')); } finally { setSubmitting(false); } }; return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000, }} >
NEW CANARY TOKEN
{(['fleet', 'topology'] as const).map((s) => ( ))}
{scope === 'topology' && ( {topologies.length === 0 ? (
No active topologies. Deploy one from MazeNET first.
) : ( )}
)} {topoLoading ? (
loading topology deckies…
) : activeDeckies.length === 0 ? (
{scope === 'topology' ? 'This topology has no deckies.' : 'No fleet deckies running. Deploy a fleet first.'}
) : ( )}
setPath(e.target.value)} placeholder="/home/admin/.aws/credentials" style={{ ...INPUT_STYLE, fontFamily: 'monospace' }} />
{(['generator', 'blob'] as const).map((s) => ( ))}
{source === 'generator' && ( )} {source === 'blob' && ( {blobs.length === 0 ? (
No blobs uploaded yet. Use "Upload artifact" on the main page first.
) : ( )}
)} {error && (
{error}
)}
); }; // ─── BLOB UPLOAD MODAL ───────────────────────────────────────────────────── interface UploadModalProps { onClose: () => void; onUploaded: (blob: BlobRow) => void; } const UploadModal: React.FC = ({ onClose, onUploaded }) => { const panelRef = useRef(null); useEscapeKey(onClose, true); useFocusTrap(panelRef, true); const [file, setFile] = useState(null); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [dragOver, setDragOver] = useState(false); const handleSubmit = async () => { if (!file) return setError('Pick a file first.'); setUploading(true); setError(null); try { const fd = new FormData(); fd.append('file', file); const res = await api.post('/canary/blobs', fd, { headers: { 'Content-Type': 'multipart/form-data' }, }); onUploaded(res.data); } catch (err) { setError(extractError(err, 'Upload failed.')); } finally { setUploading(false); } }; return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000, }} >
UPLOAD CANARY ARTIFACT
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { e.preventDefault(); setDragOver(false); const f = e.dataTransfer.files?.[0]; if (f) setFile(f); }} style={{ border: `2px dashed ${dragOver ? 'var(--accent-color, #00ff88)' : 'var(--border-color, #30363d)'}`, padding: '32px', textAlign: 'center', marginBottom: '16px', cursor: 'pointer', background: dragOver ? 'rgba(0, 255, 136, 0.05)' : 'transparent', }} onClick={() => document.getElementById('canary-blob-input')?.click()} >
{file ? `${file.name} (${fmtBytes(file.size)})` : 'Drop a file here or click to browse'}
{!file && (
DOCX · XLSX · PDF · HTML · PNG/JPEG · plain configs
)} setFile(e.target.files?.[0] || null)} />
DECNET injects the callback server-side; the original bytes stay on the master.
{error && (
{error}
)}
); }; // ─── FILE DROP MODAL ─────────────────────────────────────────────────────── // File drops aren't persisted server-side (W2 backend is fire-and-forget), // so we keep a local log per admin uuid. This is informational only — // the server has no record of what an admin dropped, by design (the // endpoint exists to let operators stage payloads, not as an audit trail). const FILEDROP_LS_KEY = 'decnet:canary:filedrops'; interface FileDropEntry { id: string; // local-only uuid decky_name: string; topology_id: string | null; path: string; size_bytes: number; filename: string; mode: number; mtime_offset: number; dropped_at: string; // ISO } function loadFileDrops(): FileDropEntry[] { try { const raw = localStorage.getItem(FILEDROP_LS_KEY); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function saveFileDrops(rows: FileDropEntry[]): void { try { localStorage.setItem(FILEDROP_LS_KEY, JSON.stringify(rows.slice(0, 200))); } catch { // localStorage may be full or disabled; the list is best-effort. } } interface FileDropModalProps { deckies: DeckyOption[]; topologies: TopologyOption[]; onClose: () => void; onDropped: (entry: FileDropEntry) => void; } const FileDropModal: React.FC = ({ deckies, topologies, onClose, onDropped }) => { const panelRef = useRef(null); useEscapeKey(onClose, true); useFocusTrap(panelRef, true); const [scope, setScope] = useState('fleet'); const [topologyId, setTopologyId] = useState(topologies[0]?.id ?? ''); const [topoDeckies, setTopoDeckies] = useState([]); const [topoLoading, setTopoLoading] = useState(false); useEffect(() => { if (scope !== 'topology' || !topologyId) { setTopoDeckies([]); return; } let cancelled = false; setTopoLoading(true); api.get(`/topologies/${encodeURIComponent(topologyId)}`) .then((res) => { if (cancelled) return; setTopoDeckies( (res.data?.deckies ?? []).map((d: { name: string; ip?: string }) => ({ name: d.name, ip: d.ip, })), ); }) .catch(() => { if (!cancelled) setTopoDeckies([]); }) .finally(() => { if (!cancelled) setTopoLoading(false); }); return () => { cancelled = true; }; }, [scope, topologyId]); const activeDeckies = scope === 'topology' ? topoDeckies : deckies; const [decky, setDecky] = useState(deckies[0]?.name ?? ''); useEffect(() => { if (activeDeckies.length === 0) setDecky(''); else if (!activeDeckies.some((d) => d.name === decky)) setDecky(activeDeckies[0].name); }, [activeDeckies]); // eslint-disable-line react-hooks/exhaustive-deps const [path, setPath] = useState('/root/payload.bin'); const [mode, setMode] = useState('644'); const [mtimeOffset, setMtimeOffset] = useState('0'); const [file, setFile] = useState(null); const [dragOver, setDragOver] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const validatePath = (p: string): string | null => { if (!p.startsWith('/')) return 'path must be absolute (start with /)'; if (p.split('/').includes('..')) return 'path must not contain .. segments'; return null; }; const handleSubmit = async () => { setError(null); if (scope === 'topology' && !topologyId) return setError('Pick a topology.'); if (!decky.trim()) return setError('Pick a decky.'); if (!file) return setError('Pick a file.'); const pathErr = validatePath(path.trim()); if (pathErr) return setError(pathErr); const modeNum = parseInt(mode, 8); if (Number.isNaN(modeNum) || modeNum < 0 || modeNum > 0o7777) { return setError('mode must be a 3- or 4-digit octal (e.g. 644, 0755).'); } const offsetNum = parseInt(mtimeOffset, 10); if (Number.isNaN(offsetNum)) return setError('mtime offset must be an integer (seconds).'); setSubmitting(true); try { // FileReader → base64. We strip the data: prefix from the // result; the backend wants raw base64 only. const reader = new FileReader(); const b64: string = await new Promise((resolve, reject) => { reader.onerror = () => reject(reader.error); reader.onload = () => { const r = reader.result; if (typeof r !== 'string') return reject(new Error('FileReader did not return a string')); const comma = r.indexOf(','); resolve(comma >= 0 ? r.slice(comma + 1) : r); }; reader.readAsDataURL(file); }); const body: Record = { decky_name: decky.trim(), path: path.trim(), content_b64: b64, mode: modeNum, mtime_offset: offsetNum, }; if (scope === 'topology') body.topology_id = topologyId; await api.post('/deckies/files', body); const entry: FileDropEntry = { id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, decky_name: decky.trim(), topology_id: scope === 'topology' ? topologyId : null, path: path.trim(), size_bytes: file.size, filename: file.name, mode: modeNum, mtime_offset: offsetNum, dropped_at: new Date().toISOString(), }; onDropped(entry); } catch (err) { setError(extractError(err, 'File drop failed.')); } finally { setSubmitting(false); } }; return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000, }} >
DROP FILE ON DECKY
{(['fleet', 'topology'] as const).map((s) => ( ))}
{scope === 'topology' && ( {topologies.length === 0 ? (
No active topologies.
) : ( )}
)} {topoLoading ? (
loading…
) : activeDeckies.length === 0 ? (
{scope === 'topology' ? 'This topology has no deckies.' : 'No fleet deckies running.'}
) : ( )}
setPath(e.target.value)} placeholder="/root/payload.bin" style={{ ...INPUT_STYLE, fontFamily: 'monospace' }} />
setMode(e.target.value)} placeholder="644" style={{ ...INPUT_STYLE, fontFamily: 'monospace' }} />
setMtimeOffset(e.target.value)} placeholder="0" style={{ ...INPUT_STYLE, fontFamily: 'monospace', flex: 1 }} />
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { e.preventDefault(); setDragOver(false); const f = e.dataTransfer.files?.[0]; if (f) setFile(f); }} onClick={() => document.getElementById('canary-filedrop-input')?.click()} style={{ border: `2px dashed ${dragOver ? 'var(--accent-color, #00ff88)' : 'var(--border-color, #30363d)'}`, padding: '20px', textAlign: 'center', marginBottom: '16px', cursor: 'pointer', background: dragOver ? 'rgba(0, 255, 136, 0.05)' : 'transparent', }} >
{file ? `${file.name} (${fmtBytes(file.size)})` : 'Drop a file here or click to browse'}
setFile(e.target.files?.[0] || null)} />
File drops bypass canary instrumentation — bytes land verbatim. The list below is local only.
{error && (
{error}
)}
); }; // ─── MAIN PAGE ───────────────────────────────────────────────────────────── const CanaryTokens: React.FC = () => { const [tokens, setTokens] = useState([]); const [blobs, setBlobs] = useState([]); const [deckies, setDeckies] = useState([]); const [topologies, setTopologies] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [tab, setTab] = useState<'tokens' | 'blobs' | 'filedrops'>('tokens'); const [fileDrops, setFileDrops] = useState(() => loadFileDrops()); const [showFileDrop, setShowFileDrop] = useState(false); const [filter, setFilter] = useState(''); const [stateFilter, setStateFilter] = useState<'all' | 'planted' | 'revoked' | 'failed'>('all'); const [scopeFilter, setScopeFilter] = useState<'all' | 'fleet' | 'topology'>('all'); const [showCreate, setShowCreate] = useState(false); const [showUpload, setShowUpload] = useState(false); const [drawerToken, setDrawerToken] = useState(null); const loadAll = async () => { setLoading(true); setError(null); try { const [t, b, d, topos] = await Promise.all([ api.get('/canary/tokens'), api.get('/canary/blobs').catch(() => ({ data: { blobs: [] } })), // viewers can't list blobs api.get('/deckies').catch(() => ({ data: [] })), // Active topologies only — planting on a torn-down or pending // topology would 422/404 anyway. Endpoint shape: { data: [...] } // Trailing slash matters: FastAPI's slash-redirect issues a 307 // and the browser re-fires without the Authorization header, // landing as 401 on the redirected URL. Hit the canonical // path (/topologies/) directly. api.get('/topologies/?status=active').catch(() => ({ data: { data: [] } })), ]); setTokens(t.data.tokens || []); setBlobs(b.data.blobs || []); setDeckies(Array.isArray(d.data) ? d.data : []); const topoRows: Array<{ id: string; name: string; status: string }> = topos.data?.data ?? []; setTopologies(topoRows.map((r) => ({ id: r.id, name: r.name, status: r.status }))); } catch (err) { setError(extractError(err, 'Failed to load canary tokens.')); } finally { setLoading(false); } }; useEffect(() => { loadAll(); }, []); // Alt+C / Alt+D — open create-token / drop-file modals (per // feedback_linux_meta_key — never Meta/⌘ on Linux). useEffect(() => { const handler = (e: KeyboardEvent) => { const anyModalOpen = showCreate || showUpload || showFileDrop || drawerToken; if (anyModalOpen) return; if (e.altKey && e.key.toLowerCase() === 'c') { e.preventDefault(); setShowCreate(true); } else if (e.altKey && e.key.toLowerCase() === 'd') { e.preventDefault(); setShowFileDrop(true); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [showCreate, showUpload, showFileDrop, drawerToken]); const visibleTokens = useMemo(() => { return tokens.filter((t) => { if (stateFilter !== 'all' && t.state !== stateFilter) return false; if (scopeFilter === 'fleet' && t.topology_id) return false; if (scopeFilter === 'topology' && !t.topology_id) return false; if (!filter) return true; const f = filter.toLowerCase(); return ( t.decky_name.toLowerCase().includes(f) || t.placement_path.toLowerCase().includes(f) || t.callback_token.toLowerCase().includes(f) || (t.generator || '').toLowerCase().includes(f) || (t.instrumenter || '').toLowerCase().includes(f) || (t.topology_id || '').toLowerCase().includes(f) ); }); }, [tokens, filter, stateFilter, scopeFilter]); const counts = useMemo(() => { const c = { planted: 0, revoked: 0, failed: 0, hits: 0 }; for (const t of tokens) { c[t.state] += 1; c.hits += t.trigger_count; } return c; }, [tokens]); const handleDeleteBlob = async (uuid: string) => { if (!window.confirm('Delete this blob? Refused if any token still references it.')) return; try { await api.delete(`/canary/blobs/${encodeURIComponent(uuid)}`); setBlobs((prev) => prev.filter((b) => b.uuid !== uuid)); } catch (err) { alert(extractError(err, 'Delete failed.')); } }; return (

CANARY TOKENS

{tokens.length} TOKEN{tokens.length === 1 ? '' : 'S'} · {counts.planted} PLANTED · {counts.hits} TOTAL HIT{counts.hits === 1 ? '' : 'S'} · {blobs.length} UPLOADED BLOB{blobs.length === 1 ? '' : 'S'}
{(['tokens', 'blobs', 'filedrops'] as const).map((t) => ( ))}
{tab === 'tokens' && ( <>
setFilter(e.target.value)} placeholder="Filter by decky / path / slug / generator…" style={{ ...INPUT_STYLE, paddingLeft: '32px', marginBottom: 0 }} />
{loading &&
loading…
} {error &&
{error}
} {!loading && visibleTokens.length === 0 && (
{tokens.length === 0 ? 'No canary tokens yet. Click NEW TOKEN to plant one, or UPLOAD ARTIFACT to start with an operator-supplied document.' : 'No tokens match the current filter.'}
)}
{visibleTokens.map((t) => ( ))}
)} {tab === 'blobs' && ( <> {blobs.length === 0 && (
No uploaded artifacts. Click UPLOAD ARTIFACT to add one.
)}
{blobs.map((b) => (
{b.filename} {b.content_type} {fmtBytes(b.size_bytes)} {fmt(b.uploaded_at)}
))}
)} {tab === 'filedrops' && ( <>
Local log only — the server doesn't persist file drops. Cleared when you clear browser storage.
{fileDrops.length > 0 && ( )}
{fileDrops.length === 0 && (
No file drops in this browser yet. Click DROP FILE to send bytes to a decky.
)}
{fileDrops.map((fd) => (
{fd.topology_id ? 'topology' : 'fleet'} {fd.decky_name} {fd.path} {fmtBytes(fd.size_bytes)} {fd.mode.toString(8)} {fmt(fd.dropped_at)}
))}
)} {showCreate && ( setShowCreate(false)} onCreated={(t) => { setTokens((prev) => [t, ...prev]); setShowCreate(false); }} /> )} {showUpload && ( setShowUpload(false)} onUploaded={(b) => { setBlobs((prev) => prev.some((x) => x.uuid === b.uuid) ? prev : [b, ...prev]); setShowUpload(false); }} /> )} {drawerToken && ( setDrawerToken(null)} onRevoked={(uuid) => { setTokens((prev) => prev.map((t) => t.uuid === uuid ? { ...t, state: 'revoked' } : t, )); setDrawerToken(null); }} /> )} {showFileDrop && ( setShowFileDrop(false)} onDropped={(entry) => { setFileDrops((prev) => { const next = [entry, ...prev].slice(0, 200); saveFileDrops(next); return next; }); setShowFileDrop(false); setTab('filedrops'); }} /> )}
); }; export default CanaryTokens;