@i4ctime/q-ring 0.9.7 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/dashboard.ts","../src/core/dashboard-html.ts"],"sourcesContent":["/**\n * Quantum Status Dashboard: local HTTP server with SSE live updates.\n *\n * Collects a full snapshot of all quantum state every few seconds and\n * pushes it to connected browsers. Never exposes secret values.\n */\n\nimport { createServer, type IncomingMessage, type ServerResponse, type Server } from \"node:http\";\nimport { listSecrets } from \"./keyring.js\";\nimport { checkDecay, type DecayStatus, type QuantumEnvelope } from \"./envelope.js\";\nimport { listEntanglements, type EntanglementPair } from \"./entanglement.js\";\nimport { tunnelList } from \"./tunnel.js\";\nimport { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly } from \"./observer.js\";\nimport { collapseEnvironment, type CollapseResult } from \"./collapse.js\";\nimport { getDashboardHtml } from \"./dashboard-html.js\";\n\nexport interface SecretSnapshot {\n key: string;\n scope: string;\n type: \"collapsed\" | \"superposition\";\n environments?: string[];\n defaultEnv?: string;\n decay: DecayStatus;\n accessCount: number;\n lastAccessedAt?: string;\n createdAt: string;\n updatedAt: string;\n description?: string;\n tags?: string[];\n entangled?: { service: string; key: string }[];\n}\n\nexport interface TunnelSnapshot {\n id: string;\n createdAt: number;\n expiresAt?: number;\n accessCount: number;\n maxReads?: number;\n}\n\nexport interface DashboardSnapshot {\n timestamp: string;\n secrets: SecretSnapshot[];\n health: { healthy: number; stale: number; expired: number; noDecay: number; total: number };\n entanglements: EntanglementPair[];\n tunnels: TunnelSnapshot[];\n audit: AuditEvent[];\n anomalies: AccessAnomaly[];\n environment: CollapseResult | null;\n}\n\nfunction toSecretSnapshot(entry: {\n key: string;\n scope: string;\n envelope?: QuantumEnvelope;\n decay?: DecayStatus;\n}): SecretSnapshot {\n const envelope = entry.envelope;\n const decay = envelope ? checkDecay(envelope) : {\n isExpired: false,\n isStale: false,\n lifetimePercent: 0,\n secondsRemaining: null,\n timeRemaining: null,\n };\n\n return {\n key: entry.key,\n scope: entry.scope,\n type: envelope?.states ? \"superposition\" : \"collapsed\",\n environments: envelope?.states ? Object.keys(envelope.states) : undefined,\n defaultEnv: envelope?.defaultEnv,\n decay,\n accessCount: envelope?.meta.accessCount ?? 0,\n lastAccessedAt: envelope?.meta.lastAccessedAt,\n createdAt: envelope?.meta.createdAt ?? \"\",\n updatedAt: envelope?.meta.updatedAt ?? \"\",\n description: envelope?.meta.description,\n tags: envelope?.meta.tags,\n entangled: envelope?.meta.entangled,\n };\n}\n\nexport function collectSnapshot(): DashboardSnapshot {\n const entries = listSecrets({ source: \"api\", silent: true });\n\n const secrets = entries.map(toSecretSnapshot);\n\n let healthy = 0;\n let stale = 0;\n let expired = 0;\n let noDecay = 0;\n\n for (const s of secrets) {\n if (!s.decay.timeRemaining) {\n noDecay++;\n } else if (s.decay.isExpired) {\n expired++;\n } else if (s.decay.isStale) {\n stale++;\n } else {\n healthy++;\n }\n }\n\n return {\n timestamp: new Date().toISOString(),\n secrets,\n health: { healthy, stale, expired, noDecay, total: secrets.length },\n entanglements: listEntanglements(),\n tunnels: tunnelList(),\n audit: queryAudit({ limit: 50 }).filter(e => e.action !== \"list\"),\n anomalies: detectAnomalies(),\n environment: collapseEnvironment(),\n };\n}\n\nexport interface DashboardServerOptions {\n port?: number;\n}\n\nexport function startDashboardServer(\n options: DashboardServerOptions = {},\n): { port: number; close: () => void; server: Server } {\n const port = options.port ?? 9876;\n const clients = new Set<ServerResponse>();\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n\n const html = getDashboardHtml();\n\n function broadcast() {\n const snapshot = collectSnapshot();\n const data = `data: ${JSON.stringify(snapshot)}\\n\\n`;\n for (const res of clients) {\n if (res.writableEnded || res.destroyed) {\n clients.delete(res);\n continue;\n }\n try {\n const ok = res.write(data);\n if (!ok) {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n } catch {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n }\n }\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n let pathname: string;\n try {\n ({ pathname } = new URL(req.url ?? \"/\", \"http://127.0.0.1\"));\n } catch {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Bad Request: invalid URL\");\n return;\n }\n\n if (pathname === \"/events\") {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n\n const snapshot = collectSnapshot();\n res.write(`data: ${JSON.stringify(snapshot)}\\n\\n`);\n\n clients.add(res);\n req.on(\"close\", () => clients.delete(res));\n return;\n }\n\n if (pathname === \"/api/status\") {\n const snapshot = collectSnapshot();\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n });\n res.end(JSON.stringify(snapshot, null, 2));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(html);\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n intervalHandle = setInterval(broadcast, 5000);\n if (intervalHandle && typeof intervalHandle === \"object\" && \"unref\" in intervalHandle) {\n intervalHandle.unref();\n }\n });\n\n return {\n port,\n close: () => {\n if (intervalHandle) clearInterval(intervalHandle);\n for (const res of clients) {\n try { res.end(); } catch { /* noop */ }\n }\n clients.clear();\n server.close();\n },\n server,\n };\n}\n","/**\n * Self-contained HTML dashboard for q-ring quantum status.\n * Matches the gh-pages site design system: Outfit + JetBrains Mono fonts,\n * deep navy palette, neon cyan→violet SVG icons, glassmorphism cards.\n *\n * Zero dependencies — inline CSS + vanilla JS with EventSource for SSE.\n */\n\nexport function getDashboardHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\"/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n<title>q-ring — quantum status</title>\n<style>\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg-deep:#04080f;\n --bg-section:#080e18;\n --bg-card:#0f1a2e;\n --bg-card-hover:#142240;\n --border:#1a2d4a;\n --border-glow:#0ea5e9;\n\n --text-primary:#f8f8ff;\n --text-secondary:#c8cfe0;\n --text-dim:#8899b4;\n\n --accent:#0ea5e9;\n --accent-bright:#38bdf8;\n --accent-dim:rgba(14,165,233,0.15);\n --accent-glow:rgba(14,165,233,0.4);\n\n --danger:#ff5e5b;\n --warning:#fbbf24;\n --green:#22c55e;\n --violet:#a855f7;\n --pink:#ff0055;\n\n --font-display:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n --font-mono:'SF Mono','Cascadia Code','Fira Code',Consolas,'Liberation Mono',monospace;\n\n --radius:12px;\n --radius-sm:8px;\n --radius-lg:20px;\n\n --glass-bg:rgba(15,26,46,0.65);\n --glass-border:rgba(26,45,74,0.8);\n --shadow-card:0 4px 24px -1px rgba(0,0,0,0.4),0 0 1px rgba(255,255,255,0.04);\n --shadow-glow:0 0 20px rgba(14,165,233,0.12),0 0 40px rgba(168,85,247,0.08);\n}\nhtml{background:var(--bg-deep);color:var(--text-primary);font-family:var(--font-display);font-size:16px;line-height:1.5}\nbody{min-height:100vh;overflow-x:hidden;position:relative}\n\n/* Hidden SVG defs for gradient icons */\n.svg-defs{position:absolute;width:0;height:0;overflow:hidden}\n\n/* Mesh blobs */\n.blob{position:fixed;border-radius:50%;filter:blur(100px);opacity:.12;pointer-events:none;z-index:0}\n.blob-1{width:600px;height:600px;top:-120px;left:-100px;background:radial-gradient(circle,var(--accent),transparent 70%);animation:drift1 22s ease-in-out infinite}\n.blob-2{width:500px;height:500px;bottom:-80px;right:-60px;background:radial-gradient(circle,var(--violet),transparent 70%);animation:drift2 26s ease-in-out infinite}\n.blob-3{width:350px;height:350px;top:40%;left:50%;background:radial-gradient(circle,var(--green),transparent 70%);animation:drift3 30s ease-in-out infinite;opacity:.06}\n@keyframes drift1{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(60px,-40px) scale(1.08)}66%{transform:translate(-30px,50px) scale(.95)}}\n@keyframes drift2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,30px) scale(1.05)}66%{transform:translate(40px,-60px) scale(.92)}}\n@keyframes drift3{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-40%,-60%) scale(1.1)}}\n\n.container{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:24px 20px 48px}\n\n/* Header */\n.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;padding:16px 20px;background:rgba(4,8,15,0.75);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius)}\n.header h1{font-family:var(--font-display);font-size:1.65rem;font-weight:700;letter-spacing:-.02em;display:flex;align-items:center;gap:10px}\n.header h1 .q-icon{display:flex;filter:drop-shadow(0 0 6px rgba(14,165,233,0.6))}\n.header h1 .brand{background:linear-gradient(135deg,#00D1FF,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}\n.header h1 .sub{color:var(--text-dim);font-weight:400;font-size:1.1rem;-webkit-text-fill-color:var(--text-dim)}\n.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);display:inline-block;animation:pulse 2s ease-in-out infinite}\n.status-dot.disconnected{background:var(--danger);box-shadow:0 0 8px var(--danger)}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}\n.conn-label{font-size:.85rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)}\n\n/* Grid */\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px}\n.grid-wide{grid-column:1/-1}\n\n/* Cards — reveal animation */\n.card{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-card);transition:border-color .3s,box-shadow .3s,transform .3s;opacity:0;transform:translateY(16px);animation:cardReveal .5s cubic-bezier(0.16,1,0.3,1) forwards}\n.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow-card),var(--shadow-glow);transform:translateY(-2px)}\n@keyframes cardReveal{to{opacity:1;transform:translateY(0)}}\n\n.card-title{font-family:var(--font-display);font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;font-weight:600}\n.card-title svg{width:16px;height:16px;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(14,165,233,0.4))}\n\n/* Health donut */\n.health-row{display:flex;align-items:center;gap:24px}\n.donut-wrap{position:relative;width:100px;height:100px;flex-shrink:0}\n.donut-wrap svg{transform:rotate(-90deg)}\n.donut-wrap .donut-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:1.5rem;font-weight:700;line-height:1;font-family:var(--font-display)}\n.donut-wrap .donut-label small{font-size:.7rem;color:var(--text-dim);font-weight:400;margin-top:2px;letter-spacing:.04em}\n.health-legend{display:flex;flex-direction:column;gap:6px}\n.legend-item{display:flex;align-items:center;gap:8px;font-size:.88rem}\n.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n\n/* Decay bars */\n.decay-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.decay-item{display:flex;align-items:center;gap:10px}\n.decay-key{font-family:var(--font-mono);font-size:.85rem;min-width:120px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.decay-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,0.06);overflow:hidden;position:relative}\n.decay-fill{height:100%;border-radius:3px;transition:width .6s ease}\n.decay-time{font-size:.8rem;color:var(--text-dim);min-width:56px;text-align:right;font-family:var(--font-mono)}\n\n/* Superposition pills */\n.super-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.super-item{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.super-key{font-family:var(--font-mono);font-size:.85rem;min-width:100px}\n.env-pill{font-size:.75rem;padding:2px 8px;border-radius:99px;font-weight:600;letter-spacing:.03em;font-family:var(--font-mono)}\n.env-prod{background:rgba(255,0,85,0.2);color:var(--pink);border:1px solid rgba(255,0,85,0.3)}\n.env-staging{background:rgba(251,191,36,0.15);color:var(--warning);border:1px solid rgba(251,191,36,0.25)}\n.env-dev{background:rgba(34,197,94,0.15);color:var(--green);border:1px solid rgba(34,197,94,0.25)}\n.env-default{background:rgba(168,85,247,0.15);color:var(--violet);border:1px solid rgba(168,85,247,0.25)}\n\n/* Entanglement */\n.entangle-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto}\n.entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.85rem}\n.entangle-arrow{color:var(--accent-bright)}\n\n/* Tunnels */\n.tunnel-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto}\n.tunnel-card{background:rgba(168,85,247,0.06);border:1px solid rgba(168,85,247,0.15);border-radius:var(--radius-sm);padding:10px 12px;font-size:.85rem;font-family:var(--font-mono)}\n.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.8rem;color:var(--text-dim)}\n\n/* Audit feed */\n.audit-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;font-size:.85rem;font-family:var(--font-mono)}\n.audit-row{display:flex;gap:8px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)}\n.audit-ts{color:var(--text-dim);min-width:70px;flex-shrink:0}\n.audit-action{min-width:64px;font-weight:600}\n.audit-action.read{color:var(--accent)}.audit-action.write{color:var(--green)}.audit-action.delete{color:var(--danger)}\n.audit-action.entangle{color:var(--violet)}.audit-action.tunnel{color:var(--violet)}.audit-action.teleport{color:var(--warning)}\n.audit-action.generate{color:var(--warning)}.audit-action.list{color:var(--text-dim)}.audit-action.export{color:var(--text-dim)}\n.audit-action.collapse{color:var(--accent)}\n\n/* Anomalies */\n.anomaly-card{background:rgba(255,94,91,0.06);border:1px solid rgba(255,94,91,0.15);border-radius:var(--radius-sm);padding:10px 14px;animation:anomaly-pulse 3s ease-in-out infinite}\n@keyframes anomaly-pulse{0%,100%{border-color:rgba(255,94,91,0.15)}50%{border-color:rgba(255,94,91,0.4)}}\n.anomaly-type{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--danger);font-weight:700;margin-bottom:2px;font-family:var(--font-display)}\n.anomaly-desc{font-size:.88rem;color:var(--text-primary)}\n\n/* Environment badge */\n.env-status{display:flex;align-items:center;gap:12px}\n.env-big{font-size:1.1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)}\n.env-source{font-size:.85rem;color:var(--text-dim)}\n\n/* Empty states */\n.empty{color:var(--text-dim);font-size:.88rem;font-style:italic;padding:8px 0}\n\n/* Scrollbar */\n::-webkit-scrollbar{width:4px;height:4px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:2px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.15)}\n</style>\n</head>\n<body>\n\n<!-- Shared SVG gradient definition -->\n<svg class=\"svg-defs\" aria-hidden=\"true\" focusable=\"false\">\n <defs>\n <linearGradient id=\"neon-grad\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stop-color=\"#00D1FF\"/>\n <stop offset=\"100%\" stop-color=\"#a855f7\"/>\n </linearGradient>\n </defs>\n</svg>\n\n<div class=\"blob blob-1\"></div>\n<div class=\"blob blob-2\"></div>\n<div class=\"blob blob-3\"></div>\n<div class=\"container\">\n <div class=\"header\">\n <h1>\n <span class=\"q-icon\"><svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg></span>\n <span class=\"brand\">q-ring</span>\n <span class=\"sub\">quantum status</span>\n </h1>\n <div class=\"conn-label\"><span class=\"status-dot\" id=\"connDot\"></span><span id=\"connText\">connecting\\u2026</span></div>\n </div>\n <div class=\"grid\" id=\"dashboard\">\n <div class=\"card\"><div class=\"empty\">Connecting to q-ring\\u2026</div></div>\n </div>\n</div>\n\n<script>\n(function(){\n const $ = (id) => document.getElementById(id);\n const dash = $('dashboard');\n const dot = $('connDot');\n const connText = $('connText');\n\n /* --- SVG icon library (matching gh-pages neon gradient) --- */\n const icons = {\n health: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/></svg>',\n environment: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>',\n decay: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/></svg>',\n superposition: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 2 7 12 12 22 7 12 2\"/><polyline points=\"2 17 12 22 22 17\"/><polyline points=\"2 12 12 17 22 12\"/></svg>',\n entangle: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>',\n tunnel: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 10h.01\"/><path d=\"M15 10h.01\"/><path d=\"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z\"/></svg>',\n anomaly: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>',\n audit: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg>'\n };\n\n function esc(s){ const d=document.createElement('div');d.textContent=s;return d.innerHTML; }\n\n function envClass(e){\n if(e==='prod'||e==='production') return 'env-prod';\n if(e==='staging'||e==='stage') return 'env-staging';\n if(e==='dev'||e==='development') return 'env-dev';\n return 'env-default';\n }\n\n function decayColor(pct,expired){\n if(expired) return 'var(--danger)';\n if(pct>=90) return 'var(--danger)';\n if(pct>=75) return 'var(--warning)';\n return 'var(--accent)';\n }\n\n function fmtTime(ts){\n const d=new Date(ts);\n return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');\n }\n\n function renderHealth(h){\n const total=h.total||1;\n const r=42, circ=2*Math.PI*r;\n const slices=[\n {v:h.healthy,c:'var(--accent)'},\n {v:h.stale,c:'var(--warning)'},\n {v:h.expired,c:'var(--danger)'},\n {v:h.noDecay,c:'var(--text-dim)'}\n ];\n let offset=0;\n let rings='';\n for(const sl of slices){\n const pct=sl.v/total;\n const len=pct*circ;\n rings+=\\`<circle cx=\"50\" cy=\"50\" r=\"\\${r}\" fill=\"none\" stroke=\"\\${sl.c}\" stroke-width=\"12\" stroke-dasharray=\"\\${len} \\${circ-len}\" stroke-dashoffset=\"-\\${offset}\" stroke-linecap=\"round\" opacity=\"0.85\"/>\\`;\n offset+=len;\n }\n return \\`<div class=\"health-row\">\n <div class=\"donut-wrap\">\n <svg viewBox=\"0 0 100 100\" width=\"100\" height=\"100\">\\${rings}</svg>\n <div class=\"donut-label\">\\${h.total}<small>secrets</small></div>\n </div>\n <div class=\"health-legend\">\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--accent)\"></span>Healthy \\${h.healthy}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--warning)\"></span>Stale \\${h.stale}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--danger)\"></span>Expired \\${h.expired}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--text-dim)\"></span>No decay \\${h.noDecay}</div>\n </div>\n </div>\\`;\n }\n\n function renderDecay(secrets){\n const withDecay=secrets.filter(s=>s.decay&&s.decay.timeRemaining);\n if(!withDecay.length) return '<div class=\"empty\">No secrets with decay configured</div>';\n return '<div class=\"decay-list\">'+withDecay.map(s=>{\n const pct=Math.min(s.decay.lifetimePercent,100);\n const col=decayColor(pct,s.decay.isExpired);\n const label=s.decay.isExpired?'expired':s.decay.timeRemaining||'';\n return \\`<div class=\"decay-item\">\n <span class=\"decay-key\" title=\"\\${esc(s.key)}\">\\${esc(s.key)}</span>\n <div class=\"decay-bar\"><div class=\"decay-fill\" style=\"width:\\${pct}%;background:\\${col}\"></div></div>\n <span class=\"decay-time\" style=\"color:\\${col}\">\\${esc(label)}</span>\n </div>\\`;\n }).join('')+'</div>';\n }\n\n function renderSuperposition(secrets){\n const sup=secrets.filter(s=>s.type==='superposition'&&s.environments);\n if(!sup.length) return '<div class=\"empty\">No secrets in superposition</div>';\n return '<div class=\"super-list\">'+sup.map(s=>{\n const pills=(s.environments||[]).map(e=>{\n const isDefault=e===s.defaultEnv;\n return \\`<span class=\"env-pill \\${envClass(e)}\">\\${esc(e)}\\${isDefault?' \\u2713':''}</span>\\`;\n }).join('');\n return \\`<div class=\"super-item\"><span class=\"super-key\">\\${esc(s.key)}</span>\\${pills}</div>\\`;\n }).join('')+'</div>';\n }\n\n function renderEntanglements(pairs){\n if(!pairs.length) return '<div class=\"empty\">No entangled secrets</div>';\n const seen=new Set();\n const unique=pairs.filter(p=>{\n const id=[p.source.service,p.source.key,p.target.service,p.target.key].sort().join('|');\n if(seen.has(id)) return false;\n seen.add(id);return true;\n });\n return '<div class=\"entangle-list\">'+unique.map(p=>\n \\`<div class=\"entangle-pair\"><span>\\${esc(p.source.key)}</span><span class=\"entangle-arrow\">\\u2194</span><span>\\${esc(p.target.key)}</span></div>\\`\n ).join('')+'</div>';\n }\n\n function renderTunnels(tunnels){\n if(!tunnels.length) return '<div class=\"empty\">No active tunnels</div>';\n return '<div class=\"tunnel-list\">'+tunnels.map(t=>{\n const rem=t.expiresAt?Math.max(0,Math.floor((t.expiresAt-Date.now())/1000)):null;\n return \\`<div class=\"tunnel-card\">\\${esc(t.id)}<div class=\"tunnel-meta\">\n <span>reads: \\${t.accessCount}\\${t.maxReads?'/'+t.maxReads:''}</span>\n \\${rem!==null?'<span>expires: '+rem+'s</span>':'<span>no expiry</span>'}\n </div></div>\\`;\n }).join('')+'</div>';\n }\n\n function renderAudit(events){\n if(!events.length) return '<div class=\"empty\">No audit events</div>';\n return '<div class=\"audit-feed\">'+events.slice(0,40).map(e=>\n \\`<div class=\"audit-row\">\n <span class=\"audit-ts\">\\${fmtTime(e.timestamp)}</span>\n <span class=\"audit-action \\${e.action}\">\\${e.action}</span>\n <span class=\"audit-key\">\\${e.key?esc(e.key):''}</span>\n <span class=\"audit-detail\">\\${e.detail?esc(e.detail):''}</span>\n </div>\\`\n ).join('')+'</div>';\n }\n\n function renderAnomalies(anomalies){\n if(!anomalies.length) return '<div class=\"empty\">No anomalies detected \\u2014 all clear</div>';\n return anomalies.map(a=>\n \\`<div class=\"anomaly-card\"><div class=\"anomaly-type\">\\${esc(a.type)}</div><div class=\"anomaly-desc\">\\${esc(a.description)}</div></div>\\`\n ).join('');\n }\n\n function renderEnvironment(env){\n if(!env) return '<div class=\"empty\">No environment detected</div>';\n return \\`<div class=\"env-status\">\n <span class=\"env-big env-pill \\${envClass(env.env)}\">\\${esc(env.env)}</span>\n <span class=\"env-source\">detected via \\${esc(env.source)}</span>\n </div>\\`;\n }\n\n const panels = [\n { id:'p-health', icon:icons.health, label:'Health Summary', wide:false, render:s=>renderHealth(s.health) },\n { id:'p-env', icon:icons.environment, label:'Environment', wide:false, render:s=>renderEnvironment(s.environment) },\n { id:'p-decay', icon:icons.decay, label:'Decay Timers', wide:false, render:s=>renderDecay(s.secrets) },\n { id:'p-super', icon:icons.superposition, label:'Superposition States', wide:false, render:s=>renderSuperposition(s.secrets) },\n { id:'p-ent', icon:icons.entangle, label:'Entanglement', wide:false, render:s=>renderEntanglements(s.entanglements) },\n { id:'p-tunnel', icon:icons.tunnel, label:'Quantum Tunnels', wide:false, render:s=>renderTunnels(s.tunnels) },\n { id:'p-anomaly', icon:icons.anomaly, label:'Anomaly Alerts', wide:true, render:s=>renderAnomalies(s.anomalies) },\n { id:'p-audit', icon:icons.audit, label:'Audit Log', wide:true, render:s=>renderAudit(s.audit) }\n ];\n\n let initialised = false;\n\n function render(snap){\n if(!initialised){\n initialised = true;\n dash.innerHTML = panels.map((p,i) =>\n \\`<div class=\"card\\${p.wide?' grid-wide':''}\" id=\"\\${p.id}\" style=\"animation-delay:\\${i*60}ms\">\n <div class=\"card-title\">\\${p.icon} \\${p.label}</div>\n <div class=\"card-body\" id=\"\\${p.id}-body\"></div>\n </div>\\`\n ).join('');\n }\n for(const p of panels){\n const body = $(\\`\\${p.id}-body\\`);\n if(body){\n const html = p.render(snap);\n if(body._prev !== html){ body.innerHTML = html; body._prev = html; }\n }\n }\n }\n\n function connect(){\n const es=new EventSource('/events');\n es.onopen=()=>{\n dot.classList.remove('disconnected');\n connText.textContent='live';\n };\n es.onmessage=(e)=>{\n try{ render(JSON.parse(e.data)); }catch(err){ console.error('render error',err); }\n };\n es.onerror=()=>{\n dot.classList.add('disconnected');\n connText.textContent='reconnecting\\u2026';\n };\n }\n\n connect();\n})();\n</script>\n</body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;AAOA,SAAS,oBAA4E;;;ACC9E,SAAS,mBAA2B;AACzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":[]}
1
+ {"version":3,"sources":["../src/core/dashboard.ts","../src/core/dashboard-html.ts"],"sourcesContent":["/**\n * Quantum Status Dashboard: local HTTP server with SSE live updates.\n *\n * Collects a full snapshot of all quantum state every few seconds and\n * pushes it to connected browsers. Never exposes secret values.\n */\n\nimport { createServer, type IncomingMessage, type ServerResponse, type Server } from \"node:http\";\nimport { listSecrets } from \"./keyring.js\";\nimport { checkDecay, type DecayStatus, type QuantumEnvelope } from \"./envelope.js\";\nimport { listEntanglements, type EntanglementPair } from \"./entanglement.js\";\nimport { tunnelList } from \"./tunnel.js\";\nimport { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly } from \"./observer.js\";\nimport { collapseEnvironment, type CollapseResult } from \"./collapse.js\";\nimport { getDashboardHtml } from \"./dashboard-html.js\";\n\nexport interface SecretSnapshot {\n key: string;\n scope: string;\n type: \"collapsed\" | \"superposition\";\n environments?: string[];\n defaultEnv?: string;\n decay: DecayStatus;\n accessCount: number;\n lastAccessedAt?: string;\n createdAt: string;\n updatedAt: string;\n description?: string;\n tags?: string[];\n entangled?: { service: string; key: string }[];\n}\n\nexport interface TunnelSnapshot {\n id: string;\n createdAt: number;\n expiresAt?: number;\n accessCount: number;\n maxReads?: number;\n}\n\nexport interface DashboardSnapshot {\n timestamp: string;\n secrets: SecretSnapshot[];\n health: { healthy: number; stale: number; expired: number; noDecay: number; total: number };\n entanglements: EntanglementPair[];\n tunnels: TunnelSnapshot[];\n audit: AuditEvent[];\n anomalies: AccessAnomaly[];\n environment: CollapseResult | null;\n}\n\nfunction toSecretSnapshot(entry: {\n key: string;\n scope: string;\n envelope?: QuantumEnvelope;\n decay?: DecayStatus;\n}): SecretSnapshot {\n const envelope = entry.envelope;\n const decay = envelope ? checkDecay(envelope) : {\n isExpired: false,\n isStale: false,\n lifetimePercent: 0,\n secondsRemaining: null,\n timeRemaining: null,\n };\n\n return {\n key: entry.key,\n scope: entry.scope,\n type: envelope?.states ? \"superposition\" : \"collapsed\",\n environments: envelope?.states ? Object.keys(envelope.states) : undefined,\n defaultEnv: envelope?.defaultEnv,\n decay,\n accessCount: envelope?.meta.accessCount ?? 0,\n lastAccessedAt: envelope?.meta.lastAccessedAt,\n createdAt: envelope?.meta.createdAt ?? \"\",\n updatedAt: envelope?.meta.updatedAt ?? \"\",\n description: envelope?.meta.description,\n tags: envelope?.meta.tags,\n entangled: envelope?.meta.entangled,\n };\n}\n\nexport function collectSnapshot(): DashboardSnapshot {\n const entries = listSecrets({ source: \"api\", silent: true });\n\n const secrets = entries.map(toSecretSnapshot);\n\n let healthy = 0;\n let stale = 0;\n let expired = 0;\n let noDecay = 0;\n\n for (const s of secrets) {\n if (!s.decay.timeRemaining) {\n noDecay++;\n } else if (s.decay.isExpired) {\n expired++;\n } else if (s.decay.isStale) {\n stale++;\n } else {\n healthy++;\n }\n }\n\n return {\n timestamp: new Date().toISOString(),\n secrets,\n health: { healthy, stale, expired, noDecay, total: secrets.length },\n entanglements: listEntanglements(),\n tunnels: tunnelList(),\n audit: queryAudit({ limit: 50 }).filter(e => e.action !== \"list\"),\n anomalies: detectAnomalies(),\n environment: collapseEnvironment(),\n };\n}\n\nexport interface DashboardServerOptions {\n port?: number;\n}\n\nexport function startDashboardServer(\n options: DashboardServerOptions = {},\n): { port: number; close: () => void; server: Server } {\n const port = options.port ?? 9876;\n const clients = new Set<ServerResponse>();\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n\n const html = getDashboardHtml();\n\n function broadcast() {\n const snapshot = collectSnapshot();\n const data = `data: ${JSON.stringify(snapshot)}\\n\\n`;\n for (const res of clients) {\n if (res.writableEnded || res.destroyed) {\n clients.delete(res);\n continue;\n }\n try {\n const ok = res.write(data);\n if (!ok) {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n } catch {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n }\n }\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n let pathname: string;\n try {\n ({ pathname } = new URL(req.url ?? \"/\", \"http://127.0.0.1\"));\n } catch {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Bad Request: invalid URL\");\n return;\n }\n\n if (pathname === \"/events\") {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n\n const snapshot = collectSnapshot();\n res.write(`data: ${JSON.stringify(snapshot)}\\n\\n`);\n\n clients.add(res);\n req.on(\"close\", () => clients.delete(res));\n return;\n }\n\n if (pathname === \"/api/status\") {\n const snapshot = collectSnapshot();\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n });\n res.end(JSON.stringify(snapshot, null, 2));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(html);\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n intervalHandle = setInterval(broadcast, 5000);\n if (intervalHandle && typeof intervalHandle === \"object\" && \"unref\" in intervalHandle) {\n intervalHandle.unref();\n }\n });\n\n return {\n port,\n close: () => {\n if (intervalHandle) clearInterval(intervalHandle);\n for (const res of clients) {\n try { res.end(); } catch { /* noop */ }\n }\n clients.clear();\n server.close();\n },\n server,\n };\n}\n","/**\n * Self-contained HTML dashboard for q-ring quantum status.\n * Matches the gh-pages site design system: Outfit + JetBrains Mono fonts,\n * deep navy palette, neon cyan→violet SVG icons, glassmorphism cards.\n *\n * Zero dependencies — inline CSS + vanilla JS with EventSource for SSE.\n */\n\nexport function getDashboardHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\"/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n<title>q-ring — quantum status</title>\n<style>\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg-deep:#04080f;\n --bg-section:#080e18;\n --bg-card:#0f1a2e;\n --bg-card-hover:#142240;\n --border:#1a2d4a;\n --border-glow:#0ea5e9;\n\n --text-primary:#f8f8ff;\n --text-secondary:#c8cfe0;\n --text-dim:#8899b4;\n\n --accent:#0ea5e9;\n --accent-bright:#38bdf8;\n --accent-dim:rgba(14,165,233,0.15);\n --accent-glow:rgba(14,165,233,0.4);\n\n --danger:#ff5e5b;\n --warning:#fbbf24;\n --green:#22c55e;\n --violet:#a855f7;\n --pink:#ff0055;\n\n --font-display:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n --font-mono:'SF Mono','Cascadia Code','Fira Code',Consolas,'Liberation Mono',monospace;\n\n --radius:12px;\n --radius-sm:8px;\n --radius-lg:20px;\n\n --glass-bg:rgba(15,26,46,0.65);\n --glass-border:rgba(26,45,74,0.8);\n --shadow-card:0 4px 24px -1px rgba(0,0,0,0.4),0 0 1px rgba(255,255,255,0.04);\n --shadow-glow:0 0 20px rgba(14,165,233,0.12),0 0 40px rgba(168,85,247,0.08);\n}\nhtml{background:var(--bg-deep);color:var(--text-primary);font-family:var(--font-display);font-size:16px;line-height:1.5}\nbody{min-height:100vh;overflow-x:hidden;position:relative}\n\n/* Hidden SVG defs for gradient icons */\n.svg-defs{position:absolute;width:0;height:0;overflow:hidden}\n\n/* Mesh blobs */\n.blob{position:fixed;border-radius:50%;filter:blur(100px);opacity:.12;pointer-events:none;z-index:0}\n.blob-1{width:600px;height:600px;top:-120px;left:-100px;background:radial-gradient(circle,var(--accent),transparent 70%);animation:drift1 22s ease-in-out infinite}\n.blob-2{width:500px;height:500px;bottom:-80px;right:-60px;background:radial-gradient(circle,var(--violet),transparent 70%);animation:drift2 26s ease-in-out infinite}\n.blob-3{width:350px;height:350px;top:40%;left:50%;background:radial-gradient(circle,var(--green),transparent 70%);animation:drift3 30s ease-in-out infinite;opacity:.06}\n@keyframes drift1{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(60px,-40px) scale(1.08)}66%{transform:translate(-30px,50px) scale(.95)}}\n@keyframes drift2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,30px) scale(1.05)}66%{transform:translate(40px,-60px) scale(.92)}}\n@keyframes drift3{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-40%,-60%) scale(1.1)}}\n\n.container{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:24px 20px 48px}\n\n/* Header */\n.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;padding:16px 20px;background:rgba(4,8,15,0.75);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius)}\n.header h1{font-family:var(--font-display);font-size:1.65rem;font-weight:700;letter-spacing:-.02em;display:flex;align-items:center;gap:10px}\n.header h1 .q-icon{display:flex;filter:drop-shadow(0 0 6px rgba(14,165,233,0.6))}\n.header h1 .brand{background:linear-gradient(135deg,#00D1FF,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}\n.header h1 .sub{color:var(--text-dim);font-weight:400;font-size:1.1rem;-webkit-text-fill-color:var(--text-dim)}\n.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);display:inline-block;animation:pulse 2s ease-in-out infinite}\n.status-dot.disconnected{background:var(--danger);box-shadow:0 0 8px var(--danger)}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}\n.conn-label{font-size:.85rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)}\n\n/* Grid */\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px}\n.grid-wide{grid-column:1/-1}\n\n/* Cards — reveal animation */\n.card{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-card);transition:border-color .3s,box-shadow .3s,transform .3s;opacity:0;transform:translateY(16px);animation:cardReveal .5s cubic-bezier(0.16,1,0.3,1) forwards}\n.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow-card),var(--shadow-glow);transform:translateY(-2px)}\n@keyframes cardReveal{to{opacity:1;transform:translateY(0)}}\n\n.card-title{font-family:var(--font-display);font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;font-weight:600}\n.card-title svg{width:16px;height:16px;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(14,165,233,0.4))}\n\n/* Health donut */\n.health-row{display:flex;align-items:center;gap:24px}\n.donut-wrap{position:relative;width:100px;height:100px;flex-shrink:0}\n.donut-wrap svg{transform:rotate(-90deg)}\n.donut-wrap .donut-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:1.5rem;font-weight:700;line-height:1;font-family:var(--font-display)}\n.donut-wrap .donut-label small{font-size:.7rem;color:var(--text-dim);font-weight:400;margin-top:2px;letter-spacing:.04em}\n.health-legend{display:flex;flex-direction:column;gap:6px}\n.legend-item{display:flex;align-items:center;gap:8px;font-size:.88rem}\n.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n\n/* Decay bars */\n.decay-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.decay-item{display:flex;align-items:center;gap:10px}\n.decay-key{font-family:var(--font-mono);font-size:.85rem;min-width:120px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.decay-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,0.06);overflow:hidden;position:relative}\n.decay-fill{height:100%;border-radius:3px;transition:width .6s ease}\n.decay-time{font-size:.8rem;color:var(--text-dim);min-width:56px;text-align:right;font-family:var(--font-mono)}\n\n/* Superposition pills */\n.super-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.super-item{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.super-key{font-family:var(--font-mono);font-size:.85rem;min-width:100px}\n.env-pill{font-size:.75rem;padding:2px 8px;border-radius:99px;font-weight:600;letter-spacing:.03em;font-family:var(--font-mono)}\n.env-prod{background:rgba(255,0,85,0.2);color:var(--pink);border:1px solid rgba(255,0,85,0.3)}\n.env-staging{background:rgba(251,191,36,0.15);color:var(--warning);border:1px solid rgba(251,191,36,0.25)}\n.env-dev{background:rgba(34,197,94,0.15);color:var(--green);border:1px solid rgba(34,197,94,0.25)}\n.env-default{background:rgba(168,85,247,0.15);color:var(--violet);border:1px solid rgba(168,85,247,0.25)}\n\n/* Entanglement */\n.entangle-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto}\n.entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.85rem}\n.entangle-arrow{color:var(--accent-bright)}\n\n/* Tunnels */\n.tunnel-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto}\n.tunnel-card{background:rgba(168,85,247,0.06);border:1px solid rgba(168,85,247,0.15);border-radius:var(--radius-sm);padding:10px 12px;font-size:.85rem;font-family:var(--font-mono)}\n.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.8rem;color:var(--text-dim)}\n\n/* Audit feed */\n.audit-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;font-size:.85rem;font-family:var(--font-mono)}\n.audit-row{display:flex;gap:8px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)}\n.audit-ts{color:var(--text-dim);min-width:70px;flex-shrink:0}\n.audit-action{min-width:64px;font-weight:600}\n.audit-action.read{color:var(--accent)}.audit-action.write{color:var(--green)}.audit-action.delete{color:var(--danger)}\n.audit-action.entangle{color:var(--violet)}.audit-action.tunnel{color:var(--violet)}.audit-action.teleport{color:var(--warning)}\n.audit-action.generate{color:var(--warning)}.audit-action.list{color:var(--text-dim)}.audit-action.export{color:var(--text-dim)}\n.audit-action.collapse{color:var(--accent)}\n\n/* Anomalies */\n.anomaly-card{background:rgba(255,94,91,0.06);border:1px solid rgba(255,94,91,0.15);border-radius:var(--radius-sm);padding:10px 14px;animation:anomaly-pulse 3s ease-in-out infinite}\n@keyframes anomaly-pulse{0%,100%{border-color:rgba(255,94,91,0.15)}50%{border-color:rgba(255,94,91,0.4)}}\n.anomaly-type{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--danger);font-weight:700;margin-bottom:2px;font-family:var(--font-display)}\n.anomaly-desc{font-size:.88rem;color:var(--text-primary)}\n\n/* Environment badge */\n.env-status{display:flex;align-items:center;gap:12px}\n.env-big{font-size:1.1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)}\n.env-source{font-size:.85rem;color:var(--text-dim)}\n\n/* Empty states */\n.empty{color:var(--text-dim);font-size:.88rem;font-style:italic;padding:8px 0}\n\n/* Scrollbar */\n::-webkit-scrollbar{width:4px;height:4px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:2px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.15)}\n</style>\n</head>\n<body>\n\n<!-- Shared SVG gradient definition -->\n<svg class=\"svg-defs\" aria-hidden=\"true\" focusable=\"false\">\n <defs>\n <linearGradient id=\"neon-grad\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stop-color=\"#00D1FF\"/>\n <stop offset=\"100%\" stop-color=\"#a855f7\"/>\n </linearGradient>\n </defs>\n</svg>\n\n<div class=\"blob blob-1\"></div>\n<div class=\"blob blob-2\"></div>\n<div class=\"blob blob-3\"></div>\n<div class=\"container\">\n <div class=\"header\">\n <h1>\n <span class=\"q-icon\"><svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg></span>\n <span class=\"brand\">q-ring</span>\n <span class=\"sub\">quantum status</span>\n </h1>\n <div class=\"conn-label\"><span class=\"status-dot\" id=\"connDot\"></span><span id=\"connText\">connecting\\u2026</span></div>\n </div>\n <div class=\"grid\" id=\"dashboard\">\n <div class=\"card\"><div class=\"empty\">Connecting to q-ring\\u2026</div></div>\n </div>\n</div>\n\n<script>\n(function(){\n const $ = (id) => document.getElementById(id);\n const dash = $('dashboard');\n const dot = $('connDot');\n const connText = $('connText');\n\n /* --- SVG icon library (matching gh-pages neon gradient) --- */\n const icons = {\n health: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/></svg>',\n environment: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>',\n decay: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/></svg>',\n superposition: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 2 7 12 12 22 7 12 2\"/><polyline points=\"2 17 12 22 22 17\"/><polyline points=\"2 12 12 17 22 12\"/></svg>',\n entangle: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>',\n tunnel: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 10h.01\"/><path d=\"M15 10h.01\"/><path d=\"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z\"/></svg>',\n anomaly: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>',\n audit: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg>'\n };\n\n function esc(s){ const d=document.createElement('div');d.textContent=s;return d.innerHTML; }\n\n function envClass(e){\n if(e==='prod'||e==='production') return 'env-prod';\n if(e==='staging'||e==='stage') return 'env-staging';\n if(e==='dev'||e==='development') return 'env-dev';\n return 'env-default';\n }\n\n function decayColor(pct,expired){\n if(expired) return 'var(--danger)';\n if(pct>=90) return 'var(--danger)';\n if(pct>=75) return 'var(--warning)';\n return 'var(--accent)';\n }\n\n function fmtTime(ts){\n const d=new Date(ts);\n return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');\n }\n\n function renderHealth(h){\n const total=h.total||1;\n const r=42, circ=2*Math.PI*r;\n const slices=[\n {v:h.healthy,c:'var(--accent)'},\n {v:h.stale,c:'var(--warning)'},\n {v:h.expired,c:'var(--danger)'},\n {v:h.noDecay,c:'var(--text-dim)'}\n ];\n let offset=0;\n let rings='';\n for(const sl of slices){\n const pct=sl.v/total;\n const len=pct*circ;\n rings+=\\`<circle cx=\"50\" cy=\"50\" r=\"\\${r}\" fill=\"none\" stroke=\"\\${sl.c}\" stroke-width=\"12\" stroke-dasharray=\"\\${len} \\${circ-len}\" stroke-dashoffset=\"-\\${offset}\" stroke-linecap=\"round\" opacity=\"0.85\"/>\\`;\n offset+=len;\n }\n return \\`<div class=\"health-row\">\n <div class=\"donut-wrap\">\n <svg viewBox=\"0 0 100 100\" width=\"100\" height=\"100\">\\${rings}</svg>\n <div class=\"donut-label\">\\${h.total}<small>secrets</small></div>\n </div>\n <div class=\"health-legend\">\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--accent)\"></span>Healthy \\${h.healthy}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--warning)\"></span>Stale \\${h.stale}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--danger)\"></span>Expired \\${h.expired}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--text-dim)\"></span>No decay \\${h.noDecay}</div>\n </div>\n </div>\\`;\n }\n\n function renderDecay(secrets){\n const withDecay=secrets.filter(s=>s.decay&&s.decay.timeRemaining);\n if(!withDecay.length) return '<div class=\"empty\">No secrets with decay configured</div>';\n return '<div class=\"decay-list\">'+withDecay.map(s=>{\n const pct=Math.min(s.decay.lifetimePercent,100);\n const col=decayColor(pct,s.decay.isExpired);\n const label=s.decay.isExpired?'expired':s.decay.timeRemaining||'';\n return \\`<div class=\"decay-item\">\n <span class=\"decay-key\" title=\"\\${esc(s.key)}\">\\${esc(s.key)}</span>\n <div class=\"decay-bar\"><div class=\"decay-fill\" style=\"width:\\${pct}%;background:\\${col}\"></div></div>\n <span class=\"decay-time\" style=\"color:\\${col}\">\\${esc(label)}</span>\n </div>\\`;\n }).join('')+'</div>';\n }\n\n function renderSuperposition(secrets){\n const sup=secrets.filter(s=>s.type==='superposition'&&s.environments);\n if(!sup.length) return '<div class=\"empty\">No secrets in superposition</div>';\n return '<div class=\"super-list\">'+sup.map(s=>{\n const pills=(s.environments||[]).map(e=>{\n const isDefault=e===s.defaultEnv;\n return \\`<span class=\"env-pill \\${envClass(e)}\">\\${esc(e)}\\${isDefault?' \\u2713':''}</span>\\`;\n }).join('');\n return \\`<div class=\"super-item\"><span class=\"super-key\">\\${esc(s.key)}</span>\\${pills}</div>\\`;\n }).join('')+'</div>';\n }\n\n function renderEntanglements(pairs){\n if(!pairs.length) return '<div class=\"empty\">No entangled secrets</div>';\n const seen=new Set();\n const unique=pairs.filter(p=>{\n const id=[p.source.service,p.source.key,p.target.service,p.target.key].sort().join('|');\n if(seen.has(id)) return false;\n seen.add(id);return true;\n });\n return '<div class=\"entangle-list\">'+unique.map(p=>\n \\`<div class=\"entangle-pair\"><span>\\${esc(p.source.key)}</span><span class=\"entangle-arrow\">\\u2194</span><span>\\${esc(p.target.key)}</span></div>\\`\n ).join('')+'</div>';\n }\n\n function renderTunnels(tunnels){\n if(!tunnels.length) return '<div class=\"empty\">No active tunnels</div>';\n return '<div class=\"tunnel-list\">'+tunnels.map(t=>{\n const rem=t.expiresAt?Math.max(0,Math.floor((t.expiresAt-Date.now())/1000)):null;\n return \\`<div class=\"tunnel-card\">\\${esc(t.id)}<div class=\"tunnel-meta\">\n <span>reads: \\${t.accessCount}\\${t.maxReads?'/'+t.maxReads:''}</span>\n \\${rem!==null?'<span>expires: '+rem+'s</span>':'<span>no expiry</span>'}\n </div></div>\\`;\n }).join('')+'</div>';\n }\n\n function renderAudit(events){\n if(!events.length) return '<div class=\"empty\">No audit events</div>';\n return '<div class=\"audit-feed\">'+events.slice(0,40).map(e=>\n \\`<div class=\"audit-row\">\n <span class=\"audit-ts\">\\${fmtTime(e.timestamp)}</span>\n <span class=\"audit-action \\${esc(e.action)}\">\\${esc(e.action)}</span>\n <span class=\"audit-key\">\\${e.key?esc(e.key):''}</span>\n <span class=\"audit-detail\">\\${e.detail?esc(e.detail):''}</span>\n </div>\\`\n ).join('')+'</div>';\n }\n\n function renderAnomalies(anomalies){\n if(!anomalies.length) return '<div class=\"empty\">No anomalies detected \\u2014 all clear</div>';\n return anomalies.map(a=>\n \\`<div class=\"anomaly-card\"><div class=\"anomaly-type\">\\${esc(a.type)}</div><div class=\"anomaly-desc\">\\${esc(a.description)}</div></div>\\`\n ).join('');\n }\n\n function renderEnvironment(env){\n if(!env) return '<div class=\"empty\">No environment detected</div>';\n return \\`<div class=\"env-status\">\n <span class=\"env-big env-pill \\${envClass(env.env)}\">\\${esc(env.env)}</span>\n <span class=\"env-source\">detected via \\${esc(env.source)}</span>\n </div>\\`;\n }\n\n const panels = [\n { id:'p-health', icon:icons.health, label:'Health Summary', wide:false, render:s=>renderHealth(s.health) },\n { id:'p-env', icon:icons.environment, label:'Environment', wide:false, render:s=>renderEnvironment(s.environment) },\n { id:'p-decay', icon:icons.decay, label:'Decay Timers', wide:false, render:s=>renderDecay(s.secrets) },\n { id:'p-super', icon:icons.superposition, label:'Superposition States', wide:false, render:s=>renderSuperposition(s.secrets) },\n { id:'p-ent', icon:icons.entangle, label:'Entanglement', wide:false, render:s=>renderEntanglements(s.entanglements) },\n { id:'p-tunnel', icon:icons.tunnel, label:'Quantum Tunnels', wide:false, render:s=>renderTunnels(s.tunnels) },\n { id:'p-anomaly', icon:icons.anomaly, label:'Anomaly Alerts', wide:true, render:s=>renderAnomalies(s.anomalies) },\n { id:'p-audit', icon:icons.audit, label:'Audit Log', wide:true, render:s=>renderAudit(s.audit) }\n ];\n\n let initialised = false;\n\n function render(snap){\n if(!initialised){\n initialised = true;\n dash.innerHTML = panels.map((p,i) =>\n \\`<div class=\"card\\${p.wide?' grid-wide':''}\" id=\"\\${p.id}\" style=\"animation-delay:\\${i*60}ms\">\n <div class=\"card-title\">\\${p.icon} \\${p.label}</div>\n <div class=\"card-body\" id=\"\\${p.id}-body\"></div>\n </div>\\`\n ).join('');\n }\n for(const p of panels){\n const body = $(\\`\\${p.id}-body\\`);\n if(body){\n const html = p.render(snap);\n if(body._prev !== html){ body.innerHTML = html; body._prev = html; }\n }\n }\n }\n\n function connect(){\n const es=new EventSource('/events');\n es.onopen=()=>{\n dot.classList.remove('disconnected');\n connText.textContent='live';\n };\n es.onmessage=(e)=>{\n try{ render(JSON.parse(e.data)); }catch(err){ console.error('render error',err); }\n };\n es.onerror=()=>{\n dot.classList.add('disconnected');\n connText.textContent='reconnecting\\u2026';\n };\n }\n\n connect();\n})();\n</script>\n</body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;AAOA,SAAS,oBAA4E;;;ACC9E,SAAS,mBAA2B;AACzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":[]}
@@ -7,7 +7,7 @@ import {
7
7
  listSecrets,
8
8
  queryAudit,
9
9
  tunnelList
10
- } from "./chunk-5JBU7TWN.js";
10
+ } from "./chunk-JV4JFWV6.js";
11
11
 
