@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.
@@ -0,0 +1,193 @@
1
+ /** CLI command group (auto-split from the dispatcher; bodies unchanged). */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { randomBytes } from "node:crypto";
6
+ import { loadBlindfoldEnv, configPath, homeDir, stateDir } from "../src/env.ts";
7
+ import { keychainAvailable, keychainBackend, keychainSet, keychainDelete } from "../src/keychain.ts";
8
+ import { readSecretLine, readLine } from "../src/prompt.ts";
9
+ import { registerSecret, registerContract } from "../src/register.ts";
10
+ import { startProxy } from "../src/proxy.ts";
11
+ import { attest, attestationGate, writePinnedRtmr3 } from "../src/attest.ts";
12
+ import { startDashboard } from "../src/dashboard.ts";
13
+ import { clearUsage, defaultLogPath, readUsage } from "../src/usage-log.ts";
14
+ import { runInit, runVerify } from "../src/init.ts";
15
+ import { runCompat } from "../src/compat.ts";
16
+ import { defaultSealedLogPath, readSealed, verifyLedgerChain } from "../src/sealed-ledger.ts";
17
+ import { type Argv, die, assetPath, fingerprint, resolveEnvVar } from "./cli-shared.ts";
18
+ import { c, head, ok } from "../src/color.ts";
19
+
20
+ /**
21
+ * Persist tenant credentials the way `login` does: DID + settings into the
22
+ * 0600 config file, the tenant key into the OS keychain (or the config file as
23
+ * a 0600 fallback). Shared by `login` and `signup`.
24
+ */
25
+ function saveTenantCredentials(
26
+ did: string,
27
+ key: string,
28
+ env: "testnet" | "production",
29
+ forceFile: boolean,
30
+ ): { inKeychain: boolean; triedKeychain: boolean; cfg: string } {
31
+ const cfg = configPath();
32
+ let existing: Record<string, string> = {};
33
+ try { if (fs.existsSync(cfg)) existing = JSON.parse(fs.readFileSync(cfg, "utf8")); } catch { /* overwrite */ }
34
+ const merged: Record<string, string> = { ...existing, DID: did, BLINDFOLD_T3_ENV: env };
35
+ const triedKeychain = !forceFile && keychainAvailable();
36
+ const inKeychain = triedKeychain && keychainSet(did, key);
37
+ if (inKeychain) {
38
+ merged.T3N_API_KEY_STORE = "keychain";
39
+ delete merged.T3N_API_KEY;
40
+ } else {
41
+ merged.T3N_API_KEY = key;
42
+ merged.T3N_API_KEY_STORE = "file";
43
+ }
44
+ fs.mkdirSync(homeDir(), { recursive: true });
45
+ fs.writeFileSync(cfg, JSON.stringify(merged, null, 2), { mode: 0o600 });
46
+ try { fs.chmodSync(cfg, 0o600); } catch { /* best effort */ }
47
+ return { inKeychain, triedKeychain, cfg };
48
+ }
49
+
50
+ export async function handleAuth(cmd: string, argv: Argv, cmdArgs: string[]): Promise<void> {
51
+ switch (cmd) {
52
+ case "signup": {
53
+ // Self-serve provisioning: create a fresh Terminal 3 testnet tenant with
54
+ // no manual step on the T3 side. Generates a key locally, eth-auths,
55
+ // proves the email via OTP, and self-admits (mints welcome credits).
56
+ const env = String(argv.flags.env ?? "").toLowerCase() === "production" ? "production" : "testnet";
57
+ if (env === "production") {
58
+ die("signup (self-admit) is testnet-only. Production tenants are provisioned by Terminal 3 directly.");
59
+ }
60
+ const email = argv.flags.email
61
+ ? String(argv.flags.email)
62
+ : (await readLine("Email for your tenant (a verification code will be sent): ")).trim();
63
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) die("Enter a valid email address (e.g. you@example.com).");
64
+
65
+ console.log(`${head("blindfold signup")} — creating a Terminal 3 ${c.cyan("testnet")} tenant for ${c.cyan(email)}`);
66
+ console.log(c.dim(" A fresh tenant key (secp256k1) is generated locally and stored in your keychain — never printed.\n"));
67
+
68
+ // T3's Level-1 self-admit requires a first + last name on the profile.
69
+ // Default from the email local part (minus any +tag) so signup stays a
70
+ // single prompt; --first/--last override.
71
+ const localPart = (email.split("@")[0] ?? "user").split("+")[0] ?? "user";
72
+ const firstName = argv.flags.first ? String(argv.flags.first) : (localPart || "Blindfold");
73
+ const lastName = argv.flags.last ? String(argv.flags.last) : "Tenant";
74
+
75
+ const { signupTenant, SignupEmailTakenError } = await import("../src/t3-client.ts");
76
+ // Allow a non-interactive OTP (for scripted/remote tests): --otp <code>.
77
+ const presetOtp = argv.flags.otp ? String(argv.flags.otp).trim() : "";
78
+ // Persisted by onKeyReady the moment the email verifies — captured here so
79
+ // both the success and the post-verify-failure paths can report where the
80
+ // key landed (and so a failed admit never strands an unrecoverable key).
81
+ let saved: { inKeychain: boolean; cfg: string } | null = null;
82
+ let res;
83
+ try {
84
+ res = await signupTenant({
85
+ env,
86
+ email,
87
+ profile: { first_name: firstName, last_name: lastName },
88
+ onKeyReady: (key, did) => {
89
+ saved = saveTenantCredentials(did, key, env, Boolean(argv.flags.file));
90
+ },
91
+ getOtpCode: async () => {
92
+ if (presetOtp) return presetOtp;
93
+ process.stderr.write(c.yellow(` A verification code was emailed to ${email}.\n`));
94
+ return (await readLine(" Enter the code: ")).trim();
95
+ },
96
+ });
97
+ } catch (e) {
98
+ if (e instanceof SignupEmailTakenError) {
99
+ console.error(c.red(`āœ– ${e.message}`));
100
+ console.error(c.dim(" This email already has a tenant. Either:"));
101
+ console.error(c.dim(` • log in with that tenant's key: blindfold login --did ${e.existingDid}`));
102
+ console.error(c.dim(" • or sign up with a different email (Gmail '+' aliases work: you+blindfold@gmail.com)."));
103
+ process.exit(1);
104
+ }
105
+ // If the key was already saved (email verified, only the self-admit
106
+ // failed), the tenant is recoverable — say so instead of implying loss.
107
+ if (saved) {
108
+ const s = saved as { inKeychain: boolean; cfg: string };
109
+ console.error(c.red(`āœ– Email verified and tenant key saved, but self-admit failed: ${(e as Error).message}`));
110
+ console.error(c.dim(` Your key + DID are stored in ${s.inKeychain ? `the ${keychainBackend()}` : `${s.cfg} (0600)`} — not lost.`));
111
+ 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."));
112
+ process.exit(1);
113
+ }
114
+ die(`signup failed: ${(e as Error).message}`);
115
+ return;
116
+ }
117
+
118
+ if (res.admitStatus === "refused") {
119
+ die(`Terminal 3 refused self-admit (${res.refusedReason ?? "unknown"})${res.refusedDetail ? `: ${res.refusedDetail}` : ""}.`);
120
+ return;
121
+ }
122
+
123
+ const { inKeychain, cfg } = saved ?? saveTenantCredentials(res.did, res.key, env, Boolean(argv.flags.file));
124
+ const verb = res.admitStatus === "already-admitted" ? "confirmed (already registered)" : "created";
125
+ console.log(ok(`\nāœ“ Tenant ${verb}: ${c.bold(res.did)}`));
126
+ console.log(` Tenant key stored in ${inKeychain ? `the ${keychainBackend()}` : `${cfg} (0600)`} — never printed.`);
127
+ if (res.grantedCredits) {
128
+ console.log(ok(` Welcome credits minted: ${res.grantedCredits} base units.`));
129
+ } else {
130
+ console.log(c.yellow(" No welcome credits were minted (the testnet dial may be 0). Check `blindfold credit`."));
131
+ }
132
+ console.log(`\n Next:`);
133
+ console.log(` ${c.bold("blindfold doctor")} ${c.dim("# confirm T3 reachability")}`);
134
+ console.log(` ${c.bold("blindfold credit")} ${c.dim("# see your token balance")}`);
135
+ console.log(` ${c.bold("blindfold register --name openai_key")} ${c.dim("# seal your first secret")}`);
136
+ return;
137
+ }
138
+ case "login": {
139
+ // Store tenant credentials in ~/.blindfold/config.json so the CLI works
140
+ // from any directory, installed globally, without a repo .env.
141
+ const did = argv.flags.did
142
+ ? String(argv.flags.did)
143
+ : (await readSecretLine("Tenant DID (did:t3n:…): ")).trim();
144
+ if (!/^did:t3n:[0-9a-fA-F]+$/.test(did)) die('DID must look like "did:t3n:<hex>".');
145
+ const key = argv.flags.key
146
+ ? String(argv.flags.key)
147
+ : (await readSecretLine("T3N_API_KEY (0x…, hidden): ")).trim();
148
+ if (!/^0x[0-9a-fA-F]{64}$/.test(key)) die("T3N_API_KEY must be a 0x-prefixed 32-byte hex.");
149
+ const env = String(argv.flags.env ?? "").toLowerCase() === "production" ? "production" : "testnet";
150
+
151
+ // Prefer the OS keychain for the tenant key; the config file then holds
152
+ // only non-secret DID + settings. Fall back to a 0600 file when no
153
+ // keychain exists OR the keychain write fails (e.g. err 1312 in a
154
+ // non-interactive session). --file forces the file path.
155
+ const { inKeychain, triedKeychain, cfg } = saveTenantCredentials(did, key, env, Boolean(argv.flags.file));
156
+ console.log(`āœ“ Saved tenant key to ${inKeychain ? `the ${keychainBackend()}` : `${cfg} (mode 0600)`}. Blindfold now works from any directory.`);
157
+ console.log(` Tenant: ${did} Ā· env: ${env} Ā· key: stored (never printed)`);
158
+ if (!inKeychain && !argv.flags.file) {
159
+ console.log(triedKeychain
160
+ ? ` (Keychain write unavailable here — stored in a 0600 file. Run \`blindfold login\` in an interactive desktop session to use ${keychainBackend()}.)`
161
+ : ` (No OS keychain found — stored in a 0600 file. On macOS/Linux/Windows an interactive session uses the OS credential store automatically.)`);
162
+ }
163
+ console.log(` Verify: blindfold doctor`);
164
+ return;
165
+ }
166
+ case "logout": {
167
+ const cfg = configPath();
168
+ let did = "";
169
+ let store = "";
170
+ try { if (fs.existsSync(cfg)) { const o = JSON.parse(fs.readFileSync(cfg, "utf8")); did = o.DID ?? ""; store = o.T3N_API_KEY_STORE ?? ""; } } catch { /* ignore */ }
171
+ let removed = false;
172
+ if (store === "keychain" && did && keychainAvailable()) { if (keychainDelete(did)) { console.log("āœ“ Removed tenant key from the OS keychain."); removed = true; } }
173
+ if (fs.existsSync(cfg)) { fs.rmSync(cfg, { force: true }); console.log(`āœ“ Removed ${cfg}.`); removed = true; }
174
+ if (!removed) console.log("Nothing to remove — no saved credentials at " + cfg);
175
+ else console.log("Tenant credentials cleared from this machine.");
176
+ return;
177
+ }
178
+ case "whoami": {
179
+ const cfg = configPath();
180
+ let store = "";
181
+ try { if (fs.existsSync(cfg)) store = (JSON.parse(fs.readFileSync(cfg, "utf8")).T3N_API_KEY_STORE) ?? ""; } catch { /* ignore */ }
182
+ const env = loadBlindfoldEnv();
183
+ const keySource = !env.t3nApiKey ? "MISSING"
184
+ : process.env.T3N_API_KEY && store === "keychain" ? `set (${keychainBackend()})`
185
+ : store === "file" ? "set (config file, 0600)"
186
+ : "set (env / repo .env)";
187
+ console.log(`${c.bold("config:")} ${c.gray(cfg)}${fs.existsSync(cfg) ? "" : " (not present)"}`);
188
+ console.log(`${c.bold("tenant:")} ${c.cyan(env.did || "(none — run `blindfold login`)")}`);
189
+ console.log(`${c.bold("env:")} ${env.t3Env} Ā· key: ${c.green(keySource)}`);
190
+ return;
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,373 @@
1
+ /** CLI command group (auto-split from the dispatcher; bodies unchanged). */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { randomBytes } from "node:crypto";
6
+ import { loadBlindfoldEnv, configPath, homeDir, stateDir } from "../src/env.ts";
7
+ import { keychainAvailable, keychainBackend, keychainSet, keychainDelete } from "../src/keychain.ts";
8
+ import { readSecretLine } from "../src/prompt.ts";
9
+ import { registerSecret, registerContract } from "../src/register.ts";
10
+ import { startProxy } from "../src/proxy.ts";
11
+ import { attest, attestationGate, writePinnedRtmr3 } from "../src/attest.ts";
12
+ import { startDashboard } from "../src/dashboard.ts";
13
+ import { clearUsage, defaultLogPath, readUsage } from "../src/usage-log.ts";
14
+ import { runInit, runVerify } from "../src/init.ts";
15
+ import { runCompat } from "../src/compat.ts";
16
+ import { defaultSealedLogPath, readSealed, verifyLedgerChain } from "../src/sealed-ledger.ts";
17
+ import { type Argv, die, assetPath, fingerprint, resolveEnvVar } from "./cli-shared.ts";
18
+ import { c, ok, bad, warn, head } from "../src/color.ts";
19
+
20
+ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]): Promise<void> {
21
+ switch (cmd) {
22
+ case "credit":
23
+ case "balance": {
24
+ // Show the tenant's token/credit balance (a session-authed read that costs
25
+ // no credit — works even when exhausted). Avoids discovering "0 credits"
26
+ // only when a seal/forward fails with a 403.
27
+ const env = loadBlindfoldEnv();
28
+ const BASE = 1_000_000; // 1 token = 1,000,000 base units
29
+ const { openT3Client } = await import("../src/t3-client.ts");
30
+ const client = await openT3Client(env);
31
+ try {
32
+ const b = await client.getBalance();
33
+ const tok = (n: number) => (n / BASE).toLocaleString(undefined, { maximumFractionDigits: 6 });
34
+ if (argv.flags.json) { console.log(JSON.stringify(b, null, 2)); return; }
35
+ console.log(head(`šŸ’³ Terminal 3 credit`) + c.gray(` — ${env.did || "(no tenant)"} (${env.mock ? "MOCK" : env.t3Env})`));
36
+ console.log(` available: ${c.bold(tok(b.available) + " tokens")} ${c.gray("(" + b.available.toLocaleString() + " base units)")}`);
37
+ console.log(` reserved: ${b.reserved.toLocaleString()} base units`);
38
+ console.log(` status: ${b.creditExhausted ? warn("⚠ EXHAUSTED") : ok("āœ… ok")}`);
39
+ if (b.creditExhausted) {
40
+ console.log(` Top up testnet credits, then re-check with \`blindfold credit\`:`);
41
+ console.log(` https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens`);
42
+ process.exitCode = 1;
43
+ }
44
+ } finally {
45
+ await client.close();
46
+ }
47
+ return;
48
+ }
49
+
50
+ case "update":
51
+ case "upgrade": {
52
+ // Update the globally-installed blindfold. Prefer the repo SOURCE if one is
53
+ // reachable (dev). Otherwise fall back to the SCOPED npm package
54
+ // (@fiscalmindset/blindfold) — never the bare `blindfold` name, which is an
55
+ // UNRELATED package. Source resolution: --from, BLINDFOLD_SRC, repo at cwd.
56
+ const PKG = "@fiscalmindset/blindfold";
57
+ const { spawnSync } = await import("node:child_process");
58
+ const run = (cmd: string, args: string[], cwd?: string) =>
59
+ spawnSync(cmd, args, { stdio: "inherit", cwd });
60
+ let from = argv.flags.from ? String(argv.flags.from) : (process.env.BLINDFOLD_SRC || "");
61
+ if (!from) {
62
+ for (const cand of [path.resolve(process.cwd(), "packages", "blindfold"), process.cwd()]) {
63
+ try {
64
+ const pkg = JSON.parse(fs.readFileSync(path.join(cand, "package.json"), "utf8"));
65
+ if ((pkg.name === PKG || pkg.name === "blindfold") && pkg.bin?.blindfold) { from = cand; break; }
66
+ } catch { /* not the package dir */ }
67
+ }
68
+ }
69
+ if (!from) {
70
+ // No local source → update from the published scoped package.
71
+ console.log(head("↻ Updating global blindfold from npm") + c.gray(` (${PKG}@latest)…`));
72
+ const r = run("npm", ["install", "-g", `${PKG}@latest`]);
73
+ if (r.status !== 0) {
74
+ console.log("");
75
+ console.log(bad("npm update failed") + c.gray(" — not published yet, or offline."));
76
+ console.log(" Update from your repo checkout instead — one of:");
77
+ console.log(c.cyan(" blindfold update --from /path/to/packages/blindfold"));
78
+ console.log(c.cyan(" cd <repo> && blindfold update"));
79
+ console.log(c.gray(" (or export BLINDFOLD_SRC=/path/to/packages/blindfold)"));
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+ console.log(ok("āœ“ blindfold updated to the latest published version."));
84
+ return;
85
+ }
86
+ console.log(head(`↻ Updating global blindfold`) + c.gray(` from ${from}`));
87
+ if (run("npm", ["run", "build"], from).status !== 0) die("build failed");
88
+ const pack = spawnSync("npm", ["pack"], { cwd: from, encoding: "utf8" });
89
+ const tgz = (pack.stdout || "").trim().split("\n").pop() || "";
90
+ if (!tgz) die("npm pack produced no tarball");
91
+ const tgzPath = path.join(from, tgz);
92
+ const inst = run("npm", ["install", "-g", tgzPath]);
93
+ try { fs.rmSync(tgzPath, { force: true }); } catch { /* ignore */ }
94
+ if (inst.status !== 0) die("global install failed");
95
+ console.log(ok(`āœ“ blindfold updated.`) + c.gray(" Open a NEW shell if the command still looks stale."));
96
+ return;
97
+ }
98
+
99
+ case "publish": {
100
+ const wasmPath =
101
+ (argv.flags.wasm as string | undefined) ??
102
+ assetPath("contract/target/wasm32-wasip2/release/blindfold_proxy.wasm", "blindfold_proxy.wasm");
103
+ if (!fs.existsSync(wasmPath)) {
104
+ die(`no wasm found at ${wasmPath} — run scripts/build-contract.sh first`);
105
+ }
106
+ const wasm = fs.readFileSync(wasmPath);
107
+ const r = await registerContract(new Uint8Array(wasm.buffer, wasm.byteOffset, wasm.byteLength));
108
+ console.log(`āœ“ Published contract. contract_id=${r.contractId}`);
109
+ return;
110
+ }
111
+ case "init": {
112
+ const seedFlag = argv.flags.seed;
113
+ const seedArr = Array.isArray(seedFlag) ? (seedFlag as string[]) : seedFlag ? [String(seedFlag)] : [];
114
+ await runInit({
115
+ skipBuild: !!argv.flags["skip-build"],
116
+ skipPublish: !!argv.flags["skip-publish"],
117
+ seed: seedArr,
118
+ yes: !!argv.flags.yes,
119
+ start: !!argv.flags.start,
120
+ });
121
+ return;
122
+ }
123
+ case "verify": {
124
+ await runVerify();
125
+ return;
126
+ }
127
+ case "compat": {
128
+ await runCompat({ json: !!argv.flags.json });
129
+ return;
130
+ }
131
+ case "sealed": {
132
+ // List sealed keys (metadata only) for the current ledger.
133
+ const entries = readSealed();
134
+ if (entries.length === 0) {
135
+ console.log(`No sealed-keys ledger yet at ${defaultSealedLogPath()}.`);
136
+ console.log(`Seal one with: blindfold register --name <KV_KEY>`);
137
+ return;
138
+ }
139
+ console.log(head("šŸ” Sealed keys") + c.gray(` (source: ${defaultSealedLogPath()})`) + "\n");
140
+ console.log(" WHEN NAME BYTES MODE WHERE");
141
+ console.log(" ──── ──── ───── ──── ─────");
142
+ for (const e of entries) {
143
+ const when = e.t.replace("T", " ").slice(0, 19);
144
+ const where = e.map_name.length > 60 ? e.map_name.slice(0, 57) + "…" : e.map_name;
145
+ console.log(` ${when} ${e.name.padEnd(26)} ${String(e.length).padStart(5)} ${e.mode.padEnd(5)} ${where}/${e.name}`);
146
+ }
147
+ console.log("\n (values are NOT stored in this ledger — only metadata. The canonical copy lives in the enclave.)");
148
+ return;
149
+ }
150
+ case "audit": {
151
+ // (1) Tamper-evidence: verify the local ledger's hash-chain.
152
+ // (2) Reconcile against the enclave — the actual source of truth.
153
+ console.log("šŸ” Blindfold audit\n");
154
+ const chain = verifyLedgerChain();
155
+ console.log(" 1. Ledger integrity (tamper-evidence)");
156
+ if (chain.total === 0) {
157
+ console.log(" (ledger is empty)");
158
+ } else if (chain.ok) {
159
+ const chained = chain.total - chain.legacy;
160
+ console.log(` āœ… hash-chain intact — ${chained} chained entr${chained === 1 ? "y" : "ies"}${chain.legacy ? `, ${chain.legacy} legacy (pre-chain, unverifiable)` : ""}`);
161
+ } else {
162
+ console.log(` āœ– TAMPERED — the chain breaks at entry #${chain.firstBrokenIndex} (a line was edited or removed after it was written)`);
163
+ process.exitCode = 1;
164
+ }
165
+
166
+ const env = loadBlindfoldEnv();
167
+ if (env.mock) {
168
+ console.log("\n 2. Enclave reconciliation: skipped (MOCK mode)");
169
+ return;
170
+ }
171
+ const realEntries = readSealed().filter((e) => e.mode === "real");
172
+ const latest = new Map<string, (typeof realEntries)[number]>();
173
+ for (const e of realEntries) latest.set(e.name, e); // last write wins
174
+ console.log(`\n 2. Enclave reconciliation — the enclave is the source of truth (${latest.size} secret${latest.size === 1 ? "" : "s"})`);
175
+ if (latest.size === 0) {
176
+ console.log(" (nothing sealed)");
177
+ return;
178
+ }
179
+ const { openT3Client } = await import("../src/t3-client.ts");
180
+ const client = await openT3Client(env);
181
+ let okCount = 0, drift = 0, missing = 0;
182
+ try {
183
+ for (const e of latest.values()) {
184
+ const v = await client.verifySecret(e.name);
185
+ if (!v.present) {
186
+ missing++;
187
+ console.log(` āœ– ${e.name.padEnd(22)} MISSING in enclave (ledger claims ${e.length} B)`);
188
+ } else if (v.length !== e.length) {
189
+ drift++;
190
+ console.log(` ⚠ ${e.name.padEnd(22)} length drift: enclave ${v.length} B vs ledger ${e.length} B fp=${v.fingerprint}`);
191
+ } else {
192
+ okCount++;
193
+ console.log(` āœ… ${e.name.padEnd(22)} present (${v.length} B, fp=${v.fingerprint})`);
194
+ }
195
+ }
196
+ } finally {
197
+ await client.close();
198
+ }
199
+ console.log(`\n Summary: ${okCount} verified Ā· ${drift} drift Ā· ${missing} missing Ā· ledger ${chain.ok ? "intact" : "TAMPERED"}`);
200
+ if (drift > 0 || missing > 0 || !chain.ok) process.exitCode = 1;
201
+ return;
202
+ }
203
+ case "status": {
204
+ // One-glance overview: health + sealed inventory + what to do next.
205
+ const env = loadBlindfoldEnv();
206
+ console.log(head("šŸ›”ļø Blindfold status") + "\n");
207
+ console.log(` mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL"} Ā· T3 env: ${env.t3Env}`);
208
+ if (env.t3BaseUrl) console.log(` node: ${env.t3BaseUrl} (override)`);
209
+ if (!env.mock) {
210
+ try {
211
+ const { openT3Client } = await import("../src/t3-client.ts");
212
+ const client = await openT3Client(env);
213
+ const info = await client.me();
214
+ console.log(` tenant: ${ok("āœ… " + info.tenant)} (status=${info.status ?? "?"})`);
215
+ } catch (e) {
216
+ console.log(` tenant: ${bad("āœ– " + (e as Error).message.slice(0, 90))}`);
217
+ console.log(` → run \`blindfold doctor\` for a full diagnosis.`);
218
+ process.exitCode = 1;
219
+ }
220
+ }
221
+ const entries = readSealed();
222
+ const latest = new Map<string, (typeof entries)[number]>();
223
+ for (const e of entries) latest.set(e.name, e); // last write wins
224
+ console.log(`\n Sealed secrets (${latest.size}):`);
225
+ if (latest.size === 0) {
226
+ console.log(` (none yet) seal one: blindfold register --name <X> --from-env <X>`);
227
+ } else {
228
+ for (const e of latest.values()) {
229
+ console.log(` • ${e.name.padEnd(22)} ${String(e.length).padStart(4)} B ${e.mode}`);
230
+ }
231
+ }
232
+ console.log(`\n Next: blindfold use --name <secret> -- <command> (use it, no code)`);
233
+ return;
234
+ }
235
+ case "doctor": {
236
+ const env = loadBlindfoldEnv();
237
+ console.log(head("🩺 Blindfold doctor"));
238
+ console.log(` mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL (T3)"}`);
239
+ console.log(` T3N_API_KEY set: ${env.t3nApiKey ? "yes" : "NO āœ–"}`);
240
+ console.log(` DID set: ${env.did ? "yes" : "NO āœ–"}`);
241
+ console.log(` T3 environment: ${env.t3Env}`);
242
+ console.log(` node URL: ${env.t3BaseUrl || `(SDK default for ${env.t3Env})`}`);
243
+ console.log(` default proxy port: ${env.port}`);
244
+ if (!env.mock && (!env.t3nApiKey || !env.did)) {
245
+ console.log("");
246
+ console.log(` ⚠ REAL mode is selected but credentials are missing.`);
247
+ console.log(` Claim them: https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens`);
248
+ console.log(` Or run \`npm run setup\` and the wizard will walk you through it.`);
249
+ process.exitCode = 1;
250
+ return;
251
+ }
252
+ if (env.mock) return;
253
+
254
+ // LIVE check: authenticate, then read the tenant behind the key. This is
255
+ // what catches the painful "key authenticates but has no tenant" case,
256
+ // which the server otherwise reports only as a bare HTTP 500.
257
+ console.log("");
258
+ console.log(" Live check (handshake + authenticate + me) …");
259
+ const { openT3Client } = await import("../src/t3-client.ts");
260
+ let client;
261
+ try {
262
+ client = await openT3Client(env);
263
+ console.log(` auth: ${ok("āœ… handshake + authenticate OK")}`);
264
+ } catch (e) {
265
+ console.log(` auth: ${bad("āœ– " + (e as Error).message)}`);
266
+ console.log(` → Check T3N_API_KEY is a 0x 32-byte hex private key and DID looks like did:t3n:<hex>.`);
267
+ process.exitCode = 1;
268
+ return;
269
+ }
270
+ try {
271
+ const info = await client.me();
272
+ console.log(` tenant: āœ… ${info.tenant} (status=${info.status ?? "?"})`);
273
+ const didHex = env.did.replace(/^did:t3n:/, "").toLowerCase();
274
+ const meHex = info.tenant.replace(/^did:t3n:/, "").toLowerCase();
275
+ if (meHex && didHex && meHex !== didHex) {
276
+ console.log("");
277
+ console.log(` ⚠ DID MISMATCH: your .env DID (${env.did}) is not this key's tenant.`);
278
+ console.log(` The tenant DID is server-assigned, not derived from the key address.`);
279
+ console.log(` Fix: set DID=${info.tenant} in .env (writes/seals target this tenant).`);
280
+ process.exitCode = 1;
281
+ } else if (info.status && info.status !== "active") {
282
+ console.log(` ⚠ tenant status is "${info.status}" (not active) — seals/writes may fail.`);
283
+ process.exitCode = 1;
284
+ } else {
285
+ console.log(` ${ok("āœ… Ready to seal & use secrets on this tenant.")}`);
286
+ }
287
+ } catch (e) {
288
+ const msg = (e as Error).message;
289
+ console.log(` tenant: āœ– ${msg}`);
290
+ console.log("");
291
+ if (/InsufficientCredit|forbidden|403/i.test(msg)) {
292
+ console.log(` ⚠ This key has a tenant but NO CREDITS. Seals/writes need credit.`);
293
+ console.log(` Fix: request testnet credits / claim tokens, then re-run \`blindfold doctor\`:`);
294
+ console.log(` https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens`);
295
+ } else if (/500|internal_error/i.test(msg)) {
296
+ console.log(` ⚠ This key AUTHENTICATES but its tenant is unusable: a read-only me()`);
297
+ console.log(` returns a server error. Every seal/write with it will also 500.`);
298
+ console.log(` Fix one of:`);
299
+ console.log(` • switch .env to a key whose tenant is active (check with this doctor),`);
300
+ console.log(` • point at a healthy node: T3_BASE_URL=<leader-node-url> (if the node itself is unhealthy), or`);
301
+ console.log(` • ask Terminal 3 to provision/claim a tenant for this key.`);
302
+ } else {
303
+ console.log(` ⚠ Could not read the tenant behind this key — seals/writes will likely fail.`);
304
+ console.log(` Verify the key is provisioned, or switch to a key that passes this doctor.`);
305
+ }
306
+ process.exitCode = 1;
307
+ }
308
+ return;
309
+ }
310
+ case "skill": {
311
+ const sub = argv._[1] ?? "help";
312
+ const skillSource = assetPath(".claude/skills/blindfold/SKILL.md", "SKILL.md");
313
+ if (!fs.existsSync(skillSource)) {
314
+ die(`skill source not found at ${skillSource} — reinstall Blindfold or run from the repo.`);
315
+ }
316
+ const skillContent = fs.readFileSync(skillSource, "utf-8");
317
+
318
+ if (sub === "install") {
319
+ const targets: { label: string; dir: string }[] = [];
320
+ const global = !!argv.flags.global;
321
+ const cursor = !!argv.flags.cursor;
322
+ const opencode = !!argv.flags.opencode;
323
+ const cline = !!argv.flags.cline;
324
+ const all = !!argv.flags.all;
325
+
326
+ if (global || all) targets.push({ label: "global (all Claude Code sessions)", dir: path.join(process.env.HOME ?? "~", ".claude", "skills", "blindfold") });
327
+ if (cursor || all) targets.push({ label: "Cursor", dir: path.resolve(".cursor", "rules") });
328
+ if (opencode || all) targets.push({ label: "OpenCode", dir: path.resolve(".opencode", "skills", "blindfold") });
329
+ if (cline || all) targets.push({ label: "Cline", dir: path.resolve(".cline", "rules") });
330
+ if (!global && !cursor && !opencode && !cline && !all) {
331
+ targets.push({ label: "this project (Claude Code)", dir: path.resolve(".claude", "skills", "blindfold") });
332
+ }
333
+
334
+ for (const t of targets) {
335
+ fs.mkdirSync(t.dir, { recursive: true });
336
+ const dest = path.join(t.dir, t.dir.includes("rules") ? "blindfold.md" : "SKILL.md");
337
+ fs.writeFileSync(dest, skillContent);
338
+ console.log(` āœ“ ${t.label} → ${dest}`);
339
+ }
340
+ console.log(`\nāœ“ Blindfold skill installed (${targets.length} target${targets.length > 1 ? "s" : ""})`);
341
+ console.log(` Your coding agent will now handle secrets safely — try asking it to "seal my API key".`);
342
+ } else if (sub === "uninstall") {
343
+ const locations = [
344
+ path.resolve(".claude", "skills", "blindfold", "SKILL.md"),
345
+ path.join(process.env.HOME ?? "~", ".claude", "skills", "blindfold", "SKILL.md"),
346
+ path.resolve(".cursor", "rules", "blindfold.md"),
347
+ path.resolve(".opencode", "skills", "blindfold", "SKILL.md"),
348
+ path.resolve(".cline", "rules", "blindfold.md"),
349
+ ];
350
+ let removed = 0;
351
+ for (const loc of locations) {
352
+ if (fs.existsSync(loc)) { fs.unlinkSync(loc); console.log(` āœ“ removed ${loc}`); removed++; }
353
+ }
354
+ console.log(removed ? `\nāœ“ Removed ${removed} skill file(s).` : " No skill files found to remove.");
355
+ } else {
356
+ console.log(`blindfold skill — install the Blindfold agent skill for your coding agent.
357
+
358
+ blindfold skill install Install for this project (Claude Code auto-discovers it)
359
+ blindfold skill install --global Install globally (~/.claude/skills/, all sessions)
360
+ blindfold skill install --cursor Install for Cursor (.cursor/rules/)
361
+ blindfold skill install --opencode Install for OpenCode (.opencode/skills/)
362
+ blindfold skill install --cline Install for Cline (.cline/rules/)
363
+ blindfold skill install --all Install for all of the above at once
364
+
365
+ blindfold skill uninstall Remove all installed skill files
366
+
367
+ What it does: teaches your coding agent to seal keys safely — no pasting secrets
368
+ into chat, release-broker pattern in generated code, fingerprint-only verification.`);
369
+ }
370
+ return;
371
+ }
372
+ }
373
+ }