@fiscalmindset/blindfold 0.4.0
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.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/assets/SKILL.md +205 -0
- package/assets/blindfold_proxy.wasm +0 -0
- package/bin/blindfold.ts +90 -0
- package/bin/cli-shared.ts +72 -0
- package/bin/cmd-auth.ts +193 -0
- package/bin/cmd-enclave.ts +373 -0
- package/bin/cmd-lifecycle.ts +151 -0
- package/bin/cmd-secrets.ts +136 -0
- package/bin/cmd-serve.ts +165 -0
- package/bin/cmd-tenant.ts +84 -0
- package/dist/cli.mjs +4317 -0
- package/dist/lib/index.mjs +2362 -0
- package/dist/lib/proxy.mjs +1078 -0
- package/dist/lib/register.mjs +756 -0
- package/dist/lib/wrap.mjs +81 -0
- package/package.json +48 -0
- package/postinstall.mjs +21 -0
- package/src/attest.ts +235 -0
- package/src/color.ts +31 -0
- package/src/compat.ts +217 -0
- package/src/constants.ts +24 -0
- package/src/dashboard.ts +785 -0
- package/src/env.ts +261 -0
- package/src/index.ts +11 -0
- package/src/init.ts +391 -0
- package/src/keychain.ts +155 -0
- package/src/log.ts +51 -0
- package/src/migrate.ts +135 -0
- package/src/prompt.ts +114 -0
- package/src/providers.ts +224 -0
- package/src/proxy.ts +454 -0
- package/src/register.ts +81 -0
- package/src/release.ts +82 -0
- package/src/sealed-ledger.ts +158 -0
- package/src/t3-client.ts +580 -0
- package/src/types.ts +50 -0
- package/src/usage-log.ts +149 -0
- package/src/versions.ts +64 -0
- package/src/wrap.ts +122 -0
package/src/dashboard.ts
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blindfold usage dashboard.
|
|
3
|
+
*
|
|
4
|
+
* blindfold dashboard --port 8799
|
|
5
|
+
*
|
|
6
|
+
* Self-contained HTTP server + inline HTML. Shows:
|
|
7
|
+
*
|
|
8
|
+
* - System status (REAL/MOCK, tenant DID, T3 env, contract ver)
|
|
9
|
+
* - Sealed keys (metadata only — name, byte-len, where, when)
|
|
10
|
+
* - Audit warnings (sealed keys ALSO present in .env = leak surface)
|
|
11
|
+
* - Traffic counters (total, by provider, success rate, latency)
|
|
12
|
+
* - Per-secret usage (which sealed keys are being released most)
|
|
13
|
+
* - Recent activity (last 50 requests with method, path, status)
|
|
14
|
+
* - Time-series (requests-per-minute, last 60 min)
|
|
15
|
+
*
|
|
16
|
+
* Inherits the privacy property: it can only render what was logged,
|
|
17
|
+
* and what was logged is metadata only.
|
|
18
|
+
*/
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import http from "node:http";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { timingSafeEqual } from "node:crypto";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import type { AddressInfo } from "node:net";
|
|
25
|
+
import { CONTRACT_VERSION, DEFAULT_DASHBOARD_PORT } from "./constants.ts";
|
|
26
|
+
import { defaultEnvPath, loadBlindfoldEnv } from "./env.ts";
|
|
27
|
+
import { clearUsage, defaultLogPath, readUsage, readUsageTail } from "./usage-log.ts";
|
|
28
|
+
import { defaultSealedLogPath, readSealed, verifyLedgerChain } from "./sealed-ledger.ts";
|
|
29
|
+
|
|
30
|
+
export interface DashboardHandle {
|
|
31
|
+
url: string;
|
|
32
|
+
port: number;
|
|
33
|
+
close: () => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function startDashboard(opts: { port?: number } = {}): Promise<DashboardHandle> {
|
|
37
|
+
const port = opts.port ?? DEFAULT_DASHBOARD_PORT;
|
|
38
|
+
|
|
39
|
+
// Optional access token (BLINDFOLD_DASHBOARD_TOKEN). The server binds to
|
|
40
|
+
// 127.0.0.1 regardless; this adds a guard if you tunnel/forward the port.
|
|
41
|
+
const TOKEN = process.env.BLINDFOLD_DASHBOARD_TOKEN || "";
|
|
42
|
+
const tokenEq = (candidate: string): boolean => {
|
|
43
|
+
// Constant-time compare so the token can't be recovered by timing.
|
|
44
|
+
const a = Buffer.from(candidate);
|
|
45
|
+
const b = Buffer.from(TOKEN);
|
|
46
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
47
|
+
};
|
|
48
|
+
const authed = (req: http.IncomingMessage, url: string): boolean => {
|
|
49
|
+
if (!TOKEN) return true;
|
|
50
|
+
// Prefer the Authorization header (query strings leak into logs/history).
|
|
51
|
+
const auth = req.headers.authorization || "";
|
|
52
|
+
if (auth.startsWith("Bearer ") && tokenEq(auth.slice(7))) return true;
|
|
53
|
+
try {
|
|
54
|
+
const t = new URL(url, "http://x").searchParams.get("token");
|
|
55
|
+
if (t && tokenEq(t)) return true;
|
|
56
|
+
} catch { /* ignore */ }
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const server = http.createServer(async (req, res) => {
|
|
61
|
+
const url = req.url ?? "/";
|
|
62
|
+
const pathname = url.split("?")[0];
|
|
63
|
+
|
|
64
|
+
if (!authed(req, url)) {
|
|
65
|
+
res.writeHead(401, { "content-type": "text/plain" });
|
|
66
|
+
res.end("unauthorized — append ?token=<BLINDFOLD_DASHBOARD_TOKEN>");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (req.method === "GET" && pathname === "/logo.png") {
|
|
71
|
+
try {
|
|
72
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
73
|
+
// Repo layout (dev) first, else the copy bundled with the package.
|
|
74
|
+
const candidates = [
|
|
75
|
+
path.resolve(HERE, "..", "..", "..", "assets", "logo.png"),
|
|
76
|
+
path.resolve(HERE, "..", "assets", "logo.png"),
|
|
77
|
+
];
|
|
78
|
+
const logo = candidates.find((p) => fs.existsSync(p));
|
|
79
|
+
if (logo) {
|
|
80
|
+
res.writeHead(200, { "content-type": "image/png", "cache-control": "max-age=3600" });
|
|
81
|
+
res.end(fs.readFileSync(logo));
|
|
82
|
+
} else {
|
|
83
|
+
// Standalone install with no bundled logo: serve an inline SVG shield
|
|
84
|
+
// so the dashboard always renders a mark (no heavy asset shipped).
|
|
85
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#8b5cf6"/><stop offset="1" stop-color="#ff8c2b"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="#161b22"/><path d="M50 14l28 10v22c0 20-13 33-28 40-15-7-28-20-28-40V24z" fill="url(#g)" opacity="0.92"/><rect x="34" y="46" width="32" height="24" rx="5" fill="#0b0e14"/><rect x="42" y="36" width="16" height="16" rx="8" fill="none" stroke="#0b0e14" stroke-width="5"/></svg>`;
|
|
86
|
+
res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "max-age=3600" });
|
|
87
|
+
res.end(svg);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
res.writeHead(404, { "content-type": "text/plain" }).end("logo not found");
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (req.method === "GET" && (pathname === "/" || pathname === "/index.html")) {
|
|
95
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
96
|
+
res.end(HTML);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (req.method === "GET" && pathname === "/api/events") {
|
|
100
|
+
// Serve only a bounded tail (not the whole log) so a polled dashboard
|
|
101
|
+
// never loads a multi-hundred-MB file into memory on every refresh.
|
|
102
|
+
const limit = Math.min(Number(new URL(url, "http://x").searchParams.get("limit")) || 500, 5000);
|
|
103
|
+
const events = readUsageTail(limit);
|
|
104
|
+
const counters: Record<string, number> = {};
|
|
105
|
+
for (const e of events) counters[e.provider] = (counters[e.provider] ?? 0) + 1;
|
|
106
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
107
|
+
res.end(JSON.stringify({ events, counters, returned: events.length, source: defaultLogPath() }));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (req.method === "GET" && pathname === "/api/sealed") {
|
|
111
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
112
|
+
res.end(JSON.stringify({ entries: readSealed(), source: defaultSealedLogPath() }));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (req.method === "GET" && pathname === "/api/stream") {
|
|
116
|
+
// Server-Sent Events: push a "change" whenever the logs change, so the
|
|
117
|
+
// UI updates near-instantly instead of only on the poll interval.
|
|
118
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
119
|
+
res.write(": connected\n\n");
|
|
120
|
+
const send = () => { try { res.write("event: change\ndata: {}\n\n"); } catch { /* closed */ } };
|
|
121
|
+
const watchers: fs.FSWatcher[] = [];
|
|
122
|
+
for (const p of [defaultLogPath(), defaultSealedLogPath()]) {
|
|
123
|
+
try { watchers.push(fs.watch(path.dirname(p), () => send())); } catch { /* dir missing */ }
|
|
124
|
+
}
|
|
125
|
+
const hb = setInterval(() => { try { res.write(": ping\n\n"); } catch { /* closed */ } }, 15000);
|
|
126
|
+
req.on("close", () => { clearInterval(hb); for (const w of watchers) { try { w.close(); } catch { /* ignore */ } } });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (req.method === "GET" && pathname === "/api/audit/full") {
|
|
130
|
+
// Slow path (live T3 calls) — only hit on demand from the UI button.
|
|
131
|
+
const env = loadBlindfoldEnv();
|
|
132
|
+
const sealed = readSealed();
|
|
133
|
+
const latest = new Map<string, (typeof sealed)[number]>();
|
|
134
|
+
for (const s of sealed) latest.set(s.name, s);
|
|
135
|
+
const results: Array<Record<string, unknown>> = [];
|
|
136
|
+
if (!env.mock) {
|
|
137
|
+
try {
|
|
138
|
+
const { openT3Client } = await import("./t3-client.ts");
|
|
139
|
+
const client = await openT3Client(env);
|
|
140
|
+
try {
|
|
141
|
+
for (const e of latest.values()) {
|
|
142
|
+
const v = await client.verifySecret(e.name);
|
|
143
|
+
results.push({ name: e.name, present: v.present, enclave_len: v.length, ledger_len: e.length, fingerprint: v.fingerprint, ok: v.present && v.length === e.length });
|
|
144
|
+
}
|
|
145
|
+
} finally { await client.close(); }
|
|
146
|
+
} catch (err) {
|
|
147
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
148
|
+
res.end(JSON.stringify({ mock: env.mock, error: (err as Error).message, results: [] }));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
153
|
+
res.end(JSON.stringify({ mock: env.mock, results }));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (req.method === "GET" && pathname === "/api/status") {
|
|
157
|
+
const env = loadBlindfoldEnv();
|
|
158
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
159
|
+
res.end(JSON.stringify({
|
|
160
|
+
mode: env.mock ? "mock" : "real",
|
|
161
|
+
tenant_did: env.did || null,
|
|
162
|
+
tenant_did_short: env.did ? shortenDid(env.did) : null,
|
|
163
|
+
t3_env: env.t3Env,
|
|
164
|
+
contract_version: CONTRACT_VERSION,
|
|
165
|
+
proxy_port: env.port,
|
|
166
|
+
sdk_installed: isSdkInstalled(),
|
|
167
|
+
}));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (req.method === "GET" && pathname === "/api/audit") {
|
|
171
|
+
const sealed = readSealed();
|
|
172
|
+
const envKeys = new Set(readEnvKeyNames());
|
|
173
|
+
// Single pass: keep the latest entry per name (entries are append-order).
|
|
174
|
+
const latestByName = new Map<string, typeof sealed[number]>();
|
|
175
|
+
for (const s of sealed) latestByName.set(s.name, s);
|
|
176
|
+
const exposed = Array.from(latestByName.values())
|
|
177
|
+
.filter((latest) => envKeys.has(latest.name))
|
|
178
|
+
.map((latest) => ({ name: latest.name, length: latest.length, sealed_at: latest.t }));
|
|
179
|
+
const uniqueSealed = latestByName.size;
|
|
180
|
+
const chain = verifyLedgerChain();
|
|
181
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
182
|
+
res.end(JSON.stringify({
|
|
183
|
+
exposed_in_env: exposed,
|
|
184
|
+
env_keys: Array.from(envKeys),
|
|
185
|
+
sealed_count: uniqueSealed,
|
|
186
|
+
ledger_chain: chain, // { ok, total, legacy, firstBrokenIndex }
|
|
187
|
+
}));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if ((req.method === "POST" || req.method === "DELETE") && pathname === "/api/clear") {
|
|
191
|
+
clearUsage();
|
|
192
|
+
res.writeHead(204).end();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
196
|
+
res.end("not found");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return await new Promise<DashboardHandle>((resolve, reject) => {
|
|
200
|
+
server.once("error", reject);
|
|
201
|
+
server.listen(port, "127.0.0.1", () => {
|
|
202
|
+
const { port: bound } = server.address() as AddressInfo;
|
|
203
|
+
resolve({
|
|
204
|
+
url: `http://127.0.0.1:${bound}`,
|
|
205
|
+
port: bound,
|
|
206
|
+
close: () => new Promise<void>((r) => server.close(() => r())),
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function shortenDid(did: string): string {
|
|
213
|
+
const prefix = "did:t3n:";
|
|
214
|
+
if (!did.startsWith(prefix)) return did;
|
|
215
|
+
const tail = did.slice(prefix.length);
|
|
216
|
+
return tail.length <= 14 ? did : `${prefix}${tail.slice(0, 6)}…${tail.slice(-4)}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function readEnvKeyNames(): string[] {
|
|
220
|
+
const ENV_PATH = defaultEnvPath();
|
|
221
|
+
if (!fs.existsSync(ENV_PATH)) return [];
|
|
222
|
+
return fs.readFileSync(ENV_PATH, "utf8")
|
|
223
|
+
.split(/\r?\n/)
|
|
224
|
+
.map((line) => line.trim())
|
|
225
|
+
.filter((line) => line && !line.startsWith("#") && line.includes("="))
|
|
226
|
+
.map((line) => line.split("=", 1)[0]?.trim() ?? "")
|
|
227
|
+
.filter(Boolean);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isSdkInstalled(): boolean {
|
|
231
|
+
try {
|
|
232
|
+
require.resolve?.("@terminal3/t3n-sdk");
|
|
233
|
+
return true;
|
|
234
|
+
} catch {
|
|
235
|
+
try {
|
|
236
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
237
|
+
return fs.existsSync(path.resolve(HERE, "..", "..", "..", "node_modules", "@terminal3", "t3n-sdk"));
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const HTML = `<!DOCTYPE html>
|
|
245
|
+
<html lang="en" data-theme="dark">
|
|
246
|
+
<head>
|
|
247
|
+
<meta charset="utf-8" />
|
|
248
|
+
<title>Blindfold — Dashboard</title>
|
|
249
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
250
|
+
<link rel="icon" type="image/png" href="/logo.png" />
|
|
251
|
+
<style>
|
|
252
|
+
:root {
|
|
253
|
+
--bg:#0b0e14; --bg2:#0e1117; --card:#161b22; --card2:#1c2230; --line:#2a313c;
|
|
254
|
+
--fg:#e6edf3; --dim:#8b949e; --dim2:#6b7280;
|
|
255
|
+
--ok:#3fb950; --warn:#d29922; --bad:#f85149; --info:#58a6ff;
|
|
256
|
+
--accent:#8b5cf6; --accent2:#58a6ff;
|
|
257
|
+
--orange:#ff8c2b; --orange2:#ff6a3d; --brand:#ff8c2b;
|
|
258
|
+
--c1:#8b5cf6; --c2:#58a6ff; --c3:#3fb950; --c4:#ff8c2b; --c5:#f85149; --c6:#ff7b72; --c7:#39c5cf; --c8:#f778ba;
|
|
259
|
+
--line:#333b47; --line-strong:#465061;
|
|
260
|
+
--shadow:0 1px 3px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.18);
|
|
261
|
+
}
|
|
262
|
+
[data-theme="light"] {
|
|
263
|
+
--bg:#f6f8fa; --bg2:#fff; --card:#fff; --card2:#f3f4f6; --line:#d8dee4; --line-strong:#c2cad3;
|
|
264
|
+
--fg:#1f2328; --dim:#57606a; --dim2:#8b949e;
|
|
265
|
+
--shadow:0 1px 2px rgba(0,0,0,.06), 0 8px 24px rgba(0,0,0,.06);
|
|
266
|
+
}
|
|
267
|
+
* { box-sizing:border-box; }
|
|
268
|
+
html,body { max-width:100vw; overflow-x:hidden; }
|
|
269
|
+
body {
|
|
270
|
+
margin:0; padding:20px clamp(12px,4vw,40px);
|
|
271
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Inter",system-ui,sans-serif;
|
|
272
|
+
background:radial-gradient(1200px 600px at 20% -10%, rgba(139,92,246,.10), transparent 60%), var(--bg);
|
|
273
|
+
color:var(--fg); line-height:1.5; transition:background .2s,color .2s;
|
|
274
|
+
}
|
|
275
|
+
a { color:var(--accent2); }
|
|
276
|
+
.topbar { display:flex; justify-content:space-between; align-items:center; gap:14px; flex-wrap:wrap; margin-bottom:18px;
|
|
277
|
+
border:1px solid var(--line-strong); border-radius:16px; padding:16px 20px;
|
|
278
|
+
background:linear-gradient(180deg,var(--card),var(--card2)); box-shadow:var(--shadow); }
|
|
279
|
+
.brandwrap { display:flex; align-items:center; gap:16px; min-width:0; }
|
|
280
|
+
.logo-img { width:clamp(76px,11vw,104px); height:clamp(76px,11vw,104px); border-radius:18px; object-fit:cover; background:var(--card); border:1px solid var(--line-strong); padding:0; box-shadow:var(--shadow); flex:none; animation:logoIn .6s cubic-bezier(.2,.8,.2,1) both; }
|
|
281
|
+
@keyframes logoIn { from{transform:scale(.7) rotate(-8deg);opacity:0;} to{transform:scale(1) rotate(0);opacity:1;} }
|
|
282
|
+
h1 { margin:0; font-size:clamp(24px,4.5vw,34px); font-weight:800; letter-spacing:-.5px; display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; }
|
|
283
|
+
/* wordmark: ribbon-wrap animation — pure CSS, no runtime deps */
|
|
284
|
+
.brand { position:relative; display:inline-block; padding-bottom:6px; overflow:hidden; }
|
|
285
|
+
.brand::after { content:""; position:absolute; left:0; bottom:0; height:3px; width:100%; background:linear-gradient(90deg,var(--orange),var(--orange2)); border-radius:3px; transform-origin:left; transform:scaleX(0); opacity:0; animation:underlineIn .5s 2.2s cubic-bezier(.2,.8,.2,1) both; }
|
|
286
|
+
@keyframes underlineIn { from{transform:scaleX(0);opacity:.3;} to{transform:scaleX(1);opacity:1;} }
|
|
287
|
+
.letter { display:inline-block; position:relative; z-index:1; color:var(--fg); text-shadow:none; animation:protectLetter .35s cubic-bezier(.2,.8,.2,1) both; }
|
|
288
|
+
@keyframes protectLetter { to{ color:#bdc7d4; text-shadow:0 1px 4px rgba(139,92,246,.12); transform:translateY(-0.5px); } }
|
|
289
|
+
[data-theme="light"] .letter { animation:protectLetterLight .35s cubic-bezier(.2,.8,.2,1) both; }
|
|
290
|
+
@keyframes protectLetterLight { to{ color:#5a6478; text-shadow:0 1px 4px rgba(139,92,246,.10); transform:translateY(-0.5px); } }
|
|
291
|
+
.brand > .letter:nth-of-type(5) { animation-delay:0.90s; } .brand > .letter:nth-of-type(4) { animation-delay:0.94s; }
|
|
292
|
+
.brand > .letter:nth-of-type(6) { animation-delay:0.98s; } .brand > .letter:nth-of-type(3) { animation-delay:1.02s; }
|
|
293
|
+
.brand > .letter:nth-of-type(7) { animation-delay:1.06s; } .brand > .letter:nth-of-type(2) { animation-delay:1.10s; }
|
|
294
|
+
.brand > .letter:nth-of-type(8) { animation-delay:1.14s; } .brand > .letter:nth-of-type(1) { animation-delay:1.18s; }
|
|
295
|
+
.brand > .letter:nth-of-type(9) { animation-delay:1.22s; }
|
|
296
|
+
.ribbon { position:absolute; top:-25%; height:150%; width:55%; background:linear-gradient(180deg, rgba(50,54,62,0) 0%, rgba(38,42,50,.6) 12%, rgba(28,30,38,.92) 32%, rgba(22,24,30,1) 48%, rgba(28,30,38,.92) 64%, rgba(38,42,50,.6) 84%, rgba(50,54,62,0) 100%); border-radius:4px; z-index:2; pointer-events:none; will-change:transform; transform-origin:left center; animation:ribbonSweep 1.0s .5s cubic-bezier(.65,0,.35,1) both, ribbonSettle .7s 1.5s cubic-bezier(.34,1.56,.64,1) both, ribbonBreeze 3s 2.2s ease-in-out infinite; }
|
|
297
|
+
@keyframes ribbonSweep { 0%{ transform:translateX(100%) scaleX(.01); } 100%{ transform:translateX(22%) scaleX(1); } }
|
|
298
|
+
@keyframes ribbonSettle { 0%{ transform:translateX(22%) scaleX(1); } 100%{ transform:translateX(-6%) scaleX(.65) rotate(-1.5deg); } }
|
|
299
|
+
@keyframes ribbonBreeze { 0%,100%{ transform:translateX(-6%) scaleX(.65) rotate(-1.5deg); } 50%{ transform:translateX(-4%) scaleX(.68) rotate(.8deg); } }
|
|
300
|
+
@keyframes logoSeal { 0%{ filter:brightness(1) saturate(1); } 25%{ filter:brightness(1.06) saturate(1.1); box-shadow:0 0 0 1px rgba(139,92,246,.15); } 60%{ filter:brightness(1.03) saturate(1.05); } 100%{ filter:brightness(1) saturate(1); } }
|
|
301
|
+
.logo-img { animation:logoIn .6s cubic-bezier(.2,.8,.2,1) both, logoSeal 2.2s .6s ease both; }
|
|
302
|
+
.eyebrow { font-size:.42em; font-weight:600; color:var(--dim); text-transform:uppercase; letter-spacing:1.5px; }
|
|
303
|
+
.tagline { font-size:12px; color:var(--dim); margin-top:7px; display:flex; align-items:center; gap:6px; }
|
|
304
|
+
.tagline .lock { color:var(--orange); display:inline-block; animation:clack .5s .45s cubic-bezier(.3,1.4,.5,1) both; }
|
|
305
|
+
@keyframes clack { from{transform:rotate(-35deg) translateY(-2px);opacity:0;} to{transform:rotate(0) translateY(0);opacity:1;} }
|
|
306
|
+
/* --- Distinctive "decrypt → seal" wordmark animation (JS-driven) --- */
|
|
307
|
+
.letter.scrambling { color:var(--accent); font-family:ui-monospace,SFMono-Regular,Menlo,monospace; opacity:.92; }
|
|
308
|
+
.letter.sealed { animation:sealSnap .55s cubic-bezier(.2,1.4,.4,1) both; }
|
|
309
|
+
@keyframes sealSnap {
|
|
310
|
+
0% { color:var(--accent); text-shadow:0 0 14px rgba(139,92,246,.95),0 0 4px rgba(139,92,246,.8); transform:translateY(-3px) scale(1.18); }
|
|
311
|
+
55% { color:var(--fg); text-shadow:0 0 8px rgba(139,92,246,.5); transform:translateY(1px) scale(.97); }
|
|
312
|
+
100% { color:var(--fg); text-shadow:none; transform:none; }
|
|
313
|
+
}
|
|
314
|
+
/* seal sweep: a vertical light bar that passes across the word once as it seals */
|
|
315
|
+
.sealbar { position:absolute; top:-22%; height:144%; width:16px; left:-8%; z-index:3; pointer-events:none; border-radius:9px; opacity:0;
|
|
316
|
+
background:linear-gradient(90deg,transparent, rgba(139,92,246,.55) 45%, rgba(88,166,255,.35) 60%, transparent); filter:blur(2px); }
|
|
317
|
+
.sealbar.run { animation:sealSweep 1.15s cubic-bezier(.5,0,.3,1) both; }
|
|
318
|
+
@keyframes sealSweep { 0%{ left:-8%; opacity:0;} 12%{opacity:1;} 86%{opacity:1;} 100%{ left:104%; opacity:0;} }
|
|
319
|
+
@media (prefers-reduced-motion:reduce){ .logo-img,.letter,.ribbon,.brand::after,.tagline .lock,.sealbar{ animation:none!important; } .ribbon,.sealbar{ display:none; } .letter{ color:var(--fg)!important; transform:none!important; text-shadow:none!important; } .brand::after{ transform:scaleX(1)!important; opacity:1!important; } }
|
|
320
|
+
.tagline b { color:var(--fg); font-weight:600; }
|
|
321
|
+
.tags { display:flex; gap:6px; margin-top:8px; flex-wrap:wrap; align-items:center; }
|
|
322
|
+
.tag { display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--dim); background:var(--card); border:1px solid var(--line); border-radius:20px; padding:3px 10px; max-width:100%; }
|
|
323
|
+
.tag.live { color:var(--ok); border-color:rgba(63,185,80,.45); background:rgba(63,185,80,.08); font-weight:600; }
|
|
324
|
+
.tag.orange { color:var(--orange); border-color:rgba(255,140,43,.45); background:rgba(255,140,43,.08); }
|
|
325
|
+
.tag.path { color:var(--dim); }
|
|
326
|
+
.tag.path code { background:none; padding:0; max-width:min(52vw,420px); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:inline-block; vertical-align:bottom; }
|
|
327
|
+
.live-dot { width:7px; height:7px; border-radius:50%; background:var(--ok); box-shadow:0 0 0 0 rgba(63,185,80,.6); animation:pulse 2s infinite; }
|
|
328
|
+
@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(63,185,80,.5);} 70%{box-shadow:0 0 0 6px rgba(63,185,80,0);} 100%{box-shadow:0 0 0 0 rgba(63,185,80,0);} }
|
|
329
|
+
.controls { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
330
|
+
.controls label { font-size:12px; color:var(--dim); display:flex; align-items:center; gap:4px; }
|
|
331
|
+
select,button { background:var(--card); color:var(--fg); border:1px solid var(--line-strong); border-radius:8px; padding:6px 10px; font-size:12px; cursor:pointer; transition:border-color .15s,background .15s,transform .05s; }
|
|
332
|
+
button:hover,select:hover { border-color:var(--orange); color:var(--orange); }
|
|
333
|
+
button:active { transform:translateY(1px); }
|
|
334
|
+
.copybtn { background:transparent; border:1px solid var(--line); border-radius:6px; padding:2px 9px; font-size:11px; cursor:pointer; color:var(--dim); transition:border-color .15s,color .15s; }
|
|
335
|
+
.copybtn:hover { border-color:var(--orange); color:var(--orange); }
|
|
336
|
+
.copybtn.done { color:var(--ok); border-color:var(--ok); }
|
|
337
|
+
.copybtn.err { color:var(--bad); border-color:var(--bad); }
|
|
338
|
+
.copybtn.ref { color:var(--orange); border-color:rgba(255,140,43,.4); }
|
|
339
|
+
.copybtn.ref:hover { background:rgba(255,140,43,.08); }
|
|
340
|
+
code.sentinel { color:var(--orange); background:rgba(255,140,43,.10); border:1px solid rgba(255,140,43,.22); font-weight:600; }
|
|
341
|
+
.cmdname { color:var(--accent2); font-weight:600; }
|
|
342
|
+
@media (max-width:620px){
|
|
343
|
+
.topbar { flex-direction:column; align-items:stretch; }
|
|
344
|
+
.controls { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
|
|
345
|
+
.controls label { width:100%; }
|
|
346
|
+
.controls label select { flex:1; width:100%; }
|
|
347
|
+
.controls > button { width:100%; }
|
|
348
|
+
}
|
|
349
|
+
.grid { display:grid; gap:12px; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); margin-bottom:14px; }
|
|
350
|
+
.two-col { display:grid; gap:12px; grid-template-columns:1fr 1fr; margin-bottom:4px; }
|
|
351
|
+
@media (max-width:860px){ .two-col{ grid-template-columns:1fr; } }
|
|
352
|
+
.card {
|
|
353
|
+
background:linear-gradient(180deg,var(--card),var(--card2)); border:1px solid var(--line);
|
|
354
|
+
border-radius:12px; padding:14px 16px; min-width:0; box-shadow:var(--shadow); position:relative; overflow:hidden;
|
|
355
|
+
transition:transform .18s cubic-bezier(.2,.8,.2,1), border-color .18s, box-shadow .18s;
|
|
356
|
+
}
|
|
357
|
+
.card:hover { transform:translateY(-3px); border-color:var(--line-strong); box-shadow:0 6px 22px rgba(0,0,0,.28), 0 0 0 1px rgba(139,92,246,.08); }
|
|
358
|
+
/* KPI cards get a soft accent glow that brightens on hover */
|
|
359
|
+
.card.kpi::before { content:""; position:absolute; left:0; top:0; bottom:0; width:3px; background:var(--kpi,var(--accent)); box-shadow:0 0 12px var(--kpi,var(--accent)); opacity:.55; transition:opacity .18s; }
|
|
360
|
+
.card.kpi:hover::before { opacity:1; }
|
|
361
|
+
.card .label { font-size:10.5px; color:var(--dim); text-transform:uppercase; letter-spacing:.6px; display:flex; align-items:center; gap:5px; }
|
|
362
|
+
.card .value { font-size:clamp(20px,3.5vw,26px); font-weight:700; margin-top:5px; overflow-wrap:anywhere; }
|
|
363
|
+
.card .sub2 { font-size:11.5px; color:var(--dim); margin-top:3px; overflow-wrap:anywhere; }
|
|
364
|
+
.trend { font-size:11px; font-weight:600; padding:1px 6px; border-radius:20px; margin-left:6px; }
|
|
365
|
+
.trend.up { color:var(--ok); background:rgba(63,185,80,.12); }
|
|
366
|
+
.trend.down { color:var(--bad); background:rgba(248,81,73,.12); }
|
|
367
|
+
.trend.flat { color:var(--dim); background:rgba(139,148,158,.12); }
|
|
368
|
+
.section-title { font-size:11px; color:var(--dim); text-transform:uppercase; letter-spacing:.6px; margin:22px 0 8px; display:flex; align-items:center; gap:8px; cursor:pointer; user-select:none; }
|
|
369
|
+
.section-title .chev { transition:transform .15s; font-size:9px; color:var(--dim2); }
|
|
370
|
+
.section-title.collapsed .chev { transform:rotate(-90deg); }
|
|
371
|
+
.collapsed + * , .collapsed + * + * { display:none !important; }
|
|
372
|
+
.scroll { background:var(--card); border:1px solid var(--line); border-radius:12px; overflow-x:auto; -webkit-overflow-scrolling:touch; box-shadow:var(--shadow); }
|
|
373
|
+
table { width:100%; border-collapse:collapse; min-width:520px; }
|
|
374
|
+
th,td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--line); font-size:13px; vertical-align:top; }
|
|
375
|
+
th { color:var(--dim); font-weight:600; text-transform:uppercase; font-size:10.5px; letter-spacing:.5px; white-space:nowrap; cursor:pointer; }
|
|
376
|
+
th:hover { color:var(--fg); }
|
|
377
|
+
tr:last-child td { border-bottom:none; }
|
|
378
|
+
tbody tr:hover { background:rgba(139,92,246,.05); }
|
|
379
|
+
code { background:rgba(127,127,127,.12); padding:2px 5px; border-radius:4px; font-size:12px; overflow-wrap:anywhere; }
|
|
380
|
+
.pill { display:inline-block; padding:2px 8px; border-radius:20px; font-size:11px; font-weight:600; }
|
|
381
|
+
.pill-ok{background:rgba(63,185,80,.15);color:var(--ok);} .pill-bad{background:rgba(248,81,73,.15);color:var(--bad);}
|
|
382
|
+
.pill-warn{background:rgba(210,153,34,.15);color:var(--warn);} .pill-info{background:rgba(88,166,255,.15);color:var(--info);}
|
|
383
|
+
.pill-mock{background:rgba(139,92,246,.15);color:var(--accent);} .pill-real{background:rgba(63,185,80,.15);color:var(--ok);}
|
|
384
|
+
.pill-dim{background:rgba(127,127,127,.12);color:var(--dim);}
|
|
385
|
+
.empty { background:var(--card); border:1px dashed var(--line); border-radius:12px; padding:22px; text-align:center; color:var(--dim); }
|
|
386
|
+
.alert { border-radius:12px; padding:12px 16px; margin-bottom:14px; border:1px solid; box-shadow:var(--shadow); }
|
|
387
|
+
.alert .head { font-weight:700; margin-bottom:4px; }
|
|
388
|
+
.alert.ok { background:rgba(63,185,80,.07); border-color:rgba(63,185,80,.3); color:var(--ok); }
|
|
389
|
+
.footer { margin-top:24px; padding:14px 16px; background:var(--card); border:1px solid var(--line); border-radius:12px; font-size:12px; color:var(--dim); }
|
|
390
|
+
.footer b { color:var(--fg); }
|
|
391
|
+
/* donut + legend */
|
|
392
|
+
.donut-wrap { display:flex; align-items:center; gap:18px; flex-wrap:wrap; }
|
|
393
|
+
.legend { display:flex; flex-direction:column; gap:5px; font-size:12px; flex:1; min-width:120px; }
|
|
394
|
+
.legend .li { display:flex; align-items:center; gap:7px; cursor:default; }
|
|
395
|
+
.legend .dot { width:10px; height:10px; border-radius:3px; flex:none; }
|
|
396
|
+
.legend .li .n { margin-left:auto; color:var(--dim); }
|
|
397
|
+
/* segmented bar */
|
|
398
|
+
.segbar { display:flex; height:22px; border-radius:7px; overflow:hidden; background:rgba(127,127,127,.1); }
|
|
399
|
+
.segbar > div { transition:width .3s; }
|
|
400
|
+
/* horizontal bars */
|
|
401
|
+
.bar-row { display:flex; align-items:center; gap:8px; margin:7px 0; font-size:12px; }
|
|
402
|
+
.bar-row .name { width:120px; color:var(--dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
403
|
+
.bar-row .track { flex:1; background:rgba(127,127,127,.1); border-radius:5px; height:16px; overflow:hidden; }
|
|
404
|
+
.bar-row .fill { height:100%; border-radius:5px; transition:width .3s; }
|
|
405
|
+
.bar-row .n { width:58px; text-align:right; }
|
|
406
|
+
/* sparkline */
|
|
407
|
+
.spark { display:flex; align-items:flex-end; gap:1px; height:60px; background:var(--card); border:1px solid var(--line); border-radius:12px; padding:8px; box-shadow:var(--shadow); }
|
|
408
|
+
.spark > .b { background:linear-gradient(180deg,var(--c2),var(--c1)); flex:1 1 0; min-width:1px; border-radius:2px 2px 0 0; transition:height .2s; }
|
|
409
|
+
.spark > .b.zero { background:var(--line); min-height:1px; }
|
|
410
|
+
.stat-row { display:flex; gap:14px; flex-wrap:wrap; font-size:12px; color:var(--dim); margin-top:7px; }
|
|
411
|
+
.stat-row span { color:var(--fg); font-weight:600; }
|
|
412
|
+
.filter { background:var(--card); color:var(--fg); border:1px solid var(--line); border-radius:8px; padding:5px 9px; font-size:12px; margin-left:auto; min-width:180px; }
|
|
413
|
+
#tooltip { position:fixed; pointer-events:none; background:var(--bg2); border:1px solid var(--line); border-radius:8px; padding:6px 9px; font-size:12px; box-shadow:var(--shadow); opacity:0; transition:opacity .1s; z-index:50; white-space:nowrap; }
|
|
414
|
+
@media (max-width:560px){ body{padding:12px;} .grid{grid-template-columns:1fr 1fr;gap:8px;} th,td{padding:7px 9px;font-size:12px;} .filter{min-width:120px;} }
|
|
415
|
+
</style>
|
|
416
|
+
</head>
|
|
417
|
+
<body>
|
|
418
|
+
<div class="topbar">
|
|
419
|
+
<div class="brandwrap">
|
|
420
|
+
<img id="logo" class="logo-img" alt="Blindfold" />
|
|
421
|
+
<div>
|
|
422
|
+
<h1><span class="brand"><span class="letter">B</span><span class="letter">l</span><span class="letter">i</span><span class="letter">n</span><span class="letter">d</span><span class="letter">f</span><span class="letter">o</span><span class="letter">l</span><span class="letter">d</span><div class="ribbon" id="brand-ribbon"></div><div class="sealbar" id="brand-sealbar"></div></span><span class="eyebrow">Dashboard</span></h1>
|
|
423
|
+
<div class="tagline"><span class="lock">🔒</span> Secrets sealed in a <b>TEE</b> — keys you can't leak</div>
|
|
424
|
+
<div class="tags" id="tags"></div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
<div class="controls">
|
|
428
|
+
<label>range
|
|
429
|
+
<select id="range" onchange="setRange()">
|
|
430
|
+
<option value="15">15m</option><option value="60" selected>1h</option>
|
|
431
|
+
<option value="1440">24h</option><option value="0">all</option>
|
|
432
|
+
</select>
|
|
433
|
+
</label>
|
|
434
|
+
<label>refresh
|
|
435
|
+
<select id="refresh" onchange="setRefresh()">
|
|
436
|
+
<option value="2000">2s</option><option value="5000">5s</option>
|
|
437
|
+
<option value="10000">10s</option><option value="0">paused</option>
|
|
438
|
+
</select>
|
|
439
|
+
</label>
|
|
440
|
+
<button onclick="toggleTheme()" id="themeBtn" title="Toggle theme">🌙</button>
|
|
441
|
+
<button onclick="exportJson()">⬇ Export</button>
|
|
442
|
+
<button onclick="clearLog()">Clear</button>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<div id="alert-banner"></div>
|
|
447
|
+
|
|
448
|
+
<div class="grid" id="kpis"></div>
|
|
449
|
+
|
|
450
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>Security posture
|
|
451
|
+
<button style="margin-left:auto" onclick="event.stopPropagation();runFullAudit()">🔎 Run full audit (live)</button>
|
|
452
|
+
</div>
|
|
453
|
+
<div class="grid" id="posture-cards"></div>
|
|
454
|
+
<div id="full-audit"></div>
|
|
455
|
+
|
|
456
|
+
<div class="two-col">
|
|
457
|
+
<div>
|
|
458
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>Providers</div>
|
|
459
|
+
<div class="card" id="provider-chart"></div>
|
|
460
|
+
</div>
|
|
461
|
+
<div>
|
|
462
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>Status codes</div>
|
|
463
|
+
<div class="card" id="status-chart"></div>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<div class="two-col">
|
|
468
|
+
<div>
|
|
469
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>p95 latency trend</div>
|
|
470
|
+
<div class="card" id="trend-latency"></div>
|
|
471
|
+
</div>
|
|
472
|
+
<div>
|
|
473
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>Success rate trend</div>
|
|
474
|
+
<div class="card" id="trend-success"></div>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>Requests per minute</div>
|
|
479
|
+
<div class="spark" id="spark"></div>
|
|
480
|
+
<div class="stat-row" id="spark-stats"></div>
|
|
481
|
+
|
|
482
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>System</div>
|
|
483
|
+
<div class="grid" id="status-cards"></div>
|
|
484
|
+
|
|
485
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>Sealed keys (metadata only)</div>
|
|
486
|
+
<div id="sealed-table-wrap"></div>
|
|
487
|
+
|
|
488
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>Per-secret usage</div>
|
|
489
|
+
<div id="per-secret-wrap"></div>
|
|
490
|
+
|
|
491
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>Blindfold commands</div>
|
|
492
|
+
<div id="commands-wrap"></div>
|
|
493
|
+
|
|
494
|
+
<div class="section-title" onclick="toggleSection(this)"><span class="chev">▼</span>Recent activity
|
|
495
|
+
<input id="filter" class="filter" placeholder="filter…" oninput="renderTable(rangeFiltered())" onclick="event.stopPropagation()" />
|
|
496
|
+
</div>
|
|
497
|
+
<div id="table-wrap"></div>
|
|
498
|
+
|
|
499
|
+
<div class="footer"><b>Privacy by design.</b> Metadata only — no request/response bodies, no header values, no secret values. See <code>usage-log.ts</code> + <code>sealed-ledger.ts</code>.</div>
|
|
500
|
+
<div id="tooltip"></div>
|
|
501
|
+
|
|
502
|
+
<script>
|
|
503
|
+
/* Distinctive wordmark: each letter scrambles through cipher glyphs, then
|
|
504
|
+
snap-seals into place — mirroring how a real key becomes __BLINDFOLD__.
|
|
505
|
+
Purely decorative; respects prefers-reduced-motion. */
|
|
506
|
+
(function sealWordmark(){
|
|
507
|
+
try {
|
|
508
|
+
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion:reduce)').matches) return;
|
|
509
|
+
var letters = Array.prototype.slice.call(document.querySelectorAll('.brand > .letter'));
|
|
510
|
+
if (!letters.length) return;
|
|
511
|
+
var GLYPHS = '▓█▒░#@$%&*/\\<>=+·:';
|
|
512
|
+
var finals = letters.map(function(el){ return el.textContent; });
|
|
513
|
+
letters.forEach(function(el){ el.classList.add('scrambling'); });
|
|
514
|
+
var bar = document.getElementById('brand-sealbar');
|
|
515
|
+
if (bar) { bar.classList.add('run'); }
|
|
516
|
+
var start = performance.now();
|
|
517
|
+
var settleAt = letters.map(function(_, i){ return 260 + i * 95; }); // staggered lock, left→right
|
|
518
|
+
function frame(now){
|
|
519
|
+
var t = now - start, done = 0;
|
|
520
|
+
letters.forEach(function(el, i){
|
|
521
|
+
if (el.dataset.sealed) { done++; return; }
|
|
522
|
+
if (t >= settleAt[i]) {
|
|
523
|
+
el.textContent = finals[i];
|
|
524
|
+
el.classList.remove('scrambling');
|
|
525
|
+
el.classList.add('sealed');
|
|
526
|
+
el.dataset.sealed = '1';
|
|
527
|
+
done++;
|
|
528
|
+
} else if (t % 60 < 20) {
|
|
529
|
+
el.textContent = GLYPHS[(Math.random() * GLYPHS.length) | 0];
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
if (done < letters.length) requestAnimationFrame(frame);
|
|
533
|
+
}
|
|
534
|
+
requestAnimationFrame(frame);
|
|
535
|
+
} catch (e) { /* animation must never break the dashboard */ }
|
|
536
|
+
})();
|
|
537
|
+
var PALETTE=['#8b5cf6','#58a6ff','#3fb950','#d29922','#f85149','#ff7b72','#39c5cf','#f778ba'];
|
|
538
|
+
function hashIdx(s){var h=0;for(var i=0;i<s.length;i++)h=(h*31+s.charCodeAt(i))>>>0;return h%PALETTE.length;}
|
|
539
|
+
function colorFor(name){return PALETTE[hashIdx(String(name||'?'))];}
|
|
540
|
+
var TOK=new URLSearchParams(location.search).get('token');
|
|
541
|
+
function api(p){return TOK?p+(p.indexOf('?')>=0?'&':'?')+'token='+encodeURIComponent(TOK):p;}
|
|
542
|
+
function esc(s){return String(s).replace(/[&<>"']/g,function(c){return({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c];});}
|
|
543
|
+
function timeAgo(iso){var ms=Date.now()-new Date(iso).getTime();if(ms<1000)return'just now';var s=(ms/1000)|0;if(s<60)return s+'s ago';var m=(s/60)|0;if(m<60)return m+'m ago';var h=(m/60)|0;if(h<24)return h+'h ago';return((h/24)|0)+'d ago';}
|
|
544
|
+
function pillStatus(s){if(s>=200&&s<300)return'<span class="pill pill-ok">'+s+'</span>';if(s>=400)return'<span class="pill pill-bad">'+s+'</span>';return'<span class="pill pill-warn">'+s+'</span>';}
|
|
545
|
+
function pillMode(m){return'<span class="pill pill-'+m+'">'+m+'</span>';}
|
|
546
|
+
function pct(arr,p){if(!arr.length)return 0;var s=arr.slice().sort(function(a,b){return a-b;});return s[Math.min(s.length-1,Math.floor(p/100*s.length))];}
|
|
547
|
+
|
|
548
|
+
// shared tooltip
|
|
549
|
+
var TIP=null;
|
|
550
|
+
function tip(ev,html){if(!TIP)TIP=document.getElementById('tooltip');TIP.innerHTML=html;TIP.style.opacity='1';TIP.style.left=(ev.clientX+12)+'px';TIP.style.top=(ev.clientY+12)+'px';}
|
|
551
|
+
function tipHide(){if(TIP)TIP.style.opacity='0';}
|
|
552
|
+
|
|
553
|
+
// state
|
|
554
|
+
window._events=[]; window._sort={key:'t',dir:-1};
|
|
555
|
+
function rangeMin(){return Number(document.getElementById('range').value);}
|
|
556
|
+
function rangeFiltered(){var m=rangeMin();if(!m)return window._events;var cut=Date.now()-m*60000;return window._events.filter(function(e){return new Date(e.t).getTime()>=cut;});}
|
|
557
|
+
|
|
558
|
+
function toggleTheme(){var h=document.documentElement;var d=h.getAttribute('data-theme')==='dark';h.setAttribute('data-theme',d?'light':'dark');document.getElementById('themeBtn').textContent=d?'☀':'🌙';try{localStorage.setItem('bf-theme',d?'light':'dark');}catch(e){}}
|
|
559
|
+
(function(){try{var t=localStorage.getItem('bf-theme');if(t){document.documentElement.setAttribute('data-theme',t);document.getElementById('themeBtn').textContent=t==='light'?'☀':'🌙';}}catch(e){}})();
|
|
560
|
+
function toggleSection(el){el.classList.toggle('collapsed');}
|
|
561
|
+
function setRange(){poll();}
|
|
562
|
+
function setRefresh(){var v=Number(document.getElementById('refresh').value);if(window._timer)clearInterval(window._timer);if(v>0)window._timer=setInterval(poll,v);}
|
|
563
|
+
function exportJson(){var b=new Blob([JSON.stringify(window._events||[],null,2)],{type:'application/json'});var a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='blindfold-usage-'+new Date().toISOString().slice(0,19).replace(/[:T]/g,'-')+'.json';a.click();}
|
|
564
|
+
async function clearLog(){await fetch(api('/api/clear'),{method:'POST'});poll();}
|
|
565
|
+
|
|
566
|
+
async function poll(){
|
|
567
|
+
try{
|
|
568
|
+
var r=await Promise.all([fetch(api('/api/status')).then(function(x){return x.json();}),fetch(api('/api/sealed')).then(function(x){return x.json();}),fetch(api('/api/events')).then(function(x){return x.json();}),fetch(api('/api/audit')).then(function(x){return x.json();})]);
|
|
569
|
+
var statusR=r[0],sealedR=r[1],eventsR=r[2],auditR=r[3];
|
|
570
|
+
window._events=eventsR.events||[];
|
|
571
|
+
var ev=rangeFiltered();
|
|
572
|
+
document.getElementById('tags').innerHTML=
|
|
573
|
+
'<span class="tag live"><span class="live-dot"></span>live</span>'
|
|
574
|
+
+'<span class="tag orange">'+new Date().toLocaleTimeString()+'</span>'
|
|
575
|
+
+'<span class="tag">'+(statusR.mode==='real'?'REAL · '+esc(statusR.t3_env||''):'MOCK')+'</span>'
|
|
576
|
+
+'<span class="tag path">usage <code title="'+esc(eventsR.source)+'">'+esc(eventsR.source)+'</code></span>';
|
|
577
|
+
renderAlert(auditR,ev);
|
|
578
|
+
renderKpis(ev,auditR,statusR);
|
|
579
|
+
renderPosture(auditR,statusR);
|
|
580
|
+
renderStatus(statusR);
|
|
581
|
+
renderDonut(ev);
|
|
582
|
+
renderStatusBar(ev);
|
|
583
|
+
renderTrends(ev);
|
|
584
|
+
renderSpark(ev);
|
|
585
|
+
renderSealed(sealedR.entries||[]);
|
|
586
|
+
renderPerSecret(ev);
|
|
587
|
+
renderTable(ev);
|
|
588
|
+
}catch(e){}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function trendBadge(cur,prev){if(prev===0&&cur===0)return'<span class="trend flat">—</span>';if(prev===0)return'<span class="trend up">new</span>';var d=Math.round((cur-prev)/prev*100);if(Math.abs(d)<2)return'<span class="trend flat">0%</span>';return'<span class="trend '+(d>0?'up':'down')+'">'+(d>0?'▲':'▼')+Math.abs(d)+'%</span>';}
|
|
592
|
+
|
|
593
|
+
function renderKpis(ev,a,s){
|
|
594
|
+
var half=ev.slice().sort(function(x,y){return new Date(x.t)-new Date(y.t);});
|
|
595
|
+
var mid=Math.floor(half.length/2);var prevHalf=half.slice(0,mid),curHalf=half.slice(mid);
|
|
596
|
+
function ok(arr){return arr.filter(function(e){return e.status>=200&&e.status<300;}).length;}
|
|
597
|
+
var total=ev.length;
|
|
598
|
+
var succ=total?Math.round(ok(ev)/total*100):0;
|
|
599
|
+
var prevSucc=prevHalf.length?Math.round(ok(prevHalf)/prevHalf.length*100):0;
|
|
600
|
+
var curSucc=curHalf.length?Math.round(ok(curHalf)/curHalf.length*100):0;
|
|
601
|
+
var p95=pct(ev.map(function(e){return e.latency_ms||0;}),95);
|
|
602
|
+
var p95p=pct(prevHalf.map(function(e){return e.latency_ms||0;}),95);
|
|
603
|
+
var p95c=pct(curHalf.map(function(e){return e.latency_ms||0;}),95);
|
|
604
|
+
var sealed=(a&&a.sealed_count)||0;
|
|
605
|
+
var posture=postureScore(a,s).score;
|
|
606
|
+
var cards=[
|
|
607
|
+
['Requests',total,'',trendBadge(curHalf.length,prevHalf.length),'var(--c1)'],
|
|
608
|
+
['Success rate',succ+'%',ok(ev)+'/'+total+' 2xx',trendBadge(curSucc,prevSucc),succ>=95?'var(--ok)':succ>=80?'var(--warn)':'var(--bad)'],
|
|
609
|
+
['p95 latency',p95+' ms',ev.length+' samples',trendBadge(p95c,p95p),'var(--c2)'],
|
|
610
|
+
['Sealed secrets',sealed,'in the enclave','','var(--c7)'],
|
|
611
|
+
['Posture',posture+'<span style="font-size:13px;color:var(--dim)">/100</span>','security score','',posture>=90?'var(--ok)':posture>=60?'var(--warn)':'var(--bad)']
|
|
612
|
+
];
|
|
613
|
+
document.getElementById('kpis').innerHTML=cards.map(function(c){return '<div class="card kpi" style="--kpi:'+c[4]+'"><div class="label">'+c[0]+'</div><div class="value">'+c[1]+c[3]+'</div><div class="sub2">'+c[2]+'</div></div>';}).join('');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function postureScore(a,s){var exposed=((a&&a.exposed_in_env)||[]).length;var chain=(a&&a.ledger_chain)||{ok:true,total:0};var sealed=(a&&a.sealed_count)||0;var score=100;var notes=[];if(exposed>0){score-=30;notes.push(exposed+' key(s) in .env');}if(chain.total>0&&!chain.ok){score-=40;notes.push('ledger TAMPERED');}if(s&&s.mode!=='real'){score-=10;notes.push('MOCK mode');}if(s&&!s.sdk_installed){score-=10;notes.push('SDK missing');}if(sealed===0){score-=10;notes.push('nothing sealed');}return{score:Math.max(0,score),notes:notes};}
|
|
617
|
+
|
|
618
|
+
function renderAlert(a,ev){
|
|
619
|
+
var msgs=[];var chain=(a&&a.ledger_chain)||{};
|
|
620
|
+
if(chain.total>0&&!chain.ok)msgs.push(['bad','🔴 Ledger TAMPERED — a sealed-keys line was edited or removed']);
|
|
621
|
+
if(((a&&a.exposed_in_env)||[]).length>0)msgs.push(['warn','🟠 '+a.exposed_in_env.length+' sealed key(s) still in .env — delete them']);
|
|
622
|
+
var recent=(ev||[]).filter(function(e){return Date.now()-new Date(e.t).getTime()<300000;});
|
|
623
|
+
var errs=recent.filter(function(e){return(e.status||0)>=400;}).length;
|
|
624
|
+
if(recent.length>=5&&errs/recent.length>0.3)msgs.push(['warn','🟠 '+Math.round(errs/recent.length*100)+'% errors in the last 5 min ('+errs+'/'+recent.length+')']);
|
|
625
|
+
var el=document.getElementById('alert-banner');
|
|
626
|
+
if(!msgs.length){el.innerHTML='';return;}
|
|
627
|
+
var bad=msgs.some(function(m){return m[0]==='bad';});
|
|
628
|
+
el.innerHTML='<div class="alert" style="background:'+(bad?'rgba(248,81,73,.08)':'rgba(210,153,34,.08)')+';border-color:'+(bad?'rgba(248,81,73,.4)':'rgba(210,153,34,.4)')+';color:'+(bad?'var(--bad)':'var(--warn)')+'"><div class="head">⚠ Attention</div>'+msgs.map(function(m){return esc(m[1]);}).join('<br/>')+'</div>';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function renderPosture(a,s){
|
|
632
|
+
var p=postureScore(a,s);var exposed=((a&&a.exposed_in_env)||[]).length;var chain=(a&&a.ledger_chain)||{ok:true,total:0,legacy:0};
|
|
633
|
+
var chainTxt=chain.total===0?'empty':(chain.ok?'intact':'TAMPERED');
|
|
634
|
+
var cards=[
|
|
635
|
+
['.env leak surface',exposed===0?'0 ✅':exposed+' ⚠','sealed keys still in .env',exposed===0?'var(--ok)':'var(--warn)'],
|
|
636
|
+
['Ledger integrity',(chainTxt==='TAMPERED'?'✖ ':chainTxt==='intact'?'✅ ':'')+chainTxt,(chain.legacy||0)+' legacy entries',chainTxt==='TAMPERED'?'var(--bad)':'var(--ok)'],
|
|
637
|
+
['Mode',s&&s.mode==='real'?'REAL':'MOCK',s&&s.mode==='real'?'connected to '+esc(s.t3_env||''):'BLINDFOLD_MOCK=1','var(--c1)'],
|
|
638
|
+
['Issues',p.notes.length,p.notes.length?esc(p.notes.join(' · ')):'all clear',p.notes.length?'var(--warn)':'var(--ok)']
|
|
639
|
+
];
|
|
640
|
+
document.getElementById('posture-cards').innerHTML=cards.map(function(c){return '<div class="card kpi" style="--kpi:'+c[3]+'"><div class="label">'+c[0]+'</div><div class="value">'+c[1]+'</div><div class="sub2">'+c[2]+'</div></div>';}).join('');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function renderStatus(s){
|
|
644
|
+
var cards=[
|
|
645
|
+
[s.mode==='real'?'REAL':'MOCK','Mode',s.mode==='real'?'T3 '+esc(s.t3_env):'mock'],
|
|
646
|
+
[esc(s.contract_version||'—'),'Contract','blindfold-proxy'],
|
|
647
|
+
[esc(s.tenant_did_short||'(none)'),'Tenant',s.tenant_did?'full in title':'set creds'],
|
|
648
|
+
[s.proxy_port,'Proxy port','127.0.0.1:'+s.proxy_port],
|
|
649
|
+
[s.sdk_installed?'✓':'✗','SDK',s.sdk_installed?'installed':'missing']
|
|
650
|
+
];
|
|
651
|
+
document.getElementById('status-cards').innerHTML=cards.map(function(c){return '<div class="card"><div class="label">'+esc(c[1])+'</div><div class="value">'+esc(c[0])+'</div><div class="sub2">'+esc(c[2])+'</div></div>';}).join('');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function donutSvg(rows,total){
|
|
655
|
+
var r=42,c=2*Math.PI*r,off=0,seg='';
|
|
656
|
+
rows.forEach(function(row){var frac=total?row[1]/total:0;var len=frac*c;var col=colorFor(row[0]);seg+='<circle cx="60" cy="60" r="'+r+'" fill="none" stroke="'+col+'" stroke-width="16" stroke-dasharray="'+len.toFixed(2)+' '+(c-len).toFixed(2)+'" stroke-dashoffset="'+(-off).toFixed(2)+'" transform="rotate(-90 60 60)"><title>'+esc(row[0])+': '+row[1]+'</title></circle>';off+=len;});
|
|
657
|
+
return '<svg viewBox="0 0 120 120" style="width:120px;height:120px;flex:none">'+seg+'<text x="60" y="56" text-anchor="middle" fill="var(--fg)" font-size="22" font-weight="700">'+total+'</text><text x="60" y="74" text-anchor="middle" fill="var(--dim)" font-size="9">requests</text></svg>';
|
|
658
|
+
}
|
|
659
|
+
function renderDonut(ev){
|
|
660
|
+
var by={};ev.forEach(function(e){var k=e.provider||'(unknown)';by[k]=(by[k]||0)+1;});
|
|
661
|
+
var rows=Object.keys(by).map(function(k){return[k,by[k]];}).sort(function(a,b){return b[1]-a[1];});
|
|
662
|
+
var el=document.getElementById('provider-chart');
|
|
663
|
+
if(!rows.length){el.innerHTML='<div style="color:var(--dim);font-size:13px">No traffic yet.</div>';return;}
|
|
664
|
+
var legend=rows.map(function(row){return '<div class="li"><span class="dot" style="background:'+colorFor(row[0])+'"></span>'+esc(row[0])+'<span class="n">'+row[1]+'</span></div>';}).join('');
|
|
665
|
+
el.innerHTML='<div class="donut-wrap">'+donutSvg(rows,ev.length)+'<div class="legend">'+legend+'</div></div>';
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function renderStatusBar(ev){
|
|
669
|
+
var b={'2xx':0,'3xx':0,'4xx':0,'5xx':0};ev.forEach(function(e){var k=Math.floor((e.status||0)/100)+'xx';if(b[k]!==undefined)b[k]++;});
|
|
670
|
+
var col={'2xx':'var(--ok)','3xx':'var(--info)','4xx':'var(--warn)','5xx':'var(--bad)'};
|
|
671
|
+
var total=ev.length;var el=document.getElementById('status-chart');
|
|
672
|
+
if(!total){el.innerHTML='<div style="color:var(--dim);font-size:13px">No traffic yet.</div>';return;}
|
|
673
|
+
var segs=Object.keys(b).filter(function(k){return b[k]>0;}).map(function(k){return '<div style="width:'+(b[k]/total*100)+'%;background:'+col[k]+'" title="'+k+': '+b[k]+'"></div>';}).join('');
|
|
674
|
+
var legend=Object.keys(b).map(function(k){return '<div class="li"><span class="dot" style="background:'+col[k]+'"></span>'+k+'<span class="n">'+b[k]+'</span></div>';}).join('');
|
|
675
|
+
el.innerHTML='<div class="segbar">'+segs+'</div><div class="legend" style="flex-direction:row;flex-wrap:wrap;gap:14px;margin-top:12px">'+legend+'</div>';
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function areaSvg(values,color,maxOpt){
|
|
679
|
+
var w=300,h=70,n=values.length;if(n<2)values=values.concat(values);n=values.length;
|
|
680
|
+
var max=Math.max(maxOpt||0,Math.max.apply(null,values),1);
|
|
681
|
+
var pts=values.map(function(v,i){return (i/(n-1)*w).toFixed(1)+','+(h-(v/max)*h).toFixed(1);});
|
|
682
|
+
var gid='g'+Math.floor(Math.random()*1e6);
|
|
683
|
+
var poly=pts.join(' ');var area='0,'+h+' '+poly+' '+w+','+h;
|
|
684
|
+
return '<svg viewBox="0 0 '+w+' '+h+'" preserveAspectRatio="none" style="width:100%;height:72px;display:block"><defs><linearGradient id="'+gid+'" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="'+color+'" stop-opacity="0.35"/><stop offset="100%" stop-color="'+color+'" stop-opacity="0"/></linearGradient></defs><polygon points="'+area+'" fill="url(#'+gid+')"/><polyline points="'+poly+'" fill="none" stroke="'+color+'" stroke-width="2" vector-effect="non-scaling-stroke"/></svg>';
|
|
685
|
+
}
|
|
686
|
+
function renderTrends(ev){
|
|
687
|
+
var now=Date.now();var lat=[],ok=[],tot=[];for(var i=0;i<60;i++){lat.push([]);ok.push(0);tot.push(0);}
|
|
688
|
+
ev.forEach(function(e){var ageMin=Math.floor((now-new Date(e.t).getTime())/60000);if(ageMin>=0&&ageMin<60){var idx=59-ageMin;lat[idx].push(e.latency_ms||0);tot[idx]++;if((e.status||0)>=200&&(e.status||0)<300)ok[idx]++;}});
|
|
689
|
+
var p95=lat.map(function(a){return a.length?pct(a,95):0;});
|
|
690
|
+
var succ=tot.map(function(t,i){return t?Math.round(ok[i]/t*100):0;});
|
|
691
|
+
document.getElementById('trend-latency').innerHTML=areaSvg(p95,'var(--c1)')+'<div class="stat-row">peak p95 <span>'+Math.max.apply(null,p95.concat(0))+' ms</span></div>';
|
|
692
|
+
document.getElementById('trend-success').innerHTML=areaSvg(succ,'var(--ok)',100)+'<div class="stat-row">latest <span>'+(succ[59]||0)+'%</span></div>';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function renderSpark(ev){
|
|
696
|
+
var now=Date.now();var b=[];for(var i=0;i<60;i++)b.push(0);
|
|
697
|
+
ev.forEach(function(e){var ageMin=Math.floor((now-new Date(e.t).getTime())/60000);if(ageMin>=0&&ageMin<60)b[59-ageMin]++;});
|
|
698
|
+
var max=Math.max.apply(null,b.concat(1));
|
|
699
|
+
document.getElementById('spark').innerHTML=b.map(function(v,i){var hh=Math.max(1,Math.round(v/max*44));return '<div class="b '+(v===0?'zero':'')+'" style="height:'+hh+'px" title="'+v+' req · '+(59-i)+'m ago"></div>';}).join('');
|
|
700
|
+
var sum=b.reduce(function(a,c){return a+c;},0);
|
|
701
|
+
document.getElementById('spark-stats').innerHTML='last 60 min <span>'+sum+'</span> · peak minute <span>'+max+'</span>';
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function renderSealed(entries){
|
|
705
|
+
if(!entries.length){document.getElementById('sealed-table-wrap').innerHTML='<div class="empty">No keys sealed yet. <code>blindfold register --name <K></code></div>';return;}
|
|
706
|
+
var latest={};entries.forEach(function(e){latest[e.name]=e;});
|
|
707
|
+
var rows=Object.keys(latest).map(function(k){return latest[k];}).sort(function(a,b){return a.t<b.t?1:-1;}).map(function(e){return '<tr><td><span class="dot" style="display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:6px;background:'+colorFor(e.name)+'"></span><code>'+esc(e.name)+'</code></td><td>'+e.length+' B</td><td>'+pillMode(e.mode)+'</td><td title="'+esc(e.t)+'">'+timeAgo(e.t)+'</td><td><code class="sentinel">__BLINDFOLD__</code></td><td style="white-space:nowrap"><button class="copybtn cmd" data-name="'+esc(e.name)+'" title="Copy a ready-to-run verify command">⧉ cmd</button> <button class="copybtn ref" data-name="'+esc(e.name)+'" title="Copy the Blindfold sentinel — put this where the real key would go; the enclave substitutes the sealed value at call time">⧉ sealed token</button></td></tr>';}).join('');
|
|
708
|
+
document.getElementById('sealed-table-wrap').innerHTML='<div class="scroll"><table><thead><tr><th>Name</th><th>Bytes</th><th>Mode</th><th>Sealed</th><th>Sealed token</th><th>Copy</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
|
|
709
|
+
function flash(b,txt,cls){var o=b.textContent;b.textContent=txt;b.classList.add(cls||'done');setTimeout(function(){b.textContent=o;b.classList.remove('done','err');},1400);}
|
|
710
|
+
var cmds=document.querySelectorAll('#sealed-table-wrap .copybtn.cmd');
|
|
711
|
+
for(var i=0;i<cmds.length;i++){(function(b){b.onclick=function(){var cmd='npm run blindfold -- use --name '+b.getAttribute('data-name')+' --check';try{navigator.clipboard.writeText(cmd);}catch(e){}flash(b,'✓ copied');};})(cmds[i]);}
|
|
712
|
+
var refs=document.querySelectorAll('#sealed-table-wrap .copybtn.ref');
|
|
713
|
+
for(var j=0;j<refs.length;j++){(function(b){b.onclick=function(){try{navigator.clipboard.writeText('__BLINDFOLD__');}catch(e){}flash(b,'✓ sentinel copied');};})(refs[j]);}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function renderPerSecret(ev){
|
|
717
|
+
var by={};ev.forEach(function(e){var k=e.secret_key||'(unknown)';by[k]=(by[k]||0)+1;});
|
|
718
|
+
var rows=Object.keys(by).map(function(k){return[k,by[k]];}).sort(function(a,b){return b[1]-a[1];});
|
|
719
|
+
var el=document.getElementById('per-secret-wrap');
|
|
720
|
+
if(!rows.length){el.innerHTML='<div class="empty">No secret usage yet. Try <code>blindfold use --name <X> -- <cmd></code> or the proxy.</div>';return;}
|
|
721
|
+
var max=Math.max.apply(null,rows.map(function(r){return r[1];}).concat(1));
|
|
722
|
+
el.innerHTML='<div class="card">'+rows.map(function(r){return '<div class="bar-row"><div class="name" title="'+esc(r[0])+'">'+esc(r[0])+'</div><div class="track"><div class="fill" style="width:'+(r[1]/max*100)+'%;background:'+colorFor(r[0])+'"></div></div><div class="n">'+r[1]+'</div></div>';}).join('')+'</div>';
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function sortBy(k){if(window._sort.key===k)window._sort.dir*=-1;else window._sort={key:k,dir:-1};renderTable(rangeFiltered());}
|
|
726
|
+
function renderTable(ev){
|
|
727
|
+
var q=(document.getElementById('filter').value||'').toLowerCase().trim();
|
|
728
|
+
var list=ev;
|
|
729
|
+
if(q)list=ev.filter(function(e){return(e.provider||'').toLowerCase().indexOf(q)>=0||(e.path||'').toLowerCase().indexOf(q)>=0||(e.via||'').toLowerCase().indexOf(q)>=0||String(e.status||'').indexOf(q)>=0||(e.mode||'').toLowerCase().indexOf(q)>=0;});
|
|
730
|
+
var k=window._sort.key,dir=window._sort.dir;
|
|
731
|
+
list=list.slice().sort(function(a,b){var x=a[k],y=b[k];if(k==='t'){x=new Date(a.t).getTime();y=new Date(b.t).getTime();}return x<y?-dir:x>y?dir:0;}).slice(0,80);
|
|
732
|
+
if(!list.length){document.getElementById('table-wrap').innerHTML='<div class="empty">'+(q?'No requests match "'+esc(q)+'".':'No traffic in range. Use a secret or the proxy.')+'</div>';return;}
|
|
733
|
+
var rows=list.map(function(e){return '<tr><td>'+timeAgo(e.t)+'</td><td><span class="dot" style="display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:6px;background:'+colorFor(e.provider)+'"></span>'+esc(e.provider)+'</td><td><span class="pill pill-info">'+esc(e.via||'proxy')+'</span></td><td><code>'+esc(e.method)+' '+esc(e.path)+'</code></td><td>'+pillStatus(e.status)+'</td><td>'+e.latency_ms+' ms</td><td>'+pillMode(e.mode)+'</td></tr>';}).join('');
|
|
734
|
+
document.getElementById('table-wrap').innerHTML='<div class="scroll"><table><thead><tr><th data-k="t">When</th><th data-k="provider">Provider</th><th data-k="via">Via</th><th>Request</th><th data-k="status">Status</th><th data-k="latency_ms">Latency</th><th data-k="mode">Mode</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
|
|
735
|
+
var ths=document.querySelectorAll('#table-wrap th[data-k]');for(var ti=0;ti<ths.length;ti++){(function(th){th.onclick=function(){sortBy(th.getAttribute('data-k'));};})(ths[ti]);}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function runFullAudit(){
|
|
739
|
+
var el=document.getElementById('full-audit');el.innerHTML='<div class="empty">Running live enclave reconciliation…</div>';
|
|
740
|
+
try{
|
|
741
|
+
var r=await fetch(api('/api/audit/full')).then(function(x){return x.json();});
|
|
742
|
+
if(r.mock){el.innerHTML='<div class="alert ok"><div class="head">MOCK mode</div>Reconciliation only runs in REAL mode.</div>';return;}
|
|
743
|
+
if(r.error){el.innerHTML='<div class="alert" style="border-color:rgba(248,81,73,.4);color:var(--bad)"><div class="head">Audit error</div>'+esc(r.error)+'</div>';return;}
|
|
744
|
+
var rows=(r.results||[]).map(function(x){return '<tr><td><code>'+esc(x.name)+'</code></td><td>'+(x.present?'<span class="pill pill-ok">present</span>':'<span class="pill pill-bad">MISSING</span>')+'</td><td>'+x.enclave_len+' B</td><td>'+x.ledger_len+' B</td><td>'+(x.ok?'<span class="pill pill-ok">ok</span>':'<span class="pill pill-warn">drift</span>')+'</td><td><code>'+esc(x.fingerprint||'')+'</code></td></tr>';}).join('');
|
|
745
|
+
var okN=(r.results||[]).filter(function(x){return x.ok;}).length;
|
|
746
|
+
el.innerHTML='<div class="scroll" style="margin-bottom:6px"><table><thead><tr><th>Secret</th><th>Enclave</th><th>Enc bytes</th><th>Ledger bytes</th><th>Match</th><th>Fingerprint</th></tr></thead><tbody>'+rows+'</tbody></table></div><div class="stat-row">'+okN+'/'+(r.results||[]).length+' verified against the enclave</div>';
|
|
747
|
+
}catch(e){el.innerHTML='<div class="alert" style="border-color:rgba(248,81,73,.4);color:var(--bad)"><div class="head">Audit failed</div>'+esc(String(e))+'</div>';}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function startStream(){try{var es=new EventSource(api('/api/stream'));es.addEventListener('change',function(){poll();});es.onerror=function(){};}catch(e){}}
|
|
751
|
+
|
|
752
|
+
function renderCommands(){
|
|
753
|
+
var C=[
|
|
754
|
+
['doctor','doctor','Check your key + tenant are healthy'],
|
|
755
|
+
['status','status','Mode, tenant, and every sealed secret'],
|
|
756
|
+
['audit','audit','Verify the ledger + reconcile against the enclave'],
|
|
757
|
+
['migrate','migrate','Seal every secret in .env in one shot'],
|
|
758
|
+
['register','register --name NAME --from-env NAME','Seal a secret into the enclave'],
|
|
759
|
+
['use','use --name NAME -- COMMAND','Run any tool with a sealed secret injected'],
|
|
760
|
+
['use --check','use --name NAME --check','Confirm a sealed secret is usable'],
|
|
761
|
+
['rotate','rotate --name NAME --from-env NAME','Replace a secret value (rollback-safe)'],
|
|
762
|
+
['rollback','rollback --name NAME','Restore the previous value'],
|
|
763
|
+
['versions','versions --name NAME','List rollback snapshots'],
|
|
764
|
+
['grant','grant --host api.openai.com','Authorize the contract to call a host'],
|
|
765
|
+
['share','share --to DID --host HOST','Let a teammate agent use your keys'],
|
|
766
|
+
['revoke','revoke --to DID','Remove a teammate access'],
|
|
767
|
+
['proxy','proxy','Run the local OpenAI/Anthropic proxy'],
|
|
768
|
+
['publish','publish','Publish the contract to T3'],
|
|
769
|
+
['sealed','sealed','List sealed keys (metadata only)'],
|
|
770
|
+
['dashboard','dashboard','Launch this dashboard'],
|
|
771
|
+
['export','export --name NAME','CI: inject a sealed secret into the job env']
|
|
772
|
+
];
|
|
773
|
+
var rows=C.map(function(c){return '<tr><td><span class="cmdname">'+esc(c[0])+'</span></td><td><code>npm run blindfold -- '+esc(c[1])+'</code></td><td>'+esc(c[2])+'</td><td><button class="copybtn" data-cmd="npm run blindfold -- '+esc(c[1])+'">⧉ copy</button></td></tr>';}).join('');
|
|
774
|
+
document.getElementById('commands-wrap').innerHTML='<div class="scroll"><table><thead><tr><th>Command</th><th>Run</th><th>What it does</th><th>Copy</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
|
|
775
|
+
var btns=document.querySelectorAll('#commands-wrap .copybtn');
|
|
776
|
+
for(var i=0;i<btns.length;i++){(function(b){b.onclick=function(){try{navigator.clipboard.writeText(b.getAttribute('data-cmd'));}catch(e){}var o=b.textContent;b.textContent='✓ copied';b.classList.add('done');setTimeout(function(){b.textContent=o;b.classList.remove('done');},1300);};})(btns[i]);}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
document.getElementById('logo').src=api('/logo.png');
|
|
780
|
+
renderCommands();
|
|
781
|
+
poll();setRefresh();startStream();
|
|
782
|
+
</script>
|
|
783
|
+
</body>
|
|
784
|
+
</html>
|
|
785
|
+
`;
|