12
12
  // src/core/dashboard.ts
13
13
  import { createServer } from "http";
@@ -322,7 +322,7 @@ body{min-height:100vh;overflow-x:hidden;position:relative}
322
322
  return '<div class="audit-feed">'+events.slice(0,40).map(e=>
323
323
  \`<div class="audit-row">
324
324
  <span class="audit-ts">\${fmtTime(e.timestamp)}</span>
325
- <span class="audit-action \${e.action}">\${e.action}</span>
325
+ <span class="audit-action \${esc(e.action)}">\${esc(e.action)}</span>
326
326
  <span class="audit-key">\${e.key?esc(e.key):''}</span>
327
327
  <span class="audit-detail">\${e.detail?esc(e.detail):''}</span>
328
328
  </div>\`
@@ -554,4 +554,4 @@ export {
554
554
  collectSnapshot,
555
555
  startDashboardServer
556
556
  };
557
- //# sourceMappingURL=dashboard-Q5OQRQCX.js.map
557
+ //# sourceMappingURL=dashboard-5L4ILERJ.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/dashboard.ts","../src/core/dashboard-html.ts"],"sourcesContent":["/**\n * Quantum Status Dashboard: local HTTP server with SSE live updates.\n *\n * Collects a full snapshot of all quantum state every few seconds and\n * pushes it to connected browsers. Never exposes secret values.\n */\n\nimport { createServer, type IncomingMessage, type ServerResponse, type Server } from \"node:http\";\nimport { listSecrets } from \"./keyring.js\";\nimport { checkDecay, type DecayStatus, type QuantumEnvelope } from \"./envelope.js\";\nimport { listEntanglements, type EntanglementPair } from \"./entanglement.js\";\nimport { tunnelList } from \"./tunnel.js\";\nimport { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly } from \"./observer.js\";\nimport { collapseEnvironment, type CollapseResult } from \"./collapse.js\";\nimport { getDashboardHtml } from \"./dashboard-html.js\";\n\nexport interface SecretSnapshot {\n key: string;\n scope: string;\n type: \"collapsed\" | \"superposition\";\n environments?: string[];\n defaultEnv?: string;\n decay: DecayStatus;\n accessCount: number;\n lastAccessedAt?: string;\n createdAt: string;\n updatedAt: string;\n description?: string;\n tags?: string[];\n entangled?: { service: string; key: string }[];\n}\n\nexport interface TunnelSnapshot {\n id: string;\n createdAt: number;\n expiresAt?: number;\n accessCount: number;\n maxReads?: number;\n}\n\nexport interface DashboardSnapshot {\n timestamp: string;\n secrets: SecretSnapshot[];\n health: { healthy: number; stale: number; expired: number; noDecay: number; total: number };\n entanglements: EntanglementPair[];\n tunnels: TunnelSnapshot[];\n audit: AuditEvent[];\n anomalies: AccessAnomaly[];\n environment: CollapseResult | null;\n}\n\nfunction toSecretSnapshot(entry: {\n key: string;\n scope: string;\n envelope?: QuantumEnvelope;\n decay?: DecayStatus;\n}): SecretSnapshot {\n const envelope = entry.envelope;\n const decay = envelope ? checkDecay(envelope) : {\n isExpired: false,\n isStale: false,\n lifetimePercent: 0,\n secondsRemaining: null,\n timeRemaining: null,\n };\n\n return {\n key: entry.key,\n scope: entry.scope,\n type: envelope?.states ? \"superposition\" : \"collapsed\",\n environments: envelope?.states ? Object.keys(envelope.states) : undefined,\n defaultEnv: envelope?.defaultEnv,\n decay,\n accessCount: envelope?.meta.accessCount ?? 0,\n lastAccessedAt: envelope?.meta.lastAccessedAt,\n createdAt: envelope?.meta.createdAt ?? \"\",\n updatedAt: envelope?.meta.updatedAt ?? \"\",\n description: envelope?.meta.description,\n tags: envelope?.meta.tags,\n entangled: envelope?.meta.entangled,\n };\n}\n\nexport function collectSnapshot(): DashboardSnapshot {\n const entries = listSecrets({ source: \"api\", silent: true });\n\n const secrets = entries.map(toSecretSnapshot);\n\n let healthy = 0;\n let stale = 0;\n let expired = 0;\n let noDecay = 0;\n\n for (const s of secrets) {\n if (!s.decay.timeRemaining) {\n noDecay++;\n } else if (s.decay.isExpired) {\n expired++;\n } else if (s.decay.isStale) {\n stale++;\n } else {\n healthy++;\n }\n }\n\n return {\n timestamp: new Date().toISOString(),\n secrets,\n health: { healthy, stale, expired, noDecay, total: secrets.length },\n entanglements: listEntanglements(),\n tunnels: tunnelList(),\n audit: queryAudit({ limit: 50 }).filter(e => e.action !== \"list\"),\n anomalies: detectAnomalies(),\n environment: collapseEnvironment(),\n };\n}\n\nexport interface DashboardServerOptions {\n port?: number;\n}\n\nexport function startDashboardServer(\n options: DashboardServerOptions = {},\n): { port: number; close: () => void; server: Server } {\n const port = options.port ?? 9876;\n const clients = new Set<ServerResponse>();\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n\n const html = getDashboardHtml();\n\n function broadcast() {\n const snapshot = collectSnapshot();\n const data = `data: ${JSON.stringify(snapshot)}\\n\\n`;\n for (const res of clients) {\n if (res.writableEnded || res.destroyed) {\n clients.delete(res);\n continue;\n }\n try {\n const ok = res.write(data);\n if (!ok) {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n } catch {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n }\n }\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n let pathname: string;\n try {\n ({ pathname } = new URL(req.url ?? \"/\", \"http://127.0.0.1\"));\n } catch {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Bad Request: invalid URL\");\n return;\n }\n\n if (pathname === \"/events\") {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n\n const snapshot = collectSnapshot();\n res.write(`data: ${JSON.stringify(snapshot)}\\n\\n`);\n\n clients.add(res);\n req.on(\"close\", () => clients.delete(res));\n return;\n }\n\n if (pathname === \"/api/status\") {\n const snapshot = collectSnapshot();\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n });\n res.end(JSON.stringify(snapshot, null, 2));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(html);\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n intervalHandle = setInterval(broadcast, 5000);\n if (intervalHandle && typeof intervalHandle === \"object\" && \"unref\" in intervalHandle) {\n intervalHandle.unref();\n }\n });\n\n return {\n port,\n close: () => {\n if (intervalHandle) clearInterval(intervalHandle);\n for (const res of clients) {\n try { res.end(); } catch { /* noop */ }\n }\n clients.clear();\n server.close();\n },\n server,\n };\n}\n","/**\n * Self-contained HTML dashboard for q-ring quantum status.\n * Matches the gh-pages site design system: Outfit + JetBrains Mono fonts,\n * deep navy palette, neon cyan→violet SVG icons, glassmorphism cards.\n *\n * Zero dependencies — inline CSS + vanilla JS with EventSource for SSE.\n */\n\nexport function getDashboardHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\"/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n<title>q-ring — quantum status</title>\n<style>\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg-deep:#04080f;\n --bg-section:#080e18;\n --bg-card:#0f1a2e;\n --bg-card-hover:#142240;\n --border:#1a2d4a;\n --border-glow:#0ea5e9;\n\n --text-primary:#f8f8ff;\n --text-secondary:#c8cfe0;\n --text-dim:#8899b4;\n\n --accent:#0ea5e9;\n --accent-bright:#38bdf8;\n --accent-dim:rgba(14,165,233,0.15);\n --accent-glow:rgba(14,165,233,0.4);\n\n --danger:#ff5e5b;\n --warning:#fbbf24;\n --green:#22c55e;\n --violet:#a855f7;\n --pink:#ff0055;\n\n --font-display:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n --font-mono:'SF Mono','Cascadia Code','Fira Code',Consolas,'Liberation Mono',monospace;\n\n --radius:12px;\n --radius-sm:8px;\n --radius-lg:20px;\n\n --glass-bg:rgba(15,26,46,0.65);\n --glass-border:rgba(26,45,74,0.8);\n --shadow-card:0 4px 24px -1px rgba(0,0,0,0.4),0 0 1px rgba(255,255,255,0.04);\n --shadow-glow:0 0 20px rgba(14,165,233,0.12),0 0 40px rgba(168,85,247,0.08);\n}\nhtml{background:var(--bg-deep);color:var(--text-primary);font-family:var(--font-display);font-size:16px;line-height:1.5}\nbody{min-height:100vh;overflow-x:hidden;position:relative}\n\n/* Hidden SVG defs for gradient icons */\n.svg-defs{position:absolute;width:0;height:0;overflow:hidden}\n\n/* Mesh blobs */\n.blob{position:fixed;border-radius:50%;filter:blur(100px);opacity:.12;pointer-events:none;z-index:0}\n.blob-1{width:600px;height:600px;top:-120px;left:-100px;background:radial-gradient(circle,var(--accent),transparent 70%);animation:drift1 22s ease-in-out infinite}\n.blob-2{width:500px;height:500px;bottom:-80px;right:-60px;background:radial-gradient(circle,var(--violet),transparent 70%);animation:drift2 26s ease-in-out infinite}\n.blob-3{width:350px;height:350px;top:40%;left:50%;background:radial-gradient(circle,var(--green),transparent 70%);animation:drift3 30s ease-in-out infinite;opacity:.06}\n@keyframes drift1{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(60px,-40px) scale(1.08)}66%{transform:translate(-30px,50px) scale(.95)}}\n@keyframes drift2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,30px) scale(1.05)}66%{transform:translate(40px,-60px) scale(.92)}}\n@keyframes drift3{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-40%,-60%) scale(1.1)}}\n\n.container{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:24px 20px 48px}\n\n/* Header */\n.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;padding:16px 20px;background:rgba(4,8,15,0.75);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius)}\n.header h1{font-family:var(--font-display);font-size:1.65rem;font-weight:700;letter-spacing:-.02em;display:flex;align-items:center;gap:10px}\n.header h1 .q-icon{display:flex;filter:drop-shadow(0 0 6px rgba(14,165,233,0.6))}\n.header h1 .brand{background:linear-gradient(135deg,#00D1FF,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}\n.header h1 .sub{color:var(--text-dim);font-weight:400;font-size:1.1rem;-webkit-text-fill-color:var(--text-dim)}\n.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);display:inline-block;animation:pulse 2s ease-in-out infinite}\n.status-dot.disconnected{background:var(--danger);box-shadow:0 0 8px var(--danger)}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}\n.conn-label{font-size:.85rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)}\n\n/* Grid */\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px}\n.grid-wide{grid-column:1/-1}\n\n/* Cards — reveal animation */\n.card{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-card);transition:border-color .3s,box-shadow .3s,transform .3s;opacity:0;transform:translateY(16px);animation:cardReveal .5s cubic-bezier(0.16,1,0.3,1) forwards}\n.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow-card),var(--shadow-glow);transform:translateY(-2px)}\n@keyframes cardReveal{to{opacity:1;transform:translateY(0)}}\n\n.card-title{font-family:var(--font-display);font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;font-weight:600}\n.card-title svg{width:16px;height:16px;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(14,165,233,0.4))}\n\n/* Health donut */\n.health-row{display:flex;align-items:center;gap:24px}\n.donut-wrap{position:relative;width:100px;height:100px;flex-shrink:0}\n.donut-wrap svg{transform:rotate(-90deg)}\n.donut-wrap .donut-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:1.5rem;font-weight:700;line-height:1;font-family:var(--font-display)}\n.donut-wrap .donut-label small{font-size:.7rem;color:var(--text-dim);font-weight:400;margin-top:2px;letter-spacing:.04em}\n.health-legend{display:flex;flex-direction:column;gap:6px}\n.legend-item{display:flex;align-items:center;gap:8px;font-size:.88rem}\n.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n\n/* Decay bars */\n.decay-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.decay-item{display:flex;align-items:center;gap:10px}\n.decay-key{font-family:var(--font-mono);font-size:.85rem;min-width:120px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.decay-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,0.06);overflow:hidden;position:relative}\n.decay-fill{height:100%;border-radius:3px;transition:width .6s ease}\n.decay-time{font-size:.8rem;color:var(--text-dim);min-width:56px;text-align:right;font-family:var(--font-mono)}\n\n/* Superposition pills */\n.super-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.super-item{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.super-key{font-family:var(--font-mono);font-size:.85rem;min-width:100px}\n.env-pill{font-size:.75rem;padding:2px 8px;border-radius:99px;font-weight:600;letter-spacing:.03em;font-family:var(--font-mono)}\n.env-prod{background:rgba(255,0,85,0.2);color:var(--pink);border:1px solid rgba(255,0,85,0.3)}\n.env-staging{background:rgba(251,191,36,0.15);color:var(--warning);border:1px solid rgba(251,191,36,0.25)}\n.env-dev{background:rgba(34,197,94,0.15);color:var(--green);border:1px solid rgba(34,197,94,0.25)}\n.env-default{background:rgba(168,85,247,0.15);color:var(--violet);border:1px solid rgba(168,85,247,0.25)}\n\n/* Entanglement */\n.entangle-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto}\n.entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.85rem}\n.entangle-arrow{color:var(--accent-bright)}\n\n/* Tunnels */\n.tunnel-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto}\n.tunnel-card{background:rgba(168,85,247,0.06);border:1px solid rgba(168,85,247,0.15);border-radius:var(--radius-sm);padding:10px 12px;font-size:.85rem;font-family:var(--font-mono)}\n.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.8rem;color:var(--text-dim)}\n\n/* Audit feed */\n.audit-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;font-size:.85rem;font-family:var(--font-mono)}\n.audit-row{display:flex;gap:8px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)}\n.audit-ts{color:var(--text-dim);min-width:70px;flex-shrink:0}\n.audit-action{min-width:64px;font-weight:600}\n.audit-action.read{color:var(--accent)}.audit-action.write{color:var(--green)}.audit-action.delete{color:var(--danger)}\n.audit-action.entangle{color:var(--violet)}.audit-action.tunnel{color:var(--violet)}.audit-action.teleport{color:var(--warning)}\n.audit-action.generate{color:var(--warning)}.audit-action.list{color:var(--text-dim)}.audit-action.export{color:var(--text-dim)}\n.audit-action.collapse{color:var(--accent)}\n\n/* Anomalies */\n.anomaly-card{background:rgba(255,94,91,0.06);border:1px solid rgba(255,94,91,0.15);border-radius:var(--radius-sm);padding:10px 14px;animation:anomaly-pulse 3s ease-in-out infinite}\n@keyframes anomaly-pulse{0%,100%{border-color:rgba(255,94,91,0.15)}50%{border-color:rgba(255,94,91,0.4)}}\n.anomaly-type{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--danger);font-weight:700;margin-bottom:2px;font-family:var(--font-display)}\n.anomaly-desc{font-size:.88rem;color:var(--text-primary)}\n\n/* Environment badge */\n.env-status{display:flex;align-items:center;gap:12px}\n.env-big{font-size:1.1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)}\n.env-source{font-size:.85rem;color:var(--text-dim)}\n\n/* Empty states */\n.empty{color:var(--text-dim);font-size:.88rem;font-style:italic;padding:8px 0}\n\n/* Scrollbar */\n::-webkit-scrollbar{width:4px;height:4px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:2px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.15)}\n</style>\n</head>\n<body>\n\n<!-- Shared SVG gradient definition -->\n<svg class=\"svg-defs\" aria-hidden=\"true\" focusable=\"false\">\n <defs>\n <linearGradient id=\"neon-grad\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stop-color=\"#00D1FF\"/>\n <stop offset=\"100%\" stop-color=\"#a855f7\"/>\n </linearGradient>\n </defs>\n</svg>\n\n<div class=\"blob blob-1\"></div>\n<div class=\"blob blob-2\"></div>\n<div class=\"blob blob-3\"></div>\n<div class=\"container\">\n <div class=\"header\">\n <h1>\n <span class=\"q-icon\"><svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg></span>\n <span class=\"brand\">q-ring</span>\n <span class=\"sub\">quantum status</span>\n </h1>\n <div class=\"conn-label\"><span class=\"status-dot\" id=\"connDot\"></span><span id=\"connText\">connecting\\u2026</span></div>\n </div>\n <div class=\"grid\" id=\"dashboard\">\n <div class=\"card\"><div class=\"empty\">Connecting to q-ring\\u2026</div></div>\n </div>\n</div>\n\n<script>\n(function(){\n const $ = (id) => document.getElementById(id);\n const dash = $('dashboard');\n const dot = $('connDot');\n const connText = $('connText');\n\n /* --- SVG icon library (matching gh-pages neon gradient) --- */\n const icons = {\n health: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/></svg>',\n environment: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>',\n decay: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/></svg>',\n superposition: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 2 7 12 12 22 7 12 2\"/><polyline points=\"2 17 12 22 22 17\"/><polyline points=\"2 12 12 17 22 12\"/></svg>',\n entangle: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>',\n tunnel: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 10h.01\"/><path d=\"M15 10h.01\"/><path d=\"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z\"/></svg>',\n anomaly: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>',\n audit: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg>'\n };\n\n function esc(s){ const d=document.createElement('div');d.textContent=s;return d.innerHTML; }\n\n function envClass(e){\n if(e==='prod'||e==='production') return 'env-prod';\n if(e==='staging'||e==='stage') return 'env-staging';\n if(e==='dev'||e==='development') return 'env-dev';\n return 'env-default';\n }\n\n function decayColor(pct,expired){\n if(expired) return 'var(--danger)';\n if(pct>=90) return 'var(--danger)';\n if(pct>=75) return 'var(--warning)';\n return 'var(--accent)';\n }\n\n function fmtTime(ts){\n const d=new Date(ts);\n return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');\n }\n\n function renderHealth(h){\n const total=h.total||1;\n const r=42, circ=2*Math.PI*r;\n const slices=[\n {v:h.healthy,c:'var(--accent)'},\n {v:h.stale,c:'var(--warning)'},\n {v:h.expired,c:'var(--danger)'},\n {v:h.noDecay,c:'var(--text-dim)'}\n ];\n let offset=0;\n let rings='';\n for(const sl of slices){\n const pct=sl.v/total;\n const len=pct*circ;\n rings+=\\`<circle cx=\"50\" cy=\"50\" r=\"\\${r}\" fill=\"none\" stroke=\"\\${sl.c}\" stroke-width=\"12\" stroke-dasharray=\"\\${len} \\${circ-len}\" stroke-dashoffset=\"-\\${offset}\" stroke-linecap=\"round\" opacity=\"0.85\"/>\\`;\n offset+=len;\n }\n return \\`<div class=\"health-row\">\n <div class=\"donut-wrap\">\n <svg viewBox=\"0 0 100 100\" width=\"100\" height=\"100\">\\${rings}</svg>\n <div class=\"donut-label\">\\${h.total}<small>secrets</small></div>\n </div>\n <div class=\"health-legend\">\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--accent)\"></span>Healthy \\${h.healthy}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--warning)\"></span>Stale \\${h.stale}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--danger)\"></span>Expired \\${h.expired}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--text-dim)\"></span>No decay \\${h.noDecay}</div>\n </div>\n </div>\\`;\n }\n\n function renderDecay(secrets){\n const withDecay=secrets.filter(s=>s.decay&&s.decay.timeRemaining);\n if(!withDecay.length) return '<div class=\"empty\">No secrets with decay configured</div>';\n return '<div class=\"decay-list\">'+withDecay.map(s=>{\n const pct=Math.min(s.decay.lifetimePercent,100);\n const col=decayColor(pct,s.decay.isExpired);\n const label=s.decay.isExpired?'expired':s.decay.timeRemaining||'';\n return \\`<div class=\"decay-item\">\n <span class=\"decay-key\" title=\"\\${esc(s.key)}\">\\${esc(s.key)}</span>\n <div class=\"decay-bar\"><div class=\"decay-fill\" style=\"width:\\${pct}%;background:\\${col}\"></div></div>\n <span class=\"decay-time\" style=\"color:\\${col}\">\\${esc(label)}</span>\n </div>\\`;\n }).join('')+'</div>';\n }\n\n function renderSuperposition(secrets){\n const sup=secrets.filter(s=>s.type==='superposition'&&s.environments);\n if(!sup.length) return '<div class=\"empty\">No secrets in superposition</div>';\n return '<div class=\"super-list\">'+sup.map(s=>{\n const pills=(s.environments||[]).map(e=>{\n const isDefault=e===s.defaultEnv;\n return \\`<span class=\"env-pill \\${envClass(e)}\">\\${esc(e)}\\${isDefault?' \\u2713':''}</span>\\`;\n }).join('');\n return \\`<div class=\"super-item\"><span class=\"super-key\">\\${esc(s.key)}</span>\\${pills}</div>\\`;\n }).join('')+'</div>';\n }\n\n function renderEntanglements(pairs){\n if(!pairs.length) return '<div class=\"empty\">No entangled secrets</div>';\n const seen=new Set();\n const unique=pairs.filter(p=>{\n const id=[p.source.service,p.source.key,p.target.service,p.target.key].sort().join('|');\n if(seen.has(id)) return false;\n seen.add(id);return true;\n });\n return '<div class=\"entangle-list\">'+unique.map(p=>\n \\`<div class=\"entangle-pair\"><span>\\${esc(p.source.key)}</span><span class=\"entangle-arrow\">\\u2194</span><span>\\${esc(p.target.key)}</span></div>\\`\n ).join('')+'</div>';\n }\n\n function renderTunnels(tunnels){\n if(!tunnels.length) return '<div class=\"empty\">No active tunnels</div>';\n return '<div class=\"tunnel-list\">'+tunnels.map(t=>{\n const rem=t.expiresAt?Math.max(0,Math.floor((t.expiresAt-Date.now())/1000)):null;\n return \\`<div class=\"tunnel-card\">\\${esc(t.id)}<div class=\"tunnel-meta\">\n <span>reads: \\${t.accessCount}\\${t.maxReads?'/'+t.maxReads:''}</span>\n \\${rem!==null?'<span>expires: '+rem+'s</span>':'<span>no expiry</span>'}\n </div></div>\\`;\n }).join('')+'</div>';\n }\n\n function renderAudit(events){\n if(!events.length) return '<div class=\"empty\">No audit events</div>';\n return '<div class=\"audit-feed\">'+events.slice(0,40).map(e=>\n \\`<div class=\"audit-row\">\n <span class=\"audit-ts\">\\${fmtTime(e.timestamp)}</span>\n <span class=\"audit-action \\${e.action}\">\\${e.action}</span>\n <span class=\"audit-key\">\\${e.key?esc(e.key):''}</span>\n <span class=\"audit-detail\">\\${e.detail?esc(e.detail):''}</span>\n </div>\\`\n ).join('')+'</div>';\n }\n\n function renderAnomalies(anomalies){\n if(!anomalies.length) return '<div class=\"empty\">No anomalies detected \\u2014 all clear</div>';\n return anomalies.map(a=>\n \\`<div class=\"anomaly-card\"><div class=\"anomaly-type\">\\${esc(a.type)}</div><div class=\"anomaly-desc\">\\${esc(a.description)}</div></div>\\`\n ).join('');\n }\n\n function renderEnvironment(env){\n if(!env) return '<div class=\"empty\">No environment detected</div>';\n return \\`<div class=\"env-status\">\n <span class=\"env-big env-pill \\${envClass(env.env)}\">\\${esc(env.env)}</span>\n <span class=\"env-source\">detected via \\${esc(env.source)}</span>\n </div>\\`;\n }\n\n const panels = [\n { id:'p-health', icon:icons.health, label:'Health Summary', wide:false, render:s=>renderHealth(s.health) },\n { id:'p-env', icon:icons.environment, label:'Environment', wide:false, render:s=>renderEnvironment(s.environment) },\n { id:'p-decay', icon:icons.decay, label:'Decay Timers', wide:false, render:s=>renderDecay(s.secrets) },\n { id:'p-super', icon:icons.superposition, label:'Superposition States', wide:false, render:s=>renderSuperposition(s.secrets) },\n { id:'p-ent', icon:icons.entangle, label:'Entanglement', wide:false, render:s=>renderEntanglements(s.entanglements) },\n { id:'p-tunnel', icon:icons.tunnel, label:'Quantum Tunnels', wide:false, render:s=>renderTunnels(s.tunnels) },\n { id:'p-anomaly', icon:icons.anomaly, label:'Anomaly Alerts', wide:true, render:s=>renderAnomalies(s.anomalies) },\n { id:'p-audit', icon:icons.audit, label:'Audit Log', wide:true, render:s=>renderAudit(s.audit) }\n ];\n\n let initialised = false;\n\n function render(snap){\n if(!initialised){\n initialised = true;\n dash.innerHTML = panels.map((p,i) =>\n \\`<div class=\"card\\${p.wide?' grid-wide':''}\" id=\"\\${p.id}\" style=\"animation-delay:\\${i*60}ms\">\n <div class=\"card-title\">\\${p.icon} \\${p.label}</div>\n <div class=\"card-body\" id=\"\\${p.id}-body\"></div>\n </div>\\`\n ).join('');\n }\n for(const p of panels){\n const body = $(\\`\\${p.id}-body\\`);\n if(body){\n const html = p.render(snap);\n if(body._prev !== html){ body.innerHTML = html; body._prev = html; }\n }\n }\n }\n\n function connect(){\n const es=new EventSource('/events');\n es.onopen=()=>{\n dot.classList.remove('disconnected');\n connText.textContent='live';\n };\n es.onmessage=(e)=>{\n try{ render(JSON.parse(e.data)); }catch(err){ console.error('render error',err); }\n };\n es.onerror=()=>{\n dot.classList.add('disconnected');\n connText.textContent='reconnecting\\u2026';\n };\n }\n\n connect();\n})();\n</script>\n</body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;AAOA,SAAS,oBAA4E;;;ACC9E,SAAS,mBAA2B;AACzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":[]}
1
+ {"version":3,"sources":["../src/core/dashboard.ts","../src/core/dashboard-html.ts"],"sourcesContent":["/**\n * Quantum Status Dashboard: local HTTP server with SSE live updates.\n *\n * Collects a full snapshot of all quantum state every few seconds and\n * pushes it to connected browsers. Never exposes secret values.\n */\n\nimport { createServer, type IncomingMessage, type ServerResponse, type Server } from \"node:http\";\nimport { listSecrets } from \"./keyring.js\";\nimport { checkDecay, type DecayStatus, type QuantumEnvelope } from \"./envelope.js\";\nimport { listEntanglements, type EntanglementPair } from \"./entanglement.js\";\nimport { tunnelList } from \"./tunnel.js\";\nimport { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly } from \"./observer.js\";\nimport { collapseEnvironment, type CollapseResult } from \"./collapse.js\";\nimport { getDashboardHtml } from \"./dashboard-html.js\";\n\nexport interface SecretSnapshot {\n key: string;\n scope: string;\n type: \"collapsed\" | \"superposition\";\n environments?: string[];\n defaultEnv?: string;\n decay: DecayStatus;\n accessCount: number;\n lastAccessedAt?: string;\n createdAt: string;\n updatedAt: string;\n description?: string;\n tags?: string[];\n entangled?: { service: string; key: string }[];\n}\n\nexport interface TunnelSnapshot {\n id: string;\n createdAt: number;\n expiresAt?: number;\n accessCount: number;\n maxReads?: number;\n}\n\nexport interface DashboardSnapshot {\n timestamp: string;\n secrets: SecretSnapshot[];\n health: { healthy: number; stale: number; expired: number; noDecay: number; total: number };\n entanglements: EntanglementPair[];\n tunnels: TunnelSnapshot[];\n audit: AuditEvent[];\n anomalies: AccessAnomaly[];\n environment: CollapseResult | null;\n}\n\nfunction toSecretSnapshot(entry: {\n key: string;\n scope: string;\n envelope?: QuantumEnvelope;\n decay?: DecayStatus;\n}): SecretSnapshot {\n const envelope = entry.envelope;\n const decay = envelope ? checkDecay(envelope) : {\n isExpired: false,\n isStale: false,\n lifetimePercent: 0,\n secondsRemaining: null,\n timeRemaining: null,\n };\n\n return {\n key: entry.key,\n scope: entry.scope,\n type: envelope?.states ? \"superposition\" : \"collapsed\",\n environments: envelope?.states ? Object.keys(envelope.states) : undefined,\n defaultEnv: envelope?.defaultEnv,\n decay,\n accessCount: envelope?.meta.accessCount ?? 0,\n lastAccessedAt: envelope?.meta.lastAccessedAt,\n createdAt: envelope?.meta.createdAt ?? \"\",\n updatedAt: envelope?.meta.updatedAt ?? \"\",\n description: envelope?.meta.description,\n tags: envelope?.meta.tags,\n entangled: envelope?.meta.entangled,\n };\n}\n\nexport function collectSnapshot(): DashboardSnapshot {\n const entries = listSecrets({ source: \"api\", silent: true });\n\n const secrets = entries.map(toSecretSnapshot);\n\n let healthy = 0;\n let stale = 0;\n let expired = 0;\n let noDecay = 0;\n\n for (const s of secrets) {\n if (!s.decay.timeRemaining) {\n noDecay++;\n } else if (s.decay.isExpired) {\n expired++;\n } else if (s.decay.isStale) {\n stale++;\n } else {\n healthy++;\n }\n }\n\n return {\n timestamp: new Date().toISOString(),\n secrets,\n health: { healthy, stale, expired, noDecay, total: secrets.length },\n entanglements: listEntanglements(),\n tunnels: tunnelList(),\n audit: queryAudit({ limit: 50 }).filter(e => e.action !== \"list\"),\n anomalies: detectAnomalies(),\n environment: collapseEnvironment(),\n };\n}\n\nexport interface DashboardServerOptions {\n port?: number;\n}\n\nexport function startDashboardServer(\n options: DashboardServerOptions = {},\n): { port: number; close: () => void; server: Server } {\n const port = options.port ?? 9876;\n const clients = new Set<ServerResponse>();\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n\n const html = getDashboardHtml();\n\n function broadcast() {\n const snapshot = collectSnapshot();\n const data = `data: ${JSON.stringify(snapshot)}\\n\\n`;\n for (const res of clients) {\n if (res.writableEnded || res.destroyed) {\n clients.delete(res);\n continue;\n }\n try {\n const ok = res.write(data);\n if (!ok) {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n } catch {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n }\n }\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n let pathname: string;\n try {\n ({ pathname } = new URL(req.url ?? \"/\", \"http://127.0.0.1\"));\n } catch {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Bad Request: invalid URL\");\n return;\n }\n\n if (pathname === \"/events\") {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n\n const snapshot = collectSnapshot();\n res.write(`data: ${JSON.stringify(snapshot)}\\n\\n`);\n\n clients.add(res);\n req.on(\"close\", () => clients.delete(res));\n return;\n }\n\n if (pathname === \"/api/status\") {\n const snapshot = collectSnapshot();\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n });\n res.end(JSON.stringify(snapshot, null, 2));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(html);\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n intervalHandle = setInterval(broadcast, 5000);\n if (intervalHandle && typeof intervalHandle === \"object\" && \"unref\" in intervalHandle) {\n intervalHandle.unref();\n }\n });\n\n return {\n port,\n close: () => {\n if (intervalHandle) clearInterval(intervalHandle);\n for (const res of clients) {\n try { res.end(); } catch { /* noop */ }\n }\n clients.clear();\n server.close();\n },\n server,\n };\n}\n","/**\n * Self-contained HTML dashboard for q-ring quantum status.\n * Matches the gh-pages site design system: Outfit + JetBrains Mono fonts,\n * deep navy palette, neon cyan→violet SVG icons, glassmorphism cards.\n *\n * Zero dependencies — inline CSS + vanilla JS with EventSource for SSE.\n */\n\nexport function getDashboardHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\"/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n<title>q-ring — quantum status</title>\n<style>\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg-deep:#04080f;\n --bg-section:#080e18;\n --bg-card:#0f1a2e;\n --bg-card-hover:#142240;\n --border:#1a2d4a;\n --border-glow:#0ea5e9;\n\n --text-primary:#f8f8ff;\n --text-secondary:#c8cfe0;\n --text-dim:#8899b4;\n\n --accent:#0ea5e9;\n --accent-bright:#38bdf8;\n --accent-dim:rgba(14,165,233,0.15);\n --accent-glow:rgba(14,165,233,0.4);\n\n --danger:#ff5e5b;\n --warning:#fbbf24;\n --green:#22c55e;\n --violet:#a855f7;\n --pink:#ff0055;\n\n --font-display:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n --font-mono:'SF Mono','Cascadia Code','Fira Code',Consolas,'Liberation Mono',monospace;\n\n --radius:12px;\n --radius-sm:8px;\n --radius-lg:20px;\n\n --glass-bg:rgba(15,26,46,0.65);\n --glass-border:rgba(26,45,74,0.8);\n --shadow-card:0 4px 24px -1px rgba(0,0,0,0.4),0 0 1px rgba(255,255,255,0.04);\n --shadow-glow:0 0 20px rgba(14,165,233,0.12),0 0 40px rgba(168,85,247,0.08);\n}\nhtml{background:var(--bg-deep);color:var(--text-primary);font-family:var(--font-display);font-size:16px;line-height:1.5}\nbody{min-height:100vh;overflow-x:hidden;position:relative}\n\n/* Hidden SVG defs for gradient icons */\n.svg-defs{position:absolute;width:0;height:0;overflow:hidden}\n\n/* Mesh blobs */\n.blob{position:fixed;border-radius:50%;filter:blur(100px);opacity:.12;pointer-events:none;z-index:0}\n.blob-1{width:600px;height:600px;top:-120px;left:-100px;background:radial-gradient(circle,var(--accent),transparent 70%);animation:drift1 22s ease-in-out infinite}\n.blob-2{width:500px;height:500px;bottom:-80px;right:-60px;background:radial-gradient(circle,var(--violet),transparent 70%);animation:drift2 26s ease-in-out infinite}\n.blob-3{width:350px;height:350px;top:40%;left:50%;background:radial-gradient(circle,var(--green),transparent 70%);animation:drift3 30s ease-in-out infinite;opacity:.06}\n@keyframes drift1{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(60px,-40px) scale(1.08)}66%{transform:translate(-30px,50px) scale(.95)}}\n@keyframes drift2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,30px) scale(1.05)}66%{transform:translate(40px,-60px) scale(.92)}}\n@keyframes drift3{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-40%,-60%) scale(1.1)}}\n\n.container{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:24px 20px 48px}\n\n/* Header */\n.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;padding:16px 20px;background:rgba(4,8,15,0.75);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius)}\n.header h1{font-family:var(--font-display);font-size:1.65rem;font-weight:700;letter-spacing:-.02em;display:flex;align-items:center;gap:10px}\n.header h1 .q-icon{display:flex;filter:drop-shadow(0 0 6px rgba(14,165,233,0.6))}\n.header h1 .brand{background:linear-gradient(135deg,#00D1FF,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}\n.header h1 .sub{color:var(--text-dim);font-weight:400;font-size:1.1rem;-webkit-text-fill-color:var(--text-dim)}\n.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);display:inline-block;animation:pulse 2s ease-in-out infinite}\n.status-dot.disconnected{background:var(--danger);box-shadow:0 0 8px var(--danger)}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}\n.conn-label{font-size:.85rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)}\n\n/* Grid */\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px}\n.grid-wide{grid-column:1/-1}\n\n/* Cards — reveal animation */\n.card{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-card);transition:border-color .3s,box-shadow .3s,transform .3s;opacity:0;transform:translateY(16px);animation:cardReveal .5s cubic-bezier(0.16,1,0.3,1) forwards}\n.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow-card),var(--shadow-glow);transform:translateY(-2px)}\n@keyframes cardReveal{to{opacity:1;transform:translateY(0)}}\n\n.card-title{font-family:var(--font-display);font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;font-weight:600}\n.card-title svg{width:16px;height:16px;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(14,165,233,0.4))}\n\n/* Health donut */\n.health-row{display:flex;align-items:center;gap:24px}\n.donut-wrap{position:relative;width:100px;height:100px;flex-shrink:0}\n.donut-wrap svg{transform:rotate(-90deg)}\n.donut-wrap .donut-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:1.5rem;font-weight:700;line-height:1;font-family:var(--font-display)}\n.donut-wrap .donut-label small{font-size:.7rem;color:var(--text-dim);font-weight:400;margin-top:2px;letter-spacing:.04em}\n.health-legend{display:flex;flex-direction:column;gap:6px}\n.legend-item{display:flex;align-items:center;gap:8px;font-size:.88rem}\n.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n\n/* Decay bars */\n.decay-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.decay-item{display:flex;align-items:center;gap:10px}\n.decay-key{font-family:var(--font-mono);font-size:.85rem;min-width:120px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.decay-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,0.06);overflow:hidden;position:relative}\n.decay-fill{height:100%;border-radius:3px;transition:width .6s ease}\n.decay-time{font-size:.8rem;color:var(--text-dim);min-width:56px;text-align:right;font-family:var(--font-mono)}\n\n/* Superposition pills */\n.super-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.super-item{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.super-key{font-family:var(--font-mono);font-size:.85rem;min-width:100px}\n.env-pill{font-size:.75rem;padding:2px 8px;border-radius:99px;font-weight:600;letter-spacing:.03em;font-family:var(--font-mono)}\n.env-prod{background:rgba(255,0,85,0.2);color:var(--pink);border:1px solid rgba(255,0,85,0.3)}\n.env-staging{background:rgba(251,191,36,0.15);color:var(--warning);border:1px solid rgba(251,191,36,0.25)}\n.env-dev{background:rgba(34,197,94,0.15);color:var(--green);border:1px solid rgba(34,197,94,0.25)}\n.env-default{background:rgba(168,85,247,0.15);color:var(--violet);border:1px solid rgba(168,85,247,0.25)}\n\n/* Entanglement */\n.entangle-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto}\n.entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.85rem}\n.entangle-arrow{color:var(--accent-bright)}\n\n/* Tunnels */\n.tunnel-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto}\n.tunnel-card{background:rgba(168,85,247,0.06);border:1px solid rgba(168,85,247,0.15);border-radius:var(--radius-sm);padding:10px 12px;font-size:.85rem;font-family:var(--font-mono)}\n.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.8rem;color:var(--text-dim)}\n\n/* Audit feed */\n.audit-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;font-size:.85rem;font-family:var(--font-mono)}\n.audit-row{display:flex;gap:8px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)}\n.audit-ts{color:var(--text-dim);min-width:70px;flex-shrink:0}\n.audit-action{min-width:64px;font-weight:600}\n.audit-action.read{color:var(--accent)}.audit-action.write{color:var(--green)}.audit-action.delete{color:var(--danger)}\n.audit-action.entangle{color:var(--violet)}.audit-action.tunnel{color:var(--violet)}.audit-action.teleport{color:var(--warning)}\n.audit-action.generate{color:var(--warning)}.audit-action.list{color:var(--text-dim)}.audit-action.export{color:var(--text-dim)}\n.audit-action.collapse{color:var(--accent)}\n\n/* Anomalies */\n.anomaly-card{background:rgba(255,94,91,0.06);border:1px solid rgba(255,94,91,0.15);border-radius:var(--radius-sm);padding:10px 14px;animation:anomaly-pulse 3s ease-in-out infinite}\n@keyframes anomaly-pulse{0%,100%{border-color:rgba(255,94,91,0.15)}50%{border-color:rgba(255,94,91,0.4)}}\n.anomaly-type{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--danger);font-weight:700;margin-bottom:2px;font-family:var(--font-display)}\n.anomaly-desc{font-size:.88rem;color:var(--text-primary)}\n\n/* Environment badge */\n.env-status{display:flex;align-items:center;gap:12px}\n.env-big{font-size:1.1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)}\n.env-source{font-size:.85rem;color:var(--text-dim)}\n\n/* Empty states */\n.empty{color:var(--text-dim);font-size:.88rem;font-style:italic;padding:8px 0}\n\n/* Scrollbar */\n::-webkit-scrollbar{width:4px;height:4px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:2px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.15)}\n</style>\n</head>\n<body>\n\n<!-- Shared SVG gradient definition -->\n<svg class=\"svg-defs\" aria-hidden=\"true\" focusable=\"false\">\n <defs>\n <linearGradient id=\"neon-grad\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stop-color=\"#00D1FF\"/>\n <stop offset=\"100%\" stop-color=\"#a855f7\"/>\n </linearGradient>\n </defs>\n</svg>\n\n<div class=\"blob blob-1\"></div>\n<div class=\"blob blob-2\"></div>\n<div class=\"blob blob-3\"></div>\n<div class=\"container\">\n <div class=\"header\">\n <h1>\n <span class=\"q-icon\"><svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg></span>\n <span class=\"brand\">q-ring</span>\n <span class=\"sub\">quantum status</span>\n </h1>\n <div class=\"conn-label\"><span class=\"status-dot\" id=\"connDot\"></span><span id=\"connText\">connecting\\u2026</span></div>\n </div>\n <div class=\"grid\" id=\"dashboard\">\n <div class=\"card\"><div class=\"empty\">Connecting to q-ring\\u2026</div></div>\n </div>\n</div>\n\n<script>\n(function(){\n const $ = (id) => document.getElementById(id);\n const dash = $('dashboard');\n const dot = $('connDot');\n const connText = $('connText');\n\n /* --- SVG icon library (matching gh-pages neon gradient) --- */\n const icons = {\n health: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/></svg>',\n environment: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>',\n decay: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/></svg>',\n superposition: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 2 7 12 12 22 7 12 2\"/><polyline points=\"2 17 12 22 22 17\"/><polyline points=\"2 12 12 17 22 12\"/></svg>',\n entangle: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>',\n tunnel: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 10h.01\"/><path d=\"M15 10h.01\"/><path d=\"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z\"/></svg>',\n anomaly: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>',\n audit: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg>'\n };\n\n function esc(s){ const d=document.createElement('div');d.textContent=s;return d.innerHTML; }\n\n function envClass(e){\n if(e==='prod'||e==='production') return 'env-prod';\n if(e==='staging'||e==='stage') return 'env-staging';\n if(e==='dev'||e==='development') return 'env-dev';\n return 'env-default';\n }\n\n function decayColor(pct,expired){\n if(expired) return 'var(--danger)';\n if(pct>=90) return 'var(--danger)';\n if(pct>=75) return 'var(--warning)';\n return 'var(--accent)';\n }\n\n function fmtTime(ts){\n const d=new Date(ts);\n return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');\n }\n\n function renderHealth(h){\n const total=h.total||1;\n const r=42, circ=2*Math.PI*r;\n const slices=[\n {v:h.healthy,c:'var(--accent)'},\n {v:h.stale,c:'var(--warning)'},\n {v:h.expired,c:'var(--danger)'},\n {v:h.noDecay,c:'var(--text-dim)'}\n ];\n let offset=0;\n let rings='';\n for(const sl of slices){\n const pct=sl.v/total;\n const len=pct*circ;\n rings+=\\`<circle cx=\"50\" cy=\"50\" r=\"\\${r}\" fill=\"none\" stroke=\"\\${sl.c}\" stroke-width=\"12\" stroke-dasharray=\"\\${len} \\${circ-len}\" stroke-dashoffset=\"-\\${offset}\" stroke-linecap=\"round\" opacity=\"0.85\"/>\\`;\n offset+=len;\n }\n return \\`<div class=\"health-row\">\n <div class=\"donut-wrap\">\n <svg viewBox=\"0 0 100 100\" width=\"100\" height=\"100\">\\${rings}</svg>\n <div class=\"donut-label\">\\${h.total}<small>secrets</small></div>\n </div>\n <div class=\"health-legend\">\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--accent)\"></span>Healthy \\${h.healthy}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--warning)\"></span>Stale \\${h.stale}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--danger)\"></span>Expired \\${h.expired}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--text-dim)\"></span>No decay \\${h.noDecay}</div>\n </div>\n </div>\\`;\n }\n\n function renderDecay(secrets){\n const withDecay=secrets.filter(s=>s.decay&&s.decay.timeRemaining);\n if(!withDecay.length) return '<div class=\"empty\">No secrets with decay configured</div>';\n return '<div class=\"decay-list\">'+withDecay.map(s=>{\n const pct=Math.min(s.decay.lifetimePercent,100);\n const col=decayColor(pct,s.decay.isExpired);\n const label=s.decay.isExpired?'expired':s.decay.timeRemaining||'';\n return \\`<div class=\"decay-item\">\n <span class=\"decay-key\" title=\"\\${esc(s.key)}\">\\${esc(s.key)}</span>\n <div class=\"decay-bar\"><div class=\"decay-fill\" style=\"width:\\${pct}%;background:\\${col}\"></div></div>\n <span class=\"decay-time\" style=\"color:\\${col}\">\\${esc(label)}</span>\n </div>\\`;\n }).join('')+'</div>';\n }\n\n function renderSuperposition(secrets){\n const sup=secrets.filter(s=>s.type==='superposition'&&s.environments);\n if(!sup.length) return '<div class=\"empty\">No secrets in superposition</div>';\n return '<div class=\"super-list\">'+sup.map(s=>{\n const pills=(s.environments||[]).map(e=>{\n const isDefault=e===s.defaultEnv;\n return \\`<span class=\"env-pill \\${envClass(e)}\">\\${esc(e)}\\${isDefault?' \\u2713':''}</span>\\`;\n }).join('');\n return \\`<div class=\"super-item\"><span class=\"super-key\">\\${esc(s.key)}</span>\\${pills}</div>\\`;\n }).join('')+'</div>';\n }\n\n function renderEntanglements(pairs){\n if(!pairs.length) return '<div class=\"empty\">No entangled secrets</div>';\n const seen=new Set();\n const unique=pairs.filter(p=>{\n const id=[p.source.service,p.source.key,p.target.service,p.target.key].sort().join('|');\n if(seen.has(id)) return false;\n seen.add(id);return true;\n });\n return '<div class=\"entangle-list\">'+unique.map(p=>\n \\`<div class=\"entangle-pair\"><span>\\${esc(p.source.key)}</span><span class=\"entangle-arrow\">\\u2194</span><span>\\${esc(p.target.key)}</span></div>\\`\n ).join('')+'</div>';\n }\n\n function renderTunnels(tunnels){\n if(!tunnels.length) return '<div class=\"empty\">No active tunnels</div>';\n return '<div class=\"tunnel-list\">'+tunnels.map(t=>{\n const rem=t.expiresAt?Math.max(0,Math.floor((t.expiresAt-Date.now())/1000)):null;\n return \\`<div class=\"tunnel-card\">\\${esc(t.id)}<div class=\"tunnel-meta\">\n <span>reads: \\${t.accessCount}\\${t.maxReads?'/'+t.maxReads:''}</span>\n \\${rem!==null?'<span>expires: '+rem+'s</span>':'<span>no expiry</span>'}\n </div></div>\\`;\n }).join('')+'</div>';\n }\n\n function renderAudit(events){\n if(!events.length) return '<div class=\"empty\">No audit events</div>';\n return '<div class=\"audit-feed\">'+events.slice(0,40).map(e=>\n \\`<div class=\"audit-row\">\n <span class=\"audit-ts\">\\${fmtTime(e.timestamp)}</span>\n <span class=\"audit-action \\${esc(e.action)}\">\\${esc(e.action)}</span>\n <span class=\"audit-key\">\\${e.key?esc(e.key):''}</span>\n <span class=\"audit-detail\">\\${e.detail?esc(e.detail):''}</span>\n </div>\\`\n ).join('')+'</div>';\n }\n\n function renderAnomalies(anomalies){\n if(!anomalies.length) return '<div class=\"empty\">No anomalies detected \\u2014 all clear</div>';\n return anomalies.map(a=>\n \\`<div class=\"anomaly-card\"><div class=\"anomaly-type\">\\${esc(a.type)}</div><div class=\"anomaly-desc\">\\${esc(a.description)}</div></div>\\`\n ).join('');\n }\n\n function renderEnvironment(env){\n if(!env) return '<div class=\"empty\">No environment detected</div>';\n return \\`<div class=\"env-status\">\n <span class=\"env-big env-pill \\${envClass(env.env)}\">\\${esc(env.env)}</span>\n <span class=\"env-source\">detected via \\${esc(env.source)}</span>\n </div>\\`;\n }\n\n const panels = [\n { id:'p-health', icon:icons.health, label:'Health Summary', wide:false, render:s=>renderHealth(s.health) },\n { id:'p-env', icon:icons.environment, label:'Environment', wide:false, render:s=>renderEnvironment(s.environment) },\n { id:'p-decay', icon:icons.decay, label:'Decay Timers', wide:false, render:s=>renderDecay(s.secrets) },\n { id:'p-super', icon:icons.superposition, label:'Superposition States', wide:false, render:s=>renderSuperposition(s.secrets) },\n { id:'p-ent', icon:icons.entangle, label:'Entanglement', wide:false, render:s=>renderEntanglements(s.entanglements) },\n { id:'p-tunnel', icon:icons.tunnel, label:'Quantum Tunnels', wide:false, render:s=>renderTunnels(s.tunnels) },\n { id:'p-anomaly', icon:icons.anomaly, label:'Anomaly Alerts', wide:true, render:s=>renderAnomalies(s.anomalies) },\n { id:'p-audit', icon:icons.audit, label:'Audit Log', wide:true, render:s=>renderAudit(s.audit) }\n ];\n\n let initialised = false;\n\n function render(snap){\n if(!initialised){\n initialised = true;\n dash.innerHTML = panels.map((p,i) =>\n \\`<div class=\"card\\${p.wide?' grid-wide':''}\" id=\"\\${p.id}\" style=\"animation-delay:\\${i*60}ms\">\n <div class=\"card-title\">\\${p.icon} \\${p.label}</div>\n <div class=\"card-body\" id=\"\\${p.id}-body\"></div>\n </div>\\`\n ).join('');\n }\n for(const p of panels){\n const body = $(\\`\\${p.id}-body\\`);\n if(body){\n const html = p.render(snap);\n if(body._prev !== html){ body.innerHTML = html; body._prev = html; }\n }\n }\n }\n\n function connect(){\n const es=new EventSource('/events');\n es.onopen=()=>{\n dot.classList.remove('disconnected');\n connText.textContent='live';\n };\n es.onmessage=(e)=>{\n try{ render(JSON.parse(e.data)); }catch(err){ console.error('render error',err); }\n };\n es.onerror=()=>{\n dot.classList.add('disconnected');\n connText.textContent='reconnecting\\u2026';\n };\n }\n\n connect();\n})();\n</script>\n</body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;AAOA,SAAS,oBAA4E;;;ACC9E,SAAS,mBAA2B;AACzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":[]}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  checkDecay,
4
4
  checkExecPolicy,
5
+ checkSSRF,
5
6
  collapseEnvironment,
6
7
  deleteSecret,
7
8
  detectAnomalies,
@@ -35,7 +36,7 @@ import {
35
36
  tunnelList,
36
37
  tunnelRead,
37
38
  verifyAuditChain
38
- } from "./chunk-WG4ZKN7Q.js";
39
+ } from "./chunk-JV4JFWV6.js";
39
40
 
40
41
  // src/cli/commands.ts
41
42
  import { Command } from "commander";
@@ -527,7 +528,10 @@ async function execCommand(opts) {
527
528
  throw new Error(`Policy Denied: ${policyDecision.reason}`);
528
529
  }
529
530
  if (profile.denyCommands) {
530
- const denied = profile.denyCommands.find((d) => fullCommand.includes(d));
531
+ const denied = profile.denyCommands.find((d) => {
532
+ const pattern = new RegExp(`(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`, "i");
533
+ return pattern.test(fullCommand);
534
+ });
531
535
  if (denied) {
532
536
  throw new Error(`Exec profile "${profile.name}" denies command containing "${denied}"`);
533
537
  }
@@ -1016,6 +1020,10 @@ var httpProvider = {
1016
1020
  if (!url) {
1017
1021
  return { valid: false, status: "unknown", message: "No validation URL configured", latencyMs: 0, provider: "http" };
1018
1022
  }
1023
+ const ssrfBlock = await checkSSRF(url);
1024
+ if (ssrfBlock) {
1025
+ return { valid: false, status: "error", message: `SSRF blocked: ${ssrfBlock}`, latencyMs: Date.now() - start, provider: "http" };
1026
+ }
1019
1027
  try {
1020
1028
  const { statusCode } = await makeRequest(url, {
1021
1029
  Authorization: `Bearer ${value}`,
@@ -1160,7 +1168,10 @@ import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync
1160
1168
  import { join as join2 } from "path";
1161
1169
  import { homedir, hostname, userInfo } from "os";
1162
1170
  import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash, randomBytes as randomBytes3 } from "crypto";
1171
+ import { Entry } from "@napi-rs/keyring";
1163
1172
  var MEMORY_FILE = "agent-memory.enc";
1173
+ var KEYRING_SERVICE = "qring-memory-key";
1174
+ var KEYRING_ACCOUNT = "encryption-key";
1164
1175
  function getMemoryDir() {
1165
1176
  const dir = join2(homedir(), ".config", "q-ring");
1166
1177
  if (!existsSync2(dir)) {
@@ -1171,29 +1182,54 @@ function getMemoryDir() {
1171
1182
  function getMemoryPath() {
1172
1183
  return join2(getMemoryDir(), MEMORY_FILE);
1173
1184
  }
1174
- function deriveKey2() {
1185
+ function deriveLegacyKey() {
1175
1186
  const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
1176
1187
  return createHash("sha256").update(fingerprint).digest();
1177
1188
  }
1178
- function encrypt(data) {
1179
- const key = deriveKey2();
1189
+ function getOrCreateKey() {
1190
+ try {
1191
+ const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
1192
+ const stored = entry.getPassword();
1193
+ if (stored) return Buffer.from(stored, "base64");
1194
+ const key = randomBytes3(32);
1195
+ entry.setPassword(key.toString("base64"));
1196
+ return key;
1197
+ } catch {
1198
+ console.warn("[q-ring] OS keyring unavailable for memory key \u2014 falling back to machine-derived key");
1199
+ return deriveLegacyKey();
1200
+ }
1201
+ }
1202
+ function encryptWith(data, key) {
1180
1203
  const iv = randomBytes3(12);
1181
1204
  const cipher = createCipheriv2("aes-256-gcm", key, iv);
1182
1205
  const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
1183
1206
  const tag = cipher.getAuthTag();
1184
1207
  return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
1185
1208
  }
1186
- function decrypt(blob) {
1209
+ function decryptWith(blob, key) {
1187
1210
  const parts = blob.split(":");
1188
1211
  if (parts.length !== 3) throw new Error("Invalid encrypted format");
1189
1212
  const iv = Buffer.from(parts[0], "base64");
1190
1213
  const tag = Buffer.from(parts[1], "base64");
1191
1214
  const encrypted = Buffer.from(parts[2], "base64");
1192
- const key = deriveKey2();
1193
1215
  const decipher = createDecipheriv2("aes-256-gcm", key, iv);
1194
1216
  decipher.setAuthTag(tag);
1195
1217
  return decipher.update(encrypted) + decipher.final("utf8");
1196
1218
  }
1219
+ function encrypt(data) {
1220
+ return encryptWith(data, getOrCreateKey());
1221
+ }
1222
+ function decrypt(blob) {
1223
+ const key = getOrCreateKey();
1224
+ try {
1225
+ return decryptWith(blob, key);
1226
+ } catch {
1227
+ const legacy = deriveLegacyKey();
1228
+ const plain = decryptWith(blob, legacy);
1229
+ writeFileSync2(getMemoryPath(), encryptWith(plain, key), "utf8");
1230
+ return plain;
1231
+ }
1232
+ }
1197
1233
  function loadStore() {
1198
1234
  const path = getMemoryPath();
1199
1235
  if (!existsSync2(path)) {
@@ -2641,7 +2677,7 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2641
2677
  }
2642
2678
  });
2643
2679
  program2.command("status").description("Launch the quantum status dashboard in your browser").option("--port <port>", "Port to serve on", "9876").option("--no-open", "Don't auto-open the browser").action(async (cmd) => {
2644
- const { startDashboardServer } = await import("./dashboard-JT5ZNLT5.js");
2680
+ const { startDashboardServer } = await import("./dashboard-5L4ILERJ.js");
2645
2681
  const { exec } = await import("child_process");
2646
2682
  const { platform } = await import("os");
2647
2683
  const port = Number(cmd.port);