@fiscalmindset/blindfold 0.4.4 → 0.4.6

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/blindfold.ts CHANGED
@@ -18,7 +18,7 @@ type Handler = (cmd: string, argv: Argv, cmdArgs: string[]) => Promise<void>;
18
18
 
19
19
  const ROUTES: Record<string, Handler> = {
20
20
  signup: handleAuth, login: handleAuth, logout: handleAuth, whoami: handleAuth,
21
- register: handleSecrets, use: handleSecrets, export: handleSecrets,
21
+ register: handleSecrets, use: handleSecrets, export: handleSecrets, delete: handleSecrets, remove: handleSecrets,
22
22
  rotate: handleLifecycle, rollback: handleLifecycle, versions: handleLifecycle, migrate: handleLifecycle,
23
23
  grant: handleTenant, share: handleTenant, revoke: handleTenant,
24
24
  proxy: handleServe, attest: handleServe, dashboard: handleServe, stats: handleServe, "stats:clear": handleServe,
@@ -5,7 +5,8 @@ import { spawn } from "node:child_process";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { loadBlindfoldEnv, configPath, homeDir, stateDir } from "../src/env.ts";
7
7
  import { keychainAvailable, keychainBackend, keychainSet, keychainDelete } from "../src/keychain.ts";
8
- import { readSecretLine } from "../src/prompt.ts";
8
+ import { readSecretLine, readLine } from "../src/prompt.ts";
9
+ import { c, ok, warn } from "../src/color.ts";
9
10
  import { registerSecret, registerContract } from "../src/register.ts";
10
11
  import { startProxy } from "../src/proxy.ts";
11
12
  import { attest, attestationGate, writePinnedRtmr3 } from "../src/attest.ts";
@@ -13,7 +14,7 @@ import { startDashboard } from "../src/dashboard.ts";
13
14
  import { clearUsage, defaultLogPath, readUsage } from "../src/usage-log.ts";
14
15
  import { runInit, runVerify } from "../src/init.ts";
15
16
  import { runCompat } from "../src/compat.ts";
16
- import { defaultSealedLogPath, readSealed, verifyLedgerChain } from "../src/sealed-ledger.ts";
17
+ import { defaultSealedLogPath, readSealed, removeSealedEntry, verifyLedgerChain } from "../src/sealed-ledger.ts";
17
18
  import { type Argv, die, assetPath, fingerprint, resolveEnvVar } from "./cli-shared.ts";
18
19
 
19
20
  export async function handleSecrets(cmd: string, argv: Argv, cmdArgs: string[]): Promise<void> {
@@ -49,6 +50,44 @@ export async function handleSecrets(cmd: string, argv: Argv, cmdArgs: string[]):
49
50
  console.log(` proxy/SDK: set the key to "__BLINDFOLD__" + route via \`blindfold proxy\`, or \`release("${name}")\` in code`);
50
51
  return;
51
52
  }
53
+ case "delete":
54
+ case "remove": {
55
+ // Delete a sealed secret: empty its value in the enclave (current tenant)
56
+ // AND remove it from the local ledger, re-chaining so `audit` stays valid.
57
+ const name = String(argv.flags.name ?? argv._[1] ?? "");
58
+ if (!name) die("usage: blindfold delete --name <secret> [--yes to skip the prompt]");
59
+
60
+ const inLedger = readSealed().some((e) => e.name === name);
61
+ if (!inLedger) console.error(warn(`⚠ "${name}" isn't in the local ledger — will still try to empty it in the enclave.`));
62
+
63
+ // Confirm (unless --yes/--force). Destructive.
64
+ const skip = Boolean(argv.flags.yes || argv.flags.force || argv.flags.y);
65
+ if (!skip) {
66
+ const ans = (await readLine(`Delete sealed secret ${c.bold(name)}? This empties it in the enclave and removes it from the ledger. Type "yes": `)).trim().toLowerCase();
67
+ if (ans !== "yes" && ans !== "y") { console.log("Aborted — nothing deleted."); return; }
68
+ }
69
+
70
+ // Enclave side (best-effort — a secret on a different/old tenant just won't be found).
71
+ const env = loadBlindfoldEnv();
72
+ let enclaveNote = "";
73
+ try {
74
+ const { openT3Client } = await import("../src/t3-client.ts");
75
+ const client = await openT3Client(env);
76
+ try {
77
+ const how = await client.deleteSecret(name);
78
+ enclaveNote = how === "deleted" ? "enclave entry deleted" : "enclave value emptied (key may remain)";
79
+ } finally { await client.close(); }
80
+ } catch (e) {
81
+ enclaveNote = `enclave not updated (${(e as Error).message.slice(0, 80)}) — ledger still cleaned`;
82
+ }
83
+
84
+ const removed = removeSealedEntry(name);
85
+ console.log(ok(`✓ Deleted "${name}".`));
86
+ console.log(` ${c.gray("ledger:")} ${removed > 0 ? `removed ${removed} entr${removed === 1 ? "y" : "ies"} (re-chained; backup kept)` : "not present"}`);
87
+ console.log(` ${c.gray("enclave:")} ${enclaveNote}`);
88
+ if (inLedger) console.log(c.gray(` Verify: blindfold sealed · blindfold audit`));
89
+ return;
90
+ }
52
91
  case "use": {
53
92
  const name = String(argv.flags.name ?? "");
54
93
  if (!name) {
package/dist/cli.mjs CHANGED
@@ -600,6 +600,18 @@ async function openRealClient(env) {
600
600
  });
601
601
  safeLog("info", { msg: "seeded", name });
602
602
  };
603
+ const deleteSecret = async (name) => {
604
+ const map_name = tenant.canonicalName("secrets");
605
+ try {
606
+ await tenant.executeControl("map-entry-delete", { map_name, key: name });
607
+ safeLog("info", { msg: "deleted", name });
608
+ return "deleted";
609
+ } catch {
610
+ await tenant.executeControl("map-entry-set", { map_name, key: name, value: "" });
611
+ safeLog("info", { msg: "emptied", name });
612
+ return "emptied";
613
+ }
614
+ };
603
615
  const registerContract4 = async (wasm) => {
604
616
  const r = await tenant.contracts.register({
605
617
  tail: CONTRACT_TAIL,
@@ -715,6 +727,7 @@ async function openRealClient(env) {
715
727
  close: async () => {
716
728
  },
717
729
  seedSecret,
730
+ deleteSecret,
718
731
  invokeForward,
719
732
  registerContract: registerContract4,
720
733
  releaseSecret,
@@ -734,6 +747,10 @@ function openMockClient() {
734
747
  if (!value || value.length === 0) throw new Error(`secret ${name} is empty`);
735
748
  safeLog("info", { msg: "mock-seed (value dropped, length only)", name, length: value.length });
736
749
  },
750
+ async deleteSecret(name) {
751
+ safeLog("info", { msg: "mock-delete", name });
752
+ return "deleted";
753
+ },
737
754
  async registerContract(wasm) {
738
755
  safeLog("info", { msg: "mock-register-contract", wasmBytes: wasm.byteLength });
739
756
  return { contractId: "mock-contract-1" };
@@ -880,6 +897,38 @@ function recordSealed(entry) {
880
897
  } catch {
881
898
  }
882
899
  }
900
+ function removeSealedEntry(name) {
901
+ const p = defaultSealedLogPath();
902
+ if (!fs5.existsSync(p)) return 0;
903
+ let removed = 0;
904
+ withFileLockSync(p, () => {
905
+ const all = fs5.readFileSync(p, "utf8").split("\n").filter(Boolean).map((l) => {
906
+ try {
907
+ return JSON.parse(l);
908
+ } catch {
909
+ return null;
910
+ }
911
+ }).filter((e) => e !== null);
912
+ const kept = all.filter((e) => e.name !== name);
913
+ removed = all.length - kept.length;
914
+ if (removed === 0) return;
915
+ try {
916
+ fs5.writeFileSync(`${p}.bak.${Date.now()}`, fs5.readFileSync(p), { mode: 384 });
917
+ } catch {
918
+ }
919
+ const key = ledgerKey();
920
+ let prev = "";
921
+ const rebuilt = kept.map((e) => {
922
+ const core = coreString(e);
923
+ const thisPrev = prev;
924
+ const { hash, alg } = chainHash(key, thisPrev, core);
925
+ prev = hash;
926
+ return { ...e, prev: thisPrev, hash, ...alg ? { alg } : {} };
927
+ });
928
+ fs5.writeFileSync(p, rebuilt.map((e) => JSON.stringify(e)).join("\n") + (rebuilt.length ? "\n" : ""));
929
+ });
930
+ return removed;
931
+ }
883
932
  function readSealed() {
884
933
  const p = defaultSealedLogPath();
885
934
  if (!fs5.existsSync(p)) return [];
@@ -1589,6 +1638,16 @@ var COMMANDS = [
1589
1638
  examples: ["blindfold rollback --name stripe_secret_key"]
1590
1639
  },
1591
1640
  { name: "versions", group: "\u{1F511} Secrets", usage: "[--name <secret>]", summary: "List the snapshots available to roll back to (metadata only).", flags: [["--name <secret>", "Limit to one secret."]], examples: ["blindfold versions --name stripe_secret_key"] },
1641
+ {
1642
+ name: "delete",
1643
+ group: "\u{1F511} Secrets",
1644
+ usage: "--name <secret> [--yes]",
1645
+ aliases: ["remove"],
1646
+ summary: "Delete a sealed secret: empty its value in the enclave AND remove it from the local ledger (re-chained; backup kept). Use if you sealed the wrong thing.",
1647
+ flags: [["--name <secret>", "The sealed secret to delete (required)."], ["--yes", "Skip the confirmation prompt (destructive)."]],
1648
+ examples: ["blindfold delete --name old_api_key"],
1649
+ notes: "Re-seal correctly with `blindfold register` afterward. If the value was a real key, revoke it at the provider too \u2014 deleting here doesn't un-expose anything already leaked."
1650
+ },
1592
1651
  {
1593
1652
  name: "migrate",
1594
1653
  group: "\u{1F511} Secrets",
@@ -1703,8 +1762,12 @@ function renderMainHelp() {
1703
1762
  const dl = wrapText(cmd.summary, descW);
1704
1763
  lines.push(pad(c.bold(c.cyan(cmd.name)), nameW) + " " + (dl[0] ?? ""));
1705
1764
  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));
1765
+ const gutter = pad("", nameW) + " ";
1766
+ if (cmd.usage) {
1767
+ lines.push(gutter + c.gray(pad("usage", 5)) + " " + c.cyan(fit(`blindfold ${cmd.name} ${cmd.usage}`, descW - 6)));
1768
+ }
1769
+ const ex = fit(cmd.examples?.[0] ?? `blindfold ${cmd.name}`, descW - 6);
1770
+ lines.push(gutter + c.gray(pad("e.g.", 5)) + " " + c.green(ex));
1708
1771
  }
1709
1772
  out.push(boxLines(group, lines));
1710
1773
  out.push("");
@@ -1919,9 +1982,10 @@ async function handleAuth(cmd, argv, cmdArgs) {
1919
1982
 
1920
1983
  // bin/cmd-secrets.ts
1921
1984
  init_env();
1922
- init_register();
1985
+ init_prompt();
1923
1986
  import fs8 from "node:fs";
1924
1987
  import { spawn } from "node:child_process";
1988
+ init_register();
1925
1989
 
1926
1990
  // src/attest.ts
1927
1991
  init_env();
@@ -2050,6 +2114,7 @@ async function attestationGate(opts = {}) {
2050
2114
  }
2051
2115
 
2052
2116
  // bin/cmd-secrets.ts
2117
+ init_sealed_ledger();
2053
2118
  async function handleSecrets(cmd, argv, cmdArgs) {
2054
2119
  switch (cmd) {
2055
2120
  case "register": {
@@ -2080,6 +2145,41 @@ async function handleSecrets(cmd, argv, cmdArgs) {
2080
2145
  console.log(` proxy/SDK: set the key to "__BLINDFOLD__" + route via \`blindfold proxy\`, or \`release("${name}")\` in code`);
2081
2146
  return;
2082
2147
  }
2148
+ case "delete":
2149
+ case "remove": {
2150
+ const name = String(argv.flags.name ?? argv._[1] ?? "");
2151
+ if (!name) die("usage: blindfold delete --name <secret> [--yes to skip the prompt]");
2152
+ const inLedger = readSealed().some((e) => e.name === name);
2153
+ if (!inLedger) console.error(warn(`\u26A0 "${name}" isn't in the local ledger \u2014 will still try to empty it in the enclave.`));
2154
+ const skip = Boolean(argv.flags.yes || argv.flags.force || argv.flags.y);
2155
+ if (!skip) {
2156
+ const ans = (await readLine(`Delete sealed secret ${c.bold(name)}? This empties it in the enclave and removes it from the ledger. Type "yes": `)).trim().toLowerCase();
2157
+ if (ans !== "yes" && ans !== "y") {
2158
+ console.log("Aborted \u2014 nothing deleted.");
2159
+ return;
2160
+ }
2161
+ }
2162
+ const env = loadBlindfoldEnv();
2163
+ let enclaveNote = "";
2164
+ try {
2165
+ const { openT3Client: openT3Client2 } = await Promise.resolve().then(() => (init_t3_client(), t3_client_exports));
2166
+ const client = await openT3Client2(env);
2167
+ try {
2168
+ const how = await client.deleteSecret(name);
2169
+ enclaveNote = how === "deleted" ? "enclave entry deleted" : "enclave value emptied (key may remain)";
2170
+ } finally {
2171
+ await client.close();
2172
+ }
2173
+ } catch (e) {
2174
+ enclaveNote = `enclave not updated (${e.message.slice(0, 80)}) \u2014 ledger still cleaned`;
2175
+ }
2176
+ const removed = removeSealedEntry(name);
2177
+ console.log(ok(`\u2713 Deleted "${name}".`));
2178
+ console.log(` ${c.gray("ledger:")} ${removed > 0 ? `removed ${removed} entr${removed === 1 ? "y" : "ies"} (re-chained; backup kept)` : "not present"}`);
2179
+ console.log(` ${c.gray("enclave:")} ${enclaveNote}`);
2180
+ if (inLedger) console.log(c.gray(` Verify: blindfold sealed \xB7 blindfold audit`));
2181
+ return;
2182
+ }
2083
2183
  case "use": {
2084
2184
  const name = String(argv.flags.name ?? "");
2085
2185
  if (!name) {
@@ -4620,6 +4720,8 @@ var ROUTES = {
4620
4720
  register: handleSecrets,
4621
4721
  use: handleSecrets,
4622
4722
  export: handleSecrets,
4723
+ delete: handleSecrets,
4724
+ remove: handleSecrets,
4623
4725
  rotate: handleLifecycle,
4624
4726
  rollback: handleLifecycle,
4625
4727
  versions: handleLifecycle,
@@ -479,6 +479,18 @@ async function openRealClient(env) {
479
479
  });
480
480
  safeLog("info", { msg: "seeded", name });
481
481
  };
482
+ const deleteSecret = async (name) => {
483
+ const map_name = tenant.canonicalName("secrets");
484
+ try {
485
+ await tenant.executeControl("map-entry-delete", { map_name, key: name });
486
+ safeLog("info", { msg: "deleted", name });
487
+ return "deleted";
488
+ } catch {
489
+ await tenant.executeControl("map-entry-set", { map_name, key: name, value: "" });
490
+ safeLog("info", { msg: "emptied", name });
491
+ return "emptied";
492
+ }
493
+ };
482
494
  const registerContract2 = async (wasm) => {
483
495
  const r = await tenant.contracts.register({
484
496
  tail: CONTRACT_TAIL,
@@ -594,6 +606,7 @@ async function openRealClient(env) {
594
606
  close: async () => {
595
607
  },
596
608
  seedSecret,
609
+ deleteSecret,
597
610
  invokeForward,
598
611
  registerContract: registerContract2,
599
612
  releaseSecret,
@@ -613,6 +626,10 @@ function openMockClient() {
613
626
  if (!value || value.length === 0) throw new Error(`secret ${name} is empty`);
614
627
  safeLog("info", { msg: "mock-seed (value dropped, length only)", name, length: value.length });
615
628
  },
629
+ async deleteSecret(name) {
630
+ safeLog("info", { msg: "mock-delete", name });
631
+ return "deleted";
632
+ },
616
633
  async registerContract(wasm) {
617
634
  safeLog("info", { msg: "mock-register-contract", wasmBytes: wasm.byteLength });
618
635
  return { contractId: "mock-contract-1" };
@@ -387,6 +387,18 @@ async function openRealClient(env) {
387
387
  });
388
388
  safeLog("info", { msg: "seeded", name });
389
389
  };
390
+ const deleteSecret = async (name) => {
391
+ const map_name = tenant.canonicalName("secrets");
392
+ try {
393
+ await tenant.executeControl("map-entry-delete", { map_name, key: name });
394
+ safeLog("info", { msg: "deleted", name });
395
+ return "deleted";
396
+ } catch {
397
+ await tenant.executeControl("map-entry-set", { map_name, key: name, value: "" });
398
+ safeLog("info", { msg: "emptied", name });
399
+ return "emptied";
400
+ }
401
+ };
390
402
  const registerContract = async (wasm) => {
391
403
  const r = await tenant.contracts.register({
392
404
  tail: CONTRACT_TAIL,
@@ -502,6 +514,7 @@ async function openRealClient(env) {
502
514
  close: async () => {
503
515
  },
504
516
  seedSecret,
517
+ deleteSecret,
505
518
  invokeForward,
506
519
  registerContract,
507
520
  releaseSecret,
@@ -521,6 +534,10 @@ function openMockClient() {
521
534
  if (!value || value.length === 0) throw new Error(`secret ${name} is empty`);
522
535
  safeLog("info", { msg: "mock-seed (value dropped, length only)", name, length: value.length });
523
536
  },
537
+ async deleteSecret(name) {
538
+ safeLog("info", { msg: "mock-delete", name });
539
+ return "deleted";
540
+ },
524
541
  async registerContract(wasm) {
525
542
  safeLog("info", { msg: "mock-register-contract", wasmBytes: wasm.byteLength });
526
543
  return { contractId: "mock-contract-1" };
@@ -524,6 +524,18 @@ async function openRealClient(env) {
524
524
  });
525
525
  safeLog("info", { msg: "seeded", name });
526
526
  };
527
+ const deleteSecret = async (name) => {
528
+ const map_name = tenant.canonicalName("secrets");
529
+ try {
530
+ await tenant.executeControl("map-entry-delete", { map_name, key: name });
531
+ safeLog("info", { msg: "deleted", name });
532
+ return "deleted";
533
+ } catch {
534
+ await tenant.executeControl("map-entry-set", { map_name, key: name, value: "" });
535
+ safeLog("info", { msg: "emptied", name });
536
+ return "emptied";
537
+ }
538
+ };
527
539
  const registerContract2 = async (wasm) => {
528
540
  const r = await tenant.contracts.register({
529
541
  tail: CONTRACT_TAIL,
@@ -639,6 +651,7 @@ async function openRealClient(env) {
639
651
  close: async () => {
640
652
  },
641
653
  seedSecret,
654
+ deleteSecret,
642
655
  invokeForward,
643
656
  registerContract: registerContract2,
644
657
  releaseSecret,
@@ -658,6 +671,10 @@ function openMockClient() {
658
671
  if (!value || value.length === 0) throw new Error(`secret ${name} is empty`);
659
672
  safeLog("info", { msg: "mock-seed (value dropped, length only)", name, length: value.length });
660
673
  },
674
+ async deleteSecret(name) {
675
+ safeLog("info", { msg: "mock-delete", name });
676
+ return "deleted";
677
+ },
661
678
  async registerContract(wasm) {
662
679
  safeLog("info", { msg: "mock-register-contract", wasmBytes: wasm.byteLength });
663
680
  return { contractId: "mock-contract-1" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiscalmindset/blindfold",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "type": "module",
5
5
  "description": "Make any AI agent's API keys un-leakable, by sealing them inside a Terminal 3 TDX enclave.",
6
6
  "license": "MIT",
package/src/help.ts CHANGED
@@ -103,6 +103,13 @@ export const COMMANDS: CmdDef[] = [
103
103
  examples: ["blindfold rollback --name stripe_secret_key"],
104
104
  },
105
105
  { name: "versions", group: "🔑 Secrets", usage: "[--name <secret>]", summary: "List the snapshots available to roll back to (metadata only).", flags: [["--name <secret>", "Limit to one secret."]], examples: ["blindfold versions --name stripe_secret_key"] },
106
+ {
107
+ name: "delete", group: "🔑 Secrets", usage: "--name <secret> [--yes]", aliases: ["remove"],
108
+ summary: "Delete a sealed secret: empty its value in the enclave AND remove it from the local ledger (re-chained; backup kept). Use if you sealed the wrong thing.",
109
+ flags: [["--name <secret>", "The sealed secret to delete (required)."], ["--yes", "Skip the confirmation prompt (destructive)."]],
110
+ examples: ["blindfold delete --name old_api_key"],
111
+ notes: "Re-seal correctly with `blindfold register` afterward. If the value was a real key, revoke it at the provider too — deleting here doesn't un-expose anything already leaked.",
112
+ },
106
113
  {
107
114
  name: "migrate", group: "🔑 Secrets", usage: "[--dry-run] [--keep]",
108
115
  summary: "Seal every secret in .env at once, then remove the plaintext lines (backup kept).",
@@ -215,9 +222,14 @@ export function renderMainHelp(): string {
215
222
  // command name (bright) + summary
216
223
  lines.push(pad(c.bold(c.cyan(cmd.name)), nameW) + " " + (dl[0] ?? ""));
217
224
  for (let i = 1; i < dl.length; i++) lines.push(pad("", nameW) + " " + (dl[i] ?? ""));
225
+ const gutter = pad("", nameW) + " ";
226
+ // the shape — with <placeholders> — so you learn the pattern (only if it takes args)
227
+ if (cmd.usage) {
228
+ lines.push(gutter + c.gray(pad("usage", 5)) + " " + c.cyan(fit(`blindfold ${cmd.name} ${cmd.usage}`, descW - 6)));
229
+ }
218
230
  // 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));
231
+ const ex = fit(cmd.examples?.[0] ?? `blindfold ${cmd.name}`, descW - 6);
232
+ lines.push(gutter + c.gray(pad("e.g.", 5)) + " " + c.green(ex));
221
233
  }
222
234
  out.push(boxLines(group, lines));
223
235
  out.push("");
@@ -111,6 +111,40 @@ export function recordSealed(entry: SealedEntry): void {
111
111
  }
112
112
  }
113
113
 
114
+ /**
115
+ * Remove every entry with the given name from the ledger and RE-CHAIN what
116
+ * remains, so the tamper-evident hash-chain stays valid (an owner-initiated
117
+ * delete is legitimate, unlike a stealth edit). Backs the old ledger up first.
118
+ * Returns the number of entries removed.
119
+ */
120
+ export function removeSealedEntry(name: string): number {
121
+ const p = defaultSealedLogPath();
122
+ if (!fs.existsSync(p)) return 0;
123
+ let removed = 0;
124
+ withFileLockSync(p, () => {
125
+ const all = fs.readFileSync(p, "utf8").split("\n").filter(Boolean)
126
+ .map((l) => { try { return JSON.parse(l) as SealedEntry; } catch { return null; } })
127
+ .filter((e): e is SealedEntry => e !== null);
128
+ const kept = all.filter((e) => e.name !== name);
129
+ removed = all.length - kept.length;
130
+ if (removed === 0) return;
131
+ // Backup the pre-delete ledger (0600) so nothing is ever silently lost.
132
+ try { fs.writeFileSync(`${p}.bak.${Date.now()}`, fs.readFileSync(p), { mode: 0o600 }); } catch { /* best effort */ }
133
+ // Re-chain the survivors into a fresh, valid HMAC chain.
134
+ const key = ledgerKey();
135
+ let prev = "";
136
+ const rebuilt = kept.map((e) => {
137
+ const core = coreString(e); // preserves original t/name/source/length/mode/tenant/map
138
+ const thisPrev = prev; // the PREVIOUS entry's hash (not this one's)
139
+ const { hash, alg } = chainHash(key, thisPrev, core);
140
+ prev = hash;
141
+ return { ...e, prev: thisPrev, hash, ...(alg ? { alg } : {}) };
142
+ });
143
+ fs.writeFileSync(p, rebuilt.map((e) => JSON.stringify(e)).join("\n") + (rebuilt.length ? "\n" : ""));
144
+ });
145
+ return removed;
146
+ }
147
+
114
148
  export function readSealed(): SealedEntry[] {
115
149
  const p = defaultSealedLogPath();
116
150
  if (!fs.existsSync(p)) return [];
package/src/t3-client.ts CHANGED
@@ -141,6 +141,12 @@ async function loadSdk(): Promise<T3Sdk> {
141
141
  export interface T3ClientHandle {
142
142
  close: () => Promise<void>;
143
143
  seedSecret: (name: string, value: string) => Promise<void>;
144
+ /**
145
+ * Delete a sealed secret from the enclave's secrets map. Tries a true
146
+ * map-entry-delete; if the control plane lacks it, overwrites the value with
147
+ * empty so the plaintext is gone. Returns which happened.
148
+ */
149
+ deleteSecret: (name: string) => Promise<"deleted" | "emptied">;
144
150
  invokeForward: (req: ForwardRequest) => Promise<ForwardResponse>;
145
151
  registerContract: (wasm: Uint8Array) => Promise<{ contractId: string | number }>;
146
152
  /**
@@ -364,6 +370,22 @@ async function openRealClient(env: BlindfoldEnv): Promise<T3ClientHandle> {
364
370
  safeLog("info", { msg: "seeded", name });
365
371
  };
366
372
 
373
+ const deleteSecret = async (name: string): Promise<"deleted" | "emptied"> => {
374
+ const map_name = tenant.canonicalName("secrets");
375
+ try {
376
+ // Preferred: a true delete of the map entry (removes key + value).
377
+ await tenant.executeControl("map-entry-delete", { map_name, key: name });
378
+ safeLog("info", { msg: "deleted", name });
379
+ return "deleted";
380
+ } catch {
381
+ // Fallback: the control plane may not expose delete — overwrite the value
382
+ // with empty so the plaintext is gone (the key may remain).
383
+ await tenant.executeControl("map-entry-set", { map_name, key: name, value: "" });
384
+ safeLog("info", { msg: "emptied", name });
385
+ return "emptied";
386
+ }
387
+ };
388
+
367
389
  const registerContract = async (wasm: Uint8Array): Promise<{ contractId: string | number }> => {
368
390
  const r = (await tenant.contracts.register({
369
391
  tail: CONTRACT_TAIL,
@@ -509,6 +531,7 @@ async function openRealClient(env: BlindfoldEnv): Promise<T3ClientHandle> {
509
531
  /* SDK has no close() */
510
532
  },
511
533
  seedSecret,
534
+ deleteSecret,
512
535
  invokeForward,
513
536
  registerContract,
514
537
  releaseSecret,
@@ -532,6 +555,10 @@ function openMockClient(): T3ClientHandle {
532
555
  if (!value || value.length === 0) throw new Error(`secret ${name} is empty`);
533
556
  safeLog("info", { msg: "mock-seed (value dropped, length only)", name, length: value.length });
534
557
  },
558
+ async deleteSecret(name) {
559
+ safeLog("info", { msg: "mock-delete", name });
560
+ return "deleted";
561
+ },
535
562
  async registerContract(wasm) {
536
563
  safeLog("info", { msg: "mock-register-contract", wasmBytes: wasm.byteLength });
537
564
  return { contractId: "mock-contract-1" };