@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/dist/cli.mjs ADDED
@@ -0,0 +1,4317 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
10
+ var __esm = (fn, res) => function __init() {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ };
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+
18
+ // src/keychain.ts
19
+ import { spawnSync } from "node:child_process";
20
+ function has(cmd) {
21
+ const finder = isWin ? "where" : "which";
22
+ const r = spawnSync(finder, [cmd], { stdio: "ignore" });
23
+ return r.status === 0;
24
+ }
25
+ function winPS(op, target, secret) {
26
+ let body;
27
+ if (op === "set") {
28
+ body = `$b=[System.Text.Encoding]::UTF8.GetBytes($env:BF_SECRET);$p=[Runtime.InteropServices.Marshal]::AllocHGlobal($b.Length);[Runtime.InteropServices.Marshal]::Copy($b,0,$p,$b.Length);$c=New-Object BFCred+CREDENTIAL;$c.Type=1;$c.TargetName=$env:BF_TARGET;$c.UserName=$env:BF_TARGET;$c.CredentialBlobSize=$b.Length;$c.CredentialBlob=$p;$c.Persist=2;$ok=[BFCred]::CredWriteW([ref]$c,0);[Runtime.InteropServices.Marshal]::FreeHGlobal($p);if($ok){[Console]::Out.Write('BFOK')}`;
29
+ } else if (op === "get") {
30
+ body = `$ptr=[IntPtr]::Zero;if([BFCred]::CredReadW($env:BF_TARGET,1,0,[ref]$ptr)){$c=[Runtime.InteropServices.Marshal]::PtrToStructure($ptr,[BFCred+CREDENTIAL]);$n=[int]$c.CredentialBlobSize;$b=New-Object byte[] $n;[Runtime.InteropServices.Marshal]::Copy([IntPtr]$c.CredentialBlob,$b,0,$n);[BFCred]::CredFree($ptr);[Console]::Out.Write([System.Text.Encoding]::UTF8.GetString($b))}else{exit 1}`;
31
+ } else {
32
+ body = `if([BFCred]::CredDeleteW($env:BF_TARGET,1,0)){[Console]::Out.Write('BFOK')}`;
33
+ }
34
+ const env = { ...process.env, BF_TARGET: target };
35
+ if (secret !== void 0) env.BF_SECRET = secret;
36
+ const r = spawnSync(
37
+ "powershell",
38
+ ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "-"],
39
+ { input: PS_HEADER + body, env, encoding: "utf8" }
40
+ );
41
+ return { status: r.status, stdout: typeof r.stdout === "string" ? r.stdout : "" };
42
+ }
43
+ function winTarget(account) {
44
+ return `${SERVICE}:${account}`;
45
+ }
46
+ function keychainAvailable() {
47
+ if (process.platform === "darwin") return has("security");
48
+ if (process.platform === "linux") return has("secret-tool");
49
+ if (isWin) return has("powershell");
50
+ return false;
51
+ }
52
+ function keychainBackend() {
53
+ if (process.platform === "darwin") return "macOS Keychain";
54
+ if (process.platform === "linux") return "libsecret (secret-tool)";
55
+ if (isWin) return "Windows Credential Manager";
56
+ return "none";
57
+ }
58
+ function keychainSet(account, secret) {
59
+ if (process.platform === "darwin") {
60
+ const r = spawnSync("security", ["add-generic-password", "-a", account, "-s", SERVICE, "-w", secret, "-U"], { stdio: "ignore" });
61
+ return r.status === 0;
62
+ }
63
+ if (process.platform === "linux") {
64
+ const r = spawnSync("secret-tool", ["store", "--label=blindfold", "service", SERVICE, "account", account], { input: secret, stdio: ["pipe", "ignore", "ignore"] });
65
+ return r.status === 0;
66
+ }
67
+ if (isWin) return winPS("set", winTarget(account), secret).stdout.includes("BFOK");
68
+ return false;
69
+ }
70
+ function keychainGet(account) {
71
+ if (process.platform === "darwin") {
72
+ const r = spawnSync("security", ["find-generic-password", "-a", account, "-s", SERVICE, "-w"], { encoding: "utf8" });
73
+ if (r.status === 0 && typeof r.stdout === "string") return r.stdout.replace(/\n$/, "");
74
+ return null;
75
+ }
76
+ if (process.platform === "linux") {
77
+ const r = spawnSync("secret-tool", ["lookup", "service", SERVICE, "account", account], { encoding: "utf8" });
78
+ if (r.status === 0 && r.stdout) return r.stdout.replace(/\n$/, "");
79
+ return null;
80
+ }
81
+ if (isWin) {
82
+ const r = winPS("get", winTarget(account));
83
+ if (r.status === 0 && r.stdout) return r.stdout.replace(/\r?\n$/, "");
84
+ return null;
85
+ }
86
+ return null;
87
+ }
88
+ function keychainDelete(account) {
89
+ if (process.platform === "darwin") {
90
+ const r = spawnSync("security", ["delete-generic-password", "-a", account, "-s", SERVICE], { stdio: "ignore" });
91
+ return r.status === 0;
92
+ }
93
+ if (process.platform === "linux") {
94
+ const r = spawnSync("secret-tool", ["clear", "service", SERVICE, "account", account], { stdio: "ignore" });
95
+ return r.status === 0;
96
+ }
97
+ if (isWin) return winPS("delete", winTarget(account)).stdout.includes("BFOK");
98
+ return false;
99
+ }
100
+ var SERVICE, isWin, WIN_CS, PS_HEADER;
101
+ var init_keychain = __esm({
102
+ "src/keychain.ts"() {
103
+ "use strict";
104
+ SERVICE = "blindfold";
105
+ isWin = process.platform === "win32";
106
+ WIN_CS = `
107
+ using System;
108
+ using System.Runtime.InteropServices;
109
+ public static class BFCred {
110
+ [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
111
+ public struct CREDENTIAL {
112
+ public UInt32 Flags; public UInt32 Type;
113
+ [MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
114
+ [MarshalAs(UnmanagedType.LPWStr)] public string Comment;
115
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
116
+ public UInt32 CredentialBlobSize; public IntPtr CredentialBlob; public UInt32 Persist;
117
+ public UInt32 AttributeCount; public IntPtr Attributes;
118
+ [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
119
+ [MarshalAs(UnmanagedType.LPWStr)] public string UserName;
120
+ }
121
+ [DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern bool CredWriteW(ref CREDENTIAL c, UInt32 f);
122
+ [DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern bool CredReadW(string t, UInt32 ty, UInt32 f, out IntPtr c);
123
+ [DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern bool CredDeleteW(string t, UInt32 ty, UInt32 f);
124
+ [DllImport("advapi32.dll")] public static extern void CredFree(IntPtr c);
125
+ }
126
+ `;
127
+ PS_HEADER = `$ErrorActionPreference='Stop'
128
+ Add-Type -TypeDefinition @"
129
+ ${WIN_CS}
130
+ "@
131
+ `;
132
+ }
133
+ });
134
+
135
+ // src/env.ts
136
+ import fs2 from "node:fs";
137
+ import os from "node:os";
138
+ import path2 from "node:path";
139
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
140
+ function repoRoot() {
141
+ if (_repoRoot) return _repoRoot;
142
+ let dir = process.cwd();
143
+ for (let i = 0; i < 12; i++) {
144
+ if (fs2.existsSync(path2.join(dir, ".env")) || fs2.existsSync(path2.join(dir, ".blindfold")) || fs2.existsSync(path2.join(dir, ".git"))) {
145
+ _repoRoot = dir;
146
+ return dir;
147
+ }
148
+ const parent = path2.dirname(dir);
149
+ if (parent === dir) break;
150
+ dir = parent;
151
+ }
152
+ _repoRoot = SRC_RELATIVE_ROOT;
153
+ return _repoRoot;
154
+ }
155
+ function homeDir() {
156
+ return path2.join(os.homedir(), ".blindfold");
157
+ }
158
+ function configPath() {
159
+ return path2.join(homeDir(), "config.json");
160
+ }
161
+ function stateDir() {
162
+ const override = process.env.BLINDFOLD_STATE_DIR;
163
+ if (override && override.trim()) return path2.resolve(override.trim());
164
+ const home = homeDir();
165
+ if (!fs2.existsSync(home)) {
166
+ try {
167
+ const legacy = path2.join(repoRoot(), ".blindfold");
168
+ if (fs2.existsSync(legacy) && fs2.statSync(legacy).isDirectory()) {
169
+ fs2.cpSync(legacy, home, { recursive: true });
170
+ } else {
171
+ fs2.mkdirSync(home, { recursive: true });
172
+ }
173
+ } catch {
174
+ try {
175
+ fs2.mkdirSync(home, { recursive: true });
176
+ } catch {
177
+ }
178
+ }
179
+ }
180
+ return home;
181
+ }
182
+ function assertSafeOverrideUrl(raw, label) {
183
+ let u;
184
+ try {
185
+ u = new URL(raw);
186
+ } catch {
187
+ throw new Error(`${label} is not a valid URL: ${raw}`);
188
+ }
189
+ const isLocal = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(u.hostname);
190
+ if (u.protocol !== "https:" && !isLocal) {
191
+ throw new Error(`${label} must be https (got ${u.protocol}//${u.hostname}); refusing to trust an insecure endpoint`);
192
+ }
193
+ }
194
+ function withFileLockSync(target, fn) {
195
+ const lockPath = `${target}.lock`;
196
+ const deadline = Date.now() + 1e4;
197
+ fs2.mkdirSync(path2.dirname(lockPath), { recursive: true });
198
+ const sleep = (ms) => {
199
+ try {
200
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
201
+ } catch {
202
+ }
203
+ };
204
+ for (; ; ) {
205
+ try {
206
+ const fd = fs2.openSync(lockPath, "wx");
207
+ fs2.writeSync(fd, String(process.pid));
208
+ fs2.closeSync(fd);
209
+ break;
210
+ } catch (e) {
211
+ if (e?.code !== "EEXIST") throw e;
212
+ try {
213
+ const st = fs2.statSync(lockPath);
214
+ if (Date.now() - st.mtimeMs > 1e4) {
215
+ fs2.rmSync(lockPath, { force: true });
216
+ continue;
217
+ }
218
+ } catch {
219
+ }
220
+ if (Date.now() > deadline) throw new Error(`timed out acquiring lock ${lockPath}`);
221
+ sleep(25);
222
+ }
223
+ }
224
+ try {
225
+ return fn();
226
+ } finally {
227
+ fs2.rmSync(lockPath, { force: true });
228
+ }
229
+ }
230
+ function defaultEnvPath() {
231
+ return path2.join(repoRoot(), ".env");
232
+ }
233
+ function loadEnvFromFile(envPath = path2.join(repoRoot(), ".env")) {
234
+ if (!fs2.existsSync(envPath)) return;
235
+ const text = fs2.readFileSync(envPath, "utf8");
236
+ for (const raw of text.split(/\r?\n/)) {
237
+ const line = raw.trim();
238
+ if (!line || line.startsWith("#")) continue;
239
+ const eq = line.indexOf("=");
240
+ if (eq <= 0) continue;
241
+ const key = line.slice(0, eq).trim();
242
+ let val = line.slice(eq + 1).trim();
243
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
244
+ val = val.slice(1, -1);
245
+ }
246
+ if (process.env[key] === void 0) process.env[key] = val;
247
+ }
248
+ }
249
+ function loadHomeConfig() {
250
+ try {
251
+ const cfg = configPath();
252
+ if (fs2.existsSync(cfg)) {
253
+ const obj = JSON.parse(fs2.readFileSync(cfg, "utf8"));
254
+ for (const [k, v] of Object.entries(obj)) {
255
+ if (k === "T3N_API_KEY_STORE") continue;
256
+ if (typeof v === "string" && process.env[k] === void 0) process.env[k] = v;
257
+ }
258
+ }
259
+ } catch {
260
+ }
261
+ loadEnvFromFile(path2.join(homeDir(), ".env"));
262
+ if (process.env.T3N_API_KEY === void 0 && process.env.DID && keychainAvailable()) {
263
+ const k = keychainGet(process.env.DID);
264
+ if (k) process.env.T3N_API_KEY = k;
265
+ }
266
+ }
267
+ function loadBlindfoldEnv() {
268
+ loadEnvFromFile();
269
+ loadHomeConfig();
270
+ const t3nApiKey = process.env.T3N_API_KEY ?? "";
271
+ const did = process.env.DID ?? "";
272
+ const port = Number.parseInt(process.env.BLINDFOLD_PORT ?? "8787", 10);
273
+ const t3EnvRaw = (process.env.BLINDFOLD_T3_ENV ?? "testnet").toLowerCase();
274
+ const t3Env = t3EnvRaw === "production" ? "production" : "testnet";
275
+ const t3BaseUrl = (process.env.T3_BASE_URL ?? process.env.BLINDFOLD_BASE_URL ?? "").trim();
276
+ if (t3BaseUrl) {
277
+ assertSafeOverrideUrl(t3BaseUrl, process.env.T3_BASE_URL ? "T3_BASE_URL" : "BLINDFOLD_BASE_URL");
278
+ }
279
+ const mock = process.env.BLINDFOLD_MOCK === "1";
280
+ return { t3nApiKey, did, port, t3Env, t3BaseUrl, mock };
281
+ }
282
+ function assertRealReady(env) {
283
+ if (env.mock) return;
284
+ const missing = [];
285
+ if (!env.t3nApiKey) missing.push("T3N_API_KEY");
286
+ if (!env.did) missing.push("DID");
287
+ if (missing.length > 0) {
288
+ throw new Error(
289
+ `Missing required env: ${missing.join(", ")}. Claim them at https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens, then put them in .env. Or set BLINDFOLD_MOCK=1 to use mock mode for the demo only.`
290
+ );
291
+ }
292
+ }
293
+ function pluckSecret(envName) {
294
+ const v = process.env[envName];
295
+ if (!v) {
296
+ throw new Error(`environment variable ${envName} is unset or empty`);
297
+ }
298
+ return v;
299
+ }
300
+ var HERE2, SRC_RELATIVE_ROOT, _repoRoot;
301
+ var init_env = __esm({
302
+ "src/env.ts"() {
303
+ "use strict";
304
+ init_keychain();
305
+ HERE2 = path2.dirname(fileURLToPath2(import.meta.url));
306
+ SRC_RELATIVE_ROOT = path2.resolve(HERE2, "..", "..", "..");
307
+ _repoRoot = null;
308
+ }
309
+ });
310
+
311
+ // src/prompt.ts
312
+ async function readSecretLine(prompt) {
313
+ process.stderr.write(prompt);
314
+ const stdin = process.stdin;
315
+ if (!stdin.isTTY) {
316
+ return await readOneLine(stdin);
317
+ }
318
+ return await new Promise((resolve, reject) => {
319
+ let buf = "";
320
+ stdin.setRawMode(true);
321
+ stdin.resume();
322
+ stdin.setEncoding("utf8");
323
+ const cleanup = () => {
324
+ stdin.removeListener("data", onData);
325
+ stdin.setRawMode(false);
326
+ stdin.pause();
327
+ };
328
+ const onData = (chunk) => {
329
+ const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
330
+ for (const ch of s) {
331
+ if (ch === "\n" || ch === "\r") {
332
+ cleanup();
333
+ process.stderr.write("\n");
334
+ resolve(buf);
335
+ return;
336
+ }
337
+ if (ch === "") {
338
+ cleanup();
339
+ process.stderr.write("\n");
340
+ reject(new Error("aborted by user"));
341
+ return;
342
+ }
343
+ if (ch === "\x7F" || ch === "\b") {
344
+ if (buf.length > 0) buf = buf.slice(0, -1);
345
+ continue;
346
+ }
347
+ if (ch >= " " || ch === " ") buf += ch;
348
+ }
349
+ };
350
+ stdin.on("data", onData);
351
+ });
352
+ }
353
+ async function readLine(prompt) {
354
+ process.stderr.write(prompt);
355
+ const stdin = process.stdin;
356
+ if (!stdin.isTTY) return await readOneLine(stdin);
357
+ return await new Promise((resolve, reject) => {
358
+ let buf = "";
359
+ stdin.resume();
360
+ stdin.setEncoding("utf8");
361
+ const cleanup = () => {
362
+ stdin.removeListener("data", onData);
363
+ stdin.pause();
364
+ };
365
+ const onData = (chunk) => {
366
+ const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
367
+ for (const ch of s) {
368
+ if (ch === "\r" || ch === "\n") {
369
+ process.stderr.write("\n");
370
+ cleanup();
371
+ resolve(buf);
372
+ return;
373
+ }
374
+ if (ch === "") {
375
+ cleanup();
376
+ reject(new Error("aborted"));
377
+ return;
378
+ }
379
+ buf += ch;
380
+ }
381
+ };
382
+ stdin.on("data", onData);
383
+ });
384
+ }
385
+ function readOneLine(stdin) {
386
+ return new Promise((resolve, reject) => {
387
+ let buf = "";
388
+ stdin.setEncoding("utf8");
389
+ stdin.on("data", (d) => {
390
+ buf += d;
391
+ });
392
+ stdin.on("end", () => resolve(buf.replace(/\r?\n$/, "")));
393
+ stdin.on("error", reject);
394
+ });
395
+ }
396
+ var init_prompt = __esm({
397
+ "src/prompt.ts"() {
398
+ "use strict";
399
+ }
400
+ });
401
+
402
+ // src/constants.ts
403
+ var SENTINEL, CONTRACT_TAIL, CONTRACT_VERSION, DEFAULT_DASHBOARD_PORT;
404
+ var init_constants = __esm({
405
+ "src/constants.ts"() {
406
+ "use strict";
407
+ SENTINEL = "__BLINDFOLD__";
408
+ CONTRACT_TAIL = "blindfold-proxy";
409
+ CONTRACT_VERSION = "0.5.6";
410
+ DEFAULT_DASHBOARD_PORT = 8799;
411
+ }
412
+ });
413
+
414
+ // src/log.ts
415
+ function safeLog(level, obj) {
416
+ const safe = typeof obj === "string" ? { msg: obj } : redact(obj);
417
+ const line = JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), level, ...safe });
418
+ process.stderr.write(line + "\n");
419
+ }
420
+ function redact(input) {
421
+ const out = {};
422
+ for (const [k, v] of Object.entries(input)) {
423
+ if (k === "headers" && v && typeof v === "object") {
424
+ out[k] = redactHeaders(v);
425
+ } else if (HEADER_BLOCKLIST.has(k.toLowerCase())) {
426
+ out[k] = "[redacted]";
427
+ } else {
428
+ out[k] = v;
429
+ }
430
+ }
431
+ return out;
432
+ }
433
+ function redactHeaders(h) {
434
+ if (Array.isArray(h)) {
435
+ return h.map(([k, v]) => [k, HEADER_BLOCKLIST.has(k.toLowerCase()) ? "[redacted]" : v]);
436
+ }
437
+ const out = {};
438
+ for (const [k, v] of Object.entries(h)) {
439
+ out[k] = HEADER_BLOCKLIST.has(k.toLowerCase()) ? "[redacted]" : String(v);
440
+ }
441
+ return out;
442
+ }
443
+ var HEADER_BLOCKLIST;
444
+ var init_log = __esm({
445
+ "src/log.ts"() {
446
+ "use strict";
447
+ HEADER_BLOCKLIST = /* @__PURE__ */ new Set([
448
+ "authorization",
449
+ "proxy-authorization",
450
+ "x-api-key",
451
+ "cookie",
452
+ "set-cookie"
453
+ ]);
454
+ }
455
+ });
456
+
457
+ // src/t3-client.ts
458
+ var t3_client_exports = {};
459
+ __export(t3_client_exports, {
460
+ SignupEmailTakenError: () => SignupEmailTakenError,
461
+ T3TimeoutError: () => T3TimeoutError,
462
+ loadEgressHosts: () => loadEgressHosts,
463
+ openT3Client: () => openT3Client,
464
+ signupTenant: () => signupTenant
465
+ });
466
+ import { createHash as createHash2, randomBytes } from "node:crypto";
467
+ import fs3 from "node:fs";
468
+ import path3 from "node:path";
469
+ function withDeadline(promise, label, ms = T3_TIMEOUT_MS) {
470
+ return new Promise((resolve, reject) => {
471
+ const timer = setTimeout(() => reject(new T3TimeoutError(`T3 ${label} timed out after ${ms}ms`)), ms);
472
+ if (typeof timer.unref === "function") timer.unref();
473
+ promise.then(
474
+ (v) => {
475
+ clearTimeout(timer);
476
+ resolve(v);
477
+ },
478
+ (e) => {
479
+ clearTimeout(timer);
480
+ reject(e);
481
+ }
482
+ );
483
+ });
484
+ }
485
+ function egressCachePath() {
486
+ return process.env.BLINDFOLD_EGRESS_CACHE ?? path3.join(stateDir(), "egress-hosts.json");
487
+ }
488
+ function loadEgressHosts(did) {
489
+ try {
490
+ const all = JSON.parse(fs3.readFileSync(egressCachePath(), "utf8"));
491
+ return Array.isArray(all[did]) ? all[did] : [];
492
+ } catch {
493
+ return [];
494
+ }
495
+ }
496
+ function saveEgressHosts(did, hosts) {
497
+ const p = egressCachePath();
498
+ withFileLockSync(p, () => {
499
+ let all = {};
500
+ try {
501
+ all = JSON.parse(fs3.readFileSync(p, "utf8"));
502
+ } catch {
503
+ }
504
+ const existing = Array.isArray(all[did]) ? all[did] : [];
505
+ all[did] = Array.from(/* @__PURE__ */ new Set([...existing, ...hosts])).sort();
506
+ fs3.mkdirSync(path3.dirname(p), { recursive: true });
507
+ fs3.writeFileSync(p, JSON.stringify(all, null, 2));
508
+ });
509
+ }
510
+ async function loadSdk() {
511
+ if (sdkCache) return sdkCache;
512
+ try {
513
+ const mod = await import("@terminal3/t3n-sdk");
514
+ sdkCache = mod;
515
+ return mod;
516
+ } catch {
517
+ throw new Error(
518
+ "@terminal3/t3n-sdk not installed. Run `npm install @terminal3/t3n-sdk` for REAL T3 mode, or set BLINDFOLD_MOCK=1 to keep using mock mode."
519
+ );
520
+ }
521
+ }
522
+ async function signupTenant(opts) {
523
+ const sdk = await loadSdk();
524
+ sdk.setEnvironment(opts.env);
525
+ const baseUrl = opts.baseUrl || sdk.NODE_URLS[opts.env];
526
+ const wasmComponent = await sdk.loadWasmComponent();
527
+ const key = "0x" + randomBytes(32).toString("hex");
528
+ const address = sdk.eth_get_address(key);
529
+ const t3n = new sdk.T3nClient({
530
+ baseUrl,
531
+ wasmComponent,
532
+ handlers: { EthSign: sdk.metamask_sign(address, void 0, key) }
533
+ });
534
+ await t3n.handshake();
535
+ const did = String(await t3n.authenticate(sdk.createEthAuthInput(address)));
536
+ const channel = { emailChannel: { emailAddress: opts.email } };
537
+ await t3n.otpRequest(channel);
538
+ const code = String(await opts.getOtpCode(opts.email, "email")).trim();
539
+ const verify = await t3n.otpVerify({ otpCode: code, request: channel });
540
+ if (verify.mergeSuggestion) {
541
+ throw new SignupEmailTakenError(opts.email, verify.mergeSuggestion.existingDid);
542
+ }
543
+ if (verify.status === "otp_failed" || verify.status === "otp_expired") {
544
+ throw new Error(
545
+ verify.status === "otp_expired" ? "the code expired \u2014 run `blindfold signup` again to get a fresh one" : "that code was incorrect \u2014 re-run `blindfold signup` and enter the emailed code"
546
+ );
547
+ }
548
+ if (opts.onKeyReady) await opts.onKeyReady(key, did);
549
+ const result = await t3n.submitUserInput({
550
+ profile: { email_address: opts.email, ...opts.profile ?? {} },
551
+ becomeDevTenant: true
552
+ });
553
+ const admit = result.tenantAdmit;
554
+ return {
555
+ key,
556
+ did,
557
+ address,
558
+ env: opts.env,
559
+ admitStatus: admit?.status ?? "unknown",
560
+ grantedCredits: admit?.grantedCredits,
561
+ refusedReason: admit?.reason,
562
+ refusedDetail: admit?.detail
563
+ };
564
+ }
565
+ async function openT3Client(env) {
566
+ if (env.mock) return openMockClient();
567
+ assertRealReady(env);
568
+ return openRealClient(env);
569
+ }
570
+ async function openRealClient(env) {
571
+ if (!/^0x[0-9a-fA-F]{64}$/.test(env.t3nApiKey)) {
572
+ throw new Error("T3N_API_KEY must be a 0x-prefixed 32-byte hex (secp256k1 private key).");
573
+ }
574
+ if (!/^did:t3n:[0-9a-fA-F]+$/.test(env.did)) {
575
+ throw new Error('DID must look like "did:t3n:<hex>".');
576
+ }
577
+ const sdk = await loadSdk();
578
+ sdk.setEnvironment(env.t3Env);
579
+ const baseUrl = env.t3BaseUrl || sdk.NODE_URLS[env.t3Env];
580
+ const wasmComponent = await sdk.loadWasmComponent();
581
+ const address = sdk.eth_get_address(env.t3nApiKey);
582
+ const t3n = new sdk.T3nClient({
583
+ baseUrl,
584
+ wasmComponent,
585
+ handlers: { EthSign: sdk.metamask_sign(address, void 0, env.t3nApiKey) }
586
+ });
587
+ await t3n.handshake();
588
+ await t3n.authenticate(sdk.createEthAuthInput(address));
589
+ const tenant = new sdk.TenantClient({
590
+ environment: env.t3Env,
591
+ baseUrl,
592
+ tenantDid: env.did,
593
+ t3n
594
+ });
595
+ const seedSecret = async (name, value) => {
596
+ await tenant.executeControl("map-entry-set", {
597
+ map_name: tenant.canonicalName("secrets"),
598
+ key: name,
599
+ value
600
+ });
601
+ safeLog("info", { msg: "seeded", name });
602
+ };
603
+ const registerContract4 = async (wasm) => {
604
+ const r = await tenant.contracts.register({
605
+ tail: CONTRACT_TAIL,
606
+ version: CONTRACT_VERSION,
607
+ wasm
608
+ });
609
+ return {
610
+ contractId: r.contract_id ?? r.contractId ?? "(unknown)"
611
+ };
612
+ };
613
+ const invokeForward = async (req) => {
614
+ const raw = await withDeadline(tenant.contracts.execute(CONTRACT_TAIL, {
615
+ version: CONTRACT_VERSION,
616
+ functionName: "forward",
617
+ input: req
618
+ }), "forward");
619
+ if (typeof raw.status === "number" && Array.isArray(raw.headers)) {
620
+ return { status: raw.status, headers: raw.headers, body: raw.body ?? "" };
621
+ }
622
+ return {
623
+ status: raw.code ?? 502,
624
+ headers: [["content-type", "application/json"]],
625
+ body: raw.body ?? ""
626
+ };
627
+ };
628
+ const decodeSecret = (v) => {
629
+ if (typeof v === "string") return v;
630
+ if (Array.isArray(v)) return Buffer.from(v).toString("utf8");
631
+ if (v && typeof v === "object") {
632
+ const o = v;
633
+ if (typeof o.value === "string") return o.value;
634
+ if (Array.isArray(o.value)) return Buffer.from(o.value).toString("utf8");
635
+ const entry = o.entry;
636
+ if (entry && typeof entry.value === "string") return entry.value;
637
+ }
638
+ return "";
639
+ };
640
+ const releaseSecret = async (name) => {
641
+ try {
642
+ const r = await withDeadline(tenant.contracts.execute(CONTRACT_TAIL, {
643
+ version: CONTRACT_VERSION,
644
+ functionName: "release-to-tenant",
645
+ input: { secret_key: name }
646
+ }), "release-to-tenant");
647
+ if (r && r.ok && r.value) return r.value;
648
+ } catch (e) {
649
+ if (e instanceof T3TimeoutError) throw e;
650
+ }
651
+ const direct = decodeSecret(
652
+ await withDeadline(tenant.executeControl("map-entry-get", {
653
+ map_name: tenant.canonicalName("secrets"),
654
+ key: name
655
+ }), "map-entry-get")
656
+ );
657
+ if (!direct) throw new Error(`secret "${name}" not found in the secrets map`);
658
+ return direct;
659
+ };
660
+ const me = async () => {
661
+ const info2 = await tenant.tenant.me();
662
+ return { tenant: String(info2.tenant ?? ""), status: info2.status };
663
+ };
664
+ const agentAuthUpdate = async (agentDid, hosts, functions) => {
665
+ let ucv = "0.1.0";
666
+ if (typeof sdk.getScriptVersion === "function") {
667
+ try {
668
+ const v = await sdk.getScriptVersion(baseUrl, "tee:user/contracts");
669
+ if (typeof v === "string" && /^\d/.test(v)) ucv = v;
670
+ } catch {
671
+ }
672
+ }
673
+ const didHex = env.did.replace(/^did:t3n:/, "");
674
+ const revoking = functions.length === 0 && hosts.length === 0;
675
+ const scripts = [{
676
+ scriptName: `z:${didHex}:${CONTRACT_TAIL}`,
677
+ versionReq: `>=${CONTRACT_VERSION}`,
678
+ functions: revoking ? ["forward"] : functions,
679
+ allowedHosts: revoking ? [] : hosts
680
+ }];
681
+ await t3n.execute({
682
+ script_name: "tee:user/contracts",
683
+ script_version: ucv,
684
+ function_name: "agent-auth-update",
685
+ input: { agents: [{ agentDid, scripts }] }
686
+ });
687
+ };
688
+ const grantEgress = async (hosts, opts) => {
689
+ const prev = opts?.replace ? [] : loadEgressHosts(env.did);
690
+ const merged = Array.from(/* @__PURE__ */ new Set([...prev, ...hosts])).sort();
691
+ await agentAuthUpdate(env.did, merged, ["forward", "release-to-tenant"]);
692
+ saveEgressHosts(env.did, merged);
693
+ return merged;
694
+ };
695
+ const setAgentGrant = (agentDid, hosts, functions) => agentAuthUpdate(agentDid, hosts, functions);
696
+ const verifySecret = async (name) => {
697
+ try {
698
+ const value = await releaseSecret(name);
699
+ return { present: true, length: value.length, fingerprint: createHash2("sha256").update(value).digest("hex").slice(0, 8) };
700
+ } catch {
701
+ return { present: false, length: 0, fingerprint: "" };
702
+ }
703
+ };
704
+ const getBalance = async () => {
705
+ const page = await withDeadline(tenant.token.getUsage(), "token.getUsage");
706
+ const b = page.balance ?? {};
707
+ return {
708
+ available: Number(b.available ?? 0),
709
+ reserved: Number(b.reserved ?? 0),
710
+ creditExhausted: Boolean(b.credit_exhausted),
711
+ storageDeposit: b.storage_deposit != null ? Number(b.storage_deposit) : void 0
712
+ };
713
+ };
714
+ return {
715
+ close: async () => {
716
+ },
717
+ seedSecret,
718
+ invokeForward,
719
+ registerContract: registerContract4,
720
+ releaseSecret,
721
+ me,
722
+ grantEgress,
723
+ setAgentGrant,
724
+ verifySecret,
725
+ getBalance,
726
+ isReal: true
727
+ };
728
+ }
729
+ function openMockClient() {
730
+ return {
731
+ close: async () => {
732
+ },
733
+ async seedSecret(name, value) {
734
+ if (!value || value.length === 0) throw new Error(`secret ${name} is empty`);
735
+ safeLog("info", { msg: "mock-seed (value dropped, length only)", name, length: value.length });
736
+ },
737
+ async registerContract(wasm) {
738
+ safeLog("info", { msg: "mock-register-contract", wasmBytes: wasm.byteLength });
739
+ return { contractId: "mock-contract-1" };
740
+ },
741
+ async invokeForward(req) {
742
+ safeLog("info", {
743
+ msg: "mock-forward",
744
+ method: req.method,
745
+ url: schemeAndHost(req.url),
746
+ secret_key: req.secret_key
747
+ });
748
+ const body = `{"mock":true,"note":"Blindfold mock mode \u2014 no real call made.","echo":{"url":${JSON.stringify(req.url)}}}`;
749
+ return { status: 200, headers: [["content-type", "application/json"]], body };
750
+ },
751
+ async releaseSecret(name) {
752
+ safeLog("info", { msg: "mock-release", name });
753
+ return `mock-released:${name}`;
754
+ },
755
+ async me() {
756
+ return { tenant: "did:t3n:mock", status: "active" };
757
+ },
758
+ async grantEgress(hosts) {
759
+ safeLog("info", { msg: "mock-grant-egress", hosts });
760
+ return hosts;
761
+ },
762
+ async setAgentGrant(agentDid, hosts, functions) {
763
+ safeLog("info", { msg: "mock-set-agent-grant", agentDid, hosts, functions });
764
+ },
765
+ async verifySecret(name) {
766
+ return { present: true, length: 0, fingerprint: `mock-${name}`.slice(0, 8) };
767
+ },
768
+ async getBalance() {
769
+ return { available: 1e9, reserved: 0, creditExhausted: false, mock: true };
770
+ },
771
+ isReal: false
772
+ };
773
+ }
774
+ function schemeAndHost(url) {
775
+ try {
776
+ const u = new URL(url);
777
+ return `${u.protocol}//${u.host}`;
778
+ } catch {
779
+ return "<bad-url>";
780
+ }
781
+ }
782
+ var T3_TIMEOUT_MS, T3TimeoutError, sdkCache, SignupEmailTakenError;
783
+ var init_t3_client = __esm({
784
+ "src/t3-client.ts"() {
785
+ "use strict";
786
+ init_constants();
787
+ init_env();
788
+ init_log();
789
+ T3_TIMEOUT_MS = Number(process.env.BLINDFOLD_T3_TIMEOUT_MS) || 3e4;
790
+ T3TimeoutError = class extends Error {
791
+ };
792
+ sdkCache = null;
793
+ SignupEmailTakenError = class extends Error {
794
+ constructor(email, existingDid) {
795
+ super(`"${email}" is already registered to Terminal 3 tenant ${existingDid}.`);
796
+ this.email = email;
797
+ this.existingDid = existingDid;
798
+ this.name = "SignupEmailTakenError";
799
+ }
800
+ };
801
+ }
802
+ });
803
+
804
+ // src/sealed-ledger.ts
805
+ import fs5 from "node:fs";
806
+ import path4 from "node:path";
807
+ import { createHash as createHash3, createHmac, randomBytes as randomBytes2 } from "node:crypto";
808
+ function defaultSealedLogPath() {
809
+ return process.env.BLINDFOLD_SEALED_LOG ?? path4.join(stateDir(), "sealed.jsonl");
810
+ }
811
+ function ledgerKey() {
812
+ try {
813
+ const keyPath = path4.join(stateDir(), "ledger.key");
814
+ if (fs5.existsSync(keyPath)) return Buffer.from(fs5.readFileSync(keyPath, "utf8").trim(), "hex");
815
+ fs5.mkdirSync(path4.dirname(keyPath), { recursive: true });
816
+ const key = randomBytes2(32);
817
+ fs5.writeFileSync(keyPath, key.toString("hex"), { mode: 384 });
818
+ try {
819
+ fs5.chmodSync(keyPath, 384);
820
+ } catch {
821
+ }
822
+ return key;
823
+ } catch {
824
+ return null;
825
+ }
826
+ }
827
+ function coreString(e) {
828
+ return JSON.stringify({
829
+ t: e.t,
830
+ name: e.name,
831
+ source: e.source,
832
+ length: e.length,
833
+ mode: e.mode,
834
+ tenant_did: e.tenant_did,
835
+ map_name: e.map_name
836
+ });
837
+ }
838
+ function sha(s) {
839
+ return createHash3("sha256").update(s).digest("hex");
840
+ }
841
+ function chainHash(key, prev, core) {
842
+ if (key) return { hash: createHmac("sha256", key).update(`${prev}
843
+ ${core}`).digest("hex"), alg: "hmac-sha256" };
844
+ return { hash: sha(`${prev}
845
+ ${core}`) };
846
+ }
847
+ function readLastLine(p) {
848
+ if (!fs5.existsSync(p)) return null;
849
+ const fd = fs5.openSync(p, "r");
850
+ try {
851
+ const size = fs5.fstatSync(fd).size;
852
+ if (size === 0) return null;
853
+ const readLen = Math.min(size, 64 * 1024);
854
+ const buf = Buffer.alloc(readLen);
855
+ fs5.readSync(fd, buf, 0, readLen, size - readLen);
856
+ const lines = buf.toString("utf8").split("\n").filter((l) => l.trim().length > 0);
857
+ return lines.length ? lines[lines.length - 1] : null;
858
+ } finally {
859
+ fs5.closeSync(fd);
860
+ }
861
+ }
862
+ function recordSealed(entry) {
863
+ const p = defaultSealedLogPath();
864
+ try {
865
+ withFileLockSync(p, () => {
866
+ let prevHash = "";
867
+ const last = readLastLine(p);
868
+ if (last) {
869
+ try {
870
+ prevHash = JSON.parse(last).hash ?? "";
871
+ } catch {
872
+ prevHash = "";
873
+ }
874
+ }
875
+ const { hash, alg } = chainHash(ledgerKey(), prevHash, coreString(entry));
876
+ const chained = { ...entry, prev: prevHash, hash, ...alg ? { alg } : {} };
877
+ fs5.mkdirSync(path4.dirname(p), { recursive: true });
878
+ fs5.appendFileSync(p, JSON.stringify(chained) + "\n");
879
+ });
880
+ } catch {
881
+ }
882
+ }
883
+ function readSealed() {
884
+ const p = defaultSealedLogPath();
885
+ if (!fs5.existsSync(p)) return [];
886
+ return fs5.readFileSync(p, "utf8").split("\n").filter(Boolean).map((l) => {
887
+ try {
888
+ return JSON.parse(l);
889
+ } catch {
890
+ return null;
891
+ }
892
+ }).filter((e) => e !== null);
893
+ }
894
+ function verifyLedgerChain() {
895
+ const entries = readSealed();
896
+ const key = ledgerKey();
897
+ let runningPrev = "";
898
+ let legacy = 0;
899
+ let firstBrokenIndex = -1;
900
+ entries.forEach((e, i) => {
901
+ if (!e.hash) {
902
+ legacy++;
903
+ return;
904
+ }
905
+ if (e.alg === "hmac-sha256") {
906
+ const expected = key ? createHmac("sha256", key).update(`${runningPrev}
907
+ ${coreString(e)}`).digest("hex") : null;
908
+ if (firstBrokenIndex < 0 && (e.prev !== runningPrev || expected !== null && e.hash !== expected)) {
909
+ firstBrokenIndex = i;
910
+ }
911
+ if (expected === null) legacy++;
912
+ } else {
913
+ legacy++;
914
+ }
915
+ runningPrev = e.hash;
916
+ });
917
+ return { ok: firstBrokenIndex < 0, total: entries.length, legacy, firstBrokenIndex };
918
+ }
919
+ var init_sealed_ledger = __esm({
920
+ "src/sealed-ledger.ts"() {
921
+ "use strict";
922
+ init_env();
923
+ }
924
+ });
925
+
926
+ // src/register.ts
927
+ async function registerSecret(opts) {
928
+ const env = loadBlindfoldEnv();
929
+ const t3 = await openT3Client(env);
930
+ try {
931
+ let source = "stdin";
932
+ let value;
933
+ if (opts.value !== void 0) {
934
+ value = opts.value;
935
+ source = "explicit";
936
+ } else if (opts.fromEnv) {
937
+ value = pluckSecret(opts.fromEnv);
938
+ source = `env:${opts.fromEnv}`;
939
+ } else {
940
+ value = await readSecretLine(` Value for "${opts.name}" (input is hidden): `);
941
+ if (!value) throw new Error("empty value");
942
+ }
943
+ if (value.includes(SENTINEL)) {
944
+ throw new Error(`Secret value contains the Blindfold sentinel string "${SENTINEL}" \u2014 this would cause infinite substitution. Use a different value.`);
945
+ }
946
+ await t3.seedSecret(opts.name, value);
947
+ const length = value.length;
948
+ const mode = env.mock ? "mock" : "real";
949
+ recordSealed({
950
+ t: (/* @__PURE__ */ new Date()).toISOString(),
951
+ name: opts.name,
952
+ source,
953
+ length,
954
+ mode,
955
+ tenant_did: env.did,
956
+ map_name: env.did ? `z:${env.did.replace(/^did:t3n:/, "")}:secrets` : "(mock)"
957
+ });
958
+ safeLog("info", { msg: "registered", name: opts.name, source, length, mode });
959
+ } finally {
960
+ await t3.close();
961
+ }
962
+ }
963
+ async function registerContract(wasm) {
964
+ const env = loadBlindfoldEnv();
965
+ const t3 = await openT3Client(env);
966
+ try {
967
+ return await t3.registerContract(wasm);
968
+ } finally {
969
+ await t3.close();
970
+ }
971
+ }
972
+ var init_register = __esm({
973
+ "src/register.ts"() {
974
+ "use strict";
975
+ init_env();
976
+ init_log();
977
+ init_prompt();
978
+ init_sealed_ledger();
979
+ init_t3_client();
980
+ init_constants();
981
+ }
982
+ });
983
+
984
+ // src/usage-log.ts
985
+ import fs7 from "node:fs";
986
+ import path6 from "node:path";
987
+ function defaultLogPath() {
988
+ return process.env.BLINDFOLD_USAGE_LOG ?? path6.join(stateDir(), "usage.jsonl");
989
+ }
990
+ function rotateIfNeeded(p) {
991
+ try {
992
+ const size = fs7.statSync(p).size;
993
+ if (size < USAGE_MAX_BYTES) return;
994
+ if (fs7.existsSync(`${p}.1`)) fs7.renameSync(`${p}.1`, `${p}.2`);
995
+ fs7.renameSync(p, `${p}.1`);
996
+ } catch {
997
+ }
998
+ }
999
+ function logUsage(event) {
1000
+ const p = defaultLogPath();
1001
+ try {
1002
+ const dir = path6.dirname(p);
1003
+ if (!ensuredDirs.has(dir)) {
1004
+ fs7.mkdirSync(dir, { recursive: true });
1005
+ ensuredDirs.add(dir);
1006
+ }
1007
+ if (++writesSinceRotateCheck >= ROTATE_CHECK_EVERY) {
1008
+ writesSinceRotateCheck = 0;
1009
+ rotateIfNeeded(p);
1010
+ }
1011
+ fs7.appendFile(p, JSON.stringify(event) + "\n", () => {
1012
+ });
1013
+ } catch {
1014
+ }
1015
+ }
1016
+ function readUsage() {
1017
+ const p = defaultLogPath();
1018
+ if (!fs7.existsSync(p)) return [];
1019
+ return fs7.readFileSync(p, "utf8").split("\n").filter(Boolean).map((line) => {
1020
+ try {
1021
+ return JSON.parse(line);
1022
+ } catch {
1023
+ return null;
1024
+ }
1025
+ }).filter((e) => e !== null);
1026
+ }
1027
+ function readUsageTail(n = 500) {
1028
+ const p = defaultLogPath();
1029
+ if (!fs7.existsSync(p)) return [];
1030
+ const fd = fs7.openSync(p, "r");
1031
+ try {
1032
+ const size = fs7.fstatSync(fd).size;
1033
+ const readLen = Math.min(size, Math.max(64 * 1024, n * 2048));
1034
+ const buf = Buffer.alloc(readLen);
1035
+ fs7.readSync(fd, buf, 0, readLen, size - readLen);
1036
+ let text = buf.toString("utf8");
1037
+ if (readLen < size) {
1038
+ const nl = text.indexOf("\n");
1039
+ if (nl >= 0) text = text.slice(nl + 1);
1040
+ }
1041
+ const events = text.split("\n").filter(Boolean).map((line) => {
1042
+ try {
1043
+ return JSON.parse(line);
1044
+ } catch {
1045
+ return null;
1046
+ }
1047
+ }).filter((e) => e !== null);
1048
+ return events.slice(-n);
1049
+ } finally {
1050
+ fs7.closeSync(fd);
1051
+ }
1052
+ }
1053
+ function clearUsage() {
1054
+ const p = defaultLogPath();
1055
+ if (fs7.existsSync(p)) fs7.unlinkSync(p);
1056
+ }
1057
+ function providerForUpstream(upstream) {
1058
+ try {
1059
+ const u = new URL(upstream);
1060
+ const h = u.hostname;
1061
+ if (h.endsWith("openai.com")) return "openai";
1062
+ if (h.endsWith("anthropic.com")) return "anthropic";
1063
+ if (h.endsWith("googleapis.com")) return "google";
1064
+ if (h.endsWith("x.ai")) return "xai";
1065
+ if (h.endsWith("groq.com")) return "groq";
1066
+ return h;
1067
+ } catch {
1068
+ return "unknown";
1069
+ }
1070
+ }
1071
+ var USAGE_MAX_BYTES, ensuredDirs, ROTATE_CHECK_EVERY, writesSinceRotateCheck;
1072
+ var init_usage_log = __esm({
1073
+ "src/usage-log.ts"() {
1074
+ "use strict";
1075
+ init_env();
1076
+ USAGE_MAX_BYTES = Number(process.env.BLINDFOLD_USAGE_MAX_BYTES) || 10 * 1024 * 1024;
1077
+ ensuredDirs = /* @__PURE__ */ new Set();
1078
+ ROTATE_CHECK_EVERY = 500;
1079
+ writesSinceRotateCheck = ROTATE_CHECK_EVERY;
1080
+ }
1081
+ });
1082
+
1083
+ // src/release.ts
1084
+ var release_exports = {};
1085
+ __export(release_exports, {
1086
+ release: () => release
1087
+ });
1088
+ function sharedClient(env) {
1089
+ const key = `${env.did}|${env.t3Env}|${env.t3BaseUrl}|${env.mock ? 1 : 0}`;
1090
+ let c2 = clientCache.get(key);
1091
+ if (!c2) {
1092
+ c2 = openT3Client(env).catch((e) => {
1093
+ clientCache.delete(key);
1094
+ throw e;
1095
+ });
1096
+ clientCache.set(key, c2);
1097
+ }
1098
+ return c2;
1099
+ }
1100
+ async function release(name, opts) {
1101
+ const env = opts?.env ?? loadBlindfoldEnv();
1102
+ const useShared = !opts?.env;
1103
+ const t3 = useShared ? await sharedClient(env) : await openT3Client(env);
1104
+ const startedAt = Date.now();
1105
+ try {
1106
+ const value = await t3.releaseSecret(name);
1107
+ logUsage({
1108
+ t: (/* @__PURE__ */ new Date()).toISOString(),
1109
+ mode: env.mock ? "mock" : "real",
1110
+ provider: "(enclave)",
1111
+ method: "RELEASE",
1112
+ path: name,
1113
+ upstream: "t3-enclave",
1114
+ status: 200,
1115
+ latency_ms: Date.now() - startedAt,
1116
+ agent_supplied_auth: false,
1117
+ sentinel_in_outbound: false,
1118
+ via: opts?.via ?? "release",
1119
+ secret_key: name
1120
+ });
1121
+ return value;
1122
+ } finally {
1123
+ if (!useShared) await t3.close();
1124
+ }
1125
+ }
1126
+ var clientCache;
1127
+ var init_release = __esm({
1128
+ "src/release.ts"() {
1129
+ "use strict";
1130
+ init_env();
1131
+ init_t3_client();
1132
+ init_usage_log();
1133
+ clientCache = /* @__PURE__ */ new Map();
1134
+ }
1135
+ });
1136
+
1137
+ // src/versions.ts
1138
+ var versions_exports = {};
1139
+ __export(versions_exports, {
1140
+ defaultVersionsPath: () => defaultVersionsPath,
1141
+ isValidVersionKey: () => isValidVersionKey,
1142
+ readVersions: () => readVersions,
1143
+ recordVersion: () => recordVersion,
1144
+ versionKeyFor: () => versionKeyFor
1145
+ });
1146
+ import fs9 from "node:fs";
1147
+ import path7 from "node:path";
1148
+ function defaultVersionsPath() {
1149
+ return process.env.BLINDFOLD_VERSIONS_LOG ?? path7.join(stateDir(), "versions.jsonl");
1150
+ }
1151
+ function versionKeyFor(name, stampMs) {
1152
+ return `__bfver__${name}__${stampMs}`;
1153
+ }
1154
+ function isValidVersionKey(name, versionKey) {
1155
+ return new RegExp(`^__bfver__${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}__\\d+$`).test(versionKey);
1156
+ }
1157
+ function recordVersion(entry) {
1158
+ const p = defaultVersionsPath();
1159
+ try {
1160
+ fs9.mkdirSync(path7.dirname(p), { recursive: true });
1161
+ fs9.appendFileSync(p, JSON.stringify(entry) + "\n");
1162
+ } catch {
1163
+ }
1164
+ }
1165
+ function readVersions(name) {
1166
+ const p = defaultVersionsPath();
1167
+ if (!fs9.existsSync(p)) return [];
1168
+ const all = fs9.readFileSync(p, "utf8").split("\n").filter(Boolean).map((l) => {
1169
+ try {
1170
+ return JSON.parse(l);
1171
+ } catch {
1172
+ return null;
1173
+ }
1174
+ }).filter((e) => e !== null);
1175
+ return name ? all.filter((e) => e.name === name) : all;
1176
+ }
1177
+ var init_versions = __esm({
1178
+ "src/versions.ts"() {
1179
+ "use strict";
1180
+ init_env();
1181
+ }
1182
+ });
1183
+
1184
+ // src/migrate.ts
1185
+ var migrate_exports = {};
1186
+ __export(migrate_exports, {
1187
+ planMigration: () => planMigration,
1188
+ runMigrate: () => runMigrate
1189
+ });
1190
+ import fs10 from "node:fs";
1191
+ function isConfigName(k) {
1192
+ return /(_HOST|_URL|_PORT|_EMAIL|_ENV|_REGION|_BASE_URL|_USER|_USERNAME)$/i.test(k) || /^(NODE_ENV|PORT|HOST|PATH|HOME|USER|SHELL|LANG|PWD)$/i.test(k);
1193
+ }
1194
+ function isAltT3(k) {
1195
+ return /^t\d+_/i.test(k) || /_DID$/i.test(k);
1196
+ }
1197
+ function looksSecret(k) {
1198
+ return /(KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|_API|API_|_PAT|CREDENTIAL|ACCESS|PRIVATE)/i.test(k);
1199
+ }
1200
+ function readEnvVars(envPath) {
1201
+ const lines = fs10.existsSync(envPath) ? fs10.readFileSync(envPath, "utf8").split(/\r?\n/) : [];
1202
+ const keys = [];
1203
+ for (const raw of lines) {
1204
+ const line = raw.trim();
1205
+ if (!line || line.startsWith("#")) continue;
1206
+ const eq = line.indexOf("=");
1207
+ if (eq <= 0) continue;
1208
+ keys.push(line.slice(0, eq).trim());
1209
+ }
1210
+ return { keys, lines };
1211
+ }
1212
+ function planMigration(envPath = defaultEnvPath()) {
1213
+ loadEnvFromFile(envPath);
1214
+ const { keys } = readEnvVars(envPath);
1215
+ const seen = /* @__PURE__ */ new Set();
1216
+ const plan = [];
1217
+ for (const k of keys) {
1218
+ if (seen.has(k)) continue;
1219
+ seen.add(k);
1220
+ const val = process.env[k] ?? "";
1221
+ const base = { envVar: k, sealName: k.toLowerCase(), bytes: val.length, action: "skip" };
1222
+ if (NEVER_SEAL.has(k)) plan.push({ ...base, reason: "root cred / config \u2014 must stay in .env" });
1223
+ else if (isAltT3(k)) plan.push({ ...base, reason: "T3 team key/DID \u2014 root cred" });
1224
+ else if (isConfigName(k)) plan.push({ ...base, reason: "config, not a secret" });
1225
+ else if (!val) plan.push({ ...base, reason: "empty" });
1226
+ else if (!looksSecret(k)) plan.push({ ...base, reason: "name doesn't look like a secret (seal manually if it is)" });
1227
+ else plan.push({ ...base, action: "seal" });
1228
+ }
1229
+ return plan;
1230
+ }
1231
+ async function runMigrate(opts = {}) {
1232
+ const envPath = opts.envPath ?? defaultEnvPath();
1233
+ const plan = planMigration(envPath);
1234
+ const results = [];
1235
+ const sealedVars = /* @__PURE__ */ new Set();
1236
+ for (const item of plan) {
1237
+ if (item.action !== "seal") {
1238
+ results.push(item);
1239
+ continue;
1240
+ }
1241
+ try {
1242
+ await registerSecret({ name: item.sealName, fromEnv: item.envVar });
1243
+ sealedVars.add(item.envVar);
1244
+ results.push({ ...item, sealed: true });
1245
+ } catch (e) {
1246
+ results.push({ ...item, sealed: false, error: e.message });
1247
+ }
1248
+ }
1249
+ if (sealedVars.size > 0) {
1250
+ const original = fs10.readFileSync(envPath, "utf8");
1251
+ const backupPath = `${envPath}.bak.${Math.floor(Date.now() / 1e3)}`;
1252
+ fs10.writeFileSync(backupPath, original, { mode: 384 });
1253
+ try {
1254
+ fs10.chmodSync(backupPath, 384);
1255
+ } catch {
1256
+ }
1257
+ console.warn(
1258
+ `\u26A0\uFE0F Wrote a PLAINTEXT backup of your secrets to ${backupPath} (mode 0600). Delete it once you've verified the migration: rm "${backupPath}"`
1259
+ );
1260
+ const out = [];
1261
+ for (const raw of original.split(/\r?\n/)) {
1262
+ const eq = raw.indexOf("=");
1263
+ const key = eq > 0 ? raw.slice(0, eq).trim() : "";
1264
+ if (key && sealedVars.has(key) && !raw.trim().startsWith("#")) {
1265
+ if (opts.keep) out.push(`# sealed\u2192enclave: ${key} (value lives in T3, use \`blindfold use --name ${key.toLowerCase()}\`)`);
1266
+ } else {
1267
+ out.push(raw);
1268
+ }
1269
+ }
1270
+ fs10.writeFileSync(envPath, out.join("\n"));
1271
+ }
1272
+ return results;
1273
+ }
1274
+ var NEVER_SEAL;
1275
+ var init_migrate = __esm({
1276
+ "src/migrate.ts"() {
1277
+ "use strict";
1278
+ init_env();
1279
+ init_register();
1280
+ NEVER_SEAL = /* @__PURE__ */ new Set([
1281
+ "T3N_API_KEY",
1282
+ "DID",
1283
+ "BLINDFOLD_MOCK",
1284
+ "BLINDFOLD_PORT",
1285
+ "BLINDFOLD_T3_ENV",
1286
+ "BLINDFOLD_DASHBOARD_PORT",
1287
+ "BLINDFOLD_BASE_URL"
1288
+ ]);
1289
+ }
1290
+ });
1291
+
1292
+ // bin/cli-shared.ts
1293
+ import fs from "node:fs";
1294
+ import path from "node:path";
1295
+ import { fileURLToPath } from "node:url";
1296
+ import { createHash } from "node:crypto";
1297
+
1298
+ // src/color.ts
1299
+ var on = process.env.FORCE_COLOR === "1" || !process.env.NO_COLOR && process.env.TERM !== "dumb" && Boolean(process.stdout.isTTY);
1300
+ var wrap = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
1301
+ var c = {
1302
+ bold: wrap("1"),
1303
+ dim: wrap("2"),
1304
+ red: wrap("31"),
1305
+ green: wrap("32"),
1306
+ yellow: wrap("33"),
1307
+ blue: wrap("34"),
1308
+ magenta: wrap("35"),
1309
+ cyan: wrap("36"),
1310
+ gray: wrap("90")
1311
+ };
1312
+ var ok = (s) => c.green(s);
1313
+ var bad = (s) => c.red(s);
1314
+ var warn = (s) => c.yellow(s);
1315
+ var head = (s) => c.bold(c.cyan(s));
1316
+
1317
+ // bin/cli-shared.ts
1318
+ var HERE = path.dirname(fileURLToPath(import.meta.url));
1319
+ var REPO_ROOT = path.resolve(HERE, "..", "..", "..");
1320
+ var PKG_ROOT = path.resolve(HERE, "..");
1321
+ function assetPath(repoRelative, assetName) {
1322
+ const repoPath = path.join(REPO_ROOT, ...repoRelative.split("/"));
1323
+ if (fs.existsSync(repoPath)) return repoPath;
1324
+ return path.join(PKG_ROOT, "assets", assetName);
1325
+ }
1326
+ function parseArgv(argv) {
1327
+ const out = { _: [], flags: {} };
1328
+ for (let i = 0; i < argv.length; i++) {
1329
+ const a = argv[i] ?? "";
1330
+ if (a.startsWith("--")) {
1331
+ const key = a.slice(2);
1332
+ const next = argv[i + 1];
1333
+ if (next && !next.startsWith("--")) {
1334
+ out.flags[key] = next;
1335
+ i += 1;
1336
+ } else {
1337
+ out.flags[key] = true;
1338
+ }
1339
+ } else {
1340
+ out._.push(a);
1341
+ }
1342
+ }
1343
+ return out;
1344
+ }
1345
+ function fingerprint(s) {
1346
+ return createHash("sha256").update(s).digest("hex").slice(0, 8);
1347
+ }
1348
+ var TOOL_ENV = {
1349
+ gh: "GH_TOKEN",
1350
+ git: "GH_TOKEN",
1351
+ glab: "GITLAB_TOKEN",
1352
+ psql: "PGPASSWORD",
1353
+ pg_dump: "PGPASSWORD",
1354
+ mysql: "MYSQL_PWD",
1355
+ aws: "AWS_SECRET_ACCESS_KEY",
1356
+ stripe: "STRIPE_API_KEY",
1357
+ vercel: "VERCEL_TOKEN",
1358
+ npm: "NPM_TOKEN",
1359
+ docker: "DOCKER_PASSWORD",
1360
+ openai: "OPENAI_API_KEY",
1361
+ anthropic: "ANTHROPIC_API_KEY",
1362
+ doctl: "DIGITALOCEAN_ACCESS_TOKEN",
1363
+ heroku: "HEROKU_API_KEY",
1364
+ cloudflared: "CLOUDFLARE_API_TOKEN",
1365
+ wrangler: "CLOUDFLARE_API_TOKEN"
1366
+ };
1367
+ function resolveEnvVar(asFlag, command, name) {
1368
+ if (asFlag) return asFlag;
1369
+ if (command && TOOL_ENV[command]) return TOOL_ENV[command];
1370
+ return name.toUpperCase();
1371
+ }
1372
+ function die(msg) {
1373
+ console.error(c.red(`\u2716 ${msg}`));
1374
+ process.exit(2);
1375
+ }
1376
+
1377
+ // bin/cmd-auth.ts
1378
+ init_env();
1379
+ init_keychain();
1380
+ init_prompt();
1381
+ import fs4 from "node:fs";
1382
+ function saveTenantCredentials(did, key, env, forceFile) {
1383
+ const cfg = configPath();
1384
+ let existing = {};
1385
+ try {
1386
+ if (fs4.existsSync(cfg)) existing = JSON.parse(fs4.readFileSync(cfg, "utf8"));
1387
+ } catch {
1388
+ }
1389
+ const merged = { ...existing, DID: did, BLINDFOLD_T3_ENV: env };
1390
+ const triedKeychain = !forceFile && keychainAvailable();
1391
+ const inKeychain = triedKeychain && keychainSet(did, key);
1392
+ if (inKeychain) {
1393
+ merged.T3N_API_KEY_STORE = "keychain";
1394
+ delete merged.T3N_API_KEY;
1395
+ } else {
1396
+ merged.T3N_API_KEY = key;
1397
+ merged.T3N_API_KEY_STORE = "file";
1398
+ }
1399
+ fs4.mkdirSync(homeDir(), { recursive: true });
1400
+ fs4.writeFileSync(cfg, JSON.stringify(merged, null, 2), { mode: 384 });
1401
+ try {
1402
+ fs4.chmodSync(cfg, 384);
1403
+ } catch {
1404
+ }
1405
+ return { inKeychain, triedKeychain, cfg };
1406
+ }
1407
+ async function handleAuth(cmd, argv, cmdArgs) {
1408
+ switch (cmd) {
1409
+ case "signup": {
1410
+ const env = String(argv.flags.env ?? "").toLowerCase() === "production" ? "production" : "testnet";
1411
+ if (env === "production") {
1412
+ die("signup (self-admit) is testnet-only. Production tenants are provisioned by Terminal 3 directly.");
1413
+ }
1414
+ const email = argv.flags.email ? String(argv.flags.email) : (await readLine("Email for your tenant (a verification code will be sent): ")).trim();
1415
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) die("Enter a valid email address (e.g. you@example.com).");
1416
+ console.log(`${head("blindfold signup")} \u2014 creating a Terminal 3 ${c.cyan("testnet")} tenant for ${c.cyan(email)}`);
1417
+ console.log(c.dim(" A fresh tenant key (secp256k1) is generated locally and stored in your keychain \u2014 never printed.\n"));
1418
+ const localPart = (email.split("@")[0] ?? "user").split("+")[0] ?? "user";
1419
+ const firstName = argv.flags.first ? String(argv.flags.first) : localPart || "Blindfold";
1420
+ const lastName = argv.flags.last ? String(argv.flags.last) : "Tenant";
1421
+ const { signupTenant: signupTenant2, SignupEmailTakenError: SignupEmailTakenError2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
1422
+ const presetOtp = argv.flags.otp ? String(argv.flags.otp).trim() : "";
1423
+ let saved = null;
1424
+ let res;
1425
+ try {
1426
+ res = await signupTenant2({
1427
+ env,
1428
+ email,
1429
+ profile: { first_name: firstName, last_name: lastName },
1430
+ onKeyReady: (key, did) => {
1431
+ saved = saveTenantCredentials(did, key, env, Boolean(argv.flags.file));
1432
+ },
1433
+ getOtpCode: async () => {
1434
+ if (presetOtp) return presetOtp;
1435
+ process.stderr.write(c.yellow(` A verification code was emailed to ${email}.
1436
+ `));
1437
+ return (await readLine(" Enter the code: ")).trim();
1438
+ }
1439
+ });
1440
+ } catch (e) {
1441
+ if (e instanceof SignupEmailTakenError2) {
1442
+ console.error(c.red(`\u2716 ${e.message}`));
1443
+ console.error(c.dim(" This email already has a tenant. Either:"));
1444
+ console.error(c.dim(` \u2022 log in with that tenant's key: blindfold login --did ${e.existingDid}`));
1445
+ console.error(c.dim(" \u2022 or sign up with a different email (Gmail '+' aliases work: you+blindfold@gmail.com)."));
1446
+ process.exit(1);
1447
+ }
1448
+ if (saved) {
1449
+ const s = saved;
1450
+ console.error(c.red(`\u2716 Email verified and tenant key saved, but self-admit failed: ${e.message}`));
1451
+ console.error(c.dim(` Your key + DID are stored in ${s.inKeychain ? `the ${keychainBackend()}` : `${s.cfg} (0600)`} \u2014 not lost.`));
1452
+ console.error(c.dim(" The tenant just wasn't funded. Check `blindfold credit`, or re-run `blindfold signup` with a fresh email for a new funded tenant."));
1453
+ process.exit(1);
1454
+ }
1455
+ die(`signup failed: ${e.message}`);
1456
+ return;
1457
+ }
1458
+ if (res.admitStatus === "refused") {
1459
+ die(`Terminal 3 refused self-admit (${res.refusedReason ?? "unknown"})${res.refusedDetail ? `: ${res.refusedDetail}` : ""}.`);
1460
+ return;
1461
+ }
1462
+ const { inKeychain, cfg } = saved ?? saveTenantCredentials(res.did, res.key, env, Boolean(argv.flags.file));
1463
+ const verb = res.admitStatus === "already-admitted" ? "confirmed (already registered)" : "created";
1464
+ console.log(ok(`
1465
+ \u2713 Tenant ${verb}: ${c.bold(res.did)}`));
1466
+ console.log(` Tenant key stored in ${inKeychain ? `the ${keychainBackend()}` : `${cfg} (0600)`} \u2014 never printed.`);
1467
+ if (res.grantedCredits) {
1468
+ console.log(ok(` Welcome credits minted: ${res.grantedCredits} base units.`));
1469
+ } else {
1470
+ console.log(c.yellow(" No welcome credits were minted (the testnet dial may be 0). Check `blindfold credit`."));
1471
+ }
1472
+ console.log(`
1473
+ Next:`);
1474
+ console.log(` ${c.bold("blindfold doctor")} ${c.dim("# confirm T3 reachability")}`);
1475
+ console.log(` ${c.bold("blindfold credit")} ${c.dim("# see your token balance")}`);
1476
+ console.log(` ${c.bold("blindfold register --name openai_key")} ${c.dim("# seal your first secret")}`);
1477
+ return;
1478
+ }
1479
+ case "login": {
1480
+ const did = argv.flags.did ? String(argv.flags.did) : (await readSecretLine("Tenant DID (did:t3n:\u2026): ")).trim();
1481
+ if (!/^did:t3n:[0-9a-fA-F]+$/.test(did)) die('DID must look like "did:t3n:<hex>".');
1482
+ const key = argv.flags.key ? String(argv.flags.key) : (await readSecretLine("T3N_API_KEY (0x\u2026, hidden): ")).trim();
1483
+ if (!/^0x[0-9a-fA-F]{64}$/.test(key)) die("T3N_API_KEY must be a 0x-prefixed 32-byte hex.");
1484
+ const env = String(argv.flags.env ?? "").toLowerCase() === "production" ? "production" : "testnet";
1485
+ const { inKeychain, triedKeychain, cfg } = saveTenantCredentials(did, key, env, Boolean(argv.flags.file));
1486
+ console.log(`\u2713 Saved tenant key to ${inKeychain ? `the ${keychainBackend()}` : `${cfg} (mode 0600)`}. Blindfold now works from any directory.`);
1487
+ console.log(` Tenant: ${did} \xB7 env: ${env} \xB7 key: stored (never printed)`);
1488
+ if (!inKeychain && !argv.flags.file) {
1489
+ console.log(triedKeychain ? ` (Keychain write unavailable here \u2014 stored in a 0600 file. Run \`blindfold login\` in an interactive desktop session to use ${keychainBackend()}.)` : ` (No OS keychain found \u2014 stored in a 0600 file. On macOS/Linux/Windows an interactive session uses the OS credential store automatically.)`);
1490
+ }
1491
+ console.log(` Verify: blindfold doctor`);
1492
+ return;
1493
+ }
1494
+ case "logout": {
1495
+ const cfg = configPath();
1496
+ let did = "";
1497
+ let store = "";
1498
+ try {
1499
+ if (fs4.existsSync(cfg)) {
1500
+ const o = JSON.parse(fs4.readFileSync(cfg, "utf8"));
1501
+ did = o.DID ?? "";
1502
+ store = o.T3N_API_KEY_STORE ?? "";
1503
+ }
1504
+ } catch {
1505
+ }
1506
+ let removed = false;
1507
+ if (store === "keychain" && did && keychainAvailable()) {
1508
+ if (keychainDelete(did)) {
1509
+ console.log("\u2713 Removed tenant key from the OS keychain.");
1510
+ removed = true;
1511
+ }
1512
+ }
1513
+ if (fs4.existsSync(cfg)) {
1514
+ fs4.rmSync(cfg, { force: true });
1515
+ console.log(`\u2713 Removed ${cfg}.`);
1516
+ removed = true;
1517
+ }
1518
+ if (!removed) console.log("Nothing to remove \u2014 no saved credentials at " + cfg);
1519
+ else console.log("Tenant credentials cleared from this machine.");
1520
+ return;
1521
+ }
1522
+ case "whoami": {
1523
+ const cfg = configPath();
1524
+ let store = "";
1525
+ try {
1526
+ if (fs4.existsSync(cfg)) store = JSON.parse(fs4.readFileSync(cfg, "utf8")).T3N_API_KEY_STORE ?? "";
1527
+ } catch {
1528
+ }
1529
+ const env = loadBlindfoldEnv();
1530
+ const keySource = !env.t3nApiKey ? "MISSING" : process.env.T3N_API_KEY && store === "keychain" ? `set (${keychainBackend()})` : store === "file" ? "set (config file, 0600)" : "set (env / repo .env)";
1531
+ console.log(`${c.bold("config:")} ${c.gray(cfg)}${fs4.existsSync(cfg) ? "" : " (not present)"}`);
1532
+ console.log(`${c.bold("tenant:")} ${c.cyan(env.did || "(none \u2014 run `blindfold login`)")}`);
1533
+ console.log(`${c.bold("env:")} ${env.t3Env} \xB7 key: ${c.green(keySource)}`);
1534
+ return;
1535
+ }
1536
+ }
1537
+ }
1538
+
1539
+ // bin/cmd-secrets.ts
1540
+ init_env();
1541
+ init_register();
1542
+ import fs8 from "node:fs";
1543
+ import { spawn } from "node:child_process";
1544
+
1545
+ // src/attest.ts
1546
+ init_env();
1547
+ import fs6 from "node:fs";
1548
+ import path5 from "node:path";
1549
+ var PIN_FIELD = "expectedRtmr3";
1550
+ function readPinnedRtmr3() {
1551
+ try {
1552
+ const obj = JSON.parse(fs6.readFileSync(configPath(), "utf8"));
1553
+ const v = obj[PIN_FIELD];
1554
+ return typeof v === "string" && v ? v : void 0;
1555
+ } catch {
1556
+ return void 0;
1557
+ }
1558
+ }
1559
+ function writePinnedRtmr3(value) {
1560
+ const cfg = configPath();
1561
+ fs6.mkdirSync(path5.dirname(cfg), { recursive: true });
1562
+ withFileLockSync(cfg, () => {
1563
+ let obj = {};
1564
+ try {
1565
+ obj = JSON.parse(fs6.readFileSync(cfg, "utf8"));
1566
+ } catch {
1567
+ }
1568
+ obj[PIN_FIELD] = value;
1569
+ fs6.writeFileSync(cfg, JSON.stringify(obj, null, 2), { mode: 384 });
1570
+ try {
1571
+ fs6.chmodSync(cfg, 384);
1572
+ } catch {
1573
+ }
1574
+ });
1575
+ }
1576
+ var ATTEST_TTL_MS = Number(process.env.BLINDFOLD_ATTEST_TTL_MS) || 3e5;
1577
+ var attestCache = /* @__PURE__ */ new Map();
1578
+ async function loadAttestSdk() {
1579
+ try {
1580
+ return await import("@terminal3/t3n-sdk");
1581
+ } catch {
1582
+ throw new Error(
1583
+ "@terminal3/t3n-sdk not installed \u2014 attestation needs the real SDK. Run `npm install @terminal3/t3n-sdk`."
1584
+ );
1585
+ }
1586
+ }
1587
+ async function attest(opts = {}) {
1588
+ const env = opts.env ?? loadBlindfoldEnv();
1589
+ if (env.mock) {
1590
+ throw new Error("attestation is unavailable in mock mode (BLINDFOLD_MOCK=1) \u2014 it needs a real T3 node.");
1591
+ }
1592
+ const cacheKey = `${env.t3Env}|${env.t3BaseUrl}|${opts.expectRtmr3 ?? ""}`;
1593
+ if (!opts.noCache) {
1594
+ const hit = attestCache.get(cacheKey);
1595
+ if (hit && Date.now() - hit.at < ATTEST_TTL_MS) return hit.result;
1596
+ }
1597
+ const sdk = await loadAttestSdk();
1598
+ sdk.setEnvironment(env.t3Env);
1599
+ if (env.t3BaseUrl) sdk.setNodeUrl(env.t3BaseUrl);
1600
+ const nodeUrl = sdk.getNodeUrl(env.t3BaseUrl || void 0);
1601
+ const bundle = await sdk.fetchDkgAttestation(nodeUrl);
1602
+ if (!bundle) {
1603
+ const unavailable = { available: false, nodeUrl, valid: false, validCount: 0, expectedCount: 0, rtmr3s: [], pinned: false };
1604
+ attestCache.set(cacheKey, { at: Date.now(), result: unavailable });
1605
+ return unavailable;
1606
+ }
1607
+ const encapsKey = await sdk.fetchMlKemPublicKey(nodeUrl);
1608
+ const r = await sdk.verifyDkgAttestation(
1609
+ encapsKey,
1610
+ bundle.attestation_msg,
1611
+ bundle.peer_ids,
1612
+ bundle.quotes,
1613
+ opts.expectRtmr3
1614
+ );
1615
+ const rtmr3s = [...new Set(r.results.map((p) => p.rtmr3).filter((x) => Boolean(x)))];
1616
+ const result = {
1617
+ available: true,
1618
+ nodeUrl,
1619
+ valid: r.valid,
1620
+ validCount: r.valid_count,
1621
+ expectedCount: r.expected_count,
1622
+ rtmr3s,
1623
+ pinned: Boolean(opts.expectRtmr3) && r.valid,
1624
+ error: r.error
1625
+ };
1626
+ attestCache.set(cacheKey, { at: Date.now(), result });
1627
+ return result;
1628
+ }
1629
+ async function attestationGate(opts = {}) {
1630
+ const env = opts.env ?? loadBlindfoldEnv();
1631
+ const pinned = readPinnedRtmr3();
1632
+ const required = Boolean(pinned) || process.env.BLINDFOLD_REQUIRE_ATTEST === "1";
1633
+ if (opts.skip) {
1634
+ return {
1635
+ enforced: false,
1636
+ ok: true,
1637
+ warning: required ? "attestation gate BYPASSED via --no-attest \u2014 the enclave was NOT verified" : void 0
1638
+ };
1639
+ }
1640
+ if (env.mock) {
1641
+ if (required) {
1642
+ return { enforced: true, ok: false, message: "refusing to run in mock mode while attestation is required (a pin or BLINDFOLD_REQUIRE_ATTEST is set) \u2014 mock mode never touches a real enclave" };
1643
+ }
1644
+ return { enforced: false, ok: true };
1645
+ }
1646
+ if (!required) return { enforced: false, ok: true };
1647
+ let r;
1648
+ try {
1649
+ r = await attest({ env, expectRtmr3: pinned });
1650
+ } catch (e) {
1651
+ return { enforced: true, ok: false, message: e.message };
1652
+ }
1653
+ if (!r.available) return { enforced: true, ok: false, message: `node published no attestation (${r.nodeUrl})` };
1654
+ if (opts.requirePin && !pinned) {
1655
+ return {
1656
+ enforced: true,
1657
+ ok: false,
1658
+ message: "refusing: this operation requires a pinned RTMR3 (run `blindfold attest --pin`). Unpinned attestation only proves it's a TDX enclave, not that it runs your expected code."
1659
+ };
1660
+ }
1661
+ const ok3 = r.valid && (!pinned || r.pinned);
1662
+ const warning = ok3 && !pinned ? "attestation valid but NO RTMR3 pinned \u2014 proves genuine TDX silicon, not that the enclave runs your expected code. Pin it: `blindfold attest --pin`." : void 0;
1663
+ return {
1664
+ enforced: true,
1665
+ ok: ok3,
1666
+ warning,
1667
+ message: ok3 ? void 0 : `enclave attestation failed${pinned ? " (RTMR3 pin mismatch)" : ""} at ${r.nodeUrl}`
1668
+ };
1669
+ }
1670
+
1671
+ // bin/cmd-secrets.ts
1672
+ async function handleSecrets(cmd, argv, cmdArgs) {
1673
+ switch (cmd) {
1674
+ case "register": {
1675
+ const name = String(argv.flags.name ?? "");
1676
+ const fromEnv = argv.flags["from-env"] ? String(argv.flags["from-env"]) : void 0;
1677
+ if (!name) {
1678
+ die("usage: blindfold register --name <KV_KEY> [--from-env <ENV_VAR>]");
1679
+ }
1680
+ const sealGate = await attestationGate({ skip: !!argv.flags["no-attest"], requirePin: true });
1681
+ if (sealGate.warning) console.error(`\u26A0\uFE0F ${sealGate.warning}`);
1682
+ if (sealGate.enforced && !sealGate.ok) {
1683
+ die(`attestation gate: ${sealGate.message}. Refusing to seal into an unverified enclave. (bypass: --no-attest, or clear the pin)`);
1684
+ }
1685
+ if (sealGate.enforced) console.log("\u2713 enclave attestation verified");
1686
+ await registerSecret({ name, fromEnv });
1687
+ if (fromEnv) {
1688
+ console.log(`\u2713 Registered "${name}" (value read from ${fromEnv} once, then dropped).`);
1689
+ console.log(` You can now DELETE ${fromEnv} from your .env.`);
1690
+ } else {
1691
+ console.log(`\u2713 Registered "${name}" \u2014 value lives only in the enclave.`);
1692
+ }
1693
+ const asVar = name.toUpperCase();
1694
+ console.log("");
1695
+ console.log(" Use it (the plaintext never returns to your env):");
1696
+ console.log(` blindfold use --name ${name} -- <your command> # injects ${asVar} into that command only`);
1697
+ console.log(` blindfold use --name ${name} --as ${asVar} -- <cmd> # custom env-var name`);
1698
+ console.log(` blindfold use --name ${name} --url <https url> # quick "does it auth?" check`);
1699
+ console.log(` proxy/SDK: set the key to "__BLINDFOLD__" + route via \`blindfold proxy\`, or \`release("${name}")\` in code`);
1700
+ return;
1701
+ }
1702
+ case "use": {
1703
+ const name = String(argv.flags.name ?? "");
1704
+ if (!name) {
1705
+ die("usage: blindfold use --name <secret> [--as <ENV_VAR>] -- <command...>\n blindfold use --name <secret> --url <https url> (quick auth test)");
1706
+ }
1707
+ const { release: release2 } = await Promise.resolve().then(() => (init_release(), release_exports));
1708
+ const value = await release2(name, { via: "use" });
1709
+ if (argv.flags.check) {
1710
+ console.log(`\u2713 "${name}" is sealed and usable \u2014 ${value.length} bytes (value never shown)`);
1711
+ return;
1712
+ }
1713
+ if (argv.flags.url) {
1714
+ const url = String(argv.flags.url);
1715
+ if (!argv.flags["allow-insecure"]) {
1716
+ let u;
1717
+ try {
1718
+ u = new URL(url);
1719
+ } catch (e) {
1720
+ die(`invalid --url: ${e.message}`);
1721
+ return;
1722
+ }
1723
+ const isLocal = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(u.hostname);
1724
+ if (u.protocol !== "https:" && !isLocal) {
1725
+ die(`refusing to send the released key to a non-https URL (${u.protocol}//${u.hostname}). Use https, target localhost, or pass --allow-insecure to override.`);
1726
+ return;
1727
+ }
1728
+ if (!isLocal) {
1729
+ const { loadEgressHosts: loadEgressHosts2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
1730
+ const env = loadBlindfoldEnv();
1731
+ const granted = loadEgressHosts2(env.did);
1732
+ const allowed = granted.some((h) => h === u.hostname || u.hostname.endsWith(`.${h}`));
1733
+ if (!allowed) {
1734
+ die(`refusing to send the released key to ${u.hostname}: host is not in the egress allowlist. Run \`blindfold grant --host ${u.hostname}\` first, or pass --allow-insecure to override.`);
1735
+ return;
1736
+ }
1737
+ }
1738
+ }
1739
+ const res = await fetch(url, {
1740
+ headers: { Authorization: `Bearer ${value}`, "User-Agent": "blindfold", Accept: "*/*" }
1741
+ });
1742
+ console.log(`\u2713 released "${name}" (${value.length} B, value not shown) \u2192 ${url}`);
1743
+ console.log(` HTTP ${res.status} ${res.statusText} ${res.ok ? "\u2705 accepted" : "\u2716 rejected"}`);
1744
+ return;
1745
+ }
1746
+ if (cmdArgs.length === 0) {
1747
+ die("provide a command after `--` (e.g. `-- gh api user`), or pass --url <url> to test auth");
1748
+ }
1749
+ const asVar = resolveEnvVar(argv.flags.as ? String(argv.flags.as) : void 0, cmdArgs[0], name);
1750
+ console.error(`\u2713 released "${name}" (${value.length} B) \u2192 injecting $${asVar} into: ${cmdArgs.join(" ")}`);
1751
+ const child = spawn(cmdArgs[0], cmdArgs.slice(1), {
1752
+ stdio: "inherit",
1753
+ env: { ...process.env, [asVar]: value }
1754
+ });
1755
+ child.on("exit", (code) => process.exit(code ?? 0));
1756
+ child.on("error", (e) => die(`failed to run "${cmdArgs[0]}": ${e.message}`));
1757
+ return;
1758
+ }
1759
+ case "export": {
1760
+ const name = String(argv.flags.name ?? "");
1761
+ if (!name) die("usage: blindfold export --name <secret> [--as <ENV_VAR>] (for GitHub Actions / CI)");
1762
+ const ghEnv = process.env.GITHUB_ENV;
1763
+ if (!ghEnv) die("`export` writes to $GITHUB_ENV (GitHub Actions only). Use `blindfold use` locally.");
1764
+ const asVar = resolveEnvVar(argv.flags.as ? String(argv.flags.as) : void 0, void 0, name);
1765
+ const { release: release2 } = await Promise.resolve().then(() => (init_release(), release_exports));
1766
+ const value = await release2(name, { via: "export" });
1767
+ console.log(`::add-mask::${value}`);
1768
+ fs8.appendFileSync(ghEnv, `${asVar}=${value}
1769
+ `);
1770
+ console.error(`\u2713 exported $${asVar} from sealed "${name}" (${value.length} B, masked in logs)`);
1771
+ return;
1772
+ }
1773
+ }
1774
+ }
1775
+
1776
+ // bin/cmd-lifecycle.ts
1777
+ init_env();
1778
+ init_register();
1779
+ async function handleLifecycle(cmd, argv, cmdArgs) {
1780
+ switch (cmd) {
1781
+ case "rotate": {
1782
+ const name = String(argv.flags.name ?? "");
1783
+ const fromEnv = argv.flags["from-env"] ? String(argv.flags["from-env"]) : void 0;
1784
+ if (!name) {
1785
+ die("usage: blindfold rotate --name <secret> [--from-env <ENV_VAR>]");
1786
+ }
1787
+ const env = loadBlindfoldEnv();
1788
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
1789
+ const { recordVersion: recordVersion2, versionKeyFor: versionKeyFor2 } = await Promise.resolve().then(() => (init_versions(), versions_exports));
1790
+ const client = await openT3Client2(env);
1791
+ try {
1792
+ try {
1793
+ const old = await client.releaseSecret(name);
1794
+ const vKey = versionKeyFor2(name, Date.now());
1795
+ await client.seedSecret(vKey, old);
1796
+ recordVersion2({ t: (/* @__PURE__ */ new Date()).toISOString(), name, versionKey: vKey, length: old.length, fingerprint: fingerprint(old) });
1797
+ console.log(` before: "${name}" ${old.length} B fp=${fingerprint(old)} (snapshot saved \u2014 rollback available)`);
1798
+ } catch {
1799
+ console.log(` before: (no existing value for "${name}" \u2014 sealing fresh)`);
1800
+ }
1801
+ await registerSecret({ name, fromEnv });
1802
+ const now = await client.releaseSecret(name);
1803
+ console.log(`\u2713 Rotated "${name}" \u2192 ${now.length} B fp=${fingerprint(now)} (mode=real)`);
1804
+ console.log(` Every place that uses "${name}" now gets the new value \u2014 no code/config change.`);
1805
+ console.log(` Made a mistake? \`blindfold rollback --name ${name}\``);
1806
+ if (fromEnv) console.log(` You can now DELETE ${fromEnv} from your .env.`);
1807
+ } finally {
1808
+ await client.close();
1809
+ }
1810
+ return;
1811
+ }
1812
+ case "rollback": {
1813
+ const name = String(argv.flags.name ?? "");
1814
+ if (!name) die("usage: blindfold rollback --name <secret> [--to <iso-ts | fingerprint>]");
1815
+ const { readVersions: readVersions2, isValidVersionKey: isValidVersionKey2 } = await Promise.resolve().then(() => (init_versions(), versions_exports));
1816
+ const versions = readVersions2(name);
1817
+ if (versions.length === 0) {
1818
+ die(`no saved versions for "${name}". \`blindfold rotate\` creates them; \`blindfold versions --name ${name}\` lists them.`);
1819
+ }
1820
+ const want = argv.flags.to ? String(argv.flags.to) : "";
1821
+ let target = versions[versions.length - 1];
1822
+ if (want) {
1823
+ const sel = versions.find((v) => v.t === want || v.fingerprint === want || v.fingerprint.startsWith(want));
1824
+ if (!sel) die(`no version of "${name}" matches --to ${want} (see \`blindfold versions --name ${name}\`)`);
1825
+ target = sel;
1826
+ }
1827
+ if (!isValidVersionKey2(name, target.versionKey)) {
1828
+ die(`refusing rollback: version key "${target.versionKey}" is not a valid snapshot key for "${name}" (tampered versions.jsonl?)`);
1829
+ }
1830
+ const env = loadBlindfoldEnv();
1831
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
1832
+ const client = await openT3Client2(env);
1833
+ try {
1834
+ const before = await client.verifySecret(name);
1835
+ const restored = await client.releaseSecret(target.versionKey);
1836
+ if (fingerprint(restored) !== target.fingerprint) {
1837
+ die(`refusing rollback: released snapshot fingerprint ${fingerprint(restored)} \u2260 recorded ${target.fingerprint} (tampered versions.jsonl?)`);
1838
+ }
1839
+ await client.seedSecret(name, restored);
1840
+ console.log(`\u2713 Rolled back "${name}"`);
1841
+ console.log(` ${before.present ? `fp ${before.fingerprint} (${before.length} B)` : "(no current value)"} \u2192 fp ${fingerprint(restored)} (${restored.length} B)`);
1842
+ console.log(` restored the snapshot from ${target.t}.`);
1843
+ } finally {
1844
+ await client.close();
1845
+ }
1846
+ return;
1847
+ }
1848
+ case "versions": {
1849
+ const name = argv.flags.name ? String(argv.flags.name) : void 0;
1850
+ const { readVersions: readVersions2 } = await Promise.resolve().then(() => (init_versions(), versions_exports));
1851
+ const list = readVersions2(name);
1852
+ if (list.length === 0) {
1853
+ console.log(name ? `No saved versions for "${name}".` : "No saved versions yet. `blindfold rotate` creates them.");
1854
+ return;
1855
+ }
1856
+ console.log(`Saved versions${name ? ` for "${name}"` : ""} (newest last):
1857
+ `);
1858
+ console.log(" WHEN NAME BYTES FINGERPRINT");
1859
+ console.log(" \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1860
+ for (const v of list) {
1861
+ const when = v.t.replace("T", " ").slice(0, 19);
1862
+ console.log(` ${when} ${v.name.padEnd(22)} ${String(v.length).padStart(5)} ${v.fingerprint}`);
1863
+ }
1864
+ console.log(`
1865
+ Restore the newest: blindfold rollback --name <name>`);
1866
+ console.log(` Restore a specific: blindfold rollback --name <name> --to <fingerprint>`);
1867
+ return;
1868
+ }
1869
+ case "migrate": {
1870
+ const { planMigration: planMigration2, runMigrate: runMigrate2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
1871
+ const dryRun = !!argv.flags["dry-run"];
1872
+ const keep = !!argv.flags.keep;
1873
+ const plan = planMigration2();
1874
+ const toSeal = plan.filter((p) => p.action === "seal");
1875
+ console.log(dryRun ? "\u{1F50D} blindfold migrate --dry-run (no changes will be made)\n" : "\u{1F69A} blindfold migrate\n");
1876
+ console.log(" Plan:");
1877
+ for (const p of plan) {
1878
+ if (p.action === "seal") console.log(` SEAL ${p.envVar.padEnd(26)} \u2192 ${p.sealName} (${p.bytes} B)`);
1879
+ else console.log(` skip ${p.envVar.padEnd(26)} \u2014 ${p.reason}`);
1880
+ }
1881
+ if (toSeal.length === 0) {
1882
+ console.log("\n Nothing to seal (no secret-looking vars found).");
1883
+ return;
1884
+ }
1885
+ if (dryRun) {
1886
+ console.log(`
1887
+ Would seal ${toSeal.length} secret(s), then ${keep ? "comment out" : "remove"} their .env lines (a .env backup is kept either way).`);
1888
+ console.log(" Re-run without --dry-run to do it.");
1889
+ return;
1890
+ }
1891
+ console.log(`
1892
+ Sealing ${toSeal.length} secret(s) \u2026`);
1893
+ const results = await runMigrate2({ keep });
1894
+ console.log("");
1895
+ let ok3 = 0, fail2 = 0;
1896
+ for (const r of results) {
1897
+ if (r.action !== "seal") continue;
1898
+ if (r.sealed) {
1899
+ ok3++;
1900
+ console.log(` \u2713 ${r.sealName} sealed (${r.bytes} B) \u2014 .env line ${keep ? "commented" : "removed"}`);
1901
+ } else {
1902
+ fail2++;
1903
+ console.log(` \u2716 ${r.sealName}: ${(r.error ?? "").slice(0, 100)}`);
1904
+ }
1905
+ }
1906
+ console.log(`
1907
+ Done: ${ok3} sealed, ${fail2} failed. (.env backup saved alongside .env)`);
1908
+ if (ok3 > 0) console.log(` Use any of them with no code: blindfold use --name <name> -- <command>`);
1909
+ if (fail2 > 0) {
1910
+ console.log(` Failed seals kept their .env line. Check \`blindfold doctor\`.`);
1911
+ process.exitCode = 1;
1912
+ }
1913
+ return;
1914
+ }
1915
+ }
1916
+ }
1917
+
1918
+ // bin/cmd-tenant.ts
1919
+ init_env();
1920
+ async function handleTenant(cmd, argv, cmdArgs) {
1921
+ switch (cmd) {
1922
+ case "grant": {
1923
+ const hosts = [];
1924
+ if (argv.flags.host) hosts.push(...String(argv.flags.host).split(",").map((h) => h.trim()).filter(Boolean));
1925
+ if (argv.flags.hosts) hosts.push(...String(argv.flags.hosts).split(",").map((h) => h.trim()).filter(Boolean));
1926
+ if (hosts.length === 0) {
1927
+ die("usage: blindfold grant --host <host>[,<host2>...] (e.g. --host api.openai.com)");
1928
+ }
1929
+ const replace = !!argv.flags.replace;
1930
+ const env = loadBlindfoldEnv();
1931
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
1932
+ const client = await openT3Client2(env);
1933
+ try {
1934
+ const authorized = await client.grantEgress(hosts, { replace });
1935
+ console.log(`\u2713 Egress granted for: ${hosts.join(", ")}${replace ? " (replaced allowlist)" : ""}`);
1936
+ console.log(` Contract is now authorized to call ALL of: ${authorized.join(", ")}`);
1937
+ console.log(` Run \`blindfold publish\` first if you haven't \u2014 the grant targets the published contract.`);
1938
+ } finally {
1939
+ await client.close();
1940
+ }
1941
+ return;
1942
+ }
1943
+ case "share": {
1944
+ const to = String(argv.flags.to ?? "");
1945
+ const hosts = [];
1946
+ if (argv.flags.host) hosts.push(...String(argv.flags.host).split(",").map((h) => h.trim()).filter(Boolean));
1947
+ if (argv.flags.hosts) hosts.push(...String(argv.flags.hosts).split(",").map((h) => h.trim()).filter(Boolean));
1948
+ if (!to || hosts.length === 0) {
1949
+ die("usage: blindfold share --to <agent-did> --host <host>[,host2] (e.g. --to did:t3n:\u2026 --host api.openai.com)");
1950
+ }
1951
+ const env = loadBlindfoldEnv();
1952
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
1953
+ const client = await openT3Client2(env);
1954
+ try {
1955
+ await client.setAgentGrant(to, hosts, ["forward"]);
1956
+ console.log(`\u2713 Shared access with ${to}`);
1957
+ console.log(` authorized: forward \u2192 ${hosts.join(", ")} (they can USE the key via the enclave; they never receive the plaintext)`);
1958
+ console.log(` Revoke any time: blindfold revoke --to ${to}`);
1959
+ } finally {
1960
+ await client.close();
1961
+ }
1962
+ return;
1963
+ }
1964
+ case "revoke": {
1965
+ const to = String(argv.flags.to ?? "");
1966
+ if (!to) die("usage: blindfold revoke --to <agent-did>");
1967
+ const env = loadBlindfoldEnv();
1968
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
1969
+ const client = await openT3Client2(env);
1970
+ try {
1971
+ await client.setAgentGrant(to, [], []);
1972
+ console.log(`\u2713 Revoked all contract access for ${to}`);
1973
+ console.log(` Nobody holds the raw key, so revocation is immediate and complete \u2014 there's no leaked copy to chase.`);
1974
+ } finally {
1975
+ await client.close();
1976
+ }
1977
+ return;
1978
+ }
1979
+ }
1980
+ }
1981
+
1982
+ // bin/cmd-serve.ts
1983
+ init_env();
1984
+ import path9 from "node:path";
1985
+ import { randomBytes as randomBytes3 } from "node:crypto";
1986
+
1987
+ // src/proxy.ts
1988
+ init_constants();
1989
+ init_env();
1990
+ init_log();
1991
+ init_t3_client();
1992
+ init_usage_log();
1993
+ import http from "node:http";
1994
+ import crypto from "node:crypto";
1995
+ import fs11 from "node:fs";
1996
+ import net from "node:net";
1997
+
1998
+ // src/providers.ts
1999
+ init_constants();
2000
+ function amzDate(now = /* @__PURE__ */ new Date()) {
2001
+ const p = (n) => String(n).padStart(2, "0");
2002
+ return `${now.getUTCFullYear()}${p(now.getUTCMonth() + 1)}${p(now.getUTCDate())}T${p(now.getUTCHours())}${p(now.getUTCMinutes())}${p(now.getUTCSeconds())}Z`;
2003
+ }
2004
+ var awsRegion = () => process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
2005
+ var awsAccessKeyId = () => process.env.AWS_ACCESS_KEY_ID || "";
2006
+ function stripPrefix(path13, prefix) {
2007
+ const rest = path13.slice(prefix.length - 1);
2008
+ return rest.startsWith("/") ? rest : `/${rest}`;
2009
+ }
2010
+ var PROVIDERS = [
2011
+ // ---- LLM providers (OpenAI-shaped, bearer). Back-compatible. --------------
2012
+ { id: "openai", prefix: "/v1/", upstream: (p) => `https://api.openai.com${p}`, auth: () => ({ scheme: "bearer" }) },
2013
+ { id: "openai", prefix: "/openai/", upstream: (p) => `https://api.openai.com${stripPrefix(p, "/openai/")}`, auth: () => ({ scheme: "bearer" }) },
2014
+ // Anthropic REQUIRES the anthropic-version header on the raw REST API.
2015
+ { id: "anthropic", prefix: "/anthropic/", upstream: (p) => `https://api.anthropic.com${stripPrefix(p, "/anthropic/")}`, auth: () => ({ scheme: "bearer" }), defaultHeaders: { "anthropic-version": "2023-06-01" } },
2016
+ { id: "xai", prefix: "/x/", upstream: (p) => `https://api.x.ai${stripPrefix(p, "/x/")}`, auth: () => ({ scheme: "bearer" }) },
2017
+ { id: "groq", prefix: "/groq/", upstream: (p) => `https://api.groq.com/openai${stripPrefix(p, "/groq/")}`, auth: () => ({ scheme: "bearer" }) },
2018
+ // ---- LLM: Google Gemini (native API — key rides in `x-goog-api-key`). ------
2019
+ // Not OpenAI-shaped and NOT `Authorization: Bearer`. The sentinel is planted
2020
+ // in Google's provider-specific header and swapped for the sealed key inside
2021
+ // the enclave — proving Blindfold handles a provider's real auth convention,
2022
+ // not just the one bearer shape.
2023
+ {
2024
+ id: "gemini",
2025
+ prefix: "/gemini/",
2026
+ upstream: (p) => `https://generativelanguage.googleapis.com${stripPrefix(p, "/gemini/")}`,
2027
+ secretKey: "gemini_api_key",
2028
+ auth: () => ({ scheme: "bearer" }),
2029
+ sentinelHeader: { name: "x-goog-api-key", prefix: "" }
2030
+ },
2031
+ // ---- Payments: Stripe (bearer; pins the API version so behaviour is stable). -----
2032
+ {
2033
+ id: "stripe",
2034
+ prefix: "/stripe/",
2035
+ upstream: (p) => `https://api.stripe.com${stripPrefix(p, "/stripe/")}`,
2036
+ secretKey: "stripe_secret_key",
2037
+ auth: () => ({ scheme: "bearer" }),
2038
+ defaultHeaders: { "stripe-version": "2024-06-20", "content-type": "application/x-www-form-urlencoded" }
2039
+ },
2040
+ // ---- Dev infra: GitHub. GitHub REJECTS requests with no User-Agent (403),
2041
+ // and best practice is to pin the REST API version + set the Accept type.
2042
+ {
2043
+ id: "github",
2044
+ prefix: "/github/",
2045
+ upstream: (p) => `https://api.github.com${stripPrefix(p, "/github/")}`,
2046
+ secretKey: "github_token",
2047
+ auth: () => ({ scheme: "bearer" }),
2048
+ defaultHeaders: {
2049
+ accept: "application/vnd.github+json",
2050
+ "x-github-api-version": "2022-11-28",
2051
+ "user-agent": "blindfold"
2052
+ }
2053
+ },
2054
+ // ---- Email: SendGrid (bearer; JSON v3 API). -------------------------------
2055
+ {
2056
+ id: "sendgrid",
2057
+ prefix: "/sendgrid/",
2058
+ upstream: (p) => `https://api.sendgrid.com${stripPrefix(p, "/sendgrid/")}`,
2059
+ secretKey: "sendgrid_api_key",
2060
+ auth: () => ({ scheme: "bearer" }),
2061
+ defaultHeaders: { "content-type": "application/json" }
2062
+ },
2063
+ // ---- Comms: Slack (bearer bot token; Web API wants JSON+charset on POST). --
2064
+ {
2065
+ id: "slack",
2066
+ prefix: "/slack/",
2067
+ upstream: (p) => `https://slack.com/api${stripPrefix(p, "/slack/")}`,
2068
+ secretKey: "slack_bot_token",
2069
+ auth: () => ({ scheme: "bearer" }),
2070
+ defaultHeaders: { "content-type": "application/json; charset=utf-8" }
2071
+ },
2072
+ // ---- Telephony: Twilio (HTTP Basic — base64 computed IN the enclave). -----
2073
+ // Username = Account SID (not secret; also appears in the URL path). The
2074
+ // sealed secret is the Auth Token. A generic proxy CANNOT do this: the
2075
+ // base64 must be computed after the secret is joined, inside TDX.
2076
+ {
2077
+ id: "twilio",
2078
+ prefix: "/twilio/",
2079
+ upstream: (p) => `https://api.twilio.com${stripPrefix(p, "/twilio/")}`,
2080
+ secretKey: "twilio_auth_token",
2081
+ auth: () => ({ scheme: "basic", username: process.env.TWILIO_ACCOUNT_SID || "" }),
2082
+ defaultHeaders: { "content-type": "application/x-www-form-urlencoded" }
2083
+ },
2084
+ // ---- Cloud: AWS SES (SigV4 — secret SIGNS, never transmitted). ------------
2085
+ {
2086
+ id: "aws-ses",
2087
+ prefix: "/aws/ses/",
2088
+ upstream: (p) => `https://email.${awsRegion()}.amazonaws.com${stripPrefix(p, "/aws/ses/")}`,
2089
+ secretKey: "aws_secret_access_key",
2090
+ auth: () => ({ scheme: "sigv4", access_key_id: awsAccessKeyId(), region: awsRegion(), service: "ses", amz_date: amzDate() })
2091
+ },
2092
+ // ---- Cloud: AWS S3 (SigV4). -----------------------------------------------
2093
+ {
2094
+ id: "aws-s3",
2095
+ prefix: "/aws/s3/",
2096
+ upstream: (p) => `https://s3.${awsRegion()}.amazonaws.com${stripPrefix(p, "/aws/s3/")}`,
2097
+ secretKey: "aws_secret_access_key",
2098
+ auth: () => ({ scheme: "sigv4", access_key_id: awsAccessKeyId(), region: awsRegion(), service: "s3", amz_date: amzDate() })
2099
+ },
2100
+ // ---- Webhook: Discord. The SECRET IS THE URL — the enclave substitutes the
2101
+ // sealed webhook URL for the sentinel, so the agent POSTs to /discord
2102
+ // with only a JSON body and never holds the URL. Needs egress for
2103
+ // discord.com (`blindfold grant --host discord.com`).
2104
+ {
2105
+ id: "discord",
2106
+ prefix: "/discord",
2107
+ upstream: () => SENTINEL,
2108
+ // enclave replaces this with the sealed webhook URL
2109
+ secretKey: "webhook_discord_url",
2110
+ auth: () => ({ scheme: "webhook" })
2111
+ }
2112
+ ];
2113
+ function resolveProvider(path13) {
2114
+ const def = PROVIDERS.filter((d) => path13.startsWith(d.prefix)).sort((a, b) => b.prefix.length - a.prefix.length)[0];
2115
+ if (!def) return null;
2116
+ return {
2117
+ id: def.id,
2118
+ upstream: def.upstream(path13),
2119
+ secretKey: def.secretKey,
2120
+ auth: def.auth(),
2121
+ sentinelHeader: def.sentinelHeader,
2122
+ defaultHeaders: def.defaultHeaders
2123
+ };
2124
+ }
2125
+
2126
+ // src/proxy.ts
2127
+ var MAX_BODY_BYTES = Number(process.env.BLINDFOLD_MAX_BODY_BYTES) || 5 * 1024 * 1024;
2128
+ var BODY_IDLE_MS = Number(process.env.BLINDFOLD_BODY_IDLE_MS) || 3e4;
2129
+ var MAX_INFLIGHT = Number(process.env.BLINDFOLD_MAX_INFLIGHT) || 64;
2130
+ var inflight = 0;
2131
+ var PayloadTooLargeError = class extends Error {
2132
+ };
2133
+ function tokenMatches(provided, expected) {
2134
+ const a = Buffer.from(provided);
2135
+ const b = Buffer.from(expected);
2136
+ if (a.length !== b.length) return false;
2137
+ return crypto.timingSafeEqual(a, b);
2138
+ }
2139
+ var PROXY_TOKEN_HEADER = "x-blindfold-token";
2140
+ function isSocketLive(socketPath) {
2141
+ return new Promise((resolve) => {
2142
+ if (!fs11.existsSync(socketPath)) {
2143
+ resolve(false);
2144
+ return;
2145
+ }
2146
+ const c2 = net.connect(socketPath);
2147
+ const done = (live) => {
2148
+ try {
2149
+ c2.destroy();
2150
+ } catch {
2151
+ }
2152
+ resolve(live);
2153
+ };
2154
+ c2.once("connect", () => done(true));
2155
+ c2.once("error", () => done(false));
2156
+ c2.setTimeout(500, () => done(false));
2157
+ });
2158
+ }
2159
+ async function startProxy(opts = {}) {
2160
+ const env = loadBlindfoldEnv();
2161
+ const port = opts.port ?? env.port ?? 8787;
2162
+ const secretKey = opts.secretKey ?? "openai_api_key";
2163
+ const token = (opts.token ?? process.env.BLINDFOLD_PROXY_TOKEN ?? "").trim() || void 0;
2164
+ const socketPath = opts.socket?.trim() || void 0;
2165
+ if (socketPath && await isSocketLive(socketPath)) {
2166
+ throw new Error(`a Blindfold proxy is already listening on ${socketPath} \u2014 stop it first, or use a different --socket path`);
2167
+ }
2168
+ const t3 = await openT3Client(env);
2169
+ const server = http.createServer((req, res) => {
2170
+ handle(req, res, t3, secretKey, env, token).catch((e) => {
2171
+ if (e instanceof PayloadTooLargeError) {
2172
+ if (!res.headersSent) res.writeHead(413, { "content-type": "text/plain", "connection": "close" });
2173
+ res.end("request body too large");
2174
+ req.destroy();
2175
+ return;
2176
+ }
2177
+ safeLog("error", { msg: "proxy_unhandled", error: e.message });
2178
+ if (!res.headersSent) res.writeHead(500, { "content-type": "text/plain" });
2179
+ res.end("internal proxy error");
2180
+ });
2181
+ });
2182
+ return await new Promise((resolve, reject) => {
2183
+ server.once("error", reject);
2184
+ let prevUmask;
2185
+ const onListening = () => {
2186
+ let url;
2187
+ let bound = 0;
2188
+ if (socketPath) {
2189
+ try {
2190
+ fs11.chmodSync(socketPath, 384);
2191
+ } catch {
2192
+ }
2193
+ if (prevUmask !== void 0) {
2194
+ try {
2195
+ process.umask(prevUmask);
2196
+ } catch {
2197
+ }
2198
+ }
2199
+ url = `unix:${socketPath}`;
2200
+ } else {
2201
+ bound = server.address().port;
2202
+ url = `http://127.0.0.1:${bound}`;
2203
+ }
2204
+ safeLog("info", { msg: "proxy_listening", url, mock: env.mock });
2205
+ resolve({
2206
+ url,
2207
+ port: bound,
2208
+ token,
2209
+ socket: socketPath,
2210
+ close: async () => {
2211
+ await new Promise((r) => server.close(() => r()));
2212
+ if (socketPath) {
2213
+ try {
2214
+ fs11.rmSync(socketPath, { force: true });
2215
+ } catch {
2216
+ }
2217
+ }
2218
+ await t3.close();
2219
+ }
2220
+ });
2221
+ };
2222
+ if (socketPath) {
2223
+ try {
2224
+ fs11.rmSync(socketPath, { force: true });
2225
+ } catch {
2226
+ }
2227
+ prevUmask = process.umask(127);
2228
+ server.listen(socketPath, onListening);
2229
+ } else {
2230
+ server.listen(port, "127.0.0.1", onListening);
2231
+ }
2232
+ });
2233
+ }
2234
+ async function handle(req, res, t3, secretKey, env, token) {
2235
+ if (req.url === "/health") {
2236
+ res.writeHead(200, { "content-type": "application/json" });
2237
+ res.end(JSON.stringify({ ok: true, mock: env.mock, auth: Boolean(token) }));
2238
+ return;
2239
+ }
2240
+ if (token) {
2241
+ const provided = String(req.headers[PROXY_TOKEN_HEADER] ?? "");
2242
+ if (!provided || !tokenMatches(provided, token)) {
2243
+ safeLog("warn", { msg: "proxy_unauthorized", path: req.url });
2244
+ res.writeHead(401, { "content-type": "text/plain" });
2245
+ res.end(`unauthorized: missing or invalid ${PROXY_TOKEN_HEADER} header`);
2246
+ return;
2247
+ }
2248
+ }
2249
+ const provider = resolveProvider(req.url ?? "/");
2250
+ if (!provider) {
2251
+ res.writeHead(404, { "content-type": "text/plain" });
2252
+ res.end(`no upstream mapping for ${req.url}`);
2253
+ return;
2254
+ }
2255
+ const upstream = provider.upstream;
2256
+ const providerSecretKey = provider.secretKey ?? secretKey;
2257
+ const body = await readBody(req, MAX_BODY_BYTES);
2258
+ const agentSuppliedAuth = Object.keys(req.headers).some((k) => k.toLowerCase() === "authorization");
2259
+ const headers = forwardableHeaders(req.headers);
2260
+ if (provider.auth.scheme === "bearer") {
2261
+ const sh = provider.sentinelHeader ?? { name: "authorization", prefix: "Bearer " };
2262
+ removeHeader(headers, "authorization");
2263
+ ensureHeader(headers, sh.name, `${sh.prefix}${SENTINEL}`);
2264
+ } else {
2265
+ removeHeader(headers, "authorization");
2266
+ }
2267
+ for (const [name, value] of Object.entries(provider.defaultHeaders ?? {})) {
2268
+ if (!headers.some(([k]) => k.toLowerCase() === name.toLowerCase())) {
2269
+ headers.push([name, value]);
2270
+ }
2271
+ }
2272
+ let outboundUrl = upstream;
2273
+ let outboundBody = body.length ? body.toString("utf8") : void 0;
2274
+ const contentType = (headers.find(([k]) => k.toLowerCase() === "content-type")?.[1] ?? "").toLowerCase();
2275
+ if (provider.auth.scheme !== "webhook" && outboundBody && contentType.includes("application/x-www-form-urlencoded")) {
2276
+ const u = new URL(outboundUrl);
2277
+ for (const [k, v] of new URLSearchParams(outboundBody)) u.searchParams.append(k, v);
2278
+ outboundUrl = u.toString();
2279
+ outboundBody = void 0;
2280
+ safeLog("info", { msg: "form_body_to_query", provider: provider.id });
2281
+ }
2282
+ const forwardReq = {
2283
+ method: req.method ?? "GET",
2284
+ url: outboundUrl,
2285
+ headers,
2286
+ body: outboundBody,
2287
+ secret_key: providerSecretKey,
2288
+ auth: provider.auth
2289
+ };
2290
+ safeLog("info", {
2291
+ msg: "proxy_forward",
2292
+ method: forwardReq.method,
2293
+ upstream: upstream.replace(/\?.*$/, "")
2294
+ });
2295
+ if (inflight >= MAX_INFLIGHT) {
2296
+ safeLog("warn", { msg: "proxy_saturated", inflight, max: MAX_INFLIGHT });
2297
+ if (!res.headersSent) res.writeHead(503, { "content-type": "text/plain", "retry-after": "1" });
2298
+ res.end("proxy saturated: too many concurrent enclave calls \u2014 retry shortly");
2299
+ return;
2300
+ }
2301
+ const startedAt = Date.now();
2302
+ let result;
2303
+ inflight++;
2304
+ try {
2305
+ result = await t3.invokeForward(forwardReq);
2306
+ } catch (e) {
2307
+ const { status, body: body2 } = explainForwardError(e);
2308
+ safeLog("error", { msg: "proxy_forward_failed", status, provider: provider.id, error: e.message });
2309
+ if (!res.headersSent) res.writeHead(status, { "content-type": "application/json" });
2310
+ res.end(body2);
2311
+ return;
2312
+ } finally {
2313
+ inflight--;
2314
+ }
2315
+ const latency = Date.now() - startedAt;
2316
+ logUsage({
2317
+ t: (/* @__PURE__ */ new Date()).toISOString(),
2318
+ mode: env.mock ? "mock" : "real",
2319
+ provider: provider.id || providerForUpstream(upstream),
2320
+ method: forwardReq.method,
2321
+ path: req.url ?? "/",
2322
+ upstream: upstream.replace(/\?.*$/, ""),
2323
+ status: result.status,
2324
+ latency_ms: latency,
2325
+ agent_supplied_auth: agentSuppliedAuth,
2326
+ auth_scheme: provider.auth.scheme,
2327
+ sentinel_in_outbound: forwardReq.headers.some(([k, v]) => k.toLowerCase() === "authorization" && v.includes(SENTINEL)),
2328
+ via: "proxy",
2329
+ secret_key: providerSecretKey
2330
+ });
2331
+ res.writeHead(result.status, headersFromTuple(result.headers));
2332
+ const bodyBytes = typeof result.body === "string" ? Buffer.from(result.body, "utf8") : Buffer.from(result.body);
2333
+ res.end(bodyBytes);
2334
+ }
2335
+ function readBody(req, maxBytes) {
2336
+ return new Promise((resolve, reject) => {
2337
+ const chunks = [];
2338
+ let size = 0;
2339
+ let aborted = false;
2340
+ let timer;
2341
+ const fail2 = (e) => {
2342
+ aborted = true;
2343
+ clearTimeout(timer);
2344
+ reject(e);
2345
+ };
2346
+ const arm = () => {
2347
+ clearTimeout(timer);
2348
+ timer = setTimeout(() => {
2349
+ if (!aborted) fail2(new Error(`request body read timed out after ${BODY_IDLE_MS}ms`));
2350
+ }, BODY_IDLE_MS);
2351
+ };
2352
+ arm();
2353
+ req.on("data", (c2) => {
2354
+ if (aborted) return;
2355
+ arm();
2356
+ size += c2.length;
2357
+ if (size > maxBytes) {
2358
+ fail2(new PayloadTooLargeError(`request body exceeds ${maxBytes} bytes`));
2359
+ return;
2360
+ }
2361
+ chunks.push(c2);
2362
+ });
2363
+ req.on("end", () => {
2364
+ clearTimeout(timer);
2365
+ if (!aborted) resolve(Buffer.concat(chunks));
2366
+ });
2367
+ req.on("error", (e) => {
2368
+ if (!aborted) fail2(e);
2369
+ });
2370
+ });
2371
+ }
2372
+ var STRIPPED = /* @__PURE__ */ new Set(["host", "content-length", "connection", "transfer-encoding"]);
2373
+ function forwardableHeaders(h) {
2374
+ const out = [];
2375
+ for (const [k, v] of Object.entries(h)) {
2376
+ if (v === void 0) continue;
2377
+ if (STRIPPED.has(k.toLowerCase())) continue;
2378
+ out.push([k, Array.isArray(v) ? v.join(", ") : v]);
2379
+ }
2380
+ return out;
2381
+ }
2382
+ function ensureHeader(h, name, value) {
2383
+ const lower = name.toLowerCase();
2384
+ const idx = h.findIndex(([k]) => k.toLowerCase() === lower);
2385
+ if (idx >= 0) h[idx] = [name, value];
2386
+ else h.push([name, value]);
2387
+ }
2388
+ function removeHeader(h, name) {
2389
+ const lower = name.toLowerCase();
2390
+ for (let i = h.length - 1; i >= 0; i--) {
2391
+ if (h[i][0].toLowerCase() === lower) h.splice(i, 1);
2392
+ }
2393
+ }
2394
+ function headersFromTuple(t) {
2395
+ const out = {};
2396
+ for (const [k, v] of t) out[k] = v;
2397
+ return out;
2398
+ }
2399
+ function explainForwardError(err) {
2400
+ const msg = err?.message ?? String(err);
2401
+ if (err instanceof T3TimeoutError) {
2402
+ return {
2403
+ status: 504,
2404
+ body: JSON.stringify({
2405
+ error: "blindfold_forward_failed",
2406
+ status: 504,
2407
+ code: "t3_timeout",
2408
+ detail: msg,
2409
+ hint: "The T3 node did not respond within the deadline (BLINDFOLD_T3_TIMEOUT_MS). It may be slow or unreachable; retry, or point T3_BASE_URL at a healthy node."
2410
+ }, null, 2)
2411
+ };
2412
+ }
2413
+ let status = Number(msg.match(/HTTP (\d{3})/)?.[1]) || 502;
2414
+ let code = "";
2415
+ let detail = msg;
2416
+ let requestId = "";
2417
+ const jsonM = msg.match(/\((\{.*\})\)\s*$/);
2418
+ if (jsonM && jsonM[1]) {
2419
+ try {
2420
+ const o = JSON.parse(jsonM[1]);
2421
+ code = o.code ?? "";
2422
+ detail = o.detail ?? detail;
2423
+ requestId = o.request_id ?? "";
2424
+ } catch {
2425
+ }
2426
+ }
2427
+ let hint = "";
2428
+ if (/egress_denied|authorised_hosts|allowlist/i.test(detail)) {
2429
+ const host = detail.match(/host '([^']+)'/)?.[1];
2430
+ hint = `Egress is not authorized${host ? ` for '${host}'` : ""}. Run: blindfold grant --host ${host ?? "<host>"} \u2014 list every host you use in ONE command (grant replaces the allowlist unless merged).`;
2431
+ if (status < 400) status = 403;
2432
+ } else if (/fuel_per_minute|too_many_requests|rate limit/i.test(detail)) {
2433
+ hint = "Rate limited by the testnet per-minute compute quota (fuel_per_minute). Retry in ~60s and space calls out \u2014 this is not an outage.";
2434
+ status = 429;
2435
+ } else if (/cannot read map|:secrets/i.test(detail)) {
2436
+ hint = "The contract isn't authorized to read your secrets map (common right after publishing a new contract id). Run: blindfold init (re-grants the secrets read ACL).";
2437
+ } else if (/parse_payload|expected value at line/i.test(detail)) {
2438
+ hint = "The T3 host egress parses request bodies as JSON. For form-encoded APIs (Stripe/Twilio), send params in the query string with an empty body.";
2439
+ if (status < 400) status = 400;
2440
+ } else if (/secret .* not found|not found in the secrets map/i.test(detail)) {
2441
+ hint = "That sealed secret name doesn't exist. Seal it: blindfold register --name <name> --from-env <ENV_VAR>.";
2442
+ }
2443
+ const payload = {
2444
+ error: "blindfold_forward_failed",
2445
+ status,
2446
+ code: code || void 0,
2447
+ detail,
2448
+ request_id: requestId || void 0,
2449
+ hint: hint || void 0
2450
+ };
2451
+ return { status, body: JSON.stringify(payload, null, 2) };
2452
+ }
2453
+
2454
+ // src/dashboard.ts
2455
+ init_constants();
2456
+ init_env();
2457
+ init_usage_log();
2458
+ init_sealed_ledger();
2459
+ import fs12 from "node:fs";
2460
+ import http2 from "node:http";
2461
+ import path8 from "node:path";
2462
+ import { timingSafeEqual } from "node:crypto";
2463
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2464
+ async function startDashboard(opts = {}) {
2465
+ const port = opts.port ?? DEFAULT_DASHBOARD_PORT;
2466
+ const TOKEN = process.env.BLINDFOLD_DASHBOARD_TOKEN || "";
2467
+ const tokenEq = (candidate) => {
2468
+ const a = Buffer.from(candidate);
2469
+ const b = Buffer.from(TOKEN);
2470
+ return a.length === b.length && timingSafeEqual(a, b);
2471
+ };
2472
+ const authed = (req, url) => {
2473
+ if (!TOKEN) return true;
2474
+ const auth = req.headers.authorization || "";
2475
+ if (auth.startsWith("Bearer ") && tokenEq(auth.slice(7))) return true;
2476
+ try {
2477
+ const t = new URL(url, "http://x").searchParams.get("token");
2478
+ if (t && tokenEq(t)) return true;
2479
+ } catch {
2480
+ }
2481
+ return false;
2482
+ };
2483
+ const server = http2.createServer(async (req, res) => {
2484
+ const url = req.url ?? "/";
2485
+ const pathname = url.split("?")[0];
2486
+ if (!authed(req, url)) {
2487
+ res.writeHead(401, { "content-type": "text/plain" });
2488
+ res.end("unauthorized \u2014 append ?token=<BLINDFOLD_DASHBOARD_TOKEN>");
2489
+ return;
2490
+ }
2491
+ if (req.method === "GET" && pathname === "/logo.png") {
2492
+ try {
2493
+ const HERE5 = path8.dirname(fileURLToPath3(import.meta.url));
2494
+ const candidates = [
2495
+ path8.resolve(HERE5, "..", "..", "..", "assets", "logo.png"),
2496
+ path8.resolve(HERE5, "..", "assets", "logo.png")
2497
+ ];
2498
+ const logo = candidates.find((p) => fs12.existsSync(p));
2499
+ if (logo) {
2500
+ res.writeHead(200, { "content-type": "image/png", "cache-control": "max-age=3600" });
2501
+ res.end(fs12.readFileSync(logo));
2502
+ } else {
2503
+ 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>`;
2504
+ res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "max-age=3600" });
2505
+ res.end(svg);
2506
+ }
2507
+ } catch {
2508
+ res.writeHead(404, { "content-type": "text/plain" }).end("logo not found");
2509
+ }
2510
+ return;
2511
+ }
2512
+ if (req.method === "GET" && (pathname === "/" || pathname === "/index.html")) {
2513
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2514
+ res.end(HTML);
2515
+ return;
2516
+ }
2517
+ if (req.method === "GET" && pathname === "/api/events") {
2518
+ const limit = Math.min(Number(new URL(url, "http://x").searchParams.get("limit")) || 500, 5e3);
2519
+ const events = readUsageTail(limit);
2520
+ const counters = {};
2521
+ for (const e of events) counters[e.provider] = (counters[e.provider] ?? 0) + 1;
2522
+ res.writeHead(200, { "content-type": "application/json" });
2523
+ res.end(JSON.stringify({ events, counters, returned: events.length, source: defaultLogPath() }));
2524
+ return;
2525
+ }
2526
+ if (req.method === "GET" && pathname === "/api/sealed") {
2527
+ res.writeHead(200, { "content-type": "application/json" });
2528
+ res.end(JSON.stringify({ entries: readSealed(), source: defaultSealedLogPath() }));
2529
+ return;
2530
+ }
2531
+ if (req.method === "GET" && pathname === "/api/stream") {
2532
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
2533
+ res.write(": connected\n\n");
2534
+ const send = () => {
2535
+ try {
2536
+ res.write("event: change\ndata: {}\n\n");
2537
+ } catch {
2538
+ }
2539
+ };
2540
+ const watchers = [];
2541
+ for (const p of [defaultLogPath(), defaultSealedLogPath()]) {
2542
+ try {
2543
+ watchers.push(fs12.watch(path8.dirname(p), () => send()));
2544
+ } catch {
2545
+ }
2546
+ }
2547
+ const hb = setInterval(() => {
2548
+ try {
2549
+ res.write(": ping\n\n");
2550
+ } catch {
2551
+ }
2552
+ }, 15e3);
2553
+ req.on("close", () => {
2554
+ clearInterval(hb);
2555
+ for (const w of watchers) {
2556
+ try {
2557
+ w.close();
2558
+ } catch {
2559
+ }
2560
+ }
2561
+ });
2562
+ return;
2563
+ }
2564
+ if (req.method === "GET" && pathname === "/api/audit/full") {
2565
+ const env = loadBlindfoldEnv();
2566
+ const sealed = readSealed();
2567
+ const latest = /* @__PURE__ */ new Map();
2568
+ for (const s of sealed) latest.set(s.name, s);
2569
+ const results = [];
2570
+ if (!env.mock) {
2571
+ try {
2572
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
2573
+ const client = await openT3Client2(env);
2574
+ try {
2575
+ for (const e of latest.values()) {
2576
+ const v = await client.verifySecret(e.name);
2577
+ 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 });
2578
+ }
2579
+ } finally {
2580
+ await client.close();
2581
+ }
2582
+ } catch (err) {
2583
+ res.writeHead(200, { "content-type": "application/json" });
2584
+ res.end(JSON.stringify({ mock: env.mock, error: err.message, results: [] }));
2585
+ return;
2586
+ }
2587
+ }
2588
+ res.writeHead(200, { "content-type": "application/json" });
2589
+ res.end(JSON.stringify({ mock: env.mock, results }));
2590
+ return;
2591
+ }
2592
+ if (req.method === "GET" && pathname === "/api/status") {
2593
+ const env = loadBlindfoldEnv();
2594
+ res.writeHead(200, { "content-type": "application/json" });
2595
+ res.end(JSON.stringify({
2596
+ mode: env.mock ? "mock" : "real",
2597
+ tenant_did: env.did || null,
2598
+ tenant_did_short: env.did ? shortenDid(env.did) : null,
2599
+ t3_env: env.t3Env,
2600
+ contract_version: CONTRACT_VERSION,
2601
+ proxy_port: env.port,
2602
+ sdk_installed: isSdkInstalled()
2603
+ }));
2604
+ return;
2605
+ }
2606
+ if (req.method === "GET" && pathname === "/api/audit") {
2607
+ const sealed = readSealed();
2608
+ const envKeys = new Set(readEnvKeyNames());
2609
+ const latestByName = /* @__PURE__ */ new Map();
2610
+ for (const s of sealed) latestByName.set(s.name, s);
2611
+ const exposed = Array.from(latestByName.values()).filter((latest) => envKeys.has(latest.name)).map((latest) => ({ name: latest.name, length: latest.length, sealed_at: latest.t }));
2612
+ const uniqueSealed = latestByName.size;
2613
+ const chain = verifyLedgerChain();
2614
+ res.writeHead(200, { "content-type": "application/json" });
2615
+ res.end(JSON.stringify({
2616
+ exposed_in_env: exposed,
2617
+ env_keys: Array.from(envKeys),
2618
+ sealed_count: uniqueSealed,
2619
+ ledger_chain: chain
2620
+ // { ok, total, legacy, firstBrokenIndex }
2621
+ }));
2622
+ return;
2623
+ }
2624
+ if ((req.method === "POST" || req.method === "DELETE") && pathname === "/api/clear") {
2625
+ clearUsage();
2626
+ res.writeHead(204).end();
2627
+ return;
2628
+ }
2629
+ res.writeHead(404, { "content-type": "text/plain" });
2630
+ res.end("not found");
2631
+ });
2632
+ return await new Promise((resolve, reject) => {
2633
+ server.once("error", reject);
2634
+ server.listen(port, "127.0.0.1", () => {
2635
+ const { port: bound } = server.address();
2636
+ resolve({
2637
+ url: `http://127.0.0.1:${bound}`,
2638
+ port: bound,
2639
+ close: () => new Promise((r) => server.close(() => r()))
2640
+ });
2641
+ });
2642
+ });
2643
+ }
2644
+ function shortenDid(did) {
2645
+ const prefix = "did:t3n:";
2646
+ if (!did.startsWith(prefix)) return did;
2647
+ const tail = did.slice(prefix.length);
2648
+ return tail.length <= 14 ? did : `${prefix}${tail.slice(0, 6)}\u2026${tail.slice(-4)}`;
2649
+ }
2650
+ function readEnvKeyNames() {
2651
+ const ENV_PATH2 = defaultEnvPath();
2652
+ if (!fs12.existsSync(ENV_PATH2)) return [];
2653
+ return fs12.readFileSync(ENV_PATH2, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=", 1)[0]?.trim() ?? "").filter(Boolean);
2654
+ }
2655
+ function isSdkInstalled() {
2656
+ try {
2657
+ __require.resolve?.("@terminal3/t3n-sdk");
2658
+ return true;
2659
+ } catch {
2660
+ try {
2661
+ const HERE5 = path8.dirname(fileURLToPath3(import.meta.url));
2662
+ return fs12.existsSync(path8.resolve(HERE5, "..", "..", "..", "node_modules", "@terminal3", "t3n-sdk"));
2663
+ } catch {
2664
+ return false;
2665
+ }
2666
+ }
2667
+ }
2668
+ var HTML = `<!DOCTYPE html>
2669
+ <html lang="en" data-theme="dark">
2670
+ <head>
2671
+ <meta charset="utf-8" />
2672
+ <title>Blindfold \u2014 Dashboard</title>
2673
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2674
+ <link rel="icon" type="image/png" href="/logo.png" />
2675
+ <style>
2676
+ :root {
2677
+ --bg:#0b0e14; --bg2:#0e1117; --card:#161b22; --card2:#1c2230; --line:#2a313c;
2678
+ --fg:#e6edf3; --dim:#8b949e; --dim2:#6b7280;
2679
+ --ok:#3fb950; --warn:#d29922; --bad:#f85149; --info:#58a6ff;
2680
+ --accent:#8b5cf6; --accent2:#58a6ff;
2681
+ --orange:#ff8c2b; --orange2:#ff6a3d; --brand:#ff8c2b;
2682
+ --c1:#8b5cf6; --c2:#58a6ff; --c3:#3fb950; --c4:#ff8c2b; --c5:#f85149; --c6:#ff7b72; --c7:#39c5cf; --c8:#f778ba;
2683
+ --line:#333b47; --line-strong:#465061;
2684
+ --shadow:0 1px 3px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.18);
2685
+ }
2686
+ [data-theme="light"] {
2687
+ --bg:#f6f8fa; --bg2:#fff; --card:#fff; --card2:#f3f4f6; --line:#d8dee4; --line-strong:#c2cad3;
2688
+ --fg:#1f2328; --dim:#57606a; --dim2:#8b949e;
2689
+ --shadow:0 1px 2px rgba(0,0,0,.06), 0 8px 24px rgba(0,0,0,.06);
2690
+ }
2691
+ * { box-sizing:border-box; }
2692
+ html,body { max-width:100vw; overflow-x:hidden; }
2693
+ body {
2694
+ margin:0; padding:20px clamp(12px,4vw,40px);
2695
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Inter",system-ui,sans-serif;
2696
+ background:radial-gradient(1200px 600px at 20% -10%, rgba(139,92,246,.10), transparent 60%), var(--bg);
2697
+ color:var(--fg); line-height:1.5; transition:background .2s,color .2s;
2698
+ }
2699
+ a { color:var(--accent2); }
2700
+ .topbar { display:flex; justify-content:space-between; align-items:center; gap:14px; flex-wrap:wrap; margin-bottom:18px;
2701
+ border:1px solid var(--line-strong); border-radius:16px; padding:16px 20px;
2702
+ background:linear-gradient(180deg,var(--card),var(--card2)); box-shadow:var(--shadow); }
2703
+ .brandwrap { display:flex; align-items:center; gap:16px; min-width:0; }
2704
+ .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; }
2705
+ @keyframes logoIn { from{transform:scale(.7) rotate(-8deg);opacity:0;} to{transform:scale(1) rotate(0);opacity:1;} }
2706
+ 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; }
2707
+ /* wordmark: ribbon-wrap animation \u2014 pure CSS, no runtime deps */
2708
+ .brand { position:relative; display:inline-block; padding-bottom:6px; overflow:hidden; }
2709
+ .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; }
2710
+ @keyframes underlineIn { from{transform:scaleX(0);opacity:.3;} to{transform:scaleX(1);opacity:1;} }
2711
+ .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; }
2712
+ @keyframes protectLetter { to{ color:#bdc7d4; text-shadow:0 1px 4px rgba(139,92,246,.12); transform:translateY(-0.5px); } }
2713
+ [data-theme="light"] .letter { animation:protectLetterLight .35s cubic-bezier(.2,.8,.2,1) both; }
2714
+ @keyframes protectLetterLight { to{ color:#5a6478; text-shadow:0 1px 4px rgba(139,92,246,.10); transform:translateY(-0.5px); } }
2715
+ .brand > .letter:nth-of-type(5) { animation-delay:0.90s; } .brand > .letter:nth-of-type(4) { animation-delay:0.94s; }
2716
+ .brand > .letter:nth-of-type(6) { animation-delay:0.98s; } .brand > .letter:nth-of-type(3) { animation-delay:1.02s; }
2717
+ .brand > .letter:nth-of-type(7) { animation-delay:1.06s; } .brand > .letter:nth-of-type(2) { animation-delay:1.10s; }
2718
+ .brand > .letter:nth-of-type(8) { animation-delay:1.14s; } .brand > .letter:nth-of-type(1) { animation-delay:1.18s; }
2719
+ .brand > .letter:nth-of-type(9) { animation-delay:1.22s; }
2720
+ .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; }
2721
+ @keyframes ribbonSweep { 0%{ transform:translateX(100%) scaleX(.01); } 100%{ transform:translateX(22%) scaleX(1); } }
2722
+ @keyframes ribbonSettle { 0%{ transform:translateX(22%) scaleX(1); } 100%{ transform:translateX(-6%) scaleX(.65) rotate(-1.5deg); } }
2723
+ @keyframes ribbonBreeze { 0%,100%{ transform:translateX(-6%) scaleX(.65) rotate(-1.5deg); } 50%{ transform:translateX(-4%) scaleX(.68) rotate(.8deg); } }
2724
+ @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); } }
2725
+ .logo-img { animation:logoIn .6s cubic-bezier(.2,.8,.2,1) both, logoSeal 2.2s .6s ease both; }
2726
+ .eyebrow { font-size:.42em; font-weight:600; color:var(--dim); text-transform:uppercase; letter-spacing:1.5px; }
2727
+ .tagline { font-size:12px; color:var(--dim); margin-top:7px; display:flex; align-items:center; gap:6px; }
2728
+ .tagline .lock { color:var(--orange); display:inline-block; animation:clack .5s .45s cubic-bezier(.3,1.4,.5,1) both; }
2729
+ @keyframes clack { from{transform:rotate(-35deg) translateY(-2px);opacity:0;} to{transform:rotate(0) translateY(0);opacity:1;} }
2730
+ /* --- Distinctive "decrypt \u2192 seal" wordmark animation (JS-driven) --- */
2731
+ .letter.scrambling { color:var(--accent); font-family:ui-monospace,SFMono-Regular,Menlo,monospace; opacity:.92; }
2732
+ .letter.sealed { animation:sealSnap .55s cubic-bezier(.2,1.4,.4,1) both; }
2733
+ @keyframes sealSnap {
2734
+ 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); }
2735
+ 55% { color:var(--fg); text-shadow:0 0 8px rgba(139,92,246,.5); transform:translateY(1px) scale(.97); }
2736
+ 100% { color:var(--fg); text-shadow:none; transform:none; }
2737
+ }
2738
+ /* seal sweep: a vertical light bar that passes across the word once as it seals */
2739
+ .sealbar { position:absolute; top:-22%; height:144%; width:16px; left:-8%; z-index:3; pointer-events:none; border-radius:9px; opacity:0;
2740
+ background:linear-gradient(90deg,transparent, rgba(139,92,246,.55) 45%, rgba(88,166,255,.35) 60%, transparent); filter:blur(2px); }
2741
+ .sealbar.run { animation:sealSweep 1.15s cubic-bezier(.5,0,.3,1) both; }
2742
+ @keyframes sealSweep { 0%{ left:-8%; opacity:0;} 12%{opacity:1;} 86%{opacity:1;} 100%{ left:104%; opacity:0;} }
2743
+ @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; } }
2744
+ .tagline b { color:var(--fg); font-weight:600; }
2745
+ .tags { display:flex; gap:6px; margin-top:8px; flex-wrap:wrap; align-items:center; }
2746
+ .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%; }
2747
+ .tag.live { color:var(--ok); border-color:rgba(63,185,80,.45); background:rgba(63,185,80,.08); font-weight:600; }
2748
+ .tag.orange { color:var(--orange); border-color:rgba(255,140,43,.45); background:rgba(255,140,43,.08); }
2749
+ .tag.path { color:var(--dim); }
2750
+ .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; }
2751
+ .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; }
2752
+ @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);} }
2753
+ .controls { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
2754
+ .controls label { font-size:12px; color:var(--dim); display:flex; align-items:center; gap:4px; }
2755
+ 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; }
2756
+ button:hover,select:hover { border-color:var(--orange); color:var(--orange); }
2757
+ button:active { transform:translateY(1px); }
2758
+ .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; }
2759
+ .copybtn:hover { border-color:var(--orange); color:var(--orange); }
2760
+ .copybtn.done { color:var(--ok); border-color:var(--ok); }
2761
+ .copybtn.err { color:var(--bad); border-color:var(--bad); }
2762
+ .copybtn.ref { color:var(--orange); border-color:rgba(255,140,43,.4); }
2763
+ .copybtn.ref:hover { background:rgba(255,140,43,.08); }
2764
+ code.sentinel { color:var(--orange); background:rgba(255,140,43,.10); border:1px solid rgba(255,140,43,.22); font-weight:600; }
2765
+ .cmdname { color:var(--accent2); font-weight:600; }
2766
+ @media (max-width:620px){
2767
+ .topbar { flex-direction:column; align-items:stretch; }
2768
+ .controls { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
2769
+ .controls label { width:100%; }
2770
+ .controls label select { flex:1; width:100%; }
2771
+ .controls > button { width:100%; }
2772
+ }
2773
+ .grid { display:grid; gap:12px; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); margin-bottom:14px; }
2774
+ .two-col { display:grid; gap:12px; grid-template-columns:1fr 1fr; margin-bottom:4px; }
2775
+ @media (max-width:860px){ .two-col{ grid-template-columns:1fr; } }
2776
+ .card {
2777
+ background:linear-gradient(180deg,var(--card),var(--card2)); border:1px solid var(--line);
2778
+ border-radius:12px; padding:14px 16px; min-width:0; box-shadow:var(--shadow); position:relative; overflow:hidden;
2779
+ transition:transform .18s cubic-bezier(.2,.8,.2,1), border-color .18s, box-shadow .18s;
2780
+ }
2781
+ .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); }
2782
+ /* KPI cards get a soft accent glow that brightens on hover */
2783
+ .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; }
2784
+ .card.kpi:hover::before { opacity:1; }
2785
+ .card .label { font-size:10.5px; color:var(--dim); text-transform:uppercase; letter-spacing:.6px; display:flex; align-items:center; gap:5px; }
2786
+ .card .value { font-size:clamp(20px,3.5vw,26px); font-weight:700; margin-top:5px; overflow-wrap:anywhere; }
2787
+ .card .sub2 { font-size:11.5px; color:var(--dim); margin-top:3px; overflow-wrap:anywhere; }
2788
+ .trend { font-size:11px; font-weight:600; padding:1px 6px; border-radius:20px; margin-left:6px; }
2789
+ .trend.up { color:var(--ok); background:rgba(63,185,80,.12); }
2790
+ .trend.down { color:var(--bad); background:rgba(248,81,73,.12); }
2791
+ .trend.flat { color:var(--dim); background:rgba(139,148,158,.12); }
2792
+ .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; }
2793
+ .section-title .chev { transition:transform .15s; font-size:9px; color:var(--dim2); }
2794
+ .section-title.collapsed .chev { transform:rotate(-90deg); }
2795
+ .collapsed + * , .collapsed + * + * { display:none !important; }
2796
+ .scroll { background:var(--card); border:1px solid var(--line); border-radius:12px; overflow-x:auto; -webkit-overflow-scrolling:touch; box-shadow:var(--shadow); }
2797
+ table { width:100%; border-collapse:collapse; min-width:520px; }
2798
+ th,td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--line); font-size:13px; vertical-align:top; }
2799
+ th { color:var(--dim); font-weight:600; text-transform:uppercase; font-size:10.5px; letter-spacing:.5px; white-space:nowrap; cursor:pointer; }
2800
+ th:hover { color:var(--fg); }
2801
+ tr:last-child td { border-bottom:none; }
2802
+ tbody tr:hover { background:rgba(139,92,246,.05); }
2803
+ code { background:rgba(127,127,127,.12); padding:2px 5px; border-radius:4px; font-size:12px; overflow-wrap:anywhere; }
2804
+ .pill { display:inline-block; padding:2px 8px; border-radius:20px; font-size:11px; font-weight:600; }
2805
+ .pill-ok{background:rgba(63,185,80,.15);color:var(--ok);} .pill-bad{background:rgba(248,81,73,.15);color:var(--bad);}
2806
+ .pill-warn{background:rgba(210,153,34,.15);color:var(--warn);} .pill-info{background:rgba(88,166,255,.15);color:var(--info);}
2807
+ .pill-mock{background:rgba(139,92,246,.15);color:var(--accent);} .pill-real{background:rgba(63,185,80,.15);color:var(--ok);}
2808
+ .pill-dim{background:rgba(127,127,127,.12);color:var(--dim);}
2809
+ .empty { background:var(--card); border:1px dashed var(--line); border-radius:12px; padding:22px; text-align:center; color:var(--dim); }
2810
+ .alert { border-radius:12px; padding:12px 16px; margin-bottom:14px; border:1px solid; box-shadow:var(--shadow); }
2811
+ .alert .head { font-weight:700; margin-bottom:4px; }
2812
+ .alert.ok { background:rgba(63,185,80,.07); border-color:rgba(63,185,80,.3); color:var(--ok); }
2813
+ .footer { margin-top:24px; padding:14px 16px; background:var(--card); border:1px solid var(--line); border-radius:12px; font-size:12px; color:var(--dim); }
2814
+ .footer b { color:var(--fg); }
2815
+ /* donut + legend */
2816
+ .donut-wrap { display:flex; align-items:center; gap:18px; flex-wrap:wrap; }
2817
+ .legend { display:flex; flex-direction:column; gap:5px; font-size:12px; flex:1; min-width:120px; }
2818
+ .legend .li { display:flex; align-items:center; gap:7px; cursor:default; }
2819
+ .legend .dot { width:10px; height:10px; border-radius:3px; flex:none; }
2820
+ .legend .li .n { margin-left:auto; color:var(--dim); }
2821
+ /* segmented bar */
2822
+ .segbar { display:flex; height:22px; border-radius:7px; overflow:hidden; background:rgba(127,127,127,.1); }
2823
+ .segbar > div { transition:width .3s; }
2824
+ /* horizontal bars */
2825
+ .bar-row { display:flex; align-items:center; gap:8px; margin:7px 0; font-size:12px; }
2826
+ .bar-row .name { width:120px; color:var(--dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
2827
+ .bar-row .track { flex:1; background:rgba(127,127,127,.1); border-radius:5px; height:16px; overflow:hidden; }
2828
+ .bar-row .fill { height:100%; border-radius:5px; transition:width .3s; }
2829
+ .bar-row .n { width:58px; text-align:right; }
2830
+ /* sparkline */
2831
+ .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); }
2832
+ .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; }
2833
+ .spark > .b.zero { background:var(--line); min-height:1px; }
2834
+ .stat-row { display:flex; gap:14px; flex-wrap:wrap; font-size:12px; color:var(--dim); margin-top:7px; }
2835
+ .stat-row span { color:var(--fg); font-weight:600; }
2836
+ .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; }
2837
+ #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; }
2838
+ @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;} }
2839
+ </style>
2840
+ </head>
2841
+ <body>
2842
+ <div class="topbar">
2843
+ <div class="brandwrap">
2844
+ <img id="logo" class="logo-img" alt="Blindfold" />
2845
+ <div>
2846
+ <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>
2847
+ <div class="tagline"><span class="lock">\u{1F512}</span> Secrets sealed in a <b>TEE</b> \u2014 keys you can't leak</div>
2848
+ <div class="tags" id="tags"></div>
2849
+ </div>
2850
+ </div>
2851
+ <div class="controls">
2852
+ <label>range
2853
+ <select id="range" onchange="setRange()">
2854
+ <option value="15">15m</option><option value="60" selected>1h</option>
2855
+ <option value="1440">24h</option><option value="0">all</option>
2856
+ </select>
2857
+ </label>
2858
+ <label>refresh
2859
+ <select id="refresh" onchange="setRefresh()">
2860
+ <option value="2000">2s</option><option value="5000">5s</option>
2861
+ <option value="10000">10s</option><option value="0">paused</option>
2862
+ </select>
2863
+ </label>
2864
+ <button onclick="toggleTheme()" id="themeBtn" title="Toggle theme">\u{1F319}</button>
2865
+ <button onclick="exportJson()">\u2B07 Export</button>
2866
+ <button onclick="clearLog()">Clear</button>
2867
+ </div>
2868
+ </div>
2869
+
2870
+ <div id="alert-banner"></div>
2871
+
2872
+ <div class="grid" id="kpis"></div>
2873
+
2874
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Security posture
2875
+ <button style="margin-left:auto" onclick="event.stopPropagation();runFullAudit()">\u{1F50E} Run full audit (live)</button>
2876
+ </div>
2877
+ <div class="grid" id="posture-cards"></div>
2878
+ <div id="full-audit"></div>
2879
+
2880
+ <div class="two-col">
2881
+ <div>
2882
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Providers</div>
2883
+ <div class="card" id="provider-chart"></div>
2884
+ </div>
2885
+ <div>
2886
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Status codes</div>
2887
+ <div class="card" id="status-chart"></div>
2888
+ </div>
2889
+ </div>
2890
+
2891
+ <div class="two-col">
2892
+ <div>
2893
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>p95 latency trend</div>
2894
+ <div class="card" id="trend-latency"></div>
2895
+ </div>
2896
+ <div>
2897
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Success rate trend</div>
2898
+ <div class="card" id="trend-success"></div>
2899
+ </div>
2900
+ </div>
2901
+
2902
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Requests per minute</div>
2903
+ <div class="spark" id="spark"></div>
2904
+ <div class="stat-row" id="spark-stats"></div>
2905
+
2906
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>System</div>
2907
+ <div class="grid" id="status-cards"></div>
2908
+
2909
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Sealed keys (metadata only)</div>
2910
+ <div id="sealed-table-wrap"></div>
2911
+
2912
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Per-secret usage</div>
2913
+ <div id="per-secret-wrap"></div>
2914
+
2915
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Blindfold commands</div>
2916
+ <div id="commands-wrap"></div>
2917
+
2918
+ <div class="section-title" onclick="toggleSection(this)"><span class="chev">\u25BC</span>Recent activity
2919
+ <input id="filter" class="filter" placeholder="filter\u2026" oninput="renderTable(rangeFiltered())" onclick="event.stopPropagation()" />
2920
+ </div>
2921
+ <div id="table-wrap"></div>
2922
+
2923
+ <div class="footer"><b>Privacy by design.</b> Metadata only \u2014 no request/response bodies, no header values, no secret values. See <code>usage-log.ts</code> + <code>sealed-ledger.ts</code>.</div>
2924
+ <div id="tooltip"></div>
2925
+
2926
+ <script>
2927
+ /* Distinctive wordmark: each letter scrambles through cipher glyphs, then
2928
+ snap-seals into place \u2014 mirroring how a real key becomes __BLINDFOLD__.
2929
+ Purely decorative; respects prefers-reduced-motion. */
2930
+ (function sealWordmark(){
2931
+ try {
2932
+ if (window.matchMedia && window.matchMedia('(prefers-reduced-motion:reduce)').matches) return;
2933
+ var letters = Array.prototype.slice.call(document.querySelectorAll('.brand > .letter'));
2934
+ if (!letters.length) return;
2935
+ var GLYPHS = '\u2593\u2588\u2592\u2591#@$%&*/\\<>=+\xB7:';
2936
+ var finals = letters.map(function(el){ return el.textContent; });
2937
+ letters.forEach(function(el){ el.classList.add('scrambling'); });
2938
+ var bar = document.getElementById('brand-sealbar');
2939
+ if (bar) { bar.classList.add('run'); }
2940
+ var start = performance.now();
2941
+ var settleAt = letters.map(function(_, i){ return 260 + i * 95; }); // staggered lock, left\u2192right
2942
+ function frame(now){
2943
+ var t = now - start, done = 0;
2944
+ letters.forEach(function(el, i){
2945
+ if (el.dataset.sealed) { done++; return; }
2946
+ if (t >= settleAt[i]) {
2947
+ el.textContent = finals[i];
2948
+ el.classList.remove('scrambling');
2949
+ el.classList.add('sealed');
2950
+ el.dataset.sealed = '1';
2951
+ done++;
2952
+ } else if (t % 60 < 20) {
2953
+ el.textContent = GLYPHS[(Math.random() * GLYPHS.length) | 0];
2954
+ }
2955
+ });
2956
+ if (done < letters.length) requestAnimationFrame(frame);
2957
+ }
2958
+ requestAnimationFrame(frame);
2959
+ } catch (e) { /* animation must never break the dashboard */ }
2960
+ })();
2961
+ var PALETTE=['#8b5cf6','#58a6ff','#3fb950','#d29922','#f85149','#ff7b72','#39c5cf','#f778ba'];
2962
+ 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;}
2963
+ function colorFor(name){return PALETTE[hashIdx(String(name||'?'))];}
2964
+ var TOK=new URLSearchParams(location.search).get('token');
2965
+ function api(p){return TOK?p+(p.indexOf('?')>=0?'&':'?')+'token='+encodeURIComponent(TOK):p;}
2966
+ function esc(s){return String(s).replace(/[&<>"']/g,function(c){return({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c];});}
2967
+ 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';}
2968
+ 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>';}
2969
+ function pillMode(m){return'<span class="pill pill-'+m+'">'+m+'</span>';}
2970
+ 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))];}
2971
+
2972
+ // shared tooltip
2973
+ var TIP=null;
2974
+ 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';}
2975
+ function tipHide(){if(TIP)TIP.style.opacity='0';}
2976
+
2977
+ // state
2978
+ window._events=[]; window._sort={key:'t',dir:-1};
2979
+ function rangeMin(){return Number(document.getElementById('range').value);}
2980
+ 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;});}
2981
+
2982
+ 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?'\u2600':'\u{1F319}';try{localStorage.setItem('bf-theme',d?'light':'dark');}catch(e){}}
2983
+ (function(){try{var t=localStorage.getItem('bf-theme');if(t){document.documentElement.setAttribute('data-theme',t);document.getElementById('themeBtn').textContent=t==='light'?'\u2600':'\u{1F319}';}}catch(e){}})();
2984
+ function toggleSection(el){el.classList.toggle('collapsed');}
2985
+ function setRange(){poll();}
2986
+ function setRefresh(){var v=Number(document.getElementById('refresh').value);if(window._timer)clearInterval(window._timer);if(v>0)window._timer=setInterval(poll,v);}
2987
+ 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();}
2988
+ async function clearLog(){await fetch(api('/api/clear'),{method:'POST'});poll();}
2989
+
2990
+ async function poll(){
2991
+ try{
2992
+ 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();})]);
2993
+ var statusR=r[0],sealedR=r[1],eventsR=r[2],auditR=r[3];
2994
+ window._events=eventsR.events||[];
2995
+ var ev=rangeFiltered();
2996
+ document.getElementById('tags').innerHTML=
2997
+ '<span class="tag live"><span class="live-dot"></span>live</span>'
2998
+ +'<span class="tag orange">'+new Date().toLocaleTimeString()+'</span>'
2999
+ +'<span class="tag">'+(statusR.mode==='real'?'REAL \xB7 '+esc(statusR.t3_env||''):'MOCK')+'</span>'
3000
+ +'<span class="tag path">usage <code title="'+esc(eventsR.source)+'">'+esc(eventsR.source)+'</code></span>';
3001
+ renderAlert(auditR,ev);
3002
+ renderKpis(ev,auditR,statusR);
3003
+ renderPosture(auditR,statusR);
3004
+ renderStatus(statusR);
3005
+ renderDonut(ev);
3006
+ renderStatusBar(ev);
3007
+ renderTrends(ev);
3008
+ renderSpark(ev);
3009
+ renderSealed(sealedR.entries||[]);
3010
+ renderPerSecret(ev);
3011
+ renderTable(ev);
3012
+ }catch(e){}
3013
+ }
3014
+
3015
+ function trendBadge(cur,prev){if(prev===0&&cur===0)return'<span class="trend flat">\u2014</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?'\u25B2':'\u25BC')+Math.abs(d)+'%</span>';}
3016
+
3017
+ function renderKpis(ev,a,s){
3018
+ var half=ev.slice().sort(function(x,y){return new Date(x.t)-new Date(y.t);});
3019
+ var mid=Math.floor(half.length/2);var prevHalf=half.slice(0,mid),curHalf=half.slice(mid);
3020
+ function ok(arr){return arr.filter(function(e){return e.status>=200&&e.status<300;}).length;}
3021
+ var total=ev.length;
3022
+ var succ=total?Math.round(ok(ev)/total*100):0;
3023
+ var prevSucc=prevHalf.length?Math.round(ok(prevHalf)/prevHalf.length*100):0;
3024
+ var curSucc=curHalf.length?Math.round(ok(curHalf)/curHalf.length*100):0;
3025
+ var p95=pct(ev.map(function(e){return e.latency_ms||0;}),95);
3026
+ var p95p=pct(prevHalf.map(function(e){return e.latency_ms||0;}),95);
3027
+ var p95c=pct(curHalf.map(function(e){return e.latency_ms||0;}),95);
3028
+ var sealed=(a&&a.sealed_count)||0;
3029
+ var posture=postureScore(a,s).score;
3030
+ var cards=[
3031
+ ['Requests',total,'',trendBadge(curHalf.length,prevHalf.length),'var(--c1)'],
3032
+ ['Success rate',succ+'%',ok(ev)+'/'+total+' 2xx',trendBadge(curSucc,prevSucc),succ>=95?'var(--ok)':succ>=80?'var(--warn)':'var(--bad)'],
3033
+ ['p95 latency',p95+' ms',ev.length+' samples',trendBadge(p95c,p95p),'var(--c2)'],
3034
+ ['Sealed secrets',sealed,'in the enclave','','var(--c7)'],
3035
+ ['Posture',posture+'<span style="font-size:13px;color:var(--dim)">/100</span>','security score','',posture>=90?'var(--ok)':posture>=60?'var(--warn)':'var(--bad)']
3036
+ ];
3037
+ 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('');
3038
+ }
3039
+
3040
+ 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};}
3041
+
3042
+ function renderAlert(a,ev){
3043
+ var msgs=[];var chain=(a&&a.ledger_chain)||{};
3044
+ if(chain.total>0&&!chain.ok)msgs.push(['bad','\u{1F534} Ledger TAMPERED \u2014 a sealed-keys line was edited or removed']);
3045
+ if(((a&&a.exposed_in_env)||[]).length>0)msgs.push(['warn','\u{1F7E0} '+a.exposed_in_env.length+' sealed key(s) still in .env \u2014 delete them']);
3046
+ var recent=(ev||[]).filter(function(e){return Date.now()-new Date(e.t).getTime()<300000;});
3047
+ var errs=recent.filter(function(e){return(e.status||0)>=400;}).length;
3048
+ if(recent.length>=5&&errs/recent.length>0.3)msgs.push(['warn','\u{1F7E0} '+Math.round(errs/recent.length*100)+'% errors in the last 5 min ('+errs+'/'+recent.length+')']);
3049
+ var el=document.getElementById('alert-banner');
3050
+ if(!msgs.length){el.innerHTML='';return;}
3051
+ var bad=msgs.some(function(m){return m[0]==='bad';});
3052
+ 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">\u26A0 Attention</div>'+msgs.map(function(m){return esc(m[1]);}).join('<br/>')+'</div>';
3053
+ }
3054
+
3055
+ function renderPosture(a,s){
3056
+ 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};
3057
+ var chainTxt=chain.total===0?'empty':(chain.ok?'intact':'TAMPERED');
3058
+ var cards=[
3059
+ ['.env leak surface',exposed===0?'0 \u2705':exposed+' \u26A0','sealed keys still in .env',exposed===0?'var(--ok)':'var(--warn)'],
3060
+ ['Ledger integrity',(chainTxt==='TAMPERED'?'\u2716 ':chainTxt==='intact'?'\u2705 ':'')+chainTxt,(chain.legacy||0)+' legacy entries',chainTxt==='TAMPERED'?'var(--bad)':'var(--ok)'],
3061
+ ['Mode',s&&s.mode==='real'?'REAL':'MOCK',s&&s.mode==='real'?'connected to '+esc(s.t3_env||''):'BLINDFOLD_MOCK=1','var(--c1)'],
3062
+ ['Issues',p.notes.length,p.notes.length?esc(p.notes.join(' \xB7 ')):'all clear',p.notes.length?'var(--warn)':'var(--ok)']
3063
+ ];
3064
+ 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('');
3065
+ }
3066
+
3067
+ function renderStatus(s){
3068
+ var cards=[
3069
+ [s.mode==='real'?'REAL':'MOCK','Mode',s.mode==='real'?'T3 '+esc(s.t3_env):'mock'],
3070
+ [esc(s.contract_version||'\u2014'),'Contract','blindfold-proxy'],
3071
+ [esc(s.tenant_did_short||'(none)'),'Tenant',s.tenant_did?'full in title':'set creds'],
3072
+ [s.proxy_port,'Proxy port','127.0.0.1:'+s.proxy_port],
3073
+ [s.sdk_installed?'\u2713':'\u2717','SDK',s.sdk_installed?'installed':'missing']
3074
+ ];
3075
+ 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('');
3076
+ }
3077
+
3078
+ function donutSvg(rows,total){
3079
+ var r=42,c=2*Math.PI*r,off=0,seg='';
3080
+ 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;});
3081
+ 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>';
3082
+ }
3083
+ function renderDonut(ev){
3084
+ var by={};ev.forEach(function(e){var k=e.provider||'(unknown)';by[k]=(by[k]||0)+1;});
3085
+ var rows=Object.keys(by).map(function(k){return[k,by[k]];}).sort(function(a,b){return b[1]-a[1];});
3086
+ var el=document.getElementById('provider-chart');
3087
+ if(!rows.length){el.innerHTML='<div style="color:var(--dim);font-size:13px">No traffic yet.</div>';return;}
3088
+ 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('');
3089
+ el.innerHTML='<div class="donut-wrap">'+donutSvg(rows,ev.length)+'<div class="legend">'+legend+'</div></div>';
3090
+ }
3091
+
3092
+ function renderStatusBar(ev){
3093
+ 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]++;});
3094
+ var col={'2xx':'var(--ok)','3xx':'var(--info)','4xx':'var(--warn)','5xx':'var(--bad)'};
3095
+ var total=ev.length;var el=document.getElementById('status-chart');
3096
+ if(!total){el.innerHTML='<div style="color:var(--dim);font-size:13px">No traffic yet.</div>';return;}
3097
+ 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('');
3098
+ 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('');
3099
+ el.innerHTML='<div class="segbar">'+segs+'</div><div class="legend" style="flex-direction:row;flex-wrap:wrap;gap:14px;margin-top:12px">'+legend+'</div>';
3100
+ }
3101
+
3102
+ function areaSvg(values,color,maxOpt){
3103
+ var w=300,h=70,n=values.length;if(n<2)values=values.concat(values);n=values.length;
3104
+ var max=Math.max(maxOpt||0,Math.max.apply(null,values),1);
3105
+ var pts=values.map(function(v,i){return (i/(n-1)*w).toFixed(1)+','+(h-(v/max)*h).toFixed(1);});
3106
+ var gid='g'+Math.floor(Math.random()*1e6);
3107
+ var poly=pts.join(' ');var area='0,'+h+' '+poly+' '+w+','+h;
3108
+ 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>';
3109
+ }
3110
+ function renderTrends(ev){
3111
+ var now=Date.now();var lat=[],ok=[],tot=[];for(var i=0;i<60;i++){lat.push([]);ok.push(0);tot.push(0);}
3112
+ 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]++;}});
3113
+ var p95=lat.map(function(a){return a.length?pct(a,95):0;});
3114
+ var succ=tot.map(function(t,i){return t?Math.round(ok[i]/t*100):0;});
3115
+ 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>';
3116
+ document.getElementById('trend-success').innerHTML=areaSvg(succ,'var(--ok)',100)+'<div class="stat-row">latest <span>'+(succ[59]||0)+'%</span></div>';
3117
+ }
3118
+
3119
+ function renderSpark(ev){
3120
+ var now=Date.now();var b=[];for(var i=0;i<60;i++)b.push(0);
3121
+ ev.forEach(function(e){var ageMin=Math.floor((now-new Date(e.t).getTime())/60000);if(ageMin>=0&&ageMin<60)b[59-ageMin]++;});
3122
+ var max=Math.max.apply(null,b.concat(1));
3123
+ 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 \xB7 '+(59-i)+'m ago"></div>';}).join('');
3124
+ var sum=b.reduce(function(a,c){return a+c;},0);
3125
+ document.getElementById('spark-stats').innerHTML='last 60 min <span>'+sum+'</span> \xB7 peak minute <span>'+max+'</span>';
3126
+ }
3127
+
3128
+ function renderSealed(entries){
3129
+ if(!entries.length){document.getElementById('sealed-table-wrap').innerHTML='<div class="empty">No keys sealed yet. <code>blindfold register --name &lt;K&gt;</code></div>';return;}
3130
+ var latest={};entries.forEach(function(e){latest[e.name]=e;});
3131
+ 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">\u29C9 cmd</button> <button class="copybtn ref" data-name="'+esc(e.name)+'" title="Copy the Blindfold sentinel \u2014 put this where the real key would go; the enclave substitutes the sealed value at call time">\u29C9 sealed token</button></td></tr>';}).join('');
3132
+ 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>';
3133
+ 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);}
3134
+ var cmds=document.querySelectorAll('#sealed-table-wrap .copybtn.cmd');
3135
+ 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,'\u2713 copied');};})(cmds[i]);}
3136
+ var refs=document.querySelectorAll('#sealed-table-wrap .copybtn.ref');
3137
+ for(var j=0;j<refs.length;j++){(function(b){b.onclick=function(){try{navigator.clipboard.writeText('__BLINDFOLD__');}catch(e){}flash(b,'\u2713 sentinel copied');};})(refs[j]);}
3138
+ }
3139
+
3140
+ function renderPerSecret(ev){
3141
+ var by={};ev.forEach(function(e){var k=e.secret_key||'(unknown)';by[k]=(by[k]||0)+1;});
3142
+ var rows=Object.keys(by).map(function(k){return[k,by[k]];}).sort(function(a,b){return b[1]-a[1];});
3143
+ var el=document.getElementById('per-secret-wrap');
3144
+ if(!rows.length){el.innerHTML='<div class="empty">No secret usage yet. Try <code>blindfold use --name &lt;X&gt; -- &lt;cmd&gt;</code> or the proxy.</div>';return;}
3145
+ var max=Math.max.apply(null,rows.map(function(r){return r[1];}).concat(1));
3146
+ 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>';
3147
+ }
3148
+
3149
+ function sortBy(k){if(window._sort.key===k)window._sort.dir*=-1;else window._sort={key:k,dir:-1};renderTable(rangeFiltered());}
3150
+ function renderTable(ev){
3151
+ var q=(document.getElementById('filter').value||'').toLowerCase().trim();
3152
+ var list=ev;
3153
+ 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;});
3154
+ var k=window._sort.key,dir=window._sort.dir;
3155
+ 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);
3156
+ 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;}
3157
+ 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('');
3158
+ 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>';
3159
+ 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]);}
3160
+ }
3161
+
3162
+ async function runFullAudit(){
3163
+ var el=document.getElementById('full-audit');el.innerHTML='<div class="empty">Running live enclave reconciliation\u2026</div>';
3164
+ try{
3165
+ var r=await fetch(api('/api/audit/full')).then(function(x){return x.json();});
3166
+ if(r.mock){el.innerHTML='<div class="alert ok"><div class="head">MOCK mode</div>Reconciliation only runs in REAL mode.</div>';return;}
3167
+ 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;}
3168
+ 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('');
3169
+ var okN=(r.results||[]).filter(function(x){return x.ok;}).length;
3170
+ 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>';
3171
+ }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>';}
3172
+ }
3173
+
3174
+ function startStream(){try{var es=new EventSource(api('/api/stream'));es.addEventListener('change',function(){poll();});es.onerror=function(){};}catch(e){}}
3175
+
3176
+ function renderCommands(){
3177
+ var C=[
3178
+ ['doctor','doctor','Check your key + tenant are healthy'],
3179
+ ['status','status','Mode, tenant, and every sealed secret'],
3180
+ ['audit','audit','Verify the ledger + reconcile against the enclave'],
3181
+ ['migrate','migrate','Seal every secret in .env in one shot'],
3182
+ ['register','register --name NAME --from-env NAME','Seal a secret into the enclave'],
3183
+ ['use','use --name NAME -- COMMAND','Run any tool with a sealed secret injected'],
3184
+ ['use --check','use --name NAME --check','Confirm a sealed secret is usable'],
3185
+ ['rotate','rotate --name NAME --from-env NAME','Replace a secret value (rollback-safe)'],
3186
+ ['rollback','rollback --name NAME','Restore the previous value'],
3187
+ ['versions','versions --name NAME','List rollback snapshots'],
3188
+ ['grant','grant --host api.openai.com','Authorize the contract to call a host'],
3189
+ ['share','share --to DID --host HOST','Let a teammate agent use your keys'],
3190
+ ['revoke','revoke --to DID','Remove a teammate access'],
3191
+ ['proxy','proxy','Run the local OpenAI/Anthropic proxy'],
3192
+ ['publish','publish','Publish the contract to T3'],
3193
+ ['sealed','sealed','List sealed keys (metadata only)'],
3194
+ ['dashboard','dashboard','Launch this dashboard'],
3195
+ ['export','export --name NAME','CI: inject a sealed secret into the job env']
3196
+ ];
3197
+ 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])+'">\u29C9 copy</button></td></tr>';}).join('');
3198
+ 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>';
3199
+ var btns=document.querySelectorAll('#commands-wrap .copybtn');
3200
+ 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='\u2713 copied';b.classList.add('done');setTimeout(function(){b.textContent=o;b.classList.remove('done');},1300);};})(btns[i]);}
3201
+ }
3202
+
3203
+ document.getElementById('logo').src=api('/logo.png');
3204
+ renderCommands();
3205
+ poll();setRefresh();startStream();
3206
+ </script>
3207
+ </body>
3208
+ </html>
3209
+ `;
3210
+
3211
+ // bin/cmd-serve.ts
3212
+ init_usage_log();
3213
+ async function handleServe(cmd, argv, cmdArgs) {
3214
+ switch (cmd) {
3215
+ case "proxy": {
3216
+ const port = argv.flags.port ? Number(argv.flags.port) : void 0;
3217
+ const secret = argv.flags.secret ? String(argv.flags.secret) : void 0;
3218
+ let token = argv.flags.token ? String(argv.flags.token) : void 0;
3219
+ if (!token && argv.flags.auth) token = randomBytes3(24).toString("hex");
3220
+ let socket;
3221
+ if (argv.flags.socket !== void 0) {
3222
+ socket = typeof argv.flags.socket === "string" && argv.flags.socket.length > 0 ? String(argv.flags.socket) : path9.join(stateDir(), "proxy.sock");
3223
+ }
3224
+ const proxyGate = await attestationGate({ skip: !!argv.flags["no-attest"] });
3225
+ if (proxyGate.warning) console.error(`\u26A0\uFE0F ${proxyGate.warning}`);
3226
+ if (proxyGate.enforced && !proxyGate.ok) {
3227
+ die(`attestation gate: ${proxyGate.message}. Refusing to start the proxy against an unverified enclave. (bypass: --no-attest, or clear the pin)`);
3228
+ }
3229
+ if (proxyGate.enforced) console.log("\u2713 enclave attestation verified");
3230
+ if (socket && !token) {
3231
+ console.error("\u26A0\uFE0F --socket without --auth: access is gated only by the socket's 0600 owner permission. Add --auth for a per-process token too.");
3232
+ }
3233
+ const handle2 = await startProxy({ port, secretKey: secret, token, socket });
3234
+ console.log(`\u2713 Blindfold proxy listening at ${handle2.url}`);
3235
+ if (handle2.socket) {
3236
+ console.log(` Unix socket (0600) \u2014 only your user's processes can connect.`);
3237
+ console.log(` Call it with: curl --unix-socket ${handle2.socket} http://localhost/v1/...`);
3238
+ } else {
3239
+ console.log(` Point your agent at: OPENAI_BASE_URL=${handle2.url}/v1`);
3240
+ }
3241
+ if (handle2.token) {
3242
+ console.error(` Auth ON \u2014 every request must send header:`);
3243
+ console.error(` x-blindfold-token: ${handle2.token}`);
3244
+ console.error(` Give it to your agent via BLINDFOLD_PROXY_TOKEN or wrap({ token }).`);
3245
+ console.error(` NOTE: env vars and argv are readable by same-user processes \u2014 on a shared`);
3246
+ console.error(` machine prefer --socket (OS-enforced) over the token alone.`);
3247
+ if (argv.flags.token) {
3248
+ console.error(` \u26A0\uFE0F --token on the command line is visible in \`ps\`/proc; prefer --auth (mints one) or BLINDFOLD_PROXY_TOKEN.`);
3249
+ }
3250
+ } else {
3251
+ console.log(` Auth OFF \u2014 add --auth to require a per-session token (recommended on shared machines).`);
3252
+ }
3253
+ console.log(` Health check: ${handle2.url}/health`);
3254
+ process.on("SIGINT", async () => {
3255
+ await handle2.close();
3256
+ process.exit(0);
3257
+ });
3258
+ return;
3259
+ }
3260
+ case "attest": {
3261
+ const expectRtmr3 = argv.flags["expect-rtmr3"] ? String(argv.flags["expect-rtmr3"]) : void 0;
3262
+ const r = await attest({ expectRtmr3, noCache: true });
3263
+ if (argv.flags.json) {
3264
+ console.log(JSON.stringify(r, null, 2));
3265
+ return;
3266
+ }
3267
+ if (!r.available) {
3268
+ console.log(`\u2139\uFE0F ${r.nodeUrl} publishes no attestation (mock signer or still bootstrapping).`);
3269
+ return;
3270
+ }
3271
+ console.log(`Attestation for ${r.nodeUrl}`);
3272
+ console.log(` chain to Intel root: ${r.valid ? "\u2705 valid" : "\u2716 INVALID"}${r.error ? ` (${r.error})` : ""}`);
3273
+ console.log(` quotes verified: ${r.validCount}/${r.expectedCount}`);
3274
+ for (const m of r.rtmr3s) console.log(` RTMR3 (code measure): ${m}`);
3275
+ if (expectRtmr3) {
3276
+ console.log(` RTMR3 pin: ${r.pinned ? "\u2705 matches expected" : "\u2716 DID NOT MATCH"}`);
3277
+ } else if (r.rtmr3s.length) {
3278
+ console.log(` Tip: pin it next time \u2192 blindfold attest --expect-rtmr3 ${r.rtmr3s[0]}`);
3279
+ }
3280
+ if (argv.flags.pin) {
3281
+ const toPin = expectRtmr3 ?? r.rtmr3s[0];
3282
+ if (r.valid && toPin) {
3283
+ if (!expectRtmr3) {
3284
+ console.error(` \u26A0\uFE0F Pinning the measurement the node just reported (trust-on-first-use).`);
3285
+ console.error(` Cross-check ${toPin} against your enclave's published/reproducible build hash;`);
3286
+ console.error(` if this first contact was with a malicious node, you'd be pinning its measurement.`);
3287
+ }
3288
+ writePinnedRtmr3(toPin);
3289
+ console.log(` \u2713 pinned \u2014 \`seal\` and \`proxy\` will now verify this measurement first.`);
3290
+ } else {
3291
+ console.log(` \u2717 not pinning: attestation must pass first.`);
3292
+ }
3293
+ }
3294
+ if (!r.valid || expectRtmr3 && !r.pinned) process.exitCode = 1;
3295
+ return;
3296
+ }
3297
+ case "dashboard": {
3298
+ const port = argv.flags.port ? Number(argv.flags.port) : void 0;
3299
+ const handle2 = await startDashboard({ port });
3300
+ console.log(`\u2713 Blindfold dashboard at ${handle2.url}`);
3301
+ console.log(` Reading: ${defaultLogPath()}`);
3302
+ console.log(` (open in your browser; auto-refreshes every 2s)`);
3303
+ process.on("SIGINT", async () => {
3304
+ await handle2.close();
3305
+ process.exit(0);
3306
+ });
3307
+ return;
3308
+ }
3309
+ case "stats": {
3310
+ const events = readUsage();
3311
+ if (events.length === 0) {
3312
+ console.log("No usage recorded yet. Reads from " + defaultLogPath());
3313
+ return;
3314
+ }
3315
+ const byProvider = {};
3316
+ let ok3 = 0, bad2 = 0, totalLat = 0, sentinel = 0;
3317
+ for (const e of events) {
3318
+ byProvider[e.provider] = (byProvider[e.provider] ?? 0) + 1;
3319
+ if (e.status >= 200 && e.status < 300) ok3++;
3320
+ else if (e.status >= 400) bad2++;
3321
+ totalLat += e.latency_ms;
3322
+ if (e.sentinel_in_outbound) sentinel++;
3323
+ }
3324
+ const total = events.length;
3325
+ console.log("Blindfold usage stats (source: " + defaultLogPath() + ")");
3326
+ console.log(` Total requests: ${total}`);
3327
+ console.log(` 2xx / 4xx+: ${ok3} / ${bad2}`);
3328
+ console.log(` Sentinel substituted: ${sentinel}/${total} (should equal total)`);
3329
+ console.log(` Avg latency: ${total ? Math.round(totalLat / total) : 0} ms`);
3330
+ console.log(` By provider: ${Object.entries(byProvider).map(([k, v]) => `${k}\xD7${v}`).join(" ")}`);
3331
+ console.log(` Recent (last 5):`);
3332
+ for (const e of events.slice(-5)) {
3333
+ console.log(` ${e.t} ${e.method.padEnd(6)} ${e.path} \u2192 ${e.status} (${e.latency_ms}ms, ${e.provider}, ${e.mode})`);
3334
+ }
3335
+ return;
3336
+ }
3337
+ case "stats:clear": {
3338
+ clearUsage();
3339
+ console.log("\u2713 Cleared " + defaultLogPath());
3340
+ return;
3341
+ }
3342
+ }
3343
+ }
3344
+
3345
+ // bin/cmd-enclave.ts
3346
+ init_env();
3347
+ init_register();
3348
+ import fs14 from "node:fs";
3349
+ import path12 from "node:path";
3350
+
3351
+ // src/init.ts
3352
+ init_env();
3353
+ init_sealed_ledger();
3354
+ init_t3_client();
3355
+ import { spawn as spawn2 } from "node:child_process";
3356
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3357
+ import path10 from "node:path";
3358
+ import readline from "node:readline/promises";
3359
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
3360
+ var HERE3 = path10.dirname(fileURLToPath4(import.meta.url));
3361
+ var REPO_ROOT2 = path10.resolve(HERE3, "..", "..", "..");
3362
+ var ENV_PATH = path10.join(REPO_ROOT2, ".env");
3363
+ var ENV_EXAMPLE_PATH = path10.join(REPO_ROOT2, ".env.example");
3364
+ var WASM_PATH = path10.join(REPO_ROOT2, "contract", "target", "wasm32-wasip2", "release", "blindfold_proxy.wasm");
3365
+ var T3_CLAIM_URL = "https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens";
3366
+ var colour = process.stdout.isTTY ? (c2, s) => `\x1B[${c2}m${s}\x1B[0m` : (_, s) => s;
3367
+ var bold = (s) => colour("1", s);
3368
+ var green = (s) => colour("32", s);
3369
+ var yellow = (s) => colour("33", s);
3370
+ var red = (s) => colour("31", s);
3371
+ var dim = (s) => colour("2", s);
3372
+ var cyan = (s) => colour("36", s);
3373
+ function header(n, total, title) {
3374
+ process.stdout.write(`
3375
+ ${dim(`[${n}/${total}]`)} ${bold(title)}
3376
+ `);
3377
+ }
3378
+ function ok2(line) {
3379
+ process.stdout.write(` ${green("\u2713")} ${line}
3380
+ `);
3381
+ }
3382
+ function info(line) {
3383
+ process.stdout.write(` ${dim("\xB7")} ${line}
3384
+ `);
3385
+ }
3386
+ function warn2(line) {
3387
+ process.stdout.write(` ${yellow("!")} ${line}
3388
+ `);
3389
+ }
3390
+ function fail(line, fixHint) {
3391
+ process.stdout.write(` ${red("\u2716")} ${line}
3392
+ `);
3393
+ if (fixHint) {
3394
+ for (const ln of fixHint.split("\n")) process.stdout.write(` ${dim("\u2192")} ${cyan(ln)}
3395
+ `);
3396
+ }
3397
+ }
3398
+ function run(cmd, args, opts = {}) {
3399
+ return new Promise((resolve) => {
3400
+ const child = spawn2(cmd, args, {
3401
+ cwd: opts.cwd ?? REPO_ROOT2,
3402
+ env: { ...process.env, ...opts.env ?? {} },
3403
+ stdio: ["ignore", "pipe", "pipe"]
3404
+ });
3405
+ const o = [];
3406
+ const e = [];
3407
+ child.stdout.on("data", (c2) => o.push(c2));
3408
+ child.stderr.on("data", (c2) => e.push(c2));
3409
+ child.on("close", (code) => resolve({ code: code ?? -1, out: Buffer.concat(o).toString("utf8"), err: Buffer.concat(e).toString("utf8") }));
3410
+ child.on("error", (err) => resolve({ code: -1, out: "", err: err.message }));
3411
+ });
3412
+ }
3413
+ async function ask(prompt) {
3414
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3415
+ try {
3416
+ return (await rl.question(prompt)).trim();
3417
+ } finally {
3418
+ rl.close();
3419
+ }
3420
+ }
3421
+ async function runInit(opts = {}) {
3422
+ const total = 5;
3423
+ process.stdout.write(`
3424
+ ${bold("\u{1F6E1}\uFE0F Blindfold \u2014 first-time setup")}
3425
+ ${dim("This wizard sets up REAL T3 mode end-to-end. Secrets are read from your .env, never typed onto the command line.")}
3426
+ `);
3427
+ header(1, total, "Preflight");
3428
+ await ensureEnvOrPrompt(opts);
3429
+ loadEnvFromFile(ENV_PATH);
3430
+ const env = loadBlindfoldEnv();
3431
+ if (!env.t3nApiKey || !env.did) {
3432
+ fail("Still missing T3N_API_KEY and/or DID after .env walkthrough.", "Open .env, paste the two values, and re-run `npm run setup`.");
3433
+ throw new Error("preflight failed");
3434
+ }
3435
+ ok2(`T3 ${env.t3Env} \xB7 tenant ${dim(env.did)}`);
3436
+ const haveSdk = await isSdkInstalled2();
3437
+ if (!haveSdk) {
3438
+ fail("@terminal3/t3n-sdk is not installed.", "Run: npm install @terminal3/t3n-sdk");
3439
+ throw new Error("sdk missing");
3440
+ }
3441
+ ok2("@terminal3/t3n-sdk present");
3442
+ let canBuild = !opts.skipBuild;
3443
+ if (canBuild) {
3444
+ const cargo = await run("which", ["cargo"]);
3445
+ if (cargo.code !== 0) {
3446
+ warn2("cargo (Rust toolchain) not found \u2014 auto-skipping contract build.");
3447
+ info(`Install rust at https://rustup.rs and re-run \`blindfold init\` to build the contract locally.`);
3448
+ canBuild = false;
3449
+ } else {
3450
+ const targets = await run("rustup", ["target", "list", "--installed"]);
3451
+ if (!targets.out.includes("wasm32-wasip2")) {
3452
+ info("Installing wasm32-wasip2 target \u2026");
3453
+ const add = await run("rustup", ["target", "add", "wasm32-wasip2"]);
3454
+ if (add.code !== 0) {
3455
+ warn2("Could not install wasm32-wasip2 automatically; skipping contract build.");
3456
+ info("Run manually: rustup target add wasm32-wasip2");
3457
+ canBuild = false;
3458
+ }
3459
+ }
3460
+ if (canBuild) ok2("Rust toolchain + wasm32-wasip2 target ready");
3461
+ }
3462
+ }
3463
+ if (canBuild) {
3464
+ header(2, total, "Build contract (Rust \u2192 WASM)");
3465
+ info("cargo build --target wasm32-wasip2 --release");
3466
+ const build = await run("cargo", ["build", "--target", "wasm32-wasip2", "--release"], {
3467
+ cwd: path10.join(REPO_ROOT2, "contract")
3468
+ });
3469
+ if (build.code !== 0) {
3470
+ fail("Contract build failed.", `Inspect with: cd contract && cargo build --target wasm32-wasip2 --release
3471
+ stderr tail:
3472
+ ${build.err.slice(-800)}`);
3473
+ throw new Error("contract build failed");
3474
+ }
3475
+ if (!existsSync(WASM_PATH)) {
3476
+ fail(`Build succeeded but artifact missing at ${WASM_PATH}.`, "Did the package name change?");
3477
+ throw new Error("artifact missing");
3478
+ }
3479
+ const wasmBytes = readFileSync(WASM_PATH);
3480
+ ok2(`Built ${WASM_PATH} (${wasmBytes.byteLength.toLocaleString()} bytes)`);
3481
+ } else {
3482
+ header(2, total, "Build contract (skipped)");
3483
+ }
3484
+ header(3, total, "Authenticate to T3");
3485
+ let t3;
3486
+ try {
3487
+ t3 = await openT3Client(env);
3488
+ if (!t3.isReal) throw new Error("Mock client returned \u2014 env misconfigured.");
3489
+ ok2("Handshake + authenticate succeeded \u2728");
3490
+ } catch (e) {
3491
+ fail("Could not connect to T3.", `Error: ${e.message}
3492
+ Check T3N_API_KEY + DID match a real T3 account (and BLINDFOLD_T3_ENV matches their environment).`);
3493
+ throw e;
3494
+ }
3495
+ await ensureTenantScaffolding(env);
3496
+ if (!opts.skipPublish && canBuild) {
3497
+ header(4, total, "Publish the wrapper contract + grant ACLs");
3498
+ try {
3499
+ const wasm = readFileSync(WASM_PATH);
3500
+ const r = await t3.registerContract(new Uint8Array(wasm.buffer, wasm.byteOffset, wasm.byteLength));
3501
+ const contractId = Number(r.contractId);
3502
+ ok2(`Published "blindfold-proxy" \xB7 contract_id=${contractId}`);
3503
+ await grantContractReads(env, contractId);
3504
+ ok2(`Granted read access on z:tid:secrets to contract ${contractId}`);
3505
+ } catch (e) {
3506
+ const msg = e.message;
3507
+ if (/version.*not higher|already.*registered/i.test(msg)) {
3508
+ warn2(`Already published at this version \u2014 skipping. (${msg.slice(0, 100)})`);
3509
+ } else {
3510
+ fail("Publish failed.", `Error: ${msg.slice(0, 300)}`);
3511
+ throw e;
3512
+ }
3513
+ }
3514
+ } else {
3515
+ header(4, total, opts.skipPublish ? "Publish (skipped: --skip-publish)" : "Publish (skipped: no contract built)");
3516
+ }
3517
+ header(5, total, "Seal a secret into the enclave");
3518
+ const toSeed = collectSeedPlan(opts);
3519
+ if (toSeed.length === 0) {
3520
+ info("No --seed flag given.");
3521
+ info(`Later, after dropping an OPENAI_API_KEY into .env: ${cyan("blindfold register --name openai_api_key --from-env OPENAI_API_KEY")}`);
3522
+ }
3523
+ for (const { name, fromEnv } of toSeed) {
3524
+ try {
3525
+ const value = pluckSecret(fromEnv);
3526
+ await t3.seedSecret(name, value);
3527
+ recordSealed({
3528
+ t: (/* @__PURE__ */ new Date()).toISOString(),
3529
+ name,
3530
+ source: `env:${fromEnv}`,
3531
+ length: value.length,
3532
+ mode: env.mock ? "mock" : "real",
3533
+ tenant_did: env.did,
3534
+ map_name: `z:${env.did.replace(/^did:t3n:/, "")}:secrets`
3535
+ });
3536
+ ok2(`Sealed ${bold(name)} (read from ${fromEnv}, ${value.length} bytes, then dropped). You can DELETE ${fromEnv} from .env now.`);
3537
+ } catch (e) {
3538
+ fail(`Could not seal "${name}".`, `Error: ${e.message}
3539
+ Make sure ${fromEnv} is set in .env (you can delete it after sealing).`);
3540
+ }
3541
+ }
3542
+ await t3.close();
3543
+ process.stdout.write(`
3544
+ ${green(bold("\u2713 All done."))}
3545
+ `);
3546
+ if (opts.start) {
3547
+ process.stdout.write(`${dim("Starting the proxy now (Ctrl+C to stop) \u2026")}
3548
+ `);
3549
+ const proxy = spawn2(process.execPath, [process.argv[1] ?? "", "proxy"], {
3550
+ cwd: REPO_ROOT2,
3551
+ stdio: "inherit"
3552
+ });
3553
+ proxy.on("exit", (code) => process.exit(code ?? 0));
3554
+ return;
3555
+ }
3556
+ process.stdout.write(`${dim("Next:")}
3557
+ `);
3558
+ process.stdout.write(` ${cyan("npm run blindfold -- proxy")} ${dim("# leave this running")}
3559
+ `);
3560
+ process.stdout.write(` ${cyan("npm run dashboard")} ${dim("# open http://127.0.0.1:8799")}
3561
+ `);
3562
+ process.stdout.write(` ${dim("Then point your agent at:")} ${cyan("OPENAI_BASE_URL=http://127.0.0.1:8787/v1 OPENAI_API_KEY=__BLINDFOLD__")}
3563
+ `);
3564
+ process.stdout.write(`${dim("Or re-run with")} ${cyan("--start")} ${dim("to launch the proxy automatically.")}
3565
+ `);
3566
+ }
3567
+ async function ensureEnvOrPrompt(opts) {
3568
+ loadEnvFromFile(ENV_PATH);
3569
+ const haveBoth = !!process.env.T3N_API_KEY && !!process.env.DID;
3570
+ if (haveBoth) return;
3571
+ if (opts.yes) {
3572
+ fail(".env is missing T3N_API_KEY and/or DID.", `Claim them here: ${T3_CLAIM_URL}
3573
+ Then put them in .env and re-run.`);
3574
+ throw new Error("env missing");
3575
+ }
3576
+ if (!existsSync(ENV_PATH) && existsSync(ENV_EXAMPLE_PATH)) {
3577
+ const template = readFileSync(ENV_EXAMPLE_PATH, "utf8");
3578
+ writeFileSync(ENV_PATH, template);
3579
+ info("Created .env from .env.example.");
3580
+ }
3581
+ warn2(".env is missing your T3 credentials.");
3582
+ info(`Claim them (free, takes 30 seconds): ${cyan(T3_CLAIM_URL)}`);
3583
+ const proceed = await ask(` Paste them now? [Y/n] `);
3584
+ if (proceed.toLowerCase().startsWith("n")) {
3585
+ info("OK \u2014 open .env, paste both values, and re-run `npm run setup`.");
3586
+ process.exit(0);
3587
+ }
3588
+ const apiKey = await promptUntilMatches(" T3N_API_KEY (0x\u2026 32-byte hex): ", /^0x[0-9a-fA-F]{64}$/, "expected 0x followed by 64 hex chars");
3589
+ const did = await promptUntilMatches(" DID (did:t3n:\u2026): ", /^did:t3n:[0-9a-fA-F]+$/, "expected did:t3n:<hex>");
3590
+ upsertEnvLines(ENV_PATH, { T3N_API_KEY: apiKey, DID: did });
3591
+ ok2("Wrote .env (T3N_API_KEY + DID).");
3592
+ loadEnvFromFile(ENV_PATH);
3593
+ process.env.T3N_API_KEY = apiKey;
3594
+ process.env.DID = did;
3595
+ }
3596
+ async function promptUntilMatches(prompt, re, hint) {
3597
+ for (let i = 0; i < 5; i++) {
3598
+ const v = await ask(prompt);
3599
+ if (re.test(v)) return v;
3600
+ warn2(`That doesn't look right \u2014 ${hint}. Try again.`);
3601
+ }
3602
+ fail("Too many invalid attempts.", "Run the wizard again when you have the right values.");
3603
+ process.exit(1);
3604
+ }
3605
+ function upsertEnvLines(envPath, kv) {
3606
+ let body = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
3607
+ for (const [k, v] of Object.entries(kv)) {
3608
+ const re = new RegExp(`^${k}=.*$`, "m");
3609
+ if (re.test(body)) body = body.replace(re, `${k}=${v}`);
3610
+ else body += (body.endsWith("\n") || body === "" ? "" : "\n") + `${k}=${v}
3611
+ `;
3612
+ }
3613
+ writeFileSync(envPath, body);
3614
+ }
3615
+ function collectSeedPlan(opts) {
3616
+ const seeds = opts.seed ?? [];
3617
+ return seeds.map((s) => {
3618
+ const [name, fromEnv] = s.split(":");
3619
+ if (!name || !fromEnv) {
3620
+ warn2(`Ignoring --seed "${s}" (expected KV_KEY:ENV_VAR, e.g. openai_api_key:OPENAI_API_KEY)`);
3621
+ return null;
3622
+ }
3623
+ return { name, fromEnv };
3624
+ }).filter((x) => x !== null);
3625
+ }
3626
+ async function ensureTenantScaffolding(env) {
3627
+ const sdk = await import("@terminal3/t3n-sdk");
3628
+ sdk.setEnvironment(env.t3Env);
3629
+ const baseUrl = sdk.NODE_URLS[env.t3Env];
3630
+ const addr = sdk.eth_get_address(env.t3nApiKey);
3631
+ const t3n = new sdk.T3nClient({ baseUrl, wasmComponent: await sdk.loadWasmComponent(), handlers: { EthSign: sdk.metamask_sign(addr, void 0, env.t3nApiKey) } });
3632
+ await t3n.handshake();
3633
+ await t3n.authenticate(sdk.createEthAuthInput(addr));
3634
+ const tenant = new sdk.TenantClient({ environment: env.t3Env, baseUrl, tenantDid: env.did, t3n });
3635
+ for (const tail of ["secrets", "authorised-hosts"]) {
3636
+ try {
3637
+ await tenant.maps.create({ tail, visibility: "private", writers: "all" });
3638
+ info(`Created tenant map "${tail}"`);
3639
+ } catch {
3640
+ }
3641
+ }
3642
+ }
3643
+ async function grantContractReads(env, contractId) {
3644
+ const sdk = await import("@terminal3/t3n-sdk");
3645
+ sdk.setEnvironment(env.t3Env);
3646
+ const baseUrl = sdk.NODE_URLS[env.t3Env];
3647
+ const addr = sdk.eth_get_address(env.t3nApiKey);
3648
+ const t3n = new sdk.T3nClient({ baseUrl, wasmComponent: await sdk.loadWasmComponent(), handlers: { EthSign: sdk.metamask_sign(addr, void 0, env.t3nApiKey) } });
3649
+ await t3n.handshake();
3650
+ await t3n.authenticate(sdk.createEthAuthInput(addr));
3651
+ const tenant = new sdk.TenantClient({ environment: env.t3Env, baseUrl, tenantDid: env.did, t3n });
3652
+ await tenant.maps.update("secrets", { readers: { only: [contractId] } });
3653
+ try {
3654
+ await tenant.maps.update("authorised-hosts", { readers: { only: [contractId] } });
3655
+ } catch {
3656
+ }
3657
+ }
3658
+ async function isSdkInstalled2() {
3659
+ try {
3660
+ await import("@terminal3/t3n-sdk");
3661
+ return true;
3662
+ } catch {
3663
+ return false;
3664
+ }
3665
+ }
3666
+ async function runVerify() {
3667
+ process.stdout.write(`
3668
+ ${bold("\u{1F6E1}\uFE0F Blindfold \u2014 verify")}
3669
+ `);
3670
+ const env = loadBlindfoldEnv();
3671
+ info(`mode: ${env.mock ? red("MOCK") : green("REAL")} \xB7 T3 env: ${env.t3Env}`);
3672
+ if (env.mock) {
3673
+ warn2("MOCK mode \u2014 there's nothing to verify on the T3 side. Set T3N_API_KEY + DID.");
3674
+ return;
3675
+ }
3676
+ process.stdout.write(` ${dim("\xB7")} attempting handshake + authenticate against T3 \u2026
3677
+ `);
3678
+ try {
3679
+ const t3 = await openT3Client(env);
3680
+ if (t3.isReal) ok2("REAL T3 round-trip succeeded.");
3681
+ await t3.close();
3682
+ } catch (e) {
3683
+ fail("REAL T3 connection failed.", `Error: ${e.message}`);
3684
+ process.exitCode = 1;
3685
+ }
3686
+ try {
3687
+ const p = path10.join(REPO_ROOT2, ".blindfold", "verify.jsonl");
3688
+ mkdirSync(path10.dirname(p), { recursive: true });
3689
+ const line = JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), mode: env.mock ? "mock" : "real", t3Env: env.t3Env, ok: !process.exitCode });
3690
+ appendFileSync(p, line + "\n");
3691
+ } catch {
3692
+ }
3693
+ }
3694
+
3695
+ // src/compat.ts
3696
+ import { spawn as spawn3 } from "node:child_process";
3697
+ import fs13 from "node:fs";
3698
+ import path11 from "node:path";
3699
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
3700
+ var HERE4 = path11.dirname(fileURLToPath5(import.meta.url));
3701
+ var REPO_ROOT3 = path11.resolve(HERE4, "..", "..", "..");
3702
+ var colour2 = process.stdout.isTTY ? (c2, s) => `\x1B[${c2}m${s}\x1B[0m` : (_, s) => s;
3703
+ var bold2 = (s) => colour2("1", s);
3704
+ var dim2 = (s) => colour2("2", s);
3705
+ var green2 = (s) => colour2("32", s);
3706
+ var yellow2 = (s) => colour2("33", s);
3707
+ var red2 = (s) => colour2("31", s);
3708
+ var cyan2 = (s) => colour2("36", s);
3709
+ async function detectBinary(name) {
3710
+ return new Promise((resolve) => {
3711
+ const child = spawn3("which", [name], { stdio: ["ignore", "pipe", "ignore"] });
3712
+ const chunks = [];
3713
+ child.stdout.on("data", (c2) => chunks.push(c2));
3714
+ child.on("close", (code) => {
3715
+ const out = Buffer.concat(chunks).toString("utf8").trim();
3716
+ resolve({ detected: code === 0 && !!out, via: "PATH", detail: out || void 0 });
3717
+ });
3718
+ child.on("error", () => resolve({ detected: false, via: "PATH" }));
3719
+ });
3720
+ }
3721
+ async function detectPackage(pkg) {
3722
+ const candidates = [
3723
+ path11.join(REPO_ROOT3, "node_modules", pkg),
3724
+ path11.join(REPO_ROOT3, "node_modules", pkg, "package.json")
3725
+ ];
3726
+ for (const c2 of candidates) {
3727
+ if (fs13.existsSync(c2)) return { detected: true, via: "node_modules", detail: c2 };
3728
+ }
3729
+ return { detected: false, via: "node_modules" };
3730
+ }
3731
+ var TOOLS = [
3732
+ {
3733
+ name: "Claude Code (claude)",
3734
+ detect: () => detectBinary("claude"),
3735
+ applies: "depends",
3736
+ recipe: {
3737
+ env: { ANTHROPIC_BASE_URL: "http://127.0.0.1:8787/anthropic", ANTHROPIC_API_KEY: "__BLINDFOLD__" },
3738
+ note: "Only applies if you authenticate Claude Code with an Anthropic API key (enterprise/proxy mode). Default Claude Code uses claude.ai OAuth \u2014 there is no exposed key to protect."
3739
+ },
3740
+ explanation: "Claude Code respects ANTHROPIC_BASE_URL when ANTHROPIC_API_KEY is set; in the default subscription/OAuth flow it does not, and Blindfold has nothing to protect."
3741
+ },
3742
+ {
3743
+ name: "OpenCode (sst.dev/opencode)",
3744
+ detect: () => detectBinary("opencode"),
3745
+ applies: "applies",
3746
+ recipe: {
3747
+ env: { OPENAI_BASE_URL: "http://127.0.0.1:8787/v1", OPENAI_API_KEY: "__BLINDFOLD__" },
3748
+ note: "Configure your provider in ~/.config/opencode/config.json to use the proxy URL."
3749
+ }
3750
+ },
3751
+ {
3752
+ name: "Aider",
3753
+ detect: () => detectBinary("aider"),
3754
+ applies: "applies",
3755
+ recipe: {
3756
+ env: { OPENAI_API_BASE: "http://127.0.0.1:8787/v1", OPENAI_API_KEY: "__BLINDFOLD__" },
3757
+ note: "Aider reads OPENAI_API_BASE (the older name). Same idea."
3758
+ }
3759
+ },
3760
+ {
3761
+ name: "Continue.dev (continue)",
3762
+ detect: () => detectBinary("continue"),
3763
+ applies: "applies",
3764
+ recipe: {
3765
+ note: "Edit ~/.continue/config.json \u2014 set apiBase to http://127.0.0.1:8787/v1 and apiKey to __BLINDFOLD__ on each OpenAI-flavoured model."
3766
+ }
3767
+ },
3768
+ {
3769
+ name: "Cline (VS Code extension)",
3770
+ detect: () => detectBinary("code"),
3771
+ applies: "applies",
3772
+ recipe: {
3773
+ note: "In Cline settings \u2192 API Provider \u2192 set baseURL to http://127.0.0.1:8787/v1 and api key to __BLINDFOLD__. (Detection here just checks for VS Code; Cline itself is a VS Code extension.)"
3774
+ }
3775
+ },
3776
+ {
3777
+ name: "OpenAI Codex CLI (codex)",
3778
+ detect: () => detectBinary("codex"),
3779
+ applies: "applies",
3780
+ recipe: { env: { OPENAI_BASE_URL: "http://127.0.0.1:8787/v1", OPENAI_API_KEY: "__BLINDFOLD__" } }
3781
+ },
3782
+ {
3783
+ name: "Cursor (desktop app)",
3784
+ detect: () => detectBinary("cursor"),
3785
+ applies: "needs-base-url",
3786
+ recipe: {
3787
+ note: "Cursor does not expose a per-request base-URL setting in current builds. If you self-host an Anthropic/OpenAI-compatible gateway, point Cursor at that, then at Blindfold from there."
3788
+ }
3789
+ },
3790
+ {
3791
+ name: "openai (Node SDK)",
3792
+ detect: () => detectPackage("openai"),
3793
+ applies: "applies",
3794
+ recipe: { env: { OPENAI_BASE_URL: "http://127.0.0.1:8787/v1", OPENAI_API_KEY: "__BLINDFOLD__" } }
3795
+ },
3796
+ {
3797
+ name: "@anthropic-ai/sdk (Node SDK)",
3798
+ detect: () => detectPackage("@anthropic-ai/sdk"),
3799
+ applies: "applies",
3800
+ recipe: { env: { ANTHROPIC_BASE_URL: "http://127.0.0.1:8787/anthropic", ANTHROPIC_API_KEY: "__BLINDFOLD__" } }
3801
+ },
3802
+ {
3803
+ name: "@langchain/openai (LangChain JS)",
3804
+ detect: () => detectPackage("@langchain/openai"),
3805
+ applies: "applies",
3806
+ recipe: { note: "Use `new ChatOpenAI({ apiKey: '__BLINDFOLD__', configuration: { baseURL: 'http://127.0.0.1:8787/v1' } })`." }
3807
+ },
3808
+ {
3809
+ name: "ollama (local model runner)",
3810
+ detect: () => detectBinary("ollama"),
3811
+ applies: "oauth-only",
3812
+ recipe: { note: "Ollama runs models locally with no external API key \u2014 there's no secret for Blindfold to protect. Skip." }
3813
+ }
3814
+ ];
3815
+ async function runCompat(opts = {}) {
3816
+ const results = await Promise.all(
3817
+ TOOLS.map(async (t) => ({ tool: t, detection: await t.detect() }))
3818
+ );
3819
+ if (opts.json) {
3820
+ process.stdout.write(JSON.stringify(results.map((r) => ({
3821
+ tool: r.tool.name,
3822
+ detected: r.detection.detected,
3823
+ detected_at: r.detection.detail,
3824
+ applies: r.tool.applies,
3825
+ env: r.tool.recipe.env,
3826
+ note: r.tool.recipe.note,
3827
+ explanation: r.tool.explanation
3828
+ })), null, 2));
3829
+ return;
3830
+ }
3831
+ process.stdout.write(`
3832
+ ${bold2("\u{1F6E1}\uFE0F Blindfold \u2014 compatibility scan")}
3833
+ ${dim2("Probing local box for agent tools and SDKs Blindfold can protect.")}
3834
+
3835
+ `);
3836
+ const detected = results.filter((r) => r.detection.detected);
3837
+ const notFound = results.filter((r) => !r.detection.detected);
3838
+ process.stdout.write(`${bold2(`Detected (${detected.length}):`)}
3839
+ `);
3840
+ if (detected.length === 0) process.stdout.write(` ${dim2("(none \u2014 install one of the tools below, or just use the OpenAI/Anthropic SDK in your own code)")}
3841
+ `);
3842
+ for (const r of detected) {
3843
+ renderTool(r.tool, r.detection, true);
3844
+ }
3845
+ process.stdout.write(`
3846
+ ${bold2(`Not found on this machine (${notFound.length}):`)}
3847
+ `);
3848
+ for (const r of notFound) {
3849
+ renderTool(r.tool, r.detection, false);
3850
+ }
3851
+ process.stdout.write(`
3852
+ ${dim2("For a longer-form compatibility writeup see docs/05-compatibility.md.")}
3853
+ `);
3854
+ }
3855
+ function renderTool(tool, detection, detected) {
3856
+ const mark = !detected ? dim2("\xB7") : tool.applies === "applies" ? green2("\u2713") : tool.applies === "depends" ? yellow2("?") : tool.applies === "needs-base-url" ? yellow2("!") : red2("\u2716");
3857
+ const status = !detected ? dim2("(not installed)") : tool.applies === "applies" ? green2("Blindfold protects this") : tool.applies === "depends" ? yellow2("Depends on how you authenticate") : tool.applies === "needs-base-url" ? yellow2("No base-URL hook \u2014 needs upstream support") : red2("Doesn't apply (no user-supplied key)");
3858
+ process.stdout.write(` ${mark} ${bold2(tool.name)} ${dim2("\xB7")} ${status}
3859
+ `);
3860
+ if (detected && detection.detail) process.stdout.write(` ${dim2("at " + detection.detail)}
3861
+ `);
3862
+ if (detected && tool.recipe.env) {
3863
+ const envLine = Object.entries(tool.recipe.env).map(([k, v]) => `${k}=${v}`).join(" ");
3864
+ process.stdout.write(` ${cyan2(envLine)}
3865
+ `);
3866
+ }
3867
+ if (detected && tool.recipe.note) process.stdout.write(` ${dim2(tool.recipe.note)}
3868
+ `);
3869
+ if (detected && tool.explanation) process.stdout.write(` ${dim2(tool.explanation)}
3870
+ `);
3871
+ }
3872
+
3873
+ // bin/cmd-enclave.ts
3874
+ init_sealed_ledger();
3875
+ async function handleEnclave(cmd, argv, cmdArgs) {
3876
+ switch (cmd) {
3877
+ case "credit":
3878
+ case "balance": {
3879
+ const env = loadBlindfoldEnv();
3880
+ const BASE = 1e6;
3881
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
3882
+ const client = await openT3Client2(env);
3883
+ try {
3884
+ const b = await client.getBalance();
3885
+ const tok = (n) => (n / BASE).toLocaleString(void 0, { maximumFractionDigits: 6 });
3886
+ if (argv.flags.json) {
3887
+ console.log(JSON.stringify(b, null, 2));
3888
+ return;
3889
+ }
3890
+ console.log(head(`\u{1F4B3} Terminal 3 credit`) + c.gray(` \u2014 ${env.did || "(no tenant)"} (${env.mock ? "MOCK" : env.t3Env})`));
3891
+ console.log(` available: ${c.bold(tok(b.available) + " tokens")} ${c.gray("(" + b.available.toLocaleString() + " base units)")}`);
3892
+ console.log(` reserved: ${b.reserved.toLocaleString()} base units`);
3893
+ console.log(` status: ${b.creditExhausted ? warn("\u26A0 EXHAUSTED") : ok("\u2705 ok")}`);
3894
+ if (b.creditExhausted) {
3895
+ console.log(` Top up testnet credits, then re-check with \`blindfold credit\`:`);
3896
+ console.log(` https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens`);
3897
+ process.exitCode = 1;
3898
+ }
3899
+ } finally {
3900
+ await client.close();
3901
+ }
3902
+ return;
3903
+ }
3904
+ case "update":
3905
+ case "upgrade": {
3906
+ const PKG = "@fiscalmindset/blindfold";
3907
+ const { spawnSync: spawnSync2 } = await import("node:child_process");
3908
+ const run2 = (cmd2, args, cwd) => spawnSync2(cmd2, args, { stdio: "inherit", cwd });
3909
+ let from = argv.flags.from ? String(argv.flags.from) : process.env.BLINDFOLD_SRC || "";
3910
+ if (!from) {
3911
+ for (const cand of [path12.resolve(process.cwd(), "packages", "blindfold"), process.cwd()]) {
3912
+ try {
3913
+ const pkg = JSON.parse(fs14.readFileSync(path12.join(cand, "package.json"), "utf8"));
3914
+ if ((pkg.name === PKG || pkg.name === "blindfold") && pkg.bin?.blindfold) {
3915
+ from = cand;
3916
+ break;
3917
+ }
3918
+ } catch {
3919
+ }
3920
+ }
3921
+ }
3922
+ if (!from) {
3923
+ console.log(head("\u21BB Updating global blindfold from npm") + c.gray(` (${PKG}@latest)\u2026`));
3924
+ const r = run2("npm", ["install", "-g", `${PKG}@latest`]);
3925
+ if (r.status !== 0) {
3926
+ console.log("");
3927
+ console.log(bad("npm update failed") + c.gray(" \u2014 not published yet, or offline."));
3928
+ console.log(" Update from your repo checkout instead \u2014 one of:");
3929
+ console.log(c.cyan(" blindfold update --from /path/to/packages/blindfold"));
3930
+ console.log(c.cyan(" cd <repo> && blindfold update"));
3931
+ console.log(c.gray(" (or export BLINDFOLD_SRC=/path/to/packages/blindfold)"));
3932
+ process.exitCode = 1;
3933
+ return;
3934
+ }
3935
+ console.log(ok("\u2713 blindfold updated to the latest published version."));
3936
+ return;
3937
+ }
3938
+ console.log(head(`\u21BB Updating global blindfold`) + c.gray(` from ${from}`));
3939
+ if (run2("npm", ["run", "build"], from).status !== 0) die("build failed");
3940
+ const pack = spawnSync2("npm", ["pack"], { cwd: from, encoding: "utf8" });
3941
+ const tgz = (pack.stdout || "").trim().split("\n").pop() || "";
3942
+ if (!tgz) die("npm pack produced no tarball");
3943
+ const tgzPath = path12.join(from, tgz);
3944
+ const inst = run2("npm", ["install", "-g", tgzPath]);
3945
+ try {
3946
+ fs14.rmSync(tgzPath, { force: true });
3947
+ } catch {
3948
+ }
3949
+ if (inst.status !== 0) die("global install failed");
3950
+ console.log(ok(`\u2713 blindfold updated.`) + c.gray(" Open a NEW shell if the command still looks stale."));
3951
+ return;
3952
+ }
3953
+ case "publish": {
3954
+ const wasmPath = argv.flags.wasm ?? assetPath("contract/target/wasm32-wasip2/release/blindfold_proxy.wasm", "blindfold_proxy.wasm");
3955
+ if (!fs14.existsSync(wasmPath)) {
3956
+ die(`no wasm found at ${wasmPath} \u2014 run scripts/build-contract.sh first`);
3957
+ }
3958
+ const wasm = fs14.readFileSync(wasmPath);
3959
+ const r = await registerContract(new Uint8Array(wasm.buffer, wasm.byteOffset, wasm.byteLength));
3960
+ console.log(`\u2713 Published contract. contract_id=${r.contractId}`);
3961
+ return;
3962
+ }
3963
+ case "init": {
3964
+ const seedFlag = argv.flags.seed;
3965
+ const seedArr = Array.isArray(seedFlag) ? seedFlag : seedFlag ? [String(seedFlag)] : [];
3966
+ await runInit({
3967
+ skipBuild: !!argv.flags["skip-build"],
3968
+ skipPublish: !!argv.flags["skip-publish"],
3969
+ seed: seedArr,
3970
+ yes: !!argv.flags.yes,
3971
+ start: !!argv.flags.start
3972
+ });
3973
+ return;
3974
+ }
3975
+ case "verify": {
3976
+ await runVerify();
3977
+ return;
3978
+ }
3979
+ case "compat": {
3980
+ await runCompat({ json: !!argv.flags.json });
3981
+ return;
3982
+ }
3983
+ case "sealed": {
3984
+ const entries = readSealed();
3985
+ if (entries.length === 0) {
3986
+ console.log(`No sealed-keys ledger yet at ${defaultSealedLogPath()}.`);
3987
+ console.log(`Seal one with: blindfold register --name <KV_KEY>`);
3988
+ return;
3989
+ }
3990
+ console.log(head("\u{1F510} Sealed keys") + c.gray(` (source: ${defaultSealedLogPath()})`) + "\n");
3991
+ console.log(" WHEN NAME BYTES MODE WHERE");
3992
+ console.log(" \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500");
3993
+ for (const e of entries) {
3994
+ const when = e.t.replace("T", " ").slice(0, 19);
3995
+ const where = e.map_name.length > 60 ? e.map_name.slice(0, 57) + "\u2026" : e.map_name;
3996
+ console.log(` ${when} ${e.name.padEnd(26)} ${String(e.length).padStart(5)} ${e.mode.padEnd(5)} ${where}/${e.name}`);
3997
+ }
3998
+ console.log("\n (values are NOT stored in this ledger \u2014 only metadata. The canonical copy lives in the enclave.)");
3999
+ return;
4000
+ }
4001
+ case "audit": {
4002
+ console.log("\u{1F50D} Blindfold audit\n");
4003
+ const chain = verifyLedgerChain();
4004
+ console.log(" 1. Ledger integrity (tamper-evidence)");
4005
+ if (chain.total === 0) {
4006
+ console.log(" (ledger is empty)");
4007
+ } else if (chain.ok) {
4008
+ const chained = chain.total - chain.legacy;
4009
+ console.log(` \u2705 hash-chain intact \u2014 ${chained} chained entr${chained === 1 ? "y" : "ies"}${chain.legacy ? `, ${chain.legacy} legacy (pre-chain, unverifiable)` : ""}`);
4010
+ } else {
4011
+ console.log(` \u2716 TAMPERED \u2014 the chain breaks at entry #${chain.firstBrokenIndex} (a line was edited or removed after it was written)`);
4012
+ process.exitCode = 1;
4013
+ }
4014
+ const env = loadBlindfoldEnv();
4015
+ if (env.mock) {
4016
+ console.log("\n 2. Enclave reconciliation: skipped (MOCK mode)");
4017
+ return;
4018
+ }
4019
+ const realEntries = readSealed().filter((e) => e.mode === "real");
4020
+ const latest = /* @__PURE__ */ new Map();
4021
+ for (const e of realEntries) latest.set(e.name, e);
4022
+ console.log(`
4023
+ 2. Enclave reconciliation \u2014 the enclave is the source of truth (${latest.size} secret${latest.size === 1 ? "" : "s"})`);
4024
+ if (latest.size === 0) {
4025
+ console.log(" (nothing sealed)");
4026
+ return;
4027
+ }
4028
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
4029
+ const client = await openT3Client2(env);
4030
+ let okCount = 0, drift = 0, missing = 0;
4031
+ try {
4032
+ for (const e of latest.values()) {
4033
+ const v = await client.verifySecret(e.name);
4034
+ if (!v.present) {
4035
+ missing++;
4036
+ console.log(` \u2716 ${e.name.padEnd(22)} MISSING in enclave (ledger claims ${e.length} B)`);
4037
+ } else if (v.length !== e.length) {
4038
+ drift++;
4039
+ console.log(` \u26A0 ${e.name.padEnd(22)} length drift: enclave ${v.length} B vs ledger ${e.length} B fp=${v.fingerprint}`);
4040
+ } else {
4041
+ okCount++;
4042
+ console.log(` \u2705 ${e.name.padEnd(22)} present (${v.length} B, fp=${v.fingerprint})`);
4043
+ }
4044
+ }
4045
+ } finally {
4046
+ await client.close();
4047
+ }
4048
+ console.log(`
4049
+ Summary: ${okCount} verified \xB7 ${drift} drift \xB7 ${missing} missing \xB7 ledger ${chain.ok ? "intact" : "TAMPERED"}`);
4050
+ if (drift > 0 || missing > 0 || !chain.ok) process.exitCode = 1;
4051
+ return;
4052
+ }
4053
+ case "status": {
4054
+ const env = loadBlindfoldEnv();
4055
+ console.log(head("\u{1F6E1}\uFE0F Blindfold status") + "\n");
4056
+ console.log(` mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL"} \xB7 T3 env: ${env.t3Env}`);
4057
+ if (env.t3BaseUrl) console.log(` node: ${env.t3BaseUrl} (override)`);
4058
+ if (!env.mock) {
4059
+ try {
4060
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
4061
+ const client = await openT3Client2(env);
4062
+ const info2 = await client.me();
4063
+ console.log(` tenant: ${ok("\u2705 " + info2.tenant)} (status=${info2.status ?? "?"})`);
4064
+ } catch (e) {
4065
+ console.log(` tenant: ${bad("\u2716 " + e.message.slice(0, 90))}`);
4066
+ console.log(` \u2192 run \`blindfold doctor\` for a full diagnosis.`);
4067
+ process.exitCode = 1;
4068
+ }
4069
+ }
4070
+ const entries = readSealed();
4071
+ const latest = /* @__PURE__ */ new Map();
4072
+ for (const e of entries) latest.set(e.name, e);
4073
+ console.log(`
4074
+ Sealed secrets (${latest.size}):`);
4075
+ if (latest.size === 0) {
4076
+ console.log(` (none yet) seal one: blindfold register --name <X> --from-env <X>`);
4077
+ } else {
4078
+ for (const e of latest.values()) {
4079
+ console.log(` \u2022 ${e.name.padEnd(22)} ${String(e.length).padStart(4)} B ${e.mode}`);
4080
+ }
4081
+ }
4082
+ console.log(`
4083
+ Next: blindfold use --name <secret> -- <command> (use it, no code)`);
4084
+ return;
4085
+ }
4086
+ case "doctor": {
4087
+ const env = loadBlindfoldEnv();
4088
+ console.log(head("\u{1FA7A} Blindfold doctor"));
4089
+ console.log(` mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL (T3)"}`);
4090
+ console.log(` T3N_API_KEY set: ${env.t3nApiKey ? "yes" : "NO \u2716"}`);
4091
+ console.log(` DID set: ${env.did ? "yes" : "NO \u2716"}`);
4092
+ console.log(` T3 environment: ${env.t3Env}`);
4093
+ console.log(` node URL: ${env.t3BaseUrl || `(SDK default for ${env.t3Env})`}`);
4094
+ console.log(` default proxy port: ${env.port}`);
4095
+ if (!env.mock && (!env.t3nApiKey || !env.did)) {
4096
+ console.log("");
4097
+ console.log(` \u26A0 REAL mode is selected but credentials are missing.`);
4098
+ console.log(` Claim them: https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens`);
4099
+ console.log(` Or run \`npm run setup\` and the wizard will walk you through it.`);
4100
+ process.exitCode = 1;
4101
+ return;
4102
+ }
4103
+ if (env.mock) return;
4104
+ console.log("");
4105
+ console.log(" Live check (handshake + authenticate + me) \u2026");
4106
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
4107
+ let client;
4108
+ try {
4109
+ client = await openT3Client2(env);
4110
+ console.log(` auth: ${ok("\u2705 handshake + authenticate OK")}`);
4111
+ } catch (e) {
4112
+ console.log(` auth: ${bad("\u2716 " + e.message)}`);
4113
+ console.log(` \u2192 Check T3N_API_KEY is a 0x 32-byte hex private key and DID looks like did:t3n:<hex>.`);
4114
+ process.exitCode = 1;
4115
+ return;
4116
+ }
4117
+ try {
4118
+ const info2 = await client.me();
4119
+ console.log(` tenant: \u2705 ${info2.tenant} (status=${info2.status ?? "?"})`);
4120
+ const didHex = env.did.replace(/^did:t3n:/, "").toLowerCase();
4121
+ const meHex = info2.tenant.replace(/^did:t3n:/, "").toLowerCase();
4122
+ if (meHex && didHex && meHex !== didHex) {
4123
+ console.log("");
4124
+ console.log(` \u26A0 DID MISMATCH: your .env DID (${env.did}) is not this key's tenant.`);
4125
+ console.log(` The tenant DID is server-assigned, not derived from the key address.`);
4126
+ console.log(` Fix: set DID=${info2.tenant} in .env (writes/seals target this tenant).`);
4127
+ process.exitCode = 1;
4128
+ } else if (info2.status && info2.status !== "active") {
4129
+ console.log(` \u26A0 tenant status is "${info2.status}" (not active) \u2014 seals/writes may fail.`);
4130
+ process.exitCode = 1;
4131
+ } else {
4132
+ console.log(` ${ok("\u2705 Ready to seal & use secrets on this tenant.")}`);
4133
+ }
4134
+ } catch (e) {
4135
+ const msg = e.message;
4136
+ console.log(` tenant: \u2716 ${msg}`);
4137
+ console.log("");
4138
+ if (/InsufficientCredit|forbidden|403/i.test(msg)) {
4139
+ console.log(` \u26A0 This key has a tenant but NO CREDITS. Seals/writes need credit.`);
4140
+ console.log(` Fix: request testnet credits / claim tokens, then re-run \`blindfold doctor\`:`);
4141
+ console.log(` https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens`);
4142
+ } else if (/500|internal_error/i.test(msg)) {
4143
+ console.log(` \u26A0 This key AUTHENTICATES but its tenant is unusable: a read-only me()`);
4144
+ console.log(` returns a server error. Every seal/write with it will also 500.`);
4145
+ console.log(` Fix one of:`);
4146
+ console.log(` \u2022 switch .env to a key whose tenant is active (check with this doctor),`);
4147
+ console.log(` \u2022 point at a healthy node: T3_BASE_URL=<leader-node-url> (if the node itself is unhealthy), or`);
4148
+ console.log(` \u2022 ask Terminal 3 to provision/claim a tenant for this key.`);
4149
+ } else {
4150
+ console.log(` \u26A0 Could not read the tenant behind this key \u2014 seals/writes will likely fail.`);
4151
+ console.log(` Verify the key is provisioned, or switch to a key that passes this doctor.`);
4152
+ }
4153
+ process.exitCode = 1;
4154
+ }
4155
+ return;
4156
+ }
4157
+ case "skill": {
4158
+ const sub = argv._[1] ?? "help";
4159
+ const skillSource = assetPath(".claude/skills/blindfold/SKILL.md", "SKILL.md");
4160
+ if (!fs14.existsSync(skillSource)) {
4161
+ die(`skill source not found at ${skillSource} \u2014 reinstall Blindfold or run from the repo.`);
4162
+ }
4163
+ const skillContent = fs14.readFileSync(skillSource, "utf-8");
4164
+ if (sub === "install") {
4165
+ const targets = [];
4166
+ const global = !!argv.flags.global;
4167
+ const cursor = !!argv.flags.cursor;
4168
+ const opencode = !!argv.flags.opencode;
4169
+ const cline = !!argv.flags.cline;
4170
+ const all = !!argv.flags.all;
4171
+ if (global || all) targets.push({ label: "global (all Claude Code sessions)", dir: path12.join(process.env.HOME ?? "~", ".claude", "skills", "blindfold") });
4172
+ if (cursor || all) targets.push({ label: "Cursor", dir: path12.resolve(".cursor", "rules") });
4173
+ if (opencode || all) targets.push({ label: "OpenCode", dir: path12.resolve(".opencode", "skills", "blindfold") });
4174
+ if (cline || all) targets.push({ label: "Cline", dir: path12.resolve(".cline", "rules") });
4175
+ if (!global && !cursor && !opencode && !cline && !all) {
4176
+ targets.push({ label: "this project (Claude Code)", dir: path12.resolve(".claude", "skills", "blindfold") });
4177
+ }
4178
+ for (const t of targets) {
4179
+ fs14.mkdirSync(t.dir, { recursive: true });
4180
+ const dest = path12.join(t.dir, t.dir.includes("rules") ? "blindfold.md" : "SKILL.md");
4181
+ fs14.writeFileSync(dest, skillContent);
4182
+ console.log(` \u2713 ${t.label} \u2192 ${dest}`);
4183
+ }
4184
+ console.log(`
4185
+ \u2713 Blindfold skill installed (${targets.length} target${targets.length > 1 ? "s" : ""})`);
4186
+ console.log(` Your coding agent will now handle secrets safely \u2014 try asking it to "seal my API key".`);
4187
+ } else if (sub === "uninstall") {
4188
+ const locations = [
4189
+ path12.resolve(".claude", "skills", "blindfold", "SKILL.md"),
4190
+ path12.join(process.env.HOME ?? "~", ".claude", "skills", "blindfold", "SKILL.md"),
4191
+ path12.resolve(".cursor", "rules", "blindfold.md"),
4192
+ path12.resolve(".opencode", "skills", "blindfold", "SKILL.md"),
4193
+ path12.resolve(".cline", "rules", "blindfold.md")
4194
+ ];
4195
+ let removed = 0;
4196
+ for (const loc of locations) {
4197
+ if (fs14.existsSync(loc)) {
4198
+ fs14.unlinkSync(loc);
4199
+ console.log(` \u2713 removed ${loc}`);
4200
+ removed++;
4201
+ }
4202
+ }
4203
+ console.log(removed ? `
4204
+ \u2713 Removed ${removed} skill file(s).` : " No skill files found to remove.");
4205
+ } else {
4206
+ console.log(`blindfold skill \u2014 install the Blindfold agent skill for your coding agent.
4207
+
4208
+ blindfold skill install Install for this project (Claude Code auto-discovers it)
4209
+ blindfold skill install --global Install globally (~/.claude/skills/, all sessions)
4210
+ blindfold skill install --cursor Install for Cursor (.cursor/rules/)
4211
+ blindfold skill install --opencode Install for OpenCode (.opencode/skills/)
4212
+ blindfold skill install --cline Install for Cline (.cline/rules/)
4213
+ blindfold skill install --all Install for all of the above at once
4214
+
4215
+ blindfold skill uninstall Remove all installed skill files
4216
+
4217
+ What it does: teaches your coding agent to seal keys safely \u2014 no pasting secrets
4218
+ into chat, release-broker pattern in generated code, fingerprint-only verification.`);
4219
+ }
4220
+ return;
4221
+ }
4222
+ }
4223
+ }
4224
+
4225
+ // bin/blindfold.ts
4226
+ var ROUTES = {
4227
+ signup: handleAuth,
4228
+ login: handleAuth,
4229
+ logout: handleAuth,
4230
+ whoami: handleAuth,
4231
+ register: handleSecrets,
4232
+ use: handleSecrets,
4233
+ export: handleSecrets,
4234
+ rotate: handleLifecycle,
4235
+ rollback: handleLifecycle,
4236
+ versions: handleLifecycle,
4237
+ migrate: handleLifecycle,
4238
+ grant: handleTenant,
4239
+ share: handleTenant,
4240
+ revoke: handleTenant,
4241
+ proxy: handleServe,
4242
+ attest: handleServe,
4243
+ dashboard: handleServe,
4244
+ stats: handleServe,
4245
+ "stats:clear": handleServe,
4246
+ publish: handleEnclave,
4247
+ init: handleEnclave,
4248
+ verify: handleEnclave,
4249
+ compat: handleEnclave,
4250
+ sealed: handleEnclave,
4251
+ audit: handleEnclave,
4252
+ status: handleEnclave,
4253
+ doctor: handleEnclave,
4254
+ skill: handleEnclave,
4255
+ credit: handleEnclave,
4256
+ balance: handleEnclave,
4257
+ update: handleEnclave,
4258
+ upgrade: handleEnclave
4259
+ };
4260
+ async function main() {
4261
+ const raw = process.argv.slice(2);
4262
+ const ddIdx = raw.indexOf("--");
4263
+ const cmdArgs = ddIdx >= 0 ? raw.slice(ddIdx + 1) : [];
4264
+ const argv = parseArgv(ddIdx >= 0 ? raw.slice(0, ddIdx) : raw);
4265
+ const cmd = argv._[0] ?? "help";
4266
+ const handler = ROUTES[cmd];
4267
+ if (handler) await handler(cmd, argv, cmdArgs);
4268
+ else printHelp();
4269
+ }
4270
+ function printHelp() {
4271
+ console.log(`${head("\u{1F6E1}\uFE0F Blindfold")} ${c.gray("\u2014 protect your AI agent's API keys with Terminal 3 enclaves.")}
4272
+
4273
+ ${c.bold("Commands:")}
4274
+ signup [--email <you@x.com>] Self-serve: create a Terminal 3 testnet tenant from scratch. Generates a key locally, verifies your email by code, and mints welcome credits \u2014 no manual provisioning. Then run doctor + register.
4275
+ init [--seed KV:ENV]... [--start] One-command zero-knowledge setup. Walks through .env, build, auth, publish, seed; can auto-launch the proxy.
4276
+ verify Handshake + auth against T3 (smoke test).
4277
+ compat [--json] Scan this machine for AI agent tools/SDKs and print the exact env-var swap for each.
4278
+ register --name <KV_KEY> [--from-env <ENV_VAR>] Seal a secret into the enclave (one-time). With --from-env: reads process.env. Without: prompts the terminal with no echo (preferred \u2014 never touches disk/history). Also accepts piped stdin.
4279
+ use --name <secret> [--as <ENV>] -- <cmd> USE a sealed secret: release it and run <cmd> with it injected as $ENV for that command only \u2014 never back in your env. --as is auto-detected for known tools (gh\u2192GH_TOKEN, psql\u2192PGPASSWORD, \u2026). Or --url <https> for a quick auth check.
4280
+ rotate --name <secret> [--from-env <ENV_VAR>] Replace a sealed secret's value (snapshots the old value for rollback; shows before/after fingerprints, never the value).
4281
+ export --name <secret> [--as <ENV_VAR>] CI-only: release a sealed secret into $GITHUB_ENV for later steps (masked in logs). Used by the Blindfold GitHub Action.
4282
+ rollback --name <secret> [--to <fp|iso-ts>] Restore a previous value snapshotted by rotate (most recent by default).
4283
+ versions [--name <secret>] List the snapshots available to roll back to (metadata only).
4284
+ migrate [--dry-run] [--keep] Seal EVERY secret in your .env in one shot, then remove the plaintext lines (backup kept). --dry-run previews; --keep comments lines instead of deleting. Skips T3 creds + config.
4285
+ status One-glance overview: mode, tenant health, and the list of sealed secrets.
4286
+ sealed List sealed keys \u2014 metadata only (name, byte-length, when, where). Never the value.
4287
+ audit Verify the ledger's tamper-evident hash-chain AND reconcile it against the enclave (the source of truth) \u2014 flags drift/missing/tampering.
4288
+ proxy [--port 8787] [--auth] [--socket [path]] Run the local OpenAI-shaped proxy. --auth mints a per-session token; --socket binds a 0600 unix socket (only your OS user can connect).
4289
+ attest [--expect-rtmr3 <b64>] [--pin] [--json] Verify the enclave's TDX attestation (chains to Intel's root CA). --pin records the RTMR3 so seal/proxy auto-verify it first.
4290
+ publish [--wasm path/to/blindfold_proxy.wasm] Publish the Rust\u2192WASM contract (one-time).
4291
+ grant --host <host>[,<host2>...] Authorize the contract to call these hosts (required before the proxy / in-enclave path can reach them). E.g. --host api.openai.com
4292
+ share --to <agent-did> --host <host>[,...] Let a teammate's agent USE your sealed keys for those hosts via the enclave \u2014 they never receive the plaintext (forward only, least privilege).
4293
+ revoke --to <agent-did> Remove a teammate's access. Immediate and complete \u2014 nobody holds a raw key copy.
4294
+
4295
+ skill install [--global|--cursor|--opencode|--cline|--all] Install the Blindfold agent skill so your coding agent handles secrets safely. Default: this project.
4296
+ skill uninstall Remove all installed skill files.
4297
+
4298
+ dashboard [--port 8799] Live HTML dashboard of proxy usage.
4299
+ stats CLI summary of proxy usage.
4300
+ stats:clear Wipe the usage log.
4301
+ doctor Show current mode + config.
4302
+ credit [--json] Show the tenant's Terminal 3 token/credit balance (no credit cost).
4303
+ update [--from <path>] Update the global blindfold (from npm, or a local repo with --from).
4304
+
4305
+ The friendliest path is just: blindfold init
4306
+
4307
+ Quick start:
4308
+ 1) ./scripts/build-contract.sh # build the Rust contract (REAL mode only)
4309
+ 2) blindfold publish # register the contract on T3
4310
+ 3) blindfold register --name openai_api_key --from-env OPENAI_API_KEY
4311
+ 4) blindfold proxy # then point your agent at it
4312
+ `);
4313
+ }
4314
+ main().catch((e) => {
4315
+ console.error("\u2716", e.message);
4316
+ process.exit(1);
4317
+ });