blindseal 0.5.0 → 0.5.1

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.
Files changed (34) hide show
  1. package/dashboard/assets/blindseal_verifier_bg-BYF7rQG1.wasm +0 -0
  2. package/dashboard/assets/geist-400-gapTbOY8.woff2 +0 -0
  3. package/dashboard/assets/geist-500-uokXdC-Q.woff2 +0 -0
  4. package/dashboard/assets/geist-600-DFOURf8L.woff2 +0 -0
  5. package/dashboard/assets/geist-mono-400-DTRLJnHl.woff2 +0 -0
  6. package/dashboard/assets/geist-mono-600-V-KvD_pi.woff2 +0 -0
  7. package/dashboard/assets/index-ErSVkiAF.css +1 -0
  8. package/dashboard/assets/index-cOSTL8eT.js +1 -0
  9. package/dashboard/assets/index-p0eB3k2y.js +70 -0
  10. package/dashboard/index.html +76 -0
  11. package/dist/attest-VWYZAOLY.js +346 -0
  12. package/dist/{audit-C756XFXR.js → audit-G4DDMAXH.js} +1 -1
  13. package/dist/chunk-6EU4GH4P.js +513 -0
  14. package/dist/chunk-B44GZXDX.js +136 -0
  15. package/dist/chunk-CIUQ6WHJ.js +88 -0
  16. package/dist/{chunk-BTIXDNTV.js → chunk-DJL2E6RF.js} +69 -6
  17. package/dist/{chunk-PQ457W6T.js → chunk-VSOUJM6X.js} +1 -0
  18. package/dist/{chunk-JJY62YXJ.js → chunk-ZZTWGFH2.js} +0 -20
  19. package/dist/dashboard-J6BA7OBL.js +105 -0
  20. package/dist/decide.js +1 -1
  21. package/dist/index.js +122 -1307
  22. package/dist/{init.js → init-ZP3SLHM3.js} +122 -36
  23. package/dist/run-YQBNEPSD.js +17 -0
  24. package/dist/{submit-2UALCRX2.js → submit-D6MQTAXT.js} +1 -1
  25. package/dist/verify-RISAGKXC.js +98 -0
  26. package/fixtures/attestation-sample.json +25 -0
  27. package/fixtures/attestation-tampered.json +29 -0
  28. package/fixtures/attestations.json +174 -0
  29. package/fixtures/audit.json +1810 -0
  30. package/fixtures/decisions.json +9899 -0
  31. package/fixtures/policy.json +216 -0
  32. package/package.json +8 -8
  33. package/dist/attest-QLIE2OSQ.js +0 -195
  34. package/dist/verify-HO4IU7XV.js +0 -45
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  isTrustedSource,
10
10
  loadConfig,
11
11
  mergeDecisions
12
- } from "./chunk-JJY62YXJ.js";
12
+ } from "./chunk-ZZTWGFH2.js";
13
13
  import {
14
14
  createClientFromEnv,
15
15
  runPipeline,
@@ -19,16 +19,10 @@ import {
19
19
  rankVerdict
20
20
  } from "./chunk-XV57KLEO.js";
21
21
  import {
22
- discloseBundle,
23
- hashEntry
24
- } from "./chunk-PQ457W6T.js";
22
+ packageVersion
23
+ } from "./chunk-CIUQ6WHJ.js";
25
24
  import "./chunk-PDX44BCA.js";
26
25
 
27
- // src/index.ts
28
- import { readFileSync as readFileSync4 } from "fs";
29
- import path7 from "path";
30
- import { fileURLToPath as fileURLToPath2 } from "url";
31
-
32
26
  // src/transports/stdio.ts
33
27
  import { spawn } from "child_process";
34
28
  import { createInterface } from "readline";
@@ -207,116 +201,6 @@ var ManifestChecker = class {
207
201
  }
208
202
  };
209
203
 
210
- // src/telemetry/event.ts
211
- import { createHash as createHash2, randomUUID } from "crypto";
212
- function sha256Hex(s) {
213
- return createHash2("sha256").update(s).digest("hex");
214
- }
215
- function newId() {
216
- return randomUUID();
217
- }
218
- function resolveInstallId() {
219
- const explicit = process.env.BLINDSEAL_TELEMETRY_INSTALL_ID;
220
- if (explicit && explicit.length > 0) return sha256Hex(explicit).slice(0, 16);
221
- return randomUUID();
222
- }
223
-
224
- // src/telemetry/reporter.ts
225
- function createReporter(config, fetchImpl = globalThis.fetch) {
226
- if (!config.enabled || !config.url) {
227
- return {
228
- enabled: false,
229
- send() {
230
- },
231
- async flush() {
232
- },
233
- async shutdown() {
234
- }
235
- };
236
- }
237
- const installId = resolveInstallId();
238
- const source = process.env.BLINDSEAL_TELEMETRY_SOURCE?.trim() || void 0;
239
- let buffer = [];
240
- let timer = null;
241
- let inFlight = null;
242
- let errorAnnouncedAt = 0;
243
- let shutdownCalled = false;
244
- function scheduleFlush() {
245
- if (timer || shutdownCalled) return;
246
- timer = setTimeout(() => {
247
- timer = null;
248
- void flushInternal();
249
- }, config.flushIntervalMs);
250
- if (typeof timer.unref === "function") timer.unref();
251
- }
252
- async function flushInternal() {
253
- if (buffer.length === 0) return;
254
- const batch = buffer;
255
- buffer = [];
256
- const headers = { "Content-Type": "application/json" };
257
- if (config.secret) headers.Authorization = `Bearer ${config.secret}`;
258
- try {
259
- const resp = await fetchImpl(config.url, {
260
- method: "POST",
261
- headers,
262
- body: JSON.stringify({ events: batch })
263
- });
264
- if (!resp.ok) {
265
- announceError(`HTTP ${resp.status}`);
266
- }
267
- } catch (err) {
268
- announceError(err instanceof Error ? err.message : String(err));
269
- }
270
- }
271
- function announceError(msg) {
272
- const now = Date.now();
273
- if (now - errorAnnouncedAt < 6e4) return;
274
- errorAnnouncedAt = now;
275
- process.stderr.write(`blindseal: telemetry POST failed (${msg}); dropping events
276
- `);
277
- }
278
- return {
279
- enabled: true,
280
- send(partial) {
281
- if (shutdownCalled) return;
282
- const event = {
283
- ...partial,
284
- id: newId(),
285
- ts: Date.now(),
286
- installId,
287
- ...source ? { source } : {}
288
- };
289
- buffer.push(event);
290
- if (buffer.length >= config.batchSize) {
291
- if (timer) {
292
- clearTimeout(timer);
293
- timer = null;
294
- }
295
- inFlight = (inFlight ?? Promise.resolve()).then(flushInternal);
296
- return;
297
- }
298
- scheduleFlush();
299
- },
300
- async flush() {
301
- if (timer) {
302
- clearTimeout(timer);
303
- timer = null;
304
- }
305
- await inFlight;
306
- await flushInternal();
307
- },
308
- async shutdown() {
309
- shutdownCalled = true;
310
- if (timer) {
311
- clearTimeout(timer);
312
- timer = null;
313
- }
314
- await inFlight;
315
- await flushInternal();
316
- }
317
- };
318
- }
319
-
320
204
  // src/audit/log.ts
321
205
  import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
322
206
  import path2 from "path";
@@ -367,6 +251,10 @@ function preview(text, max = 200) {
367
251
  }
368
252
 
369
253
  // src/transports/shared.ts
254
+ import { createHash as createHash2 } from "crypto";
255
+ function sha256Hex(s) {
256
+ return createHash2("sha256").update(s).digest("hex");
257
+ }
370
258
  function tryParse(line) {
371
259
  try {
372
260
  return JSON.parse(line);
@@ -493,7 +381,6 @@ function startProxy(cmd, args) {
493
381
  const taint = new TaintStore(config.capability.windowSize);
494
382
  const tradeRate = new TradeRateState();
495
383
  const manifest = config.manifest.mode !== "off" ? new ManifestChecker(cmd, args, config.manifest) : null;
496
- const telemetry = createReporter(config.telemetry);
497
384
  const audit = createAuditLogger(config.audit);
498
385
  const scanStore = createScanStore(config.persistence);
499
386
  if (scanStore.enabled) {
@@ -502,11 +389,6 @@ function startProxy(cmd, args) {
502
389
  `
503
390
  );
504
391
  }
505
- if (config.telemetry.bannerOnStart) {
506
- process.stderr.write(
507
- "blindseal: telemetry is enabled (sha256 hashes only, no raw content) \u2014 see PRIVACY.md; set BLINDSEAL_TELEMETRY=0 to disable\n"
508
- );
509
- }
510
392
  const child = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
511
393
  child.on("error", (err) => {
512
394
  process.stderr.write(`blindseal: failed to spawn ${cmd}: ${err.message}
@@ -538,16 +420,6 @@ function startProxy(cmd, args) {
538
420
  decideCapability(name, params?.arguments, taint, config.capability),
539
421
  decideTradePolicy(name, params?.arguments, taint, config.tradePolicy, tradeRate)
540
422
  );
541
- if (decision.action !== "allow" && telemetry.enabled) {
542
- telemetry.send({
543
- type: "capability",
544
- action: decision.action,
545
- toolName: name,
546
- argsHash: sha256Hex(JSON.stringify(params?.arguments ?? null)),
547
- sourceTools: (decision.taintSources ?? []).map((s) => s.toolName),
548
- matchedPattern: decision.matchedPattern
549
- });
550
- }
551
423
  if (audit.enabled) {
552
424
  audit.log({
553
425
  type: "capability",
@@ -609,15 +481,6 @@ function startProxy(cmd, args) {
609
481
  verdict,
610
482
  config.manifest.mode === "strict" ? "strict" : "on"
611
483
  );
612
- if (telemetry.enabled) {
613
- telemetry.send({
614
- type: "manifest",
615
- status: verdict.status,
616
- serverKey: verdict.serverKey,
617
- fingerprint: verdict.fingerprint.fingerprint,
618
- changes: verdict.changes
619
- });
620
- }
621
484
  if (audit.enabled) {
622
485
  audit.log({
623
486
  type: "manifest",
@@ -639,17 +502,6 @@ function startProxy(cmd, args) {
639
502
  `
640
503
  );
641
504
  }
642
- if (telemetry.enabled) {
643
- telemetry.send({
644
- type: "detection",
645
- layer: outcome.layer ?? null,
646
- verdict: outcome.verdict,
647
- confidence: outcome.result?.confidence ?? 0,
648
- toolName,
649
- contentHash: outcome.contentHash,
650
- patterns: outcome.result?.detectedPatterns ?? []
651
- });
652
- }
653
505
  if (audit.enabled) {
654
506
  const previewText = msg.result.content.map((c) => typeof c.text === "string" ? c.text : "").join("\n");
655
507
  audit.log({
@@ -695,7 +547,6 @@ function startProxy(cmd, args) {
695
547
  });
696
548
  for (const sig of ["SIGINT", "SIGTERM"]) {
697
549
  process.on(sig, () => {
698
- void telemetry.shutdown();
699
550
  void audit.shutdown();
700
551
  try {
701
552
  scanStore.close();
@@ -848,7 +699,6 @@ function startHttpProxy(opts) {
848
699
  const taint = new TaintStore(config.capability.windowSize);
849
700
  const tradeRate = new TradeRateState();
850
701
  const manifest = config.manifest.mode !== "off" ? new ManifestChecker("http", [opts.upstream], config.manifest) : null;
851
- const telemetry = createReporter(config.telemetry);
852
702
  const audit = createAuditLogger(config.audit);
853
703
  const scanStore = createScanStore(config.persistence);
854
704
  if (scanStore.enabled) {
@@ -857,11 +707,6 @@ function startHttpProxy(opts) {
857
707
  `
858
708
  );
859
709
  }
860
- if (config.telemetry.bannerOnStart) {
861
- process.stderr.write(
862
- "blindseal: telemetry is enabled (sha256 hashes only, no raw content) \u2014 see PRIVACY.md; set BLINDSEAL_TELEMETRY=0 to disable\n"
863
- );
864
- }
865
710
  const upstreamUrl = opts.upstream;
866
711
  const server = createServer(async (req, res) => {
867
712
  if (req.method === "GET" && (req.url === "/health" || req.url === "/")) {
@@ -892,16 +737,6 @@ function startHttpProxy(opts) {
892
737
  decideCapability(toolName, toolArgs, taint, config.capability),
893
738
  decideTradePolicy(toolName, toolArgs, taint, config.tradePolicy, tradeRate)
894
739
  );
895
- if (decision.action !== "allow" && telemetry.enabled) {
896
- telemetry.send({
897
- type: "capability",
898
- action: decision.action,
899
- toolName,
900
- argsHash: sha256Hex(JSON.stringify(toolArgs ?? null)),
901
- sourceTools: (decision.taintSources ?? []).map((s) => s.toolName),
902
- matchedPattern: decision.matchedPattern
903
- });
904
- }
905
740
  if (audit.enabled) {
906
741
  audit.log({
907
742
  type: "capability",
@@ -972,17 +807,6 @@ function startHttpProxy(opts) {
972
807
  `
973
808
  );
974
809
  }
975
- if (telemetry.enabled) {
976
- telemetry.send({
977
- type: "detection",
978
- layer: outcome.layer ?? null,
979
- verdict: outcome.verdict,
980
- confidence: outcome.result?.confidence ?? 0,
981
- toolName: scannedTool,
982
- contentHash: outcome.contentHash,
983
- patterns: outcome.result?.detectedPatterns ?? []
984
- });
985
- }
986
810
  if (audit.enabled) {
987
811
  const previewText = scannedMsg.result.content.map((c) => typeof c.text === "string" ? c.text : "").join("\n");
988
812
  audit.log({
@@ -1074,15 +898,6 @@ function startHttpProxy(opts) {
1074
898
  `);
1075
899
  if (config.manifest.mode === "strict") process.exit(3);
1076
900
  }
1077
- if (telemetry.enabled) {
1078
- telemetry.send({
1079
- type: "manifest",
1080
- status: verdict.status,
1081
- serverKey: verdict.serverKey,
1082
- fingerprint: verdict.fingerprint.fingerprint,
1083
- changes: verdict.changes
1084
- });
1085
- }
1086
901
  if (audit.enabled) {
1087
902
  audit.log({
1088
903
  type: "manifest",
@@ -1103,17 +918,6 @@ function startHttpProxy(opts) {
1103
918
  `
1104
919
  );
1105
920
  }
1106
- if (telemetry.enabled) {
1107
- telemetry.send({
1108
- type: "detection",
1109
- layer: outcome.layer ?? null,
1110
- verdict: outcome.verdict,
1111
- confidence: outcome.result?.confidence ?? 0,
1112
- toolName,
1113
- contentHash: outcome.contentHash,
1114
- patterns: outcome.result?.detectedPatterns ?? []
1115
- });
1116
- }
1117
921
  if (audit.enabled) {
1118
922
  const previewText = respMsg.result.content.map((c) => typeof c.text === "string" ? c.text : "").join("\n");
1119
923
  audit.log({
@@ -1174,7 +978,6 @@ function startHttpProxy(opts) {
1174
978
  });
1175
979
  for (const sig of ["SIGINT", "SIGTERM"]) {
1176
980
  process.on(sig, () => {
1177
- void telemetry.shutdown();
1178
981
  void audit.shutdown();
1179
982
  try {
1180
983
  scanStore.close();
@@ -1240,25 +1043,18 @@ function parseRequestBody(raw) {
1240
1043
  return out;
1241
1044
  }
1242
1045
  function startScanHttp(opts) {
1243
- const config = loadConfig();
1244
- const telemetry = createReporter(config.telemetry);
1245
- if (config.telemetry.bannerOnStart) {
1246
- process.stderr.write(
1247
- "blindseal: telemetry is enabled (sha256 hashes only, no raw content) \u2014 see PRIVACY.md; set BLINDSEAL_TELEMETRY=0 to disable\n"
1248
- );
1249
- }
1046
+ loadConfig();
1250
1047
  const defaultServerUrl = opts.defaultServerUrl ?? "scan-http:blindseal";
1251
1048
  const server = createServer2(async (req, res) => {
1252
1049
  const url = req.url ?? "/";
1253
- const path8 = url.split("?")[0] ?? "/";
1254
- if (req.method === "GET" && (path8 === "/health" || path8 === "/")) {
1050
+ const path5 = url.split("?")[0] ?? "/";
1051
+ if (req.method === "GET" && (path5 === "/health" || path5 === "/")) {
1255
1052
  sendJson2(res, 200, {
1256
- ok: true,
1257
- telemetryEnabled: telemetry.enabled
1053
+ ok: true
1258
1054
  });
1259
1055
  return;
1260
1056
  }
1261
- if (req.method !== "POST" || path8 !== "/scan") {
1057
+ if (req.method !== "POST" || path5 !== "/scan") {
1262
1058
  sendJson2(res, 404, { error: "not found" });
1263
1059
  return;
1264
1060
  }
@@ -1288,17 +1084,6 @@ function startScanHttp(opts) {
1288
1084
  sendJson2(res, 500, { error: "pipeline failed" });
1289
1085
  return;
1290
1086
  }
1291
- if (telemetry.enabled) {
1292
- telemetry.send({
1293
- type: "detection",
1294
- layer: result.layer ?? null,
1295
- verdict: result.verdict,
1296
- confidence: result.confidence,
1297
- toolName,
1298
- contentHash,
1299
- patterns: result.detectedPatterns
1300
- });
1301
- }
1302
1087
  if (result.verdict !== "clean") {
1303
1088
  process.stderr.write(
1304
1089
  `blindseal[scan-http]: ${result.verdict} (L${result.layer ?? "?"}) tool=${toolName} confidence=${result.confidence.toFixed(2)}
@@ -1326,7 +1111,6 @@ function startScanHttp(opts) {
1326
1111
  });
1327
1112
  for (const sig of ["SIGINT", "SIGTERM"]) {
1328
1113
  process.on(sig, () => {
1329
- void telemetry.shutdown();
1330
1114
  server.close(() => process.exit(0));
1331
1115
  });
1332
1116
  }
@@ -1773,999 +1557,40 @@ function runHistoryCli(argv) {
1773
1557
  return runHistory(parsed);
1774
1558
  }
1775
1559
 
1776
- // src/cli/dashboard.ts
1777
- import { createServer as createServer3 } from "http";
1778
- import { existsSync as existsSync4 } from "fs";
1779
- var HELP2 = `blindseal dashboard \u2014 local HTTP dashboard for scan history
1780
-
1781
- Usage:
1782
- blindseal dashboard [flags]
1783
-
1784
- Flags
1785
- --port <n> listen port (default 9876)
1786
- --host <addr> listen host (default 127.0.0.1)
1787
- --db <path> override DB path (default ~/.blindseal/scans.db)
1788
- --refresh <sec> auto-refresh interval (default 5)
1789
- --no-open do not open browser automatically
1790
- --help, -h this help
1791
-
1792
- Reads scans recorded by the proxy when run with BLINDSEAL_PERSIST=1. Local-only;
1793
- never connects to any external service.
1794
- `;
1795
- function parseDashboardArgs(argv) {
1796
- const out = {};
1797
- for (let i = 0; i < argv.length; i++) {
1798
- const a = argv[i];
1799
- if (a === "--help" || a === "-h") {
1800
- out.help = true;
1801
- continue;
1802
- }
1803
- if (a === "--port") {
1804
- out.port = Number.parseInt(argv[++i], 10);
1805
- continue;
1806
- }
1807
- if (a === "--host") {
1808
- out.host = argv[++i];
1809
- continue;
1810
- }
1811
- if (a === "--db") {
1812
- out.dbPath = argv[++i];
1813
- continue;
1814
- }
1815
- if (a === "--refresh") {
1816
- out.refreshSeconds = Number.parseInt(argv[++i], 10);
1817
- continue;
1818
- }
1819
- if (a === "--no-open") {
1820
- out.openBrowser = false;
1821
- continue;
1822
- }
1823
- if (a && a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
1824
- }
1825
- return out;
1826
- }
1827
- function renderIndex(refreshSeconds) {
1828
- const refresh = Math.max(1, refreshSeconds | 0);
1829
- return `<!doctype html>
1830
- <html lang="en">
1831
- <head>
1832
- <meta charset="utf-8" />
1833
- <title>blindseal dashboard</title>
1834
- <meta name="viewport" content="width=device-width,initial-scale=1" />
1835
- <style>
1836
- :root { color-scheme: dark; }
1837
- * { box-sizing: border-box; }
1838
- body {
1839
- background: #0a0e0a;
1840
- color: #d1f5d1;
1841
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1842
- margin: 0; padding: 24px;
1843
- font-size: 13px; line-height: 1.5;
1844
- }
1845
- h1, h2 { font-weight: 600; color: #5fdc7c; margin: 0 0 12px; }
1846
- h1 { font-size: 18px; }
1847
- h2 { font-size: 14px; margin-top: 24px; }
1848
- header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 20px; border-bottom: 1px solid #1a2a1a; padding-bottom: 12px; }
1849
- header .status { font-size: 12px; color: #4a7a4a; }
1850
- .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
1851
- .card { background: #0f150f; border: 1px solid #1a2a1a; border-radius: 4px; padding: 16px; }
1852
- .card .label { font-size: 11px; color: #4a7a4a; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; }
1853
- .card .value { font-size: 24px; font-weight: 600; }
1854
- .value.malicious { color: #ff6b6b; }
1855
- .value.suspicious { color: #ffce5c; }
1856
- .value.clean { color: #5fdc7c; }
1857
- table { width: 100%; border-collapse: collapse; }
1858
- th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid #1a2a1a; }
1859
- th { font-weight: 600; color: #5fdc7c; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
1860
- tr:hover td { background: #0f150f; }
1861
- .verdict { padding: 2px 6px; border-radius: 2px; font-size: 11px; font-weight: 600; }
1862
- .verdict.malicious { background: rgba(255, 107, 107, 0.15); color: #ff6b6b; }
1863
- .verdict.suspicious { background: rgba(255, 206, 92, 0.15); color: #ffce5c; }
1864
- .verdict.clean { background: rgba(95, 220, 124, 0.15); color: #5fdc7c; }
1865
- .tool { color: #87c5ee; }
1866
- .preview { color: #6a8a6a; font-size: 11px; max-width: 600px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1867
- svg { display: block; }
1868
- .bar-clean { fill: #5fdc7c; }
1869
- .bar-suspicious { fill: #ffce5c; }
1870
- .bar-malicious { fill: #ff6b6b; }
1871
- .day-label { fill: #4a7a4a; font-size: 10px; font-family: inherit; }
1872
- footer { margin-top: 32px; padding-top: 12px; border-top: 1px solid #1a2a1a; font-size: 11px; color: #4a7a4a; }
1873
- .empty { padding: 24px; text-align: center; color: #4a7a4a; }
1874
- </style>
1875
- </head>
1876
- <body>
1877
- <header>
1878
- <h1>blindseal dashboard</h1>
1879
- <div class="status">auto-refresh: ${refresh}s \xB7 <span id="last-update">\u2014</span></div>
1880
- </header>
1881
- <div id="root"><div class="empty">loading\u2026</div></div>
1882
- <footer>
1883
- Blindseal is a local prompt-injection scanning proxy. Scans here came from your local DB.
1884
- Nothing on this page leaves your machine.
1885
- </footer>
1886
- <script>
1887
- const REFRESH_MS = ${refresh * 1e3};
1888
- const verdictClass = (v) => v === 'malicious' ? 'malicious' : v === 'suspicious' ? 'suspicious' : 'clean';
1889
- function fmtAge(ts) {
1890
- const s = Math.floor((Date.now() - ts) / 1000);
1891
- if (s < 60) return s + 's ago';
1892
- if (s < 3600) return Math.floor(s/60) + 'm ago';
1893
- if (s < 86400) return Math.floor(s/3600) + 'h ago';
1894
- return Math.floor(s/86400) + 'd ago';
1895
- }
1896
- function el(tag, attrs = {}, ...children) {
1897
- const e = document.createElement(tag);
1898
- for (const [k, v] of Object.entries(attrs)) {
1899
- if (k === 'class') e.className = v;
1900
- else if (k.startsWith('on')) e.addEventListener(k.slice(2), v);
1901
- else e.setAttribute(k, v);
1902
- }
1903
- for (const c of children) e.append(c instanceof Node ? c : document.createTextNode(String(c)));
1904
- return e;
1905
- }
1906
- function renderBars(byDay) {
1907
- const w = 600, h = 120, gap = 4;
1908
- if (byDay.length === 0) return el('div', { class: 'empty' }, 'no scans in window');
1909
- const barW = Math.max(8, (w - gap * (byDay.length - 1)) / byDay.length);
1910
- const max = Math.max(1, ...byDay.map((d) => d.clean + d.suspicious + d.malicious));
1911
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1912
- svg.setAttribute('width', w); svg.setAttribute('height', h + 20);
1913
- byDay.forEach((d, i) => {
1914
- const x = i * (barW + gap);
1915
- let y = h;
1916
- for (const [cls, n] of [['clean', d.clean], ['suspicious', d.suspicious], ['malicious', d.malicious]]) {
1917
- if (n <= 0) continue;
1918
- const segH = (n / max) * (h - 4);
1919
- y -= segH;
1920
- const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
1921
- r.setAttribute('x', x); r.setAttribute('y', y);
1922
- r.setAttribute('width', barW); r.setAttribute('height', segH);
1923
- r.setAttribute('class', 'bar-' + cls);
1924
- svg.appendChild(r);
1925
- }
1926
- if (i % Math.max(1, Math.floor(byDay.length / 6)) === 0) {
1927
- const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
1928
- t.setAttribute('x', x + barW / 2); t.setAttribute('y', h + 14);
1929
- t.setAttribute('text-anchor', 'middle'); t.setAttribute('class', 'day-label');
1930
- t.textContent = d.day.slice(5); // MM-DD
1931
- svg.appendChild(t);
1932
- }
1933
- });
1934
- return svg;
1935
- }
1936
- function render(data) {
1937
- const root = document.getElementById('root');
1938
- root.replaceChildren();
1939
- const v = data.summary;
1940
- const grid = el('div', { class: 'grid' },
1941
- el('div', { class: 'card' },
1942
- el('div', { class: 'label' }, 'malicious'),
1943
- el('div', { class: 'value malicious' }, v.malicious),
1944
- ),
1945
- el('div', { class: 'card' },
1946
- el('div', { class: 'label' }, 'suspicious'),
1947
- el('div', { class: 'value suspicious' }, v.suspicious),
1948
- ),
1949
- el('div', { class: 'card' },
1950
- el('div', { class: 'label' }, 'clean'),
1951
- el('div', { class: 'value clean' }, v.clean),
1952
- ),
1953
- );
1954
- root.append(grid);
1955
- root.append(el('h2', {}, 'scans by day (last 30)'));
1956
- root.append(renderBars(data.byDay));
1957
- root.append(el('h2', {}, 'top tools'));
1958
- if (data.topTools.length === 0) root.append(el('div', { class: 'empty' }, 'no scans yet'));
1959
- else {
1960
- const t = el('table', {});
1961
- const thead = el('thead', {}, el('tr', {}, el('th', {}, 'tool'), el('th', {}, 'total'), el('th', {}, 'malicious')));
1962
- const tbody = el('tbody', {}, ...data.topTools.map((r) =>
1963
- el('tr', {},
1964
- el('td', { class: 'tool' }, r.toolName),
1965
- el('td', {}, String(r.total)),
1966
- el('td', {}, String(r.malicious)),
1967
- )
1968
- ));
1969
- t.append(thead, tbody);
1970
- root.append(t);
1971
- }
1972
- root.append(el('h2', {}, 'recent scans (50)'));
1973
- if (data.recent.length === 0) root.append(el('div', { class: 'empty' }, 'no scans'));
1974
- else {
1975
- const t = el('table', {});
1976
- const thead = el('thead', {}, el('tr', {},
1977
- el('th', {}, 'when'), el('th', {}, 'verdict'), el('th', {}, 'tool'),
1978
- el('th', {}, 'layer'), el('th', {}, 'conf'), el('th', {}, 'preview'),
1979
- ));
1980
- const tbody = el('tbody', {}, ...data.recent.map((r) =>
1981
- el('tr', {},
1982
- el('td', {}, fmtAge(r.ts)),
1983
- el('td', {}, el('span', { class: 'verdict ' + verdictClass(r.verdict) }, r.verdict)),
1984
- el('td', { class: 'tool' }, r.toolName),
1985
- el('td', {}, r.layer == null ? '\u2014' : 'L' + r.layer),
1986
- el('td', {}, r.confidence.toFixed(2)),
1987
- el('td', { class: 'preview' }, (r.contentPreview || '').replace(/\\n+/g, ' ')),
1988
- )
1989
- ));
1990
- t.append(thead, tbody);
1991
- root.append(t);
1992
- }
1993
- document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
1994
- }
1995
- async function tick() {
1996
- try {
1997
- const r = await fetch('/api/data');
1998
- const data = await r.json();
1999
- render(data);
2000
- } catch (e) {
2001
- document.getElementById('root').textContent = 'failed to load: ' + e.message;
2002
- }
2003
- }
2004
- tick();
2005
- setInterval(tick, REFRESH_MS);
2006
- </script>
2007
- </body>
2008
- </html>`;
2009
- }
2010
- function buildDashboardData(store) {
2011
- const summary = store.countByVerdict();
2012
- const total = store.total();
2013
- return {
2014
- summary: { ...summary, total, dbPath: store.dbPath ?? "" },
2015
- byDay: store.countByDay(30),
2016
- topTools: store.topTools(10),
2017
- recent: store.list({ limit: 50 }).map((r) => ({
2018
- ts: r.ts,
2019
- verdict: r.verdict,
2020
- toolName: r.toolName,
2021
- layer: r.layer,
2022
- confidence: r.confidence,
2023
- contentPreview: r.contentPreview,
2024
- serverKey: r.serverKey
2025
- }))
2026
- };
2027
- }
2028
- function makeDashboardHandler(store, refreshSeconds) {
2029
- return (req, res) => {
2030
- const url = req.url ?? "/";
2031
- if (req.method !== "GET") {
2032
- res.writeHead(405, { "Content-Type": "text/plain" });
2033
- res.end("method not allowed");
2034
- return;
2035
- }
2036
- if (url === "/" || url === "/index.html") {
2037
- const body = renderIndex(refreshSeconds);
2038
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2039
- res.end(body);
2040
- return;
2041
- }
2042
- if (url === "/api/data" || url.startsWith("/api/data?")) {
2043
- const data = buildDashboardData(store);
2044
- const body = JSON.stringify(data);
2045
- res.writeHead(200, { "Content-Type": "application/json" });
2046
- res.end(body);
2047
- return;
2048
- }
2049
- if (url === "/health") {
2050
- res.writeHead(200, { "Content-Type": "application/json" });
2051
- res.end(JSON.stringify({ ok: true, dbPath: store.dbPath }));
2052
- return;
2053
- }
2054
- res.writeHead(404, { "Content-Type": "text/plain" });
2055
- res.end("not found");
2056
- };
2057
- }
2058
- function runDashboardCli(argv) {
2059
- let parsed;
2060
- try {
2061
- parsed = parseDashboardArgs(argv);
2062
- } catch (err) {
2063
- process.stderr.write(`blindseal dashboard: ${err instanceof Error ? err.message : String(err)}
2064
- `);
2065
- process.stderr.write(HELP2);
2066
- return 2;
2067
- }
2068
- if (parsed.help) {
2069
- process.stdout.write(HELP2);
2070
- return 0;
2071
- }
2072
- const dbPath = parsed.dbPath ?? process.env.BLINDSEAL_PERSIST_PATH ?? defaultDbPath();
2073
- if (!existsSync4(dbPath)) {
2074
- process.stderr.write(`blindseal dashboard: no scan database at ${dbPath}
2075
- `);
2076
- process.stderr.write(` Run blindseal with BLINDSEAL_PERSIST=1 first.
2077
- `);
2078
- return 1;
2079
- }
2080
- const store = createScanStore({ enabled: true, dbPath, retentionDays: 0 });
2081
- if (!store.enabled) {
2082
- process.stderr.write(`blindseal dashboard: failed to open ${dbPath}
2083
- `);
2084
- return 1;
2085
- }
2086
- const port = parsed.port ?? 9876;
2087
- const host = parsed.host ?? "127.0.0.1";
2088
- const refresh = parsed.refreshSeconds ?? 5;
2089
- const server = createServer3(makeDashboardHandler(store, refresh));
2090
- server.listen(port, host, () => {
2091
- const url = `http://${host}:${port}`;
2092
- process.stdout.write(`blindseal dashboard: ${url}
2093
- `);
2094
- process.stdout.write(` serving ${dbPath} (${store.total()} scans, refresh=${refresh}s)
2095
- `);
2096
- process.stdout.write(` Ctrl-C to stop
2097
- `);
2098
- });
2099
- for (const sig of ["SIGINT", "SIGTERM"]) {
2100
- process.on(sig, () => {
2101
- server.close(() => {
2102
- try {
2103
- store.close();
2104
- } catch {
2105
- }
2106
- process.exit(0);
2107
- });
2108
- });
2109
- }
2110
- return 0;
2111
- }
2112
-
2113
- // src/cli/run.ts
2114
- import { existsSync as existsSync6 } from "fs";
2115
- import path6 from "path";
2116
-
2117
- // src/api/mock-server.ts
2118
- import { createServer as createServer4 } from "http";
2119
- import { createHash as createHash4, randomBytes } from "crypto";
2120
- import { readFileSync as readFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
2121
- import os2 from "os";
2122
- import path5 from "path";
2123
-
2124
- // ../chain-reads/src/index.ts
2125
- import { createHash as createHash3 } from "crypto";
2126
- var VERIFY_PROGRAM_ID = "merZNYKpKwwX4CGgzvma3jPvLfrMTkZcb1cSXUKANcb";
2127
- var PUBLIC_MAINNET_RPC = "https://api.mainnet-beta.solana.com";
2128
- var warned = false;
2129
- function rpcUrl() {
2130
- const u = process.env.BLINDSEAL_RPC_URL;
2131
- if (u === "off") return null;
2132
- if (u) return u;
2133
- if (!warned) {
2134
- warned = true;
2135
- process.stderr.write("blindseal: BLINDSEAL_RPC_URL unset \u2014 using the public mainnet RPC (rate-limited; ~100 req/10s per IP). Set your own endpoint for production.\n");
2136
- }
2137
- return PUBLIC_MAINNET_RPC;
2138
- }
2139
- async function rpc(method, params) {
2140
- const url = rpcUrl();
2141
- if (!url) throw new Error("rpc disabled (BLINDSEAL_RPC_URL=off)");
2142
- const res = await fetch(url, {
2143
- method: "POST",
2144
- headers: { "content-type": "application/json" },
2145
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
2146
- signal: AbortSignal.timeout(1e4)
2147
- });
2148
- if (res.status === 429) throw new Error("rpc rate-limited (429) \u2014 set BLINDSEAL_RPC_URL");
2149
- const j = await res.json();
2150
- if (j.error) throw new Error(`rpc ${method}: ${j.error.message}`);
2151
- return j.result;
2152
- }
2153
- var disc = (name) => createHash3("sha256").update(`account:${name}`).digest().subarray(0, 8);
2154
- var b58 = (bytes) => bs58encode(bytes);
2155
- var ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
2156
- function bs58encode(bytes) {
2157
- let zeros = 0;
2158
- while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
2159
- const digits = [];
2160
- for (const byte of bytes) {
2161
- let carry = byte;
2162
- for (let j = 0; j < digits.length; j++) {
2163
- carry += digits[j] << 8;
2164
- digits[j] = carry % 58;
2165
- carry = carry / 58 | 0;
2166
- }
2167
- while (carry > 0) {
2168
- digits.push(carry % 58);
2169
- carry = carry / 58 | 0;
2170
- }
2171
- }
2172
- return "1".repeat(zeros) + digits.reverse().map((d) => ALPHABET[d]).join("");
2173
- }
2174
- function bs58decode(s) {
2175
- const bytes = [];
2176
- for (const ch of s) {
2177
- let carry = ALPHABET.indexOf(ch);
2178
- if (carry < 0) throw new Error("bad base58");
2179
- for (let j = 0; j < bytes.length; j++) {
2180
- carry += bytes[j] * 58;
2181
- bytes[j] = carry & 255;
2182
- carry >>= 8;
2183
- }
2184
- while (carry > 0) {
2185
- bytes.push(carry & 255);
2186
- carry >>= 8;
2187
- }
2188
- }
2189
- let zeros = 0;
2190
- while (zeros < s.length && s[zeros] === "1") zeros++;
2191
- return Uint8Array.from([...new Array(zeros).fill(0), ...bytes.reverse()]);
2192
- }
2193
- var P = (1n << 255n) - 19n;
2194
- var D = 37095705934669439343138083508754565189542113879843219016388785533085940283555n;
2195
- var SQRT_M1 = 19681161376707505956807079304988542015446066515923890162744021073123829784752n;
2196
- function modPow(b, e, m) {
2197
- let r = 1n;
2198
- b %= m;
2199
- while (e > 0n) {
2200
- if (e & 1n) r = r * b % m;
2201
- b = b * b % m;
2202
- e >>= 1n;
2203
- }
2204
- return r;
2205
- }
2206
- function isOnCurve(pk) {
2207
- if (pk.length !== 32) return false;
2208
- let y = 0n;
2209
- for (let i = 31; i >= 0; i--) y = y << 8n | BigInt(pk[i]);
2210
- const sign = y >> 255n & 1n;
2211
- y &= (1n << 255n) - 1n;
2212
- if (y >= P) return false;
2213
- const y2 = y * y % P, u = (y2 - 1n + P) % P, v = (D * y2 + 1n) % P;
2214
- const v3 = v * v % P * v % P, v7 = v3 * v3 % P * v % P;
2215
- let x = u * v3 % P * modPow(u * v7 % P, (P - 5n) / 8n, P) % P;
2216
- const vx2 = v * (x * x % P) % P;
2217
- if (vx2 === u) {
2218
- } else if (vx2 === (P - u) % P) x = x * SQRT_M1 % P;
2219
- else return false;
2220
- if (x === 0n && sign === 1n) return false;
2221
- return true;
2222
- }
2223
- function findProgramAddress(seeds, programId) {
2224
- const prog = bs58decode(programId);
2225
- for (let bump = 255; bump >= 0; bump--) {
2226
- const h = createHash3("sha256");
2227
- for (const s of seeds) h.update(s);
2228
- h.update(Uint8Array.of(bump));
2229
- h.update(prog);
2230
- h.update("ProgramDerivedAddress");
2231
- const cand = h.digest();
2232
- if (!isOnCurve(cand)) return b58(cand);
2233
- }
2234
- throw new Error("no PDA bump");
2235
- }
2236
- async function getNullifierStatus(nullifierHex) {
2237
- const nf = Buffer.from(nullifierHex.replace(/^0x/, ""), "hex");
2238
- if (nf.length !== 32) throw new Error("nullifier must be 32 bytes");
2239
- const pda = findProgramAddress([Buffer.from("nullifier"), nf], VERIFY_PROGRAM_ID);
2240
- const r = await rpc("getAccountInfo", [pda, { encoding: "base64", commitment: "confirmed" }]);
2241
- const base = { nullifier: nullifierHex, consumed: false, pda, cluster: "mainnet-beta", rpc: process.env.BLINDSEAL_RPC_URL ? "configured" : "public" };
2242
- if (!r.value) return base;
2243
- const data = Buffer.from(r.value.data[0], "base64");
2244
- if (data.length < 81 || !data.subarray(0, 8).equals(disc("Nullifier"))) return base;
2245
- return {
2246
- ...base,
2247
- consumed: true,
2248
- consumedAtSlot: Number(data.readBigUInt64LE(8)),
2249
- consumerProgram: b58(data.subarray(16, 48)),
2250
- actionHash: "0x" + data.subarray(48, 80).toString("hex")
2251
- };
2252
- }
2253
-
2254
- // src/api/mock-server.ts
2255
- import { createPrivateKey, createPublicKey } from "crypto";
2256
- var MOCK_IDENTITY_PKCS8 = "302e020100300506032b657004220420b6c6aa0182de5f17678ed27e9515ed56b438c6bec2ca9d27d653feec4d13da1c";
2257
- var MOCK_PRIVATE_KEY = createPrivateKey({ key: Buffer.from(MOCK_IDENTITY_PKCS8, "hex"), format: "der", type: "pkcs8" });
2258
- var MOCK_PUBKEY_HEX = createPublicKey(MOCK_PRIVATE_KEY).export({ type: "spki", format: "der" }).toString("hex");
2259
- var MIME = {
2260
- ".html": "text/html; charset=utf-8",
2261
- ".js": "text/javascript; charset=utf-8",
2262
- ".css": "text/css; charset=utf-8",
2263
- ".wasm": "application/wasm",
2264
- ".json": "application/json",
2265
- ".svg": "image/svg+xml",
2266
- ".png": "image/png",
2267
- ".woff2": "font/woff2",
2268
- ".woff": "font/woff",
2269
- ".ico": "image/x-icon",
2270
- ".map": "application/json",
2271
- ".txt": "text/plain"
2272
- };
2273
- var DASHBOARD_CSP = [
2274
- "default-src 'self'",
2275
- "script-src 'self' 'wasm-unsafe-eval'",
2276
- "style-src 'self' 'unsafe-inline'",
2277
- "img-src 'self' data:",
2278
- "font-src 'self'",
2279
- "connect-src 'self' ws://127.0.0.1:* ws://localhost:* http://127.0.0.1:* http://localhost:*",
2280
- "object-src 'none'",
2281
- "base-uri 'self'",
2282
- "frame-ancestors 'none'"
2283
- ].join("; ");
2284
- function serveDashboard(res, dist, pathname) {
2285
- const rel = pathname === "/" ? "/index.html" : decodeURIComponent(pathname);
2286
- let file = path5.normalize(path5.join(dist, rel));
2287
- if (!file.startsWith(path5.normalize(dist + path5.sep)) && file !== path5.normalize(dist)) return false;
2288
- if (!existsSync5(file)) file = path5.join(dist, "index.html");
2289
- if (!existsSync5(file)) return false;
2290
- const body = readFileSync3(file);
2291
- const type = MIME[path5.extname(file).toLowerCase()] ?? "application/octet-stream";
2292
- res.writeHead(200, {
2293
- "content-type": type,
2294
- "content-length": body.length,
2295
- "content-security-policy": DASHBOARD_CSP,
2296
- "x-content-type-options": "nosniff",
2297
- "referrer-policy": "no-referrer",
2298
- "cache-control": file.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable"
2299
- });
2300
- res.end(body);
2301
- return true;
2302
- }
2303
- function loadFixtures(dir) {
2304
- const read = (f) => JSON.parse(readFileSync3(path5.join(dir, f), "utf8"));
2305
- return {
2306
- decisions: read("decisions.json"),
2307
- policy: read("policy.json"),
2308
- audit: read("audit.json"),
2309
- attestations: read("attestations.json"),
2310
- sample: read("attestation-sample.json")
2311
- };
2312
- }
2313
- function resolveToken(explicit) {
2314
- if (explicit) return explicit;
2315
- const dir = path5.join(os2.homedir(), ".blindseal");
2316
- const file = path5.join(dir, "dashboard.token");
2317
- try {
2318
- if (existsSync5(file)) return readFileSync3(file, "utf8").trim();
2319
- mkdirSync3(dir, { recursive: true });
2320
- const tok = randomBytes(24).toString("base64url");
2321
- writeFileSync2(file, tok + "\n", { mode: 384 });
2322
- return tok;
2323
- } catch {
2324
- return randomBytes(24).toString("base64url");
2325
- }
2326
- }
2327
- var sha = (s) => "0x" + createHash4("sha256").update(s).digest("hex");
2328
- function sendJson3(res, status, body) {
2329
- const buf = Buffer.from(JSON.stringify(body));
2330
- res.writeHead(status, { "content-type": "application/json", "content-length": buf.length });
2331
- res.end(buf);
2332
- }
2333
- function sendErr(res, status, code, message) {
2334
- sendJson3(res, status, { error: { code, message } });
2335
- }
2336
- function bearerOk(req, token, url) {
2337
- const auth = req.headers["authorization"];
2338
- if (typeof auth === "string" && auth === `Bearer ${token}`) return true;
2339
- if (url.searchParams.get("token") === token) return true;
2340
- return false;
2341
- }
2342
- async function readBody3(req) {
2343
- const chunks = [];
2344
- for await (const c of req) chunks.push(c);
2345
- if (chunks.length === 0) return {};
2346
- try {
2347
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
2348
- } catch {
2349
- return void 0;
2350
- }
2351
- }
2352
- var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
2353
- function wsAccept(key) {
2354
- return createHash4("sha1").update(key + WS_GUID).digest("base64");
2355
- }
2356
- function wsFrame(text) {
2357
- const payload = Buffer.from(text);
2358
- const len = payload.length;
2359
- let header;
2360
- if (len < 126) {
2361
- header = Buffer.from([129, len]);
2362
- } else if (len < 65536) {
2363
- header = Buffer.from([129, 126, len >> 8 & 255, len & 255]);
2364
- } else {
2365
- header = Buffer.alloc(10);
2366
- header[0] = 129;
2367
- header[1] = 127;
2368
- header.writeBigUInt64BE(BigInt(len), 2);
2369
- }
2370
- return Buffer.concat([header, payload]);
2371
- }
2372
- function startMockApi(opts) {
2373
- const port = opts.port ?? 7777;
2374
- const host = opts.host ?? "127.0.0.1";
2375
- const token = resolveToken(opts.token);
2376
- const emitIntervalMs = opts.emitIntervalMs ?? 3e3;
2377
- const fx = loadFixtures(opts.fixturesDir);
2378
- const startedAt = Date.now();
2379
- const envState = process.env.BLINDSEAL_MOCK_STATE ?? "";
2380
- const stateOf = (url) => url.searchParams.get("state") ?? envState;
2381
- const TEE_QUOTE = { quote: sha("mock-tee-quote").slice(2), quoteHash: sha("mock-tee-quote-hash") };
2382
- const status = (st) => ({
2383
- version: "0.0.0-mock",
2384
- uptimeSec: Math.floor((Date.now() - startedAt) / 1e3),
2385
- connection: st === "offline" ? "offline" : st === "reconnecting" ? "reconnecting" : "live",
2386
- upstreams: [{ name: "mock-upstream", transport: "stdio", healthy: st !== "upstream-down" }],
2387
- policyRoot: fx.policy.root,
2388
- detectorVersion: fx.decisions[0]?.detectorVersion ?? "0x",
2389
- audit: st === "empty" ? { index: -1, hash: fx.audit.genesis, ts: new Date(startedAt).toISOString() } : fx.audit.head,
2390
- prover: { available: st === "prover-down" ? false : true },
2391
- l3: { mode: st === "tee" ? "tee" : "local" },
2392
- mock: true
2393
- });
2394
- const wsClients = /* @__PURE__ */ new Set();
2395
- const broadcast = (obj) => {
2396
- const frame = wsFrame(JSON.stringify(obj));
2397
- for (const s of wsClients) {
2398
- if (!s.destroyed) s.write(frame);
2399
- }
2400
- };
2401
- const createdAttestations = /* @__PURE__ */ new Map();
2402
- const liveDecisions = /* @__PURE__ */ new Map();
2403
- const server = createServer4(async (req, res) => {
2404
- const url = new URL(req.url ?? "/", `http://${host}:${port}`);
2405
- const p = url.pathname;
2406
- const method = req.method ?? "GET";
2407
- const origin = req.headers["origin"];
2408
- if (typeof origin === "string" && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) {
2409
- res.setHeader("Access-Control-Allow-Origin", origin);
2410
- res.setHeader("Access-Control-Allow-Headers", "authorization, content-type, x-confirm");
2411
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
2412
- res.setHeader("Vary", "Origin");
2413
- }
2414
- if (method === "OPTIONS") {
2415
- res.writeHead(204);
2416
- return res.end();
2417
- }
2418
- if (!p.startsWith("/api/")) {
2419
- if (opts.dashboardDist && method === "GET" && serveDashboard(res, opts.dashboardDist, p)) return;
2420
- return sendErr(res, 404, "NOT_FOUND", "unknown path");
2421
- }
2422
- if (!bearerOk(req, token, url)) return sendErr(res, 401, "UNAUTHORIZED", "missing or invalid bearer token");
2423
- const seg = p.slice("/api/".length).split("/").filter(Boolean);
2424
- const st = stateOf(url);
2425
- if (st === "unauthorized") return sendErr(res, 401, "UNAUTHORIZED", "missing or invalid dashboard token");
2426
- if (st === "offline" && !(seg[0] === "status")) return sendErr(res, 503, "OFFLINE", "the proxy is unreachable");
2427
- try {
2428
- if (method === "GET" && seg[0] === "status" && seg.length === 1) return sendJson3(res, 200, status(st));
2429
- if (method === "GET" && seg[0] === "decisions" && seg.length === 1) {
2430
- const q = url.searchParams;
2431
- let items = st === "empty" ? [] : fx.decisions.slice();
2432
- const vd = q.get("verdict");
2433
- if (vd) items = items.filter((d) => d.verdict === vd);
2434
- const sv = q.get("server");
2435
- if (sv) items = items.filter((d) => d.server === sv);
2436
- const tl = q.get("tool");
2437
- if (tl) items = items.filter((d) => d.tool === tl);
2438
- const from = q.get("from");
2439
- if (from) items = items.filter((d) => d.ts >= from);
2440
- const to = q.get("to");
2441
- if (to) items = items.filter((d) => d.ts <= to);
2442
- const limit = Math.min(Math.max(Number.parseInt(q.get("limit") ?? "50", 10) || 50, 1), 200);
2443
- const cursor = Number.parseInt(q.get("cursor") ?? "0", 10) || 0;
2444
- const page = items.slice(cursor, cursor + limit);
2445
- const nextCursor = cursor + limit < items.length ? String(cursor + limit) : void 0;
2446
- return sendJson3(res, 200, { items: page, ...nextCursor ? { nextCursor } : {} });
2447
- }
2448
- if (seg[0] === "decisions" && seg.length >= 2) {
2449
- const d = fx.decisions.find((x) => x.id === seg[1]) ?? liveDecisions.get(seg[1]);
2450
- if (!d) return sendErr(res, 404, "NOT_FOUND", "decision not found");
2451
- if (method === "GET" && seg.length === 2) {
2452
- return sendJson3(res, 200, { ...d, payloadRedacted: `[mock redacted payload for ${d.id}]` });
2453
- }
2454
- if (method === "POST" && seg[2] === "reveal" && seg.length === 3) {
2455
- if (req.headers["x-confirm"] !== "reveal") return sendErr(res, 400, "VALIDATION", "X-Confirm: reveal required");
2456
- return sendJson3(res, 200, { payload: `[mock raw payload for ${d.id} \u2014 synthetic]` });
2457
- }
2458
- }
2459
- if (seg[0] === "audit") {
2460
- if (method === "GET" && seg[1] === "head") {
2461
- if (st === "empty") return sendJson3(res, 200, { index: -1, hash: fx.audit.genesis, ts: new Date(startedAt).toISOString() });
2462
- return sendJson3(res, 200, fx.audit.head);
2463
- }
2464
- if (method === "GET" && seg[1] === "entries") {
2465
- if (st === "empty") return sendJson3(res, 200, []);
2466
- const from = Number.parseInt(url.searchParams.get("from") ?? "0", 10) || 0;
2467
- const to = Number.parseInt(url.searchParams.get("to") ?? String(fx.audit.head.index), 10);
2468
- return sendJson3(res, 200, fx.audit.entries.filter((e) => e.index >= from && e.index <= to));
2469
- }
2470
- if (method === "POST" && seg[1] === "verify") {
2471
- if (st === "chain-broken") return sendJson3(res, 200, { ok: false, checkedFrom: 0, checkedTo: fx.audit.head.index, brokenAt: 143 });
2472
- let prev = fx.audit.genesis;
2473
- let brokenAt;
2474
- for (const e of fx.audit.entries) {
2475
- const h = hashEntry({ index: e.index, ts: e.ts, decisionId: e.decisionId, verdict: e.verdict, payloadHash: e.payloadHash, prevHash: prev });
2476
- if (h !== e.hash || e.prevHash !== prev) {
2477
- brokenAt = e.index;
2478
- break;
2479
- }
2480
- prev = e.hash;
2481
- }
2482
- return sendJson3(res, 200, {
2483
- ok: brokenAt === void 0,
2484
- checkedFrom: 0,
2485
- checkedTo: fx.audit.head.index,
2486
- ...brokenAt !== void 0 ? { brokenAt } : {}
2487
- });
2488
- }
2489
- if (method === "POST" && seg[1] === "disclose") {
2490
- const body = await readBody3(req);
2491
- if (!body || typeof body.from !== "number" || typeof body.to !== "number") return sendErr(res, 400, "VALIDATION", "from/to required");
2492
- if (st === "chain-broken" && body.from <= 143 && body.to >= 143)
2493
- return sendErr(res, 409, "CHAIN_BROKEN", "the chain is broken at #143; cannot disclose a range over it");
2494
- const entries = fx.audit.entries.filter((e) => e.index >= body.from && e.index <= body.to);
2495
- return sendJson3(res, 200, discloseBundle({
2496
- from: body.from,
2497
- to: body.to,
2498
- head: fx.audit.head,
2499
- entries,
2500
- privateKey: MOCK_PRIVATE_KEY,
2501
- pubkeyHex: MOCK_PUBKEY_HEX
2502
- }));
2503
- }
2504
- }
2505
- if (seg[0] === "policy") {
2506
- if (method === "GET" && seg.length === 1) return sendJson3(res, 200, fx.policy);
2507
- if (method === "PUT" && seg.length === 1) {
2508
- const body = await readBody3(req);
2509
- if (!body || !Array.isArray(body.rules)) return sendErr(res, 400, "VALIDATION", "rules[] required");
2510
- const root = sha("policy:" + JSON.stringify(body.rules));
2511
- return sendJson3(res, 200, { ...fx.policy, rules: body.rules, root, version: (fx.policy.version ?? 1) + 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2512
- }
2513
- if (method === "POST" && seg[1] === "dry-run") {
2514
- const body = await readBody3(req);
2515
- if (body === void 0) return sendErr(res, 400, "VALIDATION", "invalid JSON body");
2516
- const d = { ...fx.decisions[0], id: "dryrun_" + Date.now(), ts: (/* @__PURE__ */ new Date()).toISOString() };
2517
- return sendJson3(res, 200, d);
2518
- }
2519
- }
2520
- if (seg[0] === "attestations") {
2521
- if (method === "GET" && seg.length === 1) {
2522
- if (st === "empty") return sendJson3(res, 200, []);
2523
- const statusFilter = url.searchParams.get("status");
2524
- const items = statusFilter ? fx.attestations.filter((a) => a.status === statusFilter) : fx.attestations;
2525
- return sendJson3(res, 200, items);
2526
- }
2527
- if (method === "POST" && seg.length === 1) {
2528
- if (st === "prover-down") return sendErr(res, 503, "PROVER_UNAVAILABLE", "the local prover is not running");
2529
- const body = await readBody3(req);
2530
- if (!body || typeof body.decisionId !== "string") return sendErr(res, 400, "VALIDATION", "decisionId required");
2531
- const d = fx.decisions.find((x) => x.id === body.decisionId);
2532
- if (!d) return sendErr(res, 404, "NOT_FOUND", "decision not found");
2533
- if (!d.action) return sendErr(res, 422, "NO_ACTION_FIELDS", "decision has no on-chain action to attest");
2534
- const id = "att_" + randomBytes(5).toString("hex");
2535
- createdAttestations.set(id, {
2536
- ...fx.sample,
2537
- id,
2538
- decisionId: d.id,
2539
- status: "generating",
2540
- progressPct: 50,
2541
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2542
- });
2543
- setTimeout(() => broadcast({ type: "attestation", data: { id, status: "generating", progressPct: 50 } }), 50);
2544
- setTimeout(() => {
2545
- const a = createdAttestations.get(id);
2546
- if (a) {
2547
- a.status = "generated";
2548
- a.progressPct = 100;
2549
- }
2550
- broadcast({ type: "attestation", data: { id, status: "generated", progressPct: 100 } });
2551
- }, 150);
2552
- return sendJson3(res, 202, { id });
2553
- }
2554
- if (seg.length >= 2) {
2555
- const a = fx.attestations.find((x) => x.id === seg[1]) ?? createdAttestations.get(seg[1]) ?? (seg[1] === "sample" ? fx.sample : void 0);
2556
- if (method === "GET" && seg.length === 2) {
2557
- if (!a) return sendErr(res, 404, "NOT_FOUND", "attestation not found");
2558
- return sendJson3(res, 200, a);
2559
- }
2560
- if (method === "POST" && seg[2] === "submit") {
2561
- const body = await readBody3(req);
2562
- if (!body || body.cluster !== "mainnet-beta") return sendErr(res, 400, "VALIDATION", "cluster must be mainnet-beta");
2563
- return sendJson3(res, 200, { txSig: sha("mock-txsig:" + seg[1]).slice(2, 90) });
2564
- }
2565
- if (method === "GET" && seg[2] === "export") {
2566
- if (!a) return sendErr(res, 404, "NOT_FOUND", "attestation not found");
2567
- const buf = Buffer.from(JSON.stringify(a, null, 2));
2568
- res.writeHead(200, { "content-type": "application/json", "content-disposition": `attachment; filename="attestation-${seg[1]}.json"`, "content-length": buf.length });
2569
- return res.end(buf);
2570
- }
2571
- }
2572
- }
2573
- if (method === "GET" && seg[0] === "tee" && seg[1] === "quote") {
2574
- if (st === "tee") return sendJson3(res, 200, { mode: "tee", quote: TEE_QUOTE.quote, quoteHash: TEE_QUOTE.quoteHash });
2575
- return sendJson3(res, 200, { mode: "local" });
2576
- }
2577
- if (seg[0] === "nullifiers" && seg[1] && method === "GET") {
2578
- try {
2579
- return sendJson3(res, 200, await getNullifierStatus(seg[1]));
2580
- } catch (e) {
2581
- return sendErr(res, 502, "RPC_UNAVAILABLE", e instanceof Error ? e.message : String(e));
2582
- }
2583
- }
2584
- return sendErr(res, 404, "NOT_FOUND", `no route for ${method} ${p}`);
2585
- } catch (e) {
2586
- return sendErr(res, 500, "VALIDATION", e.message);
2587
- }
2588
- });
2589
- server.on("upgrade", (req, socket) => {
2590
- const url = new URL(req.url ?? "/", `http://${host}:${port}`);
2591
- const isEvents = url.pathname === "/api/events" || url.pathname === "/events";
2592
- if (!isEvents || !bearerOk(req, token, url)) {
2593
- socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
2594
- socket.destroy();
2595
- return;
2596
- }
2597
- const key = req.headers["sec-websocket-key"];
2598
- if (typeof key !== "string") {
2599
- socket.destroy();
2600
- return;
2601
- }
2602
- socket.write(
2603
- `HTTP/1.1 101 Switching Protocols\r
2604
- Upgrade: websocket\r
2605
- Connection: Upgrade\r
2606
- Sec-WebSocket-Accept: ${wsAccept(key)}\r
2607
- \r
2608
- `
2609
- );
2610
- wsClients.add(socket);
2611
- socket.write(wsFrame(JSON.stringify({ type: "status", data: status(envState) })));
2612
- socket.on("close", () => wsClients.delete(socket));
2613
- socket.on("error", () => wsClients.delete(socket));
2614
- });
2615
- let idx = 0;
2616
- const timer = setInterval(() => {
2617
- if (wsClients.size === 0) return;
2618
- const base = fx.decisions[idx % fx.decisions.length];
2619
- idx++;
2620
- const live = { ...base, id: base.id + "_live" + idx, ts: (/* @__PURE__ */ new Date()).toISOString() };
2621
- liveDecisions.set(live.id, live);
2622
- if (liveDecisions.size > 100) liveDecisions.delete(liveDecisions.keys().next().value);
2623
- broadcast({ type: "decision", data: live });
2624
- }, emitIntervalMs);
2625
- timer.unref?.();
2626
- return new Promise((resolve) => {
2627
- server.listen(port, host, () => {
2628
- const addr = server.address();
2629
- const actualPort = typeof addr === "object" && addr ? addr.port : port;
2630
- resolve({
2631
- url: `http://${host}:${actualPort}/api`,
2632
- token,
2633
- port: actualPort,
2634
- close: () => new Promise((r) => {
2635
- clearInterval(timer);
2636
- for (const s of wsClients) s.destroy();
2637
- wsClients.clear();
2638
- server.close(() => r());
2639
- })
2640
- });
2641
- });
2642
- });
2643
- }
2644
-
2645
- // src/cli/run.ts
2646
- var HELP3 = `blindseal run \u2014 start the local API + WS (FRONTEND.md \xA75)
2647
-
2648
- Usage:
2649
- BLINDSEAL_MOCK=1 blindseal run [--mock] [flags]
2650
-
2651
- Flags
2652
- --mock serve fixtures/ with a fake event stream (M0)
2653
- --port <n> listen port (default 7777)
2654
- --host <addr> listen host (default 127.0.0.1)
2655
- --fixtures <dir> fixtures directory (default: nearest fixtures/)
2656
- --dashboard <dir> serve the built dashboard at / (default: $BLINDSEAL_DASHBOARD_DIST or
2657
- packages/dashboard/dist next to fixtures/, if built)
2658
- --help, -h this help
2659
-
2660
- M0 supports --mock only. The engine-backed API lands in M1.
2661
- `;
2662
- function parseRunArgs(argv) {
2663
- const out = { mock: process.env.BLINDSEAL_MOCK === "1" };
2664
- for (let i = 0; i < argv.length; i++) {
2665
- const a = argv[i];
2666
- if (a === "--help" || a === "-h") out.help = true;
2667
- else if (a === "--mock") out.mock = true;
2668
- else if (a === "--port") out.port = Number.parseInt(argv[++i] ?? "", 10);
2669
- else if (a === "--host") out.host = argv[++i];
2670
- else if (a === "--fixtures") out.fixturesDir = argv[++i];
2671
- else if (a === "--dashboard") out.dashboard = argv[++i];
2672
- else if (a && a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
2673
- }
2674
- return out;
2675
- }
2676
- function resolveFixturesDir(explicit) {
2677
- if (explicit) return explicit;
2678
- if (process.env.BLINDSEAL_FIXTURES) return process.env.BLINDSEAL_FIXTURES;
2679
- let dir = process.cwd();
2680
- for (let i = 0; i < 7; i++) {
2681
- const candidate = path6.join(dir, "fixtures");
2682
- if (existsSync6(path6.join(candidate, "decisions.json"))) return candidate;
2683
- const parent = path6.dirname(dir);
2684
- if (parent === dir) break;
2685
- dir = parent;
2686
- }
2687
- throw new Error("fixtures/ not found; pass --fixtures <dir> or set BLINDSEAL_FIXTURES");
2688
- }
2689
- async function runRun(argv) {
2690
- let opts;
2691
- try {
2692
- opts = parseRunArgs(argv);
2693
- } catch (e) {
2694
- process.stderr.write(`blindseal run: ${e.message}
2695
- `);
2696
- return 1;
2697
- }
2698
- if (opts.help) {
2699
- process.stdout.write(HELP3);
2700
- return 0;
2701
- }
2702
- if (!opts.mock) {
2703
- process.stderr.write(
2704
- "blindseal run: the engine-backed API is not built yet (M1).\nFor now: BLINDSEAL_MOCK=1 blindseal run --mock\n"
2705
- );
2706
- return 1;
2707
- }
2708
- const fixturesDir = resolveFixturesDir(opts.fixturesDir);
2709
- const autoDist = path6.join(path6.dirname(fixturesDir), "packages", "dashboard", "dist");
2710
- const dashboardDist = opts.dashboard ?? process.env.BLINDSEAL_DASHBOARD_DIST ?? (existsSync6(path6.join(autoDist, "index.html")) ? autoDist : void 0);
2711
- const handle = await startMockApi({ port: opts.port, host: opts.host, fixturesDir, dashboardDist });
2712
- process.stdout.write(
2713
- `blindseal (MOCK): local API on ${handle.url}
2714
- token: ${handle.token}
2715
- ` + (dashboardDist ? ` dashboard: ${handle.url}/#token=${handle.token} (served from ${dashboardDist}; application/wasm + CSP)
2716
- ` : ` dashboard: not served (build packages/dashboard or pass --dashboard <dist>); open the dev app with #token=${handle.token}
2717
- `) + ` fixtures: ${fixturesDir}
2718
- WS: ${handle.url.replace(/^http/, "ws")}/events?token=${handle.token}
2719
- Ctrl-C to stop.
2720
- `
2721
- );
2722
- return new Promise((resolve) => {
2723
- const shutdown = () => {
2724
- void handle.close().then(() => resolve(0));
2725
- };
2726
- process.on("SIGINT", shutdown);
2727
- process.on("SIGTERM", shutdown);
2728
- });
2729
- }
2730
-
2731
1560
  // src/index.ts
2732
- function readVersion2() {
2733
- try {
2734
- const here = path7.dirname(fileURLToPath2(import.meta.url));
2735
- for (const rel of ["../package.json", "../../package.json"]) {
2736
- const p = path7.resolve(here, rel);
2737
- try {
2738
- const pkg = JSON.parse(readFileSync4(p, "utf8"));
2739
- if (pkg.name && pkg.version) return String(pkg.version);
2740
- } catch {
2741
- }
2742
- }
2743
- } catch {
2744
- }
2745
- return "0.0.0";
2746
- }
2747
- var HELP4 = `blindseal \u2014 scans MCP tool responses for prompt injection
1561
+ var HELP2 = `blindseal \u2014 a local MCP proxy that screens every tool call, keeps a hash-chained audit
1562
+ log, and produces zero-knowledge attestations verifiable on Solana.
2748
1563
 
2749
1564
  Usage
2750
- npx blindseal demo # interactive demo (30s first-run experience)
2751
- BLINDSEAL_MOCK=1 blindseal run --mock # local API + WS over fixtures (FRONTEND.md \xA75)
2752
- npx blindseal -- <command> [args...] # stdio (wrap a local MCP server)
2753
- npx blindseal --transport http --upstream <url> [--port] # http (proxy a remote MCP server)
2754
- npx blindseal history [flags] # show local scan history (requires BLINDSEAL_PERSIST=1)
2755
- npx blindseal dashboard [flags] # localhost dashboard for scan history
1565
+ blindseal init set up ~/.blindseal (identity key, default policy, dashboard token)
1566
+ blindseal init --wrap [--yes --dry-run --config <path>]
1567
+ route the MCP servers in your Claude configs through blindseal
1568
+ blindseal init --unwrap restore those configs from their backups
1569
+ blindseal run --mock local API + dashboard over bundled sample data
1570
+ blindseal dashboard open the dashboard (starts run --mock if nothing is listening)
1571
+ blindseal -- <command> [args...] wrap a stdio MCP server
1572
+ blindseal --transport http --upstream <url> proxy a remote MCP server over HTTP
1573
+ blindseal --transport scan-http [--port <n>] stand-alone POST /scan endpoint
1574
+ blindseal attest <toolcall.json> --target-program <id> [--out <file>] [--expiry <slot> | --no-expiry]
1575
+ blindseal submit <attestation.json> verify on Solana mainnet and consume the nullifier
1576
+ blindseal verify <attestation.json | disclosure.json>
1577
+ blindseal audit verify | disclose --from <i> --to <j>
1578
+ blindseal history [flags] local scan history (needs BLINDSEAL_PERSIST=1)
1579
+ blindseal demo interactive detection demo
1580
+ blindseal <command> --help help for any command
2756
1581
 
2757
1582
  Examples
2758
- npx blindseal -- npx -y @modelcontextprotocol/server-filesystem /tmp
2759
- npx blindseal --transport http --upstream https://mcp.example.com/v1 --port 8800
1583
+ blindseal -- npx -y @modelcontextprotocol/server-filesystem /tmp
1584
+ blindseal --transport http --upstream https://mcp.example.com/v1 --port 8800
2760
1585
 
2761
- Flags
2762
- --transport <stdio|http|scan-http> default: stdio
1586
+ Proxy flags
1587
+ --transport <stdio|http|scan-http> default: stdio (requires \`-- <command>\`)
2763
1588
  --upstream <url> required when --transport http
2764
1589
  --port, --listen-port <n> default: 8800 (http) / 9787 (scan-http)
2765
1590
  --host <addr> default: 127.0.0.1
2766
1591
  --server-url <url> logical server identifier for scan-http (default: scan-http:blindseal)
2767
- --version, -v print version
2768
- --help, -h this help
1592
+ --version, -v print version
1593
+ --help, -h this help
2769
1594
 
2770
1595
  Detection (env)
2771
1596
  BLINDSEAL_MODE block (default) | warn | log
@@ -2774,10 +1599,10 @@ Detection (env)
2774
1599
  BLINDSEAL_LAYER3_MODEL model id override
2775
1600
  BLINDSEAL_LAYER3_BASE_URL base URL override (ollama or custom endpoint)
2776
1601
  BLINDSEAL_LAYER3_TIMEOUT_MS judge call hard timeout (default 15000)
2777
- BLINDSEAL_L3_FP_SAFE 1 = demote suspicious\u2192clean when L3 unavailable (old behaviour)
2778
- ANTHROPIC_API_KEY BYO key for Layer 3 (Anthropic) \u2190 strongly recommended
2779
- OPENAI_API_KEY BYO key for Layer 3 (OpenAI / compat)
2780
- OLLAMA_HOST Ollama base URL (default: http://localhost:11434); set to enable offline L3
1602
+ BLINDSEAL_L3_FP_SAFE 1 = demote suspicious\u2192clean when L3 unavailable
1603
+ ANTHROPIC_API_KEY BYO key for Layer 3 (Anthropic) \u2190 strongly recommended
1604
+ OPENAI_API_KEY BYO key for Layer 3 (OpenAI / compat)
1605
+ OLLAMA_HOST Ollama base URL (default: http://localhost:11434); set to enable offline L3
2781
1606
 
2782
1607
  Capability firewall (env, default off)
2783
1608
  BLINDSEAL_CAPABILITY 1 to enable taint-tracking + gate
@@ -2790,38 +1615,25 @@ Manifest verification (env)
2790
1615
  BLINDSEAL_MANIFEST_CHECK on (default) | off | strict
2791
1616
  BLINDSEAL_MANIFEST_CACHE_DIR override $XDG_CACHE_HOME/blindseal/manifests
2792
1617
 
2793
- Telemetry (env, default on when URL configured)
2794
- BLINDSEAL_TELEMETRY 0 to disable
2795
- BLINDSEAL_TELEMETRY_URL collector ingest URL
2796
- BLINDSEAL_TELEMETRY_SECRET optional Bearer token
2797
- BLINDSEAL_TELEMETRY_BATCH batch size (default 100)
2798
- BLINDSEAL_TELEMETRY_FLUSH_MS flush interval (default 5000)
2799
- BLINDSEAL_TELEMETRY_INSTALL_ID stable install id (hashed before send)
2800
-
2801
1618
  Audit log (env, default off)
2802
1619
  BLINDSEAL_AUDIT_LOG path to append-only JSONL of every decision
2803
1620
 
2804
- Local scan history (env, default off \u2014 opt in for visibility)
1621
+ Local scan history (env, default off)
2805
1622
  BLINDSEAL_PERSIST 1 to record every scan to ~/.blindseal/scans.db
2806
1623
  BLINDSEAL_PERSIST_PATH override DB path (default ~/.blindseal/scans.db)
2807
1624
  BLINDSEAL_RETENTION_DAYS rolling retention (default 30, purged on open)
2808
1625
 
2809
- On-chain attestation (env, default off)
2810
- BLINDSEAL_ATTEST 1 to enable EAS attestations on Base
2811
- BLINDSEAL_BASE_RPC_URL default: https://mainnet.base.org
2812
- BLINDSEAL_ATTESTER_PRIVATE_KEY hot wallet (fund with ~0.05 ETH on Base)
2813
- Privacy: see PRIVACY.md. SHA-256 hashes only \u2014 raw content never leaves the proxy.
1626
+ Chain + prover (env)
1627
+ BLINDSEAL_RPC_URL Solana RPC (default: the public mainnet endpoint)
1628
+ BLINDSEAL_PROVER path to a prover binary (otherwise ~/.blindseal/bin or a release download)
1629
+ BLINDSEAL_DASHBOARD_DIST override the dashboard directory served by \`blindseal run\`
1630
+ BLINDSEAL_FIXTURES override the sample data directory for \`run --mock\`
1631
+
1632
+ Nothing leaves this machine except the RPC calls you ask for (attest/submit/verify on chain).
2814
1633
  `;
1634
+ var SUBCOMMANDS = /* @__PURE__ */ new Set(["init", "run", "dashboard", "demo", "attest", "submit", "verify", "audit", "history"]);
1635
+ var PROXY_FLAGS = /* @__PURE__ */ new Set(["--", "--transport", "--upstream", "--port", "--listen-port", "--host", "--server-url"]);
2815
1636
  function parseArgs(argv) {
2816
- if (argv.length === 0) return { transport: "stdio", showHelp: true, noArgs: true };
2817
- if (argv.includes("--version") || argv.includes("-v")) {
2818
- process.stdout.write(`blindseal ${readVersion2()}
2819
- `);
2820
- process.exit(0);
2821
- }
2822
- if (argv.includes("--help") || argv.includes("-h")) {
2823
- return { transport: "stdio", showHelp: true, noArgs: false };
2824
- }
2825
1637
  const sepIdx = argv.indexOf("--");
2826
1638
  const flagArgs = sepIdx >= 0 ? argv.slice(0, sepIdx) : argv;
2827
1639
  const afterSep = sepIdx >= 0 ? argv.slice(sepIdx + 1) : [];
@@ -2849,80 +1661,88 @@ function parseArgs(argv) {
2849
1661
  serverUrl = flagArgs[++i];
2850
1662
  } else if (a && a.startsWith("--")) {
2851
1663
  throw new Error(`unknown flag: ${a}`);
1664
+ } else if (a) {
1665
+ throw new Error(`unexpected argument "${a}" \u2014 to wrap an MCP server use: blindseal -- ${a} ...`);
2852
1666
  }
2853
1667
  }
2854
1668
  if (transport === "http") {
2855
1669
  if (!upstream) throw new Error("--transport http requires --upstream <url>");
2856
- return { transport, upstream, port: port ?? 8800, host: host ?? "127.0.0.1", showHelp: false, noArgs: false };
1670
+ return { transport, upstream, port: port ?? 8800, host: host ?? "127.0.0.1" };
2857
1671
  }
2858
1672
  if (transport === "scan-http") {
2859
- return {
2860
- transport,
2861
- port: port ?? 9787,
2862
- host: host ?? "127.0.0.1",
2863
- serverUrl,
2864
- showHelp: false,
2865
- noArgs: false
2866
- };
1673
+ return { transport, port: port ?? 9787, host: host ?? "127.0.0.1", serverUrl };
2867
1674
  }
2868
- const target = sepIdx >= 0 ? afterSep : flagArgs;
2869
- const cmd = target[0];
2870
- const args = target.slice(1);
2871
- if (!cmd) throw new Error("no target command (use `--` to separate args)");
2872
- return { transport: "stdio", stdioTarget: { cmd, args }, showHelp: false, noArgs: false };
1675
+ if (sepIdx < 0) throw new Error('wrapping an MCP server requires "--": blindseal -- <command> [args...]');
1676
+ const cmd = afterSep[0];
1677
+ if (!cmd) throw new Error('no command after "--": blindseal -- <command> [args...]');
1678
+ return { transport: "stdio", stdioTarget: { cmd, args: afterSep.slice(1) } };
1679
+ }
1680
+ function fail(prefix, err) {
1681
+ process.stderr.write(`${prefix}: ${err instanceof Error ? err.message : String(err)}
1682
+ `);
1683
+ process.exit(1);
2873
1684
  }
2874
1685
  function main() {
2875
1686
  const argv = process.argv.slice(2);
2876
- if (argv[0] === "demo") {
2877
- runDemo().then(() => process.exit(0)).catch((err) => {
2878
- process.stderr.write(`blindseal demo: ${err instanceof Error ? err.message : String(err)}
2879
- `);
2880
- process.exit(1);
2881
- });
2882
- return;
2883
- }
2884
- if (argv[0] === "run") {
2885
- runRun(argv.slice(1)).then((code) => {
2886
- if (code !== 0) process.exit(code);
2887
- }).catch((err) => {
2888
- process.stderr.write(`blindseal run: ${err instanceof Error ? err.message : String(err)}
2889
- `);
2890
- process.exit(1);
2891
- });
2892
- return;
2893
- }
2894
- if (argv[0] === "attest") {
2895
- import("./attest-QLIE2OSQ.js").then(({ runAttestCli }) => runAttestCli(argv.slice(1))).then((code) => process.exit(code)).catch((err) => {
2896
- process.stderr.write(`blindseal attest: ${err instanceof Error ? err.message : String(err)}
2897
- `);
2898
- process.exit(1);
2899
- });
2900
- return;
1687
+ const first = argv[0];
1688
+ if (first === void 0) {
1689
+ process.stdout.write(HELP2);
1690
+ process.exit(2);
2901
1691
  }
2902
- if (argv[0] === "submit") {
2903
- import("./submit-2UALCRX2.js").then(({ runSubmitCli }) => runSubmitCli(argv.slice(1))).then((code) => process.exit(code)).catch((err) => {
2904
- process.stderr.write(`blindseal submit: ${err instanceof Error ? err.message : String(err)}
2905
- `);
2906
- process.exit(1);
2907
- });
2908
- return;
1692
+ if (first === "--help" || first === "-h") {
1693
+ process.stdout.write(HELP2);
1694
+ process.exit(0);
2909
1695
  }
2910
- if (argv[0] === "audit" || argv[0] === "verify") {
2911
- const which = argv[0];
2912
- (which === "audit" ? import("./audit-C756XFXR.js").then((m) => m.runAuditCli(argv.slice(1))) : import("./verify-HO4IU7XV.js").then((m) => m.runVerifyCli(argv.slice(1)))).then((code) => process.exit(code)).catch((err) => {
2913
- process.stderr.write(`blindseal ${which}: ${err instanceof Error ? err.message : String(err)}
1696
+ if (first === "--version" || first === "-v") {
1697
+ process.stdout.write(`blindseal ${packageVersion()}
2914
1698
  `);
2915
- process.exit(1);
2916
- });
2917
- return;
1699
+ process.exit(0);
2918
1700
  }
2919
- if (argv[0] === "history") {
2920
- process.exit(runHistoryCli(argv.slice(1)));
1701
+ if (SUBCOMMANDS.has(first)) {
1702
+ const rest = argv.slice(1);
1703
+ switch (first) {
1704
+ case "init":
1705
+ import("./init-ZP3SLHM3.js").then((m) => m.runInit(rest)).then((code) => process.exit(code)).catch((e) => fail("blindseal init", e));
1706
+ return;
1707
+ case "run":
1708
+ import("./run-YQBNEPSD.js").then((m) => m.runRun(rest)).then((code) => {
1709
+ if (code !== 0) process.exit(code);
1710
+ }).catch((e) => fail("blindseal run", e));
1711
+ return;
1712
+ // the server keeps the process alive
1713
+ case "dashboard":
1714
+ import("./dashboard-J6BA7OBL.js").then((m) => m.runDashboardCli(rest)).then((code) => {
1715
+ if (code !== 0) process.exit(code);
1716
+ }).catch((e) => fail("blindseal dashboard", e));
1717
+ return;
1718
+ // may keep a `run --mock` alive
1719
+ case "demo":
1720
+ runDemo().then(() => process.exit(0)).catch((e) => fail("blindseal demo", e));
1721
+ return;
1722
+ case "attest":
1723
+ import("./attest-VWYZAOLY.js").then((m) => m.runAttestCli(rest)).then((code) => process.exit(code)).catch((e) => fail("blindseal attest", e));
1724
+ return;
1725
+ case "submit":
1726
+ import("./submit-D6MQTAXT.js").then((m) => m.runSubmitCli(rest)).then((code) => process.exit(code)).catch((e) => fail("blindseal submit", e));
1727
+ return;
1728
+ case "verify":
1729
+ import("./verify-RISAGKXC.js").then((m) => m.runVerifyCli(rest)).then((code) => process.exit(code)).catch((e) => fail("blindseal verify", e));
1730
+ return;
1731
+ case "audit":
1732
+ import("./audit-G4DDMAXH.js").then((m) => m.runAuditCli(rest)).then((code) => process.exit(code)).catch((e) => fail("blindseal audit", e));
1733
+ return;
1734
+ case "history":
1735
+ process.exit(runHistoryCli(rest));
1736
+ }
2921
1737
  }
2922
- if (argv[0] === "dashboard") {
2923
- const code = runDashboardCli(argv.slice(1));
2924
- if (code !== 0) process.exit(code);
2925
- return;
1738
+ if (!PROXY_FLAGS.has(first) && !(first.startsWith("-") && argv.includes("--"))) {
1739
+ process.stderr.write(
1740
+ first.startsWith("-") ? `blindseal: unknown flag "${first}"
1741
+ ` : `blindseal: unknown command "${first}"
1742
+ `
1743
+ );
1744
+ process.stderr.write(HELP2);
1745
+ process.exit(2);
2926
1746
  }
2927
1747
  let parsed;
2928
1748
  try {
@@ -2930,26 +1750,21 @@ function main() {
2930
1750
  } catch (err) {
2931
1751
  process.stderr.write(`blindseal: ${err instanceof Error ? err.message : String(err)}
2932
1752
  `);
2933
- process.stderr.write(HELP4);
1753
+ process.stderr.write(HELP2);
2934
1754
  process.exit(2);
2935
1755
  }
2936
- if (parsed.showHelp) {
2937
- process.stdout.write(HELP4);
2938
- process.exit(parsed.noArgs ? 2 : 0);
2939
- }
2940
1756
  if (parsed.transport === "http") {
2941
1757
  startHttpProxy({ upstream: parsed.upstream, listenPort: parsed.port, listenHost: parsed.host });
2942
1758
  return;
2943
1759
  }
2944
1760
  if (parsed.transport === "scan-http") {
2945
- startScanHttp({
2946
- listenPort: parsed.port,
2947
- listenHost: parsed.host,
2948
- defaultServerUrl: parsed.serverUrl
2949
- });
1761
+ startScanHttp({ listenPort: parsed.port, listenHost: parsed.host, defaultServerUrl: parsed.serverUrl });
2950
1762
  return;
2951
1763
  }
2952
1764
  const { cmd, args } = parsed.stdioTarget;
2953
1765
  startProxy(cmd, args);
2954
1766
  }
2955
1767
  main();
1768
+ export {
1769
+ parseArgs
1770
+ };