@fiscalmindset/blindfold 0.4.2 β 0.4.4
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/bin/cmd-enclave.ts +38 -30
- package/dist/cli.mjs +84 -53
- package/package.json +1 -1
- package/src/help.ts +22 -17
- package/src/tui.ts +23 -4
package/bin/cmd-enclave.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { runCompat } from "../src/compat.ts";
|
|
|
16
16
|
import { defaultSealedLogPath, readSealed, verifyLedgerChain } from "../src/sealed-ledger.ts";
|
|
17
17
|
import { type Argv, die, assetPath, fingerprint, resolveEnvVar } from "./cli-shared.ts";
|
|
18
18
|
import { c, ok, bad, warn, head } from "../src/color.ts";
|
|
19
|
+
import { boxLines, rule } from "../src/tui.ts";
|
|
19
20
|
|
|
20
21
|
export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]): Promise<void> {
|
|
21
22
|
switch (cmd) {
|
|
@@ -32,15 +33,19 @@ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]):
|
|
|
32
33
|
const b = await client.getBalance();
|
|
33
34
|
const tok = (n: number) => (n / BASE).toLocaleString(undefined, { maximumFractionDigits: 6 });
|
|
34
35
|
if (argv.flags.json) { console.log(JSON.stringify(b, null, 2)); return; }
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const lines = [
|
|
37
|
+
c.gray(`${env.did || "(no tenant)"} Β· ${env.mock ? "MOCK" : env.t3Env}`),
|
|
38
|
+
"",
|
|
39
|
+
`available: ${c.bold(c.green(tok(b.available) + " tokens"))} ${c.gray("(" + b.available.toLocaleString() + " base units)")}`,
|
|
40
|
+
`reserved: ${b.reserved.toLocaleString()} base units`,
|
|
41
|
+
`status: ${b.creditExhausted ? warn("β EXHAUSTED") : ok("β
ok")}`,
|
|
42
|
+
];
|
|
39
43
|
if (b.creditExhausted) {
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
lines.push("", c.yellow("Top up testnet credits, then re-check with `blindfold credit`:"),
|
|
45
|
+
c.cyan(" https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens"));
|
|
42
46
|
process.exitCode = 1;
|
|
43
47
|
}
|
|
48
|
+
console.log(boxLines("π³ Terminal 3 credit", lines));
|
|
44
49
|
} finally {
|
|
45
50
|
await client.close();
|
|
46
51
|
}
|
|
@@ -136,9 +141,10 @@ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]):
|
|
|
136
141
|
console.log(`Seal one with: blindfold register --name <KV_KEY>`);
|
|
137
142
|
return;
|
|
138
143
|
}
|
|
139
|
-
console.log(
|
|
140
|
-
console.log("
|
|
141
|
-
console.log("
|
|
144
|
+
console.log(boxLines("π Sealed keys", [c.gray(`source: ${defaultSealedLogPath()}`)]));
|
|
145
|
+
console.log("");
|
|
146
|
+
console.log(c.gray(" WHEN NAME BYTES MODE WHERE"));
|
|
147
|
+
console.log(c.gray(" ββββ ββββ βββββ ββββ βββββ"));
|
|
142
148
|
for (const e of entries) {
|
|
143
149
|
const when = e.t.replace("T", " ").slice(0, 19);
|
|
144
150
|
const where = e.map_name.length > 60 ? e.map_name.slice(0, 57) + "β¦" : e.map_name;
|
|
@@ -150,9 +156,10 @@ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]):
|
|
|
150
156
|
case "audit": {
|
|
151
157
|
// (1) Tamper-evidence: verify the local ledger's hash-chain.
|
|
152
158
|
// (2) Reconcile against the enclave β the actual source of truth.
|
|
153
|
-
console.log("π Blindfold audit
|
|
159
|
+
console.log(boxLines("π Blindfold audit", [c.gray("ledger hash-chain + reconciliation against the enclave")]));
|
|
160
|
+
console.log("");
|
|
154
161
|
const chain = verifyLedgerChain();
|
|
155
|
-
console.log(" 1. Ledger integrity (tamper-evidence)");
|
|
162
|
+
console.log(c.bold(" 1. Ledger integrity (tamper-evidence)"));
|
|
156
163
|
if (chain.total === 0) {
|
|
157
164
|
console.log(" (ledger is empty)");
|
|
158
165
|
} else if (chain.ok) {
|
|
@@ -203,44 +210,46 @@ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]):
|
|
|
203
210
|
case "status": {
|
|
204
211
|
// One-glance overview: health + sealed inventory + what to do next.
|
|
205
212
|
const env = loadBlindfoldEnv();
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (env.t3BaseUrl)
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
lines.push(`mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL"} Β· T3 env: ${env.t3Env}`);
|
|
215
|
+
if (env.t3BaseUrl) lines.push(`node: ${env.t3BaseUrl} (override)`);
|
|
209
216
|
if (!env.mock) {
|
|
210
217
|
try {
|
|
211
218
|
const { openT3Client } = await import("../src/t3-client.ts");
|
|
212
219
|
const client = await openT3Client(env);
|
|
213
220
|
const info = await client.me();
|
|
214
|
-
|
|
221
|
+
lines.push(`tenant: ${ok("β
" + info.tenant)} (status=${info.status ?? "?"})`);
|
|
215
222
|
} catch (e) {
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
lines.push(`tenant: ${bad("β " + (e as Error).message.slice(0, 90))}`);
|
|
224
|
+
lines.push(` ${c.gray("β run `blindfold doctor` for a full diagnosis.")}`);
|
|
218
225
|
process.exitCode = 1;
|
|
219
226
|
}
|
|
220
227
|
}
|
|
221
228
|
const entries = readSealed();
|
|
222
229
|
const latest = new Map<string, (typeof entries)[number]>();
|
|
223
230
|
for (const e of entries) latest.set(e.name, e); // last write wins
|
|
224
|
-
|
|
231
|
+
lines.push("", c.bold(`Sealed secrets (${latest.size})`));
|
|
225
232
|
if (latest.size === 0) {
|
|
226
|
-
|
|
233
|
+
lines.push(` ${c.gray("(none yet) seal one:")} ${c.cyan("blindfold register --name <X>")}`);
|
|
227
234
|
} else {
|
|
228
235
|
for (const e of latest.values()) {
|
|
229
|
-
|
|
236
|
+
lines.push(` ${c.green("β’")} ${c.cyan(e.name.padEnd(22))} ${String(e.length).padStart(4)} B ${c.gray(e.mode)}`);
|
|
230
237
|
}
|
|
231
238
|
}
|
|
232
|
-
|
|
239
|
+
lines.push("", c.gray("Next: ") + c.cyan("blindfold use --name <secret> -- <command>") + c.gray(" (use it, no code)"));
|
|
240
|
+
console.log(boxLines("π‘οΈ Blindfold status", lines));
|
|
233
241
|
return;
|
|
234
242
|
}
|
|
235
243
|
case "doctor": {
|
|
236
244
|
const env = loadBlindfoldEnv();
|
|
237
|
-
console.log(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
console.log(boxLines("π©Ί Blindfold doctor", [
|
|
246
|
+
`mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL (T3)"}`,
|
|
247
|
+
`T3N_API_KEY: ${env.t3nApiKey ? ok("set") : bad("NOT set β")}`,
|
|
248
|
+
`DID: ${env.did ? ok("set") : bad("NOT set β")}`,
|
|
249
|
+
`T3 env: ${env.t3Env}`,
|
|
250
|
+
`node URL: ${env.t3BaseUrl || c.gray(`(SDK default for ${env.t3Env})`)}`,
|
|
251
|
+
`proxy port: ${env.port}`,
|
|
252
|
+
]));
|
|
244
253
|
if (!env.mock && (!env.t3nApiKey || !env.did)) {
|
|
245
254
|
console.log("");
|
|
246
255
|
console.log(` β REAL mode is selected but credentials are missing.`);
|
|
@@ -254,8 +263,7 @@ export async function handleEnclave(cmd: string, argv: Argv, cmdArgs: string[]):
|
|
|
254
263
|
// LIVE check: authenticate, then read the tenant behind the key. This is
|
|
255
264
|
// what catches the painful "key authenticates but has no tenant" case,
|
|
256
265
|
// which the server otherwise reports only as a bare HTTP 500.
|
|
257
|
-
console.log("");
|
|
258
|
-
console.log(" Live check (handshake + authenticate + me) β¦");
|
|
266
|
+
console.log("\n" + rule("Live check β handshake + authenticate + me"));
|
|
259
267
|
const { openT3Client } = await import("../src/t3-client.ts");
|
|
260
268
|
let client;
|
|
261
269
|
try {
|
package/dist/cli.mjs
CHANGED
|
@@ -1309,6 +1309,7 @@ var c = {
|
|
|
1309
1309
|
cyan: wrap("36"),
|
|
1310
1310
|
gray: wrap("90")
|
|
1311
1311
|
};
|
|
1312
|
+
var colorOn = on;
|
|
1312
1313
|
var ok = (s) => c.green(s);
|
|
1313
1314
|
var bad = (s) => c.red(s);
|
|
1314
1315
|
var warn = (s) => c.yellow(s);
|
|
@@ -1434,12 +1435,31 @@ function boxLines(title, lines) {
|
|
|
1434
1435
|
const w = termWidth();
|
|
1435
1436
|
const inner = w - 4;
|
|
1436
1437
|
const prefix = B.tl + B.h + " ";
|
|
1437
|
-
const fill = Math.max(0, w - vlen(prefix) - vlen(title) - 2);
|
|
1438
|
-
const out = [dim(prefix) + c.bold(title) + dim(" " + B.h.repeat(fill) + B.tr)];
|
|
1439
|
-
for (const l of lines) out.push(dim(B.v) + " " + pad(l, inner) + " " + dim(B.v));
|
|
1438
|
+
const fill = Math.max(0, w - vlen(prefix) - vlen(clip(title, inner)) - 2);
|
|
1439
|
+
const out = [dim(prefix) + c.bold(clip(title, inner)) + dim(" " + B.h.repeat(fill) + B.tr)];
|
|
1440
|
+
for (const l of lines) out.push(dim(B.v) + " " + pad(clip(l, inner), inner) + " " + dim(B.v));
|
|
1440
1441
|
out.push(dim(B.bl + B.h.repeat(w - 2) + B.br));
|
|
1441
1442
|
return out.join("\n");
|
|
1442
1443
|
}
|
|
1444
|
+
function clip(s, width) {
|
|
1445
|
+
if (vlen(s) <= width) return s;
|
|
1446
|
+
let out = "";
|
|
1447
|
+
let wSoFar = 0;
|
|
1448
|
+
for (const part of s.split(/(\x1b\[[0-9;]*m)/)) {
|
|
1449
|
+
if (part.startsWith("\x1B")) {
|
|
1450
|
+
out += part;
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
for (const ch of part) {
|
|
1454
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
1455
|
+
const cw = cp === 8205 || cp >= 65024 && cp <= 65039 || cp >= 768 && cp <= 879 ? 0 : isWide(cp) ? 2 : 1;
|
|
1456
|
+
if (wSoFar + cw > width - 1) return out + "\u2026" + (colorOn ? "\x1B[0m" : "");
|
|
1457
|
+
out += ch;
|
|
1458
|
+
wSoFar += cw;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return out + (colorOn ? "\x1B[0m" : "");
|
|
1462
|
+
}
|
|
1443
1463
|
function rule(label = "") {
|
|
1444
1464
|
const w = termWidth();
|
|
1445
1465
|
if (!label) return dim(B.h.repeat(w));
|
|
@@ -1611,19 +1631,20 @@ var COMMANDS = [
|
|
|
1611
1631
|
name: "grant",
|
|
1612
1632
|
group: "\u{1F465} Team & sharing",
|
|
1613
1633
|
usage: "--host <host>[,<host2>...]",
|
|
1614
|
-
summary: "
|
|
1615
|
-
flags: [["--host <host,...>", "
|
|
1616
|
-
examples: ["blindfold grant --host api.openai.com", "blindfold grant --host api.github.com,api.stripe.com"]
|
|
1634
|
+
summary: "Allow the enclave to reach an API's server. Do this once per API before the proxy can call it \u2014 e.g. allow api.openai.com so a sealed OpenAI key can be used.",
|
|
1635
|
+
flags: [["--host <host,...>", "The API hostname(s) to allow, comma-separated. Each grant adds to the list."]],
|
|
1636
|
+
examples: ["blindfold grant --host api.openai.com", "blindfold grant --host api.github.com,api.stripe.com"],
|
|
1637
|
+
notes: "Why it's needed: the enclave refuses to call any host you haven't allowed (deny-by-default). Grant the host of each API whose key you've sealed."
|
|
1617
1638
|
},
|
|
1618
1639
|
{
|
|
1619
1640
|
name: "share",
|
|
1620
1641
|
group: "\u{1F465} Team & sharing",
|
|
1621
1642
|
usage: "--to <agent-did> --host <host>[,...]",
|
|
1622
|
-
summary: "Let a teammate
|
|
1623
|
-
flags: [["--to <agent-did>", "The teammate's agent DID
|
|
1624
|
-
examples: ["blindfold share --to did:t3n
|
|
1643
|
+
summary: "Let a teammate use your sealed key for an API \u2014 they can make calls but never see the key itself.",
|
|
1644
|
+
flags: [["--to <agent-did>", "The teammate's agent DID (their Terminal 3 identity)."], ["--host <host,...>", "Which API hosts they're allowed to reach through your enclave."]],
|
|
1645
|
+
examples: ["blindfold share --to did:t3n:abc\u2026 --host api.openai.com"]
|
|
1625
1646
|
},
|
|
1626
|
-
{ name: "revoke", group: "\u{1F465} Team & sharing", usage: "--to <agent-did>", summary: "
|
|
1647
|
+
{ name: "revoke", group: "\u{1F465} Team & sharing", usage: "--to <agent-did>", summary: "Take back a teammate's access you granted with `share` \u2014 immediate and complete.", flags: [["--to <agent-did>", "The teammate's agent DID to cut off."]], examples: ["blindfold revoke --to did:t3n:abc\u2026"] },
|
|
1627
1648
|
// ββ Enclave & admin ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1628
1649
|
{ name: "publish", group: "\u{1F4E6} Enclave & admin", usage: "[--wasm <path>]", summary: "Publish the Rust\u2192WASM contract to your tenant (one-time).", flags: [["--wasm <path>", "Path to blindfold_proxy.wasm (defaults to the bundled build)."]], examples: ["blindfold publish"] },
|
|
1629
1650
|
{ name: "status", group: "\u{1F4E6} Enclave & admin", usage: "", summary: "One-glance overview: mode, tenant health, and the list of sealed secrets.", examples: ["blindfold status"] },
|
|
@@ -1673,27 +1694,29 @@ function renderMainHelp() {
|
|
|
1673
1694
|
];
|
|
1674
1695
|
const nameW = Math.min(12, COMMANDS.reduce((m, cmd) => Math.max(m, vlen(cmd.name)), 0));
|
|
1675
1696
|
const descW = Math.max(16, w - 4 - nameW - 2);
|
|
1697
|
+
const fit = (s, max) => s.length > max ? s.slice(0, Math.max(1, max - 1)) + "\u2026" : s;
|
|
1676
1698
|
for (const group of GROUP_ORDER) {
|
|
1677
1699
|
const cmds = COMMANDS.filter((cmd) => cmd.group === group);
|
|
1678
1700
|
const lines = [];
|
|
1679
|
-
for (const cmd of cmds) {
|
|
1701
|
+
for (const [idx, cmd] of cmds.entries()) {
|
|
1702
|
+
if (idx > 0) lines.push("");
|
|
1680
1703
|
const dl = wrapText(cmd.summary, descW);
|
|
1681
|
-
lines.push(pad(c.cyan(cmd.name), nameW) + " " + (dl[0] ?? ""));
|
|
1682
|
-
for (let i = 1; i < dl.length; i++) lines.push(pad("", nameW) + " " +
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
}
|
|
1704
|
+
lines.push(pad(c.bold(c.cyan(cmd.name)), nameW) + " " + (dl[0] ?? ""));
|
|
1705
|
+
for (let i = 1; i < dl.length; i++) lines.push(pad("", nameW) + " " + (dl[i] ?? ""));
|
|
1706
|
+
const ex = fit(cmd.examples?.[0] ?? `blindfold ${cmd.name}`, descW - 5);
|
|
1707
|
+
lines.push(pad("", nameW) + " " + c.gray("e.g. ") + c.green(ex));
|
|
1686
1708
|
}
|
|
1687
1709
|
out.push(boxLines(group, lines));
|
|
1688
1710
|
out.push("");
|
|
1689
1711
|
}
|
|
1690
1712
|
out.push(
|
|
1691
|
-
c.bold("Quick start"),
|
|
1692
|
-
` ${c.
|
|
1693
|
-
` ${c.
|
|
1694
|
-
` ${c.
|
|
1713
|
+
c.bold(c.magenta("\u25B6 Quick start")),
|
|
1714
|
+
` ${c.green("blindfold signup --email you@x.com")} ${c.gray("# create a funded testnet tenant")}`,
|
|
1715
|
+
` ${c.green("blindfold register --name openai_api_key")} ${c.gray("# seal a key (hidden prompt)")}`,
|
|
1716
|
+
` ${c.green("blindfold proxy")} ${c.gray("# point your agent at http://127.0.0.1:8787")}`,
|
|
1695
1717
|
"",
|
|
1696
|
-
`${c.gray("
|
|
1718
|
+
`${c.gray("Full flags + more examples for any command:")} ${c.cyan("blindfold <command> --help")}`,
|
|
1719
|
+
`${c.gray("Docs:")} ${c.cyan("npmjs.com/package/@fiscalmindset/blindfold")}`,
|
|
1697
1720
|
""
|
|
1698
1721
|
);
|
|
1699
1722
|
return out.join("\n");
|
|
@@ -4245,15 +4268,22 @@ async function handleEnclave(cmd, argv, cmdArgs) {
|
|
|
4245
4268
|
console.log(JSON.stringify(b, null, 2));
|
|
4246
4269
|
return;
|
|
4247
4270
|
}
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4271
|
+
const lines = [
|
|
4272
|
+
c.gray(`${env.did || "(no tenant)"} \xB7 ${env.mock ? "MOCK" : env.t3Env}`),
|
|
4273
|
+
"",
|
|
4274
|
+
`available: ${c.bold(c.green(tok(b.available) + " tokens"))} ${c.gray("(" + b.available.toLocaleString() + " base units)")}`,
|
|
4275
|
+
`reserved: ${b.reserved.toLocaleString()} base units`,
|
|
4276
|
+
`status: ${b.creditExhausted ? warn("\u26A0 EXHAUSTED") : ok("\u2705 ok")}`
|
|
4277
|
+
];
|
|
4252
4278
|
if (b.creditExhausted) {
|
|
4253
|
-
|
|
4254
|
-
|
|
4279
|
+
lines.push(
|
|
4280
|
+
"",
|
|
4281
|
+
c.yellow("Top up testnet credits, then re-check with `blindfold credit`:"),
|
|
4282
|
+
c.cyan(" https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens")
|
|
4283
|
+
);
|
|
4255
4284
|
process.exitCode = 1;
|
|
4256
4285
|
}
|
|
4286
|
+
console.log(boxLines("\u{1F4B3} Terminal 3 credit", lines));
|
|
4257
4287
|
} finally {
|
|
4258
4288
|
await client.close();
|
|
4259
4289
|
}
|
|
@@ -4345,9 +4375,10 @@ async function handleEnclave(cmd, argv, cmdArgs) {
|
|
|
4345
4375
|
console.log(`Seal one with: blindfold register --name <KV_KEY>`);
|
|
4346
4376
|
return;
|
|
4347
4377
|
}
|
|
4348
|
-
console.log(
|
|
4349
|
-
console.log("
|
|
4350
|
-
console.log("
|
|
4378
|
+
console.log(boxLines("\u{1F510} Sealed keys", [c.gray(`source: ${defaultSealedLogPath()}`)]));
|
|
4379
|
+
console.log("");
|
|
4380
|
+
console.log(c.gray(" WHEN NAME BYTES MODE WHERE"));
|
|
4381
|
+
console.log(c.gray(" \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500"));
|
|
4351
4382
|
for (const e of entries) {
|
|
4352
4383
|
const when = e.t.replace("T", " ").slice(0, 19);
|
|
4353
4384
|
const where = e.map_name.length > 60 ? e.map_name.slice(0, 57) + "\u2026" : e.map_name;
|
|
@@ -4357,9 +4388,10 @@ async function handleEnclave(cmd, argv, cmdArgs) {
|
|
|
4357
4388
|
return;
|
|
4358
4389
|
}
|
|
4359
4390
|
case "audit": {
|
|
4360
|
-
console.log("\u{1F50D} Blindfold audit
|
|
4391
|
+
console.log(boxLines("\u{1F50D} Blindfold audit", [c.gray("ledger hash-chain + reconciliation against the enclave")]));
|
|
4392
|
+
console.log("");
|
|
4361
4393
|
const chain = verifyLedgerChain();
|
|
4362
|
-
console.log(" 1. Ledger integrity (tamper-evidence)");
|
|
4394
|
+
console.log(c.bold(" 1. Ledger integrity (tamper-evidence)"));
|
|
4363
4395
|
if (chain.total === 0) {
|
|
4364
4396
|
console.log(" (ledger is empty)");
|
|
4365
4397
|
} else if (chain.ok) {
|
|
@@ -4410,46 +4442,46 @@ async function handleEnclave(cmd, argv, cmdArgs) {
|
|
|
4410
4442
|
}
|
|
4411
4443
|
case "status": {
|
|
4412
4444
|
const env = loadBlindfoldEnv();
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
if (env.t3BaseUrl)
|
|
4445
|
+
const lines = [];
|
|
4446
|
+
lines.push(`mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL"} \xB7 T3 env: ${env.t3Env}`);
|
|
4447
|
+
if (env.t3BaseUrl) lines.push(`node: ${env.t3BaseUrl} (override)`);
|
|
4416
4448
|
if (!env.mock) {
|
|
4417
4449
|
try {
|
|
4418
4450
|
const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
|
|
4419
4451
|
const client = await openT3Client2(env);
|
|
4420
4452
|
const info2 = await client.me();
|
|
4421
|
-
|
|
4453
|
+
lines.push(`tenant: ${ok("\u2705 " + info2.tenant)} (status=${info2.status ?? "?"})`);
|
|
4422
4454
|
} catch (e) {
|
|
4423
|
-
|
|
4424
|
-
|
|
4455
|
+
lines.push(`tenant: ${bad("\u2716 " + e.message.slice(0, 90))}`);
|
|
4456
|
+
lines.push(` ${c.gray("\u2192 run `blindfold doctor` for a full diagnosis.")}`);
|
|
4425
4457
|
process.exitCode = 1;
|
|
4426
4458
|
}
|
|
4427
4459
|
}
|
|
4428
4460
|
const entries = readSealed();
|
|
4429
4461
|
const latest = /* @__PURE__ */ new Map();
|
|
4430
4462
|
for (const e of entries) latest.set(e.name, e);
|
|
4431
|
-
|
|
4432
|
-
Sealed secrets (${latest.size}):`);
|
|
4463
|
+
lines.push("", c.bold(`Sealed secrets (${latest.size})`));
|
|
4433
4464
|
if (latest.size === 0) {
|
|
4434
|
-
|
|
4465
|
+
lines.push(` ${c.gray("(none yet) seal one:")} ${c.cyan("blindfold register --name <X>")}`);
|
|
4435
4466
|
} else {
|
|
4436
4467
|
for (const e of latest.values()) {
|
|
4437
|
-
|
|
4468
|
+
lines.push(` ${c.green("\u2022")} ${c.cyan(e.name.padEnd(22))} ${String(e.length).padStart(4)} B ${c.gray(e.mode)}`);
|
|
4438
4469
|
}
|
|
4439
4470
|
}
|
|
4440
|
-
|
|
4441
|
-
|
|
4471
|
+
lines.push("", c.gray("Next: ") + c.cyan("blindfold use --name <secret> -- <command>") + c.gray(" (use it, no code)"));
|
|
4472
|
+
console.log(boxLines("\u{1F6E1}\uFE0F Blindfold status", lines));
|
|
4442
4473
|
return;
|
|
4443
4474
|
}
|
|
4444
4475
|
case "doctor": {
|
|
4445
4476
|
const env = loadBlindfoldEnv();
|
|
4446
|
-
console.log(
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4477
|
+
console.log(boxLines("\u{1FA7A} Blindfold doctor", [
|
|
4478
|
+
`mode: ${env.mock ? "MOCK (BLINDFOLD_MOCK=1)" : "REAL (T3)"}`,
|
|
4479
|
+
`T3N_API_KEY: ${env.t3nApiKey ? ok("set") : bad("NOT set \u2716")}`,
|
|
4480
|
+
`DID: ${env.did ? ok("set") : bad("NOT set \u2716")}`,
|
|
4481
|
+
`T3 env: ${env.t3Env}`,
|
|
4482
|
+
`node URL: ${env.t3BaseUrl || c.gray(`(SDK default for ${env.t3Env})`)}`,
|
|
4483
|
+
`proxy port: ${env.port}`
|
|
4484
|
+
]));
|
|
4453
4485
|
if (!env.mock && (!env.t3nApiKey || !env.did)) {
|
|
4454
4486
|
console.log("");
|
|
4455
4487
|
console.log(` \u26A0 REAL mode is selected but credentials are missing.`);
|
|
@@ -4459,8 +4491,7 @@ async function handleEnclave(cmd, argv, cmdArgs) {
|
|
|
4459
4491
|
return;
|
|
4460
4492
|
}
|
|
4461
4493
|
if (env.mock) return;
|
|
4462
|
-
console.log("");
|
|
4463
|
-
console.log(" Live check (handshake + authenticate + me) \u2026");
|
|
4494
|
+
console.log("\n" + rule("Live check \u2014 handshake + authenticate + me"));
|
|
4464
4495
|
const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
|
|
4465
4496
|
let client;
|
|
4466
4497
|
try {
|
package/package.json
CHANGED
package/src/help.ts
CHANGED
|
@@ -139,17 +139,18 @@ export const COMMANDS: CmdDef[] = [
|
|
|
139
139
|
// ββ Team & sharing βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
140
140
|
{
|
|
141
141
|
name: "grant", group: "π₯ Team & sharing", usage: "--host <host>[,<host2>...]",
|
|
142
|
-
summary: "
|
|
143
|
-
flags: [["--host <host,...>", "
|
|
142
|
+
summary: "Allow the enclave to reach an API's server. Do this once per API before the proxy can call it β e.g. allow api.openai.com so a sealed OpenAI key can be used.",
|
|
143
|
+
flags: [["--host <host,...>", "The API hostname(s) to allow, comma-separated. Each grant adds to the list."]],
|
|
144
144
|
examples: ["blindfold grant --host api.openai.com", "blindfold grant --host api.github.com,api.stripe.com"],
|
|
145
|
+
notes: "Why it's needed: the enclave refuses to call any host you haven't allowed (deny-by-default). Grant the host of each API whose key you've sealed.",
|
|
145
146
|
},
|
|
146
147
|
{
|
|
147
148
|
name: "share", group: "π₯ Team & sharing", usage: "--to <agent-did> --host <host>[,...]",
|
|
148
|
-
summary: "Let a teammate
|
|
149
|
-
flags: [["--to <agent-did>", "The teammate's agent DID
|
|
150
|
-
examples: ["blindfold share --to did:t3n
|
|
149
|
+
summary: "Let a teammate use your sealed key for an API β they can make calls but never see the key itself.",
|
|
150
|
+
flags: [["--to <agent-did>", "The teammate's agent DID (their Terminal 3 identity)."], ["--host <host,...>", "Which API hosts they're allowed to reach through your enclave."]],
|
|
151
|
+
examples: ["blindfold share --to did:t3n:abc⦠--host api.openai.com"],
|
|
151
152
|
},
|
|
152
|
-
{ name: "revoke", group: "π₯ Team & sharing", usage: "--to <agent-did>", summary: "
|
|
153
|
+
{ name: "revoke", group: "π₯ Team & sharing", usage: "--to <agent-did>", summary: "Take back a teammate's access you granted with `share` β immediate and complete.", flags: [["--to <agent-did>", "The teammate's agent DID to cut off."]], examples: ["blindfold revoke --to did:t3n:abcβ¦"] },
|
|
153
154
|
|
|
154
155
|
// ββ Enclave & admin ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
155
156
|
{ name: "publish", group: "π¦ Enclave & admin", usage: "[--wasm <path>]", summary: "Publish the RustβWASM contract to your tenant (one-time).", flags: [["--wasm <path>", "Path to blindfold_proxy.wasm (defaults to the bundled build)."]], examples: ["blindfold publish"] },
|
|
@@ -203,29 +204,33 @@ export function renderMainHelp(): string {
|
|
|
203
204
|
|
|
204
205
|
const nameW = Math.min(12, COMMANDS.reduce((m, cmd) => Math.max(m, vlen(cmd.name)), 0));
|
|
205
206
|
const descW = Math.max(16, w - 4 - nameW - 2);
|
|
207
|
+
const fit = (s: string, max: number): string => (s.length > max ? s.slice(0, Math.max(1, max - 1)) + "β¦" : s);
|
|
206
208
|
|
|
207
209
|
for (const group of GROUP_ORDER) {
|
|
208
210
|
const cmds = COMMANDS.filter((cmd) => cmd.group === group);
|
|
209
211
|
const lines: string[] = [];
|
|
210
|
-
for (const cmd of cmds) {
|
|
212
|
+
for (const [idx, cmd] of cmds.entries()) {
|
|
213
|
+
if (idx > 0) lines.push(""); // breathing room between commands
|
|
211
214
|
const dl = wrapText(cmd.summary, descW);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
215
|
+
// command name (bright) + summary
|
|
216
|
+
lines.push(pad(c.bold(c.cyan(cmd.name)), nameW) + " " + (dl[0] ?? ""));
|
|
217
|
+
for (let i = 1; i < dl.length; i++) lines.push(pad("", nameW) + " " + (dl[i] ?? ""));
|
|
218
|
+
// one concrete example β the fastest way to "get" the command
|
|
219
|
+
const ex = fit(cmd.examples?.[0] ?? `blindfold ${cmd.name}`, descW - 5);
|
|
220
|
+
lines.push(pad("", nameW) + " " + c.gray("e.g. ") + c.green(ex));
|
|
217
221
|
}
|
|
218
222
|
out.push(boxLines(group, lines));
|
|
219
223
|
out.push("");
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
out.push(
|
|
223
|
-
c.bold("Quick start"),
|
|
224
|
-
` ${c.
|
|
225
|
-
` ${c.
|
|
226
|
-
` ${c.
|
|
227
|
+
c.bold(c.magenta("βΆ Quick start")),
|
|
228
|
+
` ${c.green("blindfold signup --email you@x.com")} ${c.gray("# create a funded testnet tenant")}`,
|
|
229
|
+
` ${c.green("blindfold register --name openai_api_key")} ${c.gray("# seal a key (hidden prompt)")}`,
|
|
230
|
+
` ${c.green("blindfold proxy")} ${c.gray("# point your agent at http://127.0.0.1:8787")}`,
|
|
227
231
|
"",
|
|
228
|
-
`${c.gray("
|
|
232
|
+
`${c.gray("Full flags + more examples for any command:")} ${c.cyan("blindfold <command> --help")}`,
|
|
233
|
+
`${c.gray("Docs:")} ${c.cyan("npmjs.com/package/@fiscalmindset/blindfold")}`,
|
|
229
234
|
"",
|
|
230
235
|
);
|
|
231
236
|
return out.join("\n");
|
package/src/tui.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* wrapping, rounded boxes, and a two-column command table. Used to render a
|
|
4
4
|
* structured `blindfold help` that reflows to the terminal.
|
|
5
5
|
*/
|
|
6
|
-
import { c } from "./color.ts";
|
|
6
|
+
import { c, colorOn } from "./color.ts";
|
|
7
7
|
|
|
8
8
|
/** Terminal width, clamped to a readable range. */
|
|
9
9
|
export function termWidth(): number {
|
|
@@ -124,13 +124,32 @@ export function boxLines(title: string, lines: string[]): string {
|
|
|
124
124
|
const w = termWidth();
|
|
125
125
|
const inner = w - 4;
|
|
126
126
|
const prefix = B.tl + B.h + " ";
|
|
127
|
-
const fill = Math.max(0, w - vlen(prefix) - vlen(title) - 2);
|
|
128
|
-
const out: string[] = [dim(prefix) + c.bold(title) + dim(" " + B.h.repeat(fill) + B.tr)];
|
|
129
|
-
for (const l of lines) out.push(dim(B.v) + " " + pad(l, inner) + " " + dim(B.v));
|
|
127
|
+
const fill = Math.max(0, w - vlen(prefix) - vlen(clip(title, inner)) - 2);
|
|
128
|
+
const out: string[] = [dim(prefix) + c.bold(clip(title, inner)) + dim(" " + B.h.repeat(fill) + B.tr)];
|
|
129
|
+
for (const l of lines) out.push(dim(B.v) + " " + pad(clip(l, inner), inner) + " " + dim(B.v));
|
|
130
130
|
out.push(dim(B.bl + B.h.repeat(w - 2) + B.br));
|
|
131
131
|
return out.join("\n");
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/** Truncate a string to display width `width` (ANSI/emoji aware), appending β¦
|
|
135
|
+
* and a reset so a clipped color can't bleed. No-op if it already fits. */
|
|
136
|
+
export function clip(s: string, width: number): string {
|
|
137
|
+
if (vlen(s) <= width) return s;
|
|
138
|
+
let out = "";
|
|
139
|
+
let wSoFar = 0;
|
|
140
|
+
for (const part of s.split(/(\x1b\[[0-9;]*m)/)) {
|
|
141
|
+
if (part.startsWith("\x1b")) { out += part; continue; }
|
|
142
|
+
for (const ch of part) {
|
|
143
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
144
|
+
const cw = cp === 0x200d || (cp >= 0xfe00 && cp <= 0xfe0f) || (cp >= 0x300 && cp <= 0x36f) ? 0 : isWide(cp) ? 2 : 1;
|
|
145
|
+
if (wSoFar + cw > width - 1) return out + "β¦" + (colorOn ? "\x1b[0m" : "");
|
|
146
|
+
out += ch;
|
|
147
|
+
wSoFar += cw;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out + (colorOn ? "\x1b[0m" : "");
|
|
151
|
+
}
|
|
152
|
+
|
|
134
153
|
/** A plain full-width rule with an optional bold label: "ββ Label βββββ". */
|
|
135
154
|
export function rule(label = ""): string {
|
|
136
155
|
const w = termWidth();
|