@akasecurity/cli 0.9.3 → 0.9.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.
Files changed (169) hide show
  1. package/dist/cli.js +1776 -215
  2. package/package.json +5 -5
  3. package/web-ui/package.json +6 -2
  4. package/web-ui/packages/dashboard-ui/package.json +2 -1
  5. package/web-ui/packages/detections/package.json +1 -1
  6. package/web-ui/packages/local-ops/package.json +1 -1
  7. package/web-ui/packages/persistence/package.json +1 -1
  8. package/web-ui/packages/plugin-sdk/package.json +1 -1
  9. package/web-ui/packages/schema/package.json +1 -1
  10. package/web-ui/packages/ui-kit/package.json +1 -1
  11. package/web-ui/web-ui/.next/BUILD_ID +1 -1
  12. package/web-ui/web-ui/.next/app-build-manifest.json +135 -126
  13. package/web-ui/web-ui/.next/app-path-routes-manifest.json +7 -6
  14. package/web-ui/web-ui/.next/build-manifest.json +14 -14
  15. package/web-ui/web-ui/.next/prerender-manifest.json +10 -10
  16. package/web-ui/web-ui/.next/required-server-files.json +1 -2
  17. package/web-ui/web-ui/.next/routes-manifest.json +6 -0
  18. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
  19. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js.nft.json +1 -1
  20. package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
  21. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
  22. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js.nft.json +1 -1
  23. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
  24. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
  25. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js.nft.json +1 -1
  26. package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
  27. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
  28. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
  29. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
  30. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +2 -2
  31. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
  32. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
  33. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +2 -2
  34. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
  35. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
  36. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +2 -2
  37. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js.nft.json +1 -1
  38. package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
  39. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +2 -2
  40. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js.nft.json +1 -1
  41. package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
  42. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
  43. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js.nft.json +1 -1
  44. package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
  45. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +2 -2
  46. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
  47. package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
  48. package/web-ui/web-ui/.next/server/app/(app)/security/page.js +2 -2
  49. package/web-ui/web-ui/.next/server/app/(app)/security/page.js.nft.json +1 -1
  50. package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
  51. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
  52. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js.nft.json +1 -1
  53. package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
  54. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
  55. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
  56. package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
  57. package/web-ui/web-ui/.next/server/app/(app)/vault/page.js +2 -0
  58. package/web-ui/web-ui/.next/server/app/(app)/vault/page.js.nft.json +1 -0
  59. package/web-ui/web-ui/.next/server/app/(app)/vault/page_client-reference-manifest.js +1 -0
  60. package/web-ui/web-ui/.next/server/app/_not-found/page.js +2 -2
  61. package/web-ui/web-ui/.next/server/app/_not-found/page.js.nft.json +1 -1
  62. package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  63. package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
  64. package/web-ui/web-ui/.next/server/app/_not-found.rsc +10 -10
  65. package/web-ui/web-ui/.next/server/app/icon.svg/route.js +1 -1
  66. package/web-ui/web-ui/.next/server/app/icon.svg/route.js.nft.json +1 -1
  67. package/web-ui/web-ui/.next/server/app/index.html +1 -1
  68. package/web-ui/web-ui/.next/server/app/index.rsc +10 -10
  69. package/web-ui/web-ui/.next/server/app/page.js +2 -2
  70. package/web-ui/web-ui/.next/server/app/page.js.nft.json +1 -1
  71. package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
  72. package/web-ui/web-ui/.next/server/app-paths-manifest.json +7 -6
  73. package/web-ui/web-ui/.next/server/chunks/243.js +1 -0
  74. package/web-ui/web-ui/.next/server/chunks/28.js +1 -0
  75. package/web-ui/web-ui/.next/server/chunks/330.js +1 -0
  76. package/web-ui/web-ui/.next/server/chunks/{924.js → 334.js} +1 -1
  77. package/web-ui/web-ui/.next/server/chunks/447.js +6 -0
  78. package/web-ui/web-ui/.next/server/chunks/514.js +755 -0
  79. package/web-ui/web-ui/.next/server/chunks/539.js +22 -0
  80. package/web-ui/web-ui/.next/server/chunks/628.js +84 -0
  81. package/web-ui/web-ui/.next/server/chunks/817.js +1 -0
  82. package/web-ui/web-ui/.next/server/chunks/830.js +750 -123
  83. package/web-ui/web-ui/.next/server/chunks/935.js +11 -0
  84. package/web-ui/web-ui/.next/server/chunks/949.js +123 -0
  85. package/web-ui/web-ui/.next/server/functions-config-manifest.json +5 -4
  86. package/web-ui/web-ui/.next/server/middleware-build-manifest.js +1 -1
  87. package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
  88. package/web-ui/web-ui/.next/server/middleware.js +2 -2
  89. package/web-ui/web-ui/.next/server/pages/404.html +1 -1
  90. package/web-ui/web-ui/.next/server/pages/500.html +1 -1
  91. package/web-ui/web-ui/.next/server/pages/_app.js +1 -1
  92. package/web-ui/web-ui/.next/server/pages/_app.js.nft.json +1 -1
  93. package/web-ui/web-ui/.next/server/pages/_document.js +1 -1
  94. package/web-ui/web-ui/.next/server/pages/_document.js.nft.json +1 -1
  95. package/web-ui/web-ui/.next/server/pages/_error.js +3 -3
  96. package/web-ui/web-ui/.next/server/pages/_error.js.nft.json +1 -1
  97. package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
  98. package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
  99. package/web-ui/web-ui/.next/static/cVKs25XbQsC1083sU5GOD/_buildManifest.js +1 -0
  100. package/web-ui/web-ui/.next/static/chunks/321-46ce36c1ef95d231.js +1 -0
  101. package/web-ui/web-ui/.next/static/chunks/501-939e9e83b25a6293.js +1 -0
  102. package/web-ui/web-ui/.next/static/chunks/84-8205054e8901dbde.js +62 -0
  103. package/web-ui/web-ui/.next/static/chunks/89-d23f5e27ab3b468f.js +1 -0
  104. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-e3ce4be93d2f8ac5.js +1 -0
  105. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-8bb13b53c7bde504.js +1 -0
  106. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-07307e6d8ddb5cbf.js +1 -0
  107. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-aecbc7b6fbd143c2.js +1 -0
  108. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-1d468a1a0cf12aea.js +1 -0
  109. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-863e759b7e26c759.js +1 -0
  110. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-7636a56ffa40d5c6.js +1 -0
  111. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-c80a692b9d05f321.js +1 -0
  112. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-01e6dceb36cb882f.js +1 -0
  113. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-d98e303b17524dad.js +1 -0
  114. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-178c53b1f56b5e97.js +1 -0
  115. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-7f69b2579337c729.js +1 -0
  116. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-7019a1567cd9aadd.js +1 -0
  117. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-90ce0ad1b9403f48.js +1 -0
  118. package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/page-7ef77f76ff07ea1f.js +1 -0
  119. package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-3cf97f2dd544e8cb.js +1 -0
  120. package/web-ui/web-ui/.next/static/chunks/app/layout-f1727d7b45e0779b.js +1 -0
  121. package/web-ui/web-ui/.next/static/chunks/app/page-3fbfddda391d9939.js +1 -0
  122. package/web-ui/web-ui/.next/static/chunks/{dddf6c0a-f09ab3fc84be1c52.js → d043c60e-31360d0d5cb78983.js} +1 -1
  123. package/web-ui/web-ui/.next/static/chunks/framework-3444de8758288822.js +1 -0
  124. package/web-ui/web-ui/.next/static/chunks/main-app-9e734a14592bc9c4.js +1 -0
  125. package/web-ui/web-ui/.next/static/chunks/main-b13545f01f111855.js +1 -0
  126. package/web-ui/web-ui/.next/static/chunks/pages/_app-7f0d920cc5af1c9f.js +1 -0
  127. package/web-ui/web-ui/.next/static/chunks/pages/_error-3d8b61361c4b2c6b.js +1 -0
  128. package/web-ui/web-ui/.next/static/chunks/{webpack-ea80c9c368a8364e.js → webpack-61986b0a62ba55ed.js} +1 -1
  129. package/web-ui/web-ui/.next/static/css/10425160b1a85ff4.css +2 -0
  130. package/web-ui/web-ui/package.json +1 -1
  131. package/web-ui/web-ui/server.js +1 -1
  132. package/web-ui/web-ui/.next/server/chunks/100.js +0 -1
  133. package/web-ui/web-ui/.next/server/chunks/142.js +0 -700
  134. package/web-ui/web-ui/.next/server/chunks/472.js +0 -84
  135. package/web-ui/web-ui/.next/server/chunks/723.js +0 -22
  136. package/web-ui/web-ui/.next/server/chunks/75.js +0 -6
  137. package/web-ui/web-ui/.next/server/chunks/850.js +0 -1
  138. package/web-ui/web-ui/.next/server/chunks/853.js +0 -698
  139. package/web-ui/web-ui/.next/server/chunks/871.js +0 -1
  140. package/web-ui/web-ui/.next/server/chunks/959.js +0 -9
  141. package/web-ui/web-ui/.next/static/X_Z1LZQpnGXTuZXKkv4Vb/_buildManifest.js +0 -1
  142. package/web-ui/web-ui/.next/static/chunks/137-8f757e04d1877a6c.js +0 -1
  143. package/web-ui/web-ui/.next/static/chunks/517-a4643cfd51efbca4.js +0 -1
  144. package/web-ui/web-ui/.next/static/chunks/613-734dcad67e1fa08c.js +0 -1
  145. package/web-ui/web-ui/.next/static/chunks/784-25fa9bc074877c93.js +0 -62
  146. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-72558bcb945a0abc.js +0 -1
  147. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-540aa3dbe1c0b3ad.js +0 -1
  148. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-8fac90d5b77b7f89.js +0 -1
  149. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-06ee1868466199df.js +0 -1
  150. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-8b2ea0bbbc4fc69c.js +0 -1
  151. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-2aa604a6c36786fc.js +0 -1
  152. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-28f48db02051d88c.js +0 -1
  153. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-15bc43471843cf2e.js +0 -1
  154. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-f034effcd10a332b.js +0 -1
  155. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-de465eacbb359987.js +0 -1
  156. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-fa10c50b1436102d.js +0 -1
  157. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-a61b90eb3a575f85.js +0 -1
  158. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-be0284caa1e78227.js +0 -1
  159. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-eec4e54274dcbb4d.js +0 -1
  160. package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-26662e9d7ee32abc.js +0 -1
  161. package/web-ui/web-ui/.next/static/chunks/app/layout-e531d303d8c8a313.js +0 -1
  162. package/web-ui/web-ui/.next/static/chunks/app/page-3ffe075f223c0af9.js +0 -1
  163. package/web-ui/web-ui/.next/static/chunks/framework-f8855380599951ba.js +0 -1
  164. package/web-ui/web-ui/.next/static/chunks/main-app-7291de73392cac56.js +0 -1
  165. package/web-ui/web-ui/.next/static/chunks/main-ff0082770df07ddc.js +0 -1
  166. package/web-ui/web-ui/.next/static/chunks/pages/_app-56d1709db70ee49f.js +0 -1
  167. package/web-ui/web-ui/.next/static/chunks/pages/_error-63dd320a91a40541.js +0 -1
  168. package/web-ui/web-ui/.next/static/css/a30725ee12aca680.css +0 -2
  169. /package/web-ui/web-ui/.next/static/{X_Z1LZQpnGXTuZXKkv4Vb → cVKs25XbQsC1083sU5GOD}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -751,7 +751,7 @@ function installAgentPlugin(agentId, mode = "capture") {
751
751
  }
752
752
 
753
753
  // ../packages/local-ops/src/egress-record.ts
754
- import { statSync as statSync4 } from "fs";
754
+ import { statSync as statSync5 } from "fs";
755
755
  import { dirname as dirname4, relative as relative2, resolve } from "path";
756
756
 
757
757
  // ../packages/detections/src/egress/registry.ts
@@ -1855,9 +1855,9 @@ function extractGoMod(text) {
1855
1855
  let blockKeyword = null;
1856
1856
  eachLine(text, (rawLine, lineNumber) => {
1857
1857
  if (blockKeyword === null) {
1858
- const open = GO_BLOCK_OPEN.exec(rawLine)?.[1];
1859
- if (open !== void 0) {
1860
- blockKeyword = open;
1858
+ const open2 = GO_BLOCK_OPEN.exec(rawLine)?.[1];
1859
+ if (open2 !== void 0) {
1860
+ blockKeyword = open2;
1861
1861
  return;
1862
1862
  }
1863
1863
  const path = GO_REQUIRE_SINGLE_LINE.exec(rawLine)?.[1];
@@ -2375,12 +2375,12 @@ function redact(text, findings) {
2375
2375
  const regions = [];
2376
2376
  for (const f of sorted) {
2377
2377
  const rank = SEVERITY_RANK[f.severity];
2378
- const open = regions[regions.length - 1];
2379
- if (open && f.span.start < open.end) {
2380
- open.end = Math.max(open.end, f.span.end);
2381
- if (rank > open.rank) {
2382
- open.rank = rank;
2383
- open.category = f.category;
2378
+ const open2 = regions[regions.length - 1];
2379
+ if (open2 && f.span.start < open2.end) {
2380
+ open2.end = Math.max(open2.end, f.span.end);
2381
+ if (rank > open2.rank) {
2382
+ open2.rank = rank;
2383
+ open2.category = f.category;
2384
2384
  }
2385
2385
  } else {
2386
2386
  regions.push({ start: f.span.start, end: f.span.end, category: f.category, rank });
@@ -2409,94 +2409,6 @@ function maskMatch(raw) {
2409
2409
  return `${raw.charAt(0)}${"*".repeat(6)}${raw.charAt(raw.length - 1)}`;
2410
2410
  }
2411
2411
 
2412
- // ../packages/detections/src/posture/config-posture.ts
2413
- var RULE_VERSION = "1";
2414
- var KNOWN_TOOLS = /* @__PURE__ */ new Set([
2415
- "prettier",
2416
- "eslint",
2417
- "biome",
2418
- "black",
2419
- "ruff",
2420
- "gofmt",
2421
- "rustfmt",
2422
- "node",
2423
- "npx",
2424
- "npm",
2425
- "pnpm",
2426
- "yarn",
2427
- "make",
2428
- "git",
2429
- "jq",
2430
- "echo"
2431
- ]);
2432
- var EGRESS_RE = /\b(curl|wget|nc|ncat|netcat|ssh|scp|rsync)\b|https?:\/\//;
2433
- var MUTATION_RE = /(--write|--fix|\s-w\b|\s-i\b|(?<![\d&>])>\s?\S)/;
2434
- var CONFIG_POSTURE_RULES = [
2435
- {
2436
- ruleId: "hook-conflict",
2437
- version: RULE_VERSION,
2438
- name: "Overlapping hooks \u2014 run order is undefined",
2439
- category: "config",
2440
- severity: "medium",
2441
- definition: JSON.stringify({
2442
- kind: "hook-conflict",
2443
- mutation: MUTATION_RE.source,
2444
- note: "two hooks on the same event with overlapping matchers, both mutating the tool target"
2445
- })
2446
- },
2447
- {
2448
- ruleId: "hook-unknown",
2449
- version: RULE_VERSION,
2450
- name: "Unknown hook \u2014 not attributable to a plugin or known tool",
2451
- category: "config",
2452
- // medium, not high: with a deliberately tiny allowlist this v1 heuristic
2453
- // fires on ordinary interpreter hooks (python3 x.py) and compound commands
2454
- // (cd … && tool); 'high' is reserved for egress, which is genuinely
2455
- // dangerous. Revisit when the known-hooks registry replaces the allowlist.
2456
- severity: "medium",
2457
- definition: JSON.stringify({ kind: "hook-unknown", knownTools: [...KNOWN_TOOLS] })
2458
- },
2459
- {
2460
- ruleId: "hook-external-egress",
2461
- version: RULE_VERSION,
2462
- name: "Hook command can send data externally",
2463
- category: "config",
2464
- severity: "high",
2465
- definition: JSON.stringify({ kind: "hook-external-egress", pattern: EGRESS_RE.source })
2466
- }
2467
- ];
2468
-
2469
- // ../packages/detections/src/security/redos-probe.ts
2470
- var EXPONENTIAL_UNITS = [
2471
- "a",
2472
- "0",
2473
- " ",
2474
- "x",
2475
- "ab",
2476
- "a.",
2477
- "a-",
2478
- "a_",
2479
- "a@",
2480
- "a/",
2481
- "a:",
2482
- "a=",
2483
- "a;",
2484
- "aA0",
2485
- " "
2486
- ];
2487
- var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
2488
- (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
2489
- );
2490
- var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
2491
- (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
2492
- );
2493
-
2494
- // ../packages/persistence/src/database.ts
2495
- import { randomUUID as randomUUID8 } from "crypto";
2496
- import { existsSync as existsSync2, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
2497
- import { join as join2, sep } from "path";
2498
- import { DatabaseSync } from "node:sqlite";
2499
-
2500
2412
  // ../packages/schema/src/drizzle/sqlite-ddl.ts
2501
2413
  var SQLITE_MIGRATIONS = [
2502
2414
  {
@@ -2558,6 +2470,22 @@ var SQLITE_MIGRATIONS = [
2558
2470
  {
2559
2471
  tag: "0014_drop_legacy_events_findings",
2560
2472
  sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
2473
+ },
2474
+ {
2475
+ tag: "0015_busy_vengeance",
2476
+ sql: "CREATE TABLE `secret_vault` (\n `pointer_id` text PRIMARY KEY NOT NULL,\n `value_fingerprint` text NOT NULL,\n `fingerprint_key_version` integer NOT NULL,\n `key_version` integer NOT NULL,\n `category` text NOT NULL,\n `rule_id` text NOT NULL,\n `masked_match` text NOT NULL,\n `provider` text,\n `ciphertext` text NOT NULL,\n `nonce` text NOT NULL,\n `auth_tag` text NOT NULL,\n `occurrence_count` integer DEFAULT 1 NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_value` ON `secret_vault` (`value_fingerprint`);--> statement-breakpoint\nCREATE TABLE `secret_vault_deref` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `at` integer NOT NULL,\n `target` text NOT NULL,\n `reason` text NOT NULL,\n `outcome` text NOT NULL,\n `grant_id` text,\n `pointer_count` integer DEFAULT 1 NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_pointer` ON `secret_vault_deref` (`pointer_id`);--> statement-breakpoint\nCREATE INDEX `idx_secret_vault_deref_reason_at` ON `secret_vault_deref` (`reason`,`at`);"
2477
+ },
2478
+ {
2479
+ tag: "0016_breezy_zodiak",
2480
+ sql: "ALTER TABLE `exceptions` ADD `capability` text DEFAULT 'suppress' NOT NULL;"
2481
+ },
2482
+ {
2483
+ tag: "0017_rainy_kat_farrell",
2484
+ sql: "CREATE TABLE `secret_vault_sighting` (\n `id` text PRIMARY KEY NOT NULL,\n `pointer_id` text NOT NULL,\n `location` text NOT NULL,\n `kind` text NOT NULL,\n `first_seen` integer NOT NULL,\n `last_seen` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_secret_vault_sighting` ON `secret_vault_sighting` (`pointer_id`,`location`);"
2485
+ },
2486
+ {
2487
+ tag: "0018_serious_tana_nile",
2488
+ sql: "ALTER TABLE `secret_vault` ADD `format_version` integer DEFAULT 2 NOT NULL;"
2561
2489
  }
2562
2490
  ];
2563
2491
 
@@ -18337,6 +18265,7 @@ var ExceptionConditions = external_exports.object({
18337
18265
  sourceTool: external_exports.string().optional(),
18338
18266
  provider: external_exports.string().optional()
18339
18267
  }).strict();
18268
+ var ExceptionCapability = external_exports.enum(["suppress", "reveal_to_model"]);
18340
18269
  var DetectionException = external_exports.object({
18341
18270
  id: external_exports.guid(),
18342
18271
  ruleId: external_exports.string(),
@@ -18353,6 +18282,7 @@ var DetectionException = external_exports.object({
18353
18282
  keyVersion: external_exports.number().int().positive(),
18354
18283
  // maskMatch() preview of the approved value — never the raw value.
18355
18284
  maskedValue: external_exports.string(),
18285
+ capability: ExceptionCapability.default("suppress"),
18356
18286
  scope: ExceptionScope,
18357
18287
  expiresAt: external_exports.iso.datetime().nullable(),
18358
18288
  maxUses: external_exports.number().int().positive().nullable(),
@@ -18376,6 +18306,7 @@ var ExceptionBundleEntry = DetectionException.pick({
18376
18306
  ruleId: true,
18377
18307
  valueFingerprint: true,
18378
18308
  keyVersion: true,
18309
+ capability: true,
18379
18310
  expiresAt: true,
18380
18311
  maxUses: true,
18381
18312
  useCount: true,
@@ -19483,8 +19414,128 @@ var PatchInstalledPackRequest = external_exports.object({
19483
19414
  message: "At least one field must be provided"
19484
19415
  }).meta({ id: "PatchInstalledPackRequest" });
19485
19416
 
19417
+ // ../packages/schema/src/zod/vault.ts
19418
+ var POINTER_FORMAT_VERSION = 2;
19419
+ var CATEGORY_ALTERNATION = DetectionCategory.options.join("|");
19420
+ var POINTER_TOKEN_PATTERN = new RegExp(
19421
+ `\\[\\[aka:(?:${CATEGORY_ALTERNATION}):[A-Z2-7]{2,7}\\.[A-Z2-7]{26}\\.[A-Z2-7]{16}\\]\\]`
19422
+ );
19423
+ var POINTER_TOKEN_ANCHORED = new RegExp(`^${POINTER_TOKEN_PATTERN.source}$`);
19424
+ function pointerTokenScanner() {
19425
+ return new RegExp(POINTER_TOKEN_PATTERN.source, "g");
19426
+ }
19427
+ var PointerToken = external_exports.string().regex(POINTER_TOKEN_ANCHORED);
19428
+ var ParsedPointer = external_exports.object({
19429
+ category: DetectionCategory,
19430
+ keyVersion: external_exports.number().int().positive(),
19431
+ pointerId: external_exports.string(),
19432
+ tag: external_exports.string()
19433
+ });
19434
+ var VaultEntry = external_exports.object({
19435
+ pointerId: external_exports.string(),
19436
+ // The keyed HMAC of the raw value under `exception.key`, and the epoch it was
19437
+ // derived under. This is what a reveal-to-model grant matches on, and it rotates
19438
+ // independently of the vault encryption key below.
19439
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
19440
+ fingerprintKeyVersion: external_exports.number().int().positive(),
19441
+ // The vault-key epoch this row's ciphertext was sealed under.
19442
+ keyVersion: external_exports.number().int().positive(),
19443
+ // Fixed at first mint and never updated: the same value detected later under a
19444
+ // different rule's category keeps the category it was minted with, so one
19445
+ // value always produces exactly one wire token.
19446
+ category: DetectionCategory,
19447
+ ruleId: external_exports.string(),
19448
+ // Partial-reveal preview for badges and listings. Never the raw value.
19449
+ maskedMatch: external_exports.string(),
19450
+ provider: external_exports.string().optional(),
19451
+ ciphertext: external_exports.string(),
19452
+ nonce: external_exports.string(),
19453
+ authTag: external_exports.string(),
19454
+ // How many times this value has been detected on this machine — the reuse
19455
+ // signal the vault dashboard reads. Distinct from VaultDeref.pointerCount.
19456
+ occurrenceCount: external_exports.number().int().nonnegative(),
19457
+ firstSeen: external_exports.string(),
19458
+ lastSeen: external_exports.string()
19459
+ });
19460
+ var PointerDescriptor = external_exports.object({
19461
+ category: DetectionCategory,
19462
+ provider: external_exports.string().optional(),
19463
+ maskedMatch: external_exports.string(),
19464
+ occurrences: external_exports.number().int().nonnegative(),
19465
+ firstSeen: external_exports.string(),
19466
+ lastSeen: external_exports.string()
19467
+ });
19468
+ var PointerIdentity = external_exports.object({
19469
+ ruleId: external_exports.string(),
19470
+ valueFingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
19471
+ fingerprintKeyVersion: external_exports.number().int().positive()
19472
+ });
19473
+ var DetokenizeTarget = external_exports.enum(["human", "model"]);
19474
+ var VaultDerefReason = external_exports.enum([
19475
+ "display",
19476
+ "explicit-reveal",
19477
+ "view-render",
19478
+ "model-input",
19479
+ "remediation",
19480
+ "purge"
19481
+ ]);
19482
+ var VaultDerefOutcome = external_exports.enum(["revealed", "refused", "unavailable"]);
19483
+ var BATCHED_DEREF_REASONS = ["display", "view-render"];
19484
+ function isBatchedDerefReason(reason) {
19485
+ return BATCHED_DEREF_REASONS.includes(reason);
19486
+ }
19487
+ var VaultDeref = external_exports.object({
19488
+ id: external_exports.guid(),
19489
+ pointerId: external_exports.string(),
19490
+ at: external_exports.string(),
19491
+ target: DetokenizeTarget,
19492
+ reason: VaultDerefReason,
19493
+ outcome: VaultDerefOutcome,
19494
+ // Present only on a model-target crossing that a reveal grant authorized.
19495
+ grantId: external_exports.string().optional(),
19496
+ // How many pointers ONE batched render resolved. 1 for unbatched rows. Named
19497
+ // apart from VaultEntry.occurrenceCount, which counts detections of a value.
19498
+ pointerCount: external_exports.number().int().positive().default(1)
19499
+ });
19500
+ var VaultSightingKind = external_exports.enum([
19501
+ "prompt",
19502
+ "tool-input",
19503
+ "tool-output",
19504
+ "file",
19505
+ "transcript"
19506
+ ]);
19507
+ var VaultSighting = external_exports.object({
19508
+ location: external_exports.string(),
19509
+ kind: VaultSightingKind,
19510
+ firstSeen: external_exports.string(),
19511
+ lastSeen: external_exports.string()
19512
+ });
19513
+ var VaultInventoryEntry = external_exports.object({
19514
+ pointerId: external_exports.string(),
19515
+ category: DetectionCategory,
19516
+ provider: external_exports.string().optional(),
19517
+ maskedMatch: external_exports.string(),
19518
+ occurrences: external_exports.number().int().nonnegative(),
19519
+ firstSeen: external_exports.string(),
19520
+ lastSeen: external_exports.string(),
19521
+ // The active reveal-to-model grant covering this value, when one exists —
19522
+ // the inventory badges it, the row links to revocation.
19523
+ revealGrantId: external_exports.string().nullable(),
19524
+ sightings: external_exports.array(VaultSighting)
19525
+ });
19526
+ var VaultKeyCustody = external_exports.string();
19527
+ var VaultInlineReveal = external_exports.enum(["masked", "full", "off"]);
19528
+ var VAULT_CONSENT_VERSION = 1;
19529
+ var VaultConsent = external_exports.object({
19530
+ acknowledgedAt: external_exports.iso.datetime(),
19531
+ version: external_exports.number().int().positive()
19532
+ });
19533
+ function isVaultConsentValid(consent) {
19534
+ return consent?.version === VAULT_CONSENT_VERSION;
19535
+ }
19536
+
19486
19537
  // ../packages/schema/src/zod/local.ts
19487
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
19538
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
19488
19539
  var RunMode = external_exports.enum(["standalone"]);
19489
19540
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
19490
19541
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -19506,6 +19557,16 @@ var WorkspaceSettings = external_exports.object({
19506
19557
  // In-place egress extraction on the scan paths; disable to stop all Data
19507
19558
  // Shares writes.
19508
19559
  dataSharesInPlace: external_exports.boolean().default(true),
19560
+ // Consent to keep a RECOVERABLE encrypted copy of detected values in the local
19561
+ // vault, instead of destroying them. Absent by default: this is a custody
19562
+ // change from one-way redaction, so it is never an assumed grant on upgrade.
19563
+ // Revoking stops future vaulting; it does not erase what is already stored —
19564
+ // purging the vault is the eraser.
19565
+ vaultConsent: VaultConsent.optional(),
19566
+ // Where the vault master key lives.
19567
+ vaultKeyCustody: VaultKeyCustody.default("file"),
19568
+ // How a pointer renders in assistant prose on screen (see VaultInlineReveal).
19569
+ vaultInlineReveal: VaultInlineReveal.default("masked"),
19509
19570
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
19510
19571
  onboardedAt: external_exports.iso.datetime().optional(),
19511
19572
  // Records that the user consented to sending findings to the model API for
@@ -20052,6 +20113,112 @@ function reviewSeverityRank(reasons) {
20052
20113
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
20053
20114
  }
20054
20115
 
20116
+ // ../packages/detections/src/pointer-shield.ts
20117
+ function shieldPointers(text) {
20118
+ const spans = [];
20119
+ let out = null;
20120
+ for (const match of text.matchAll(pointerTokenScanner())) {
20121
+ spans.push({ start: match.index, end: match.index + match[0].length });
20122
+ out ??= text;
20123
+ out = out.slice(0, match.index) + " ".repeat(match[0].length) + out.slice(match.index + match[0].length);
20124
+ }
20125
+ return { text: out ?? text, spans };
20126
+ }
20127
+ function dropShieldedFindings(findings, spans) {
20128
+ if (spans.length === 0) return findings;
20129
+ return findings.filter(
20130
+ (finding) => !spans.some((s) => finding.span.start < s.end && finding.span.end > s.start)
20131
+ );
20132
+ }
20133
+
20134
+ // ../packages/detections/src/posture/config-posture.ts
20135
+ var RULE_VERSION = "1";
20136
+ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
20137
+ "prettier",
20138
+ "eslint",
20139
+ "biome",
20140
+ "black",
20141
+ "ruff",
20142
+ "gofmt",
20143
+ "rustfmt",
20144
+ "node",
20145
+ "npx",
20146
+ "npm",
20147
+ "pnpm",
20148
+ "yarn",
20149
+ "make",
20150
+ "git",
20151
+ "jq",
20152
+ "echo"
20153
+ ]);
20154
+ var EGRESS_RE = /\b(curl|wget|nc|ncat|netcat|ssh|scp|rsync)\b|https?:\/\//;
20155
+ var MUTATION_RE = /(--write|--fix|\s-w\b|\s-i\b|(?<![\d&>])>\s?\S)/;
20156
+ var CONFIG_POSTURE_RULES = [
20157
+ {
20158
+ ruleId: "hook-conflict",
20159
+ version: RULE_VERSION,
20160
+ name: "Overlapping hooks \u2014 run order is undefined",
20161
+ category: "config",
20162
+ severity: "medium",
20163
+ definition: JSON.stringify({
20164
+ kind: "hook-conflict",
20165
+ mutation: MUTATION_RE.source,
20166
+ note: "two hooks on the same event with overlapping matchers, both mutating the tool target"
20167
+ })
20168
+ },
20169
+ {
20170
+ ruleId: "hook-unknown",
20171
+ version: RULE_VERSION,
20172
+ name: "Unknown hook \u2014 not attributable to a plugin or known tool",
20173
+ category: "config",
20174
+ // medium, not high: with a deliberately tiny allowlist this v1 heuristic
20175
+ // fires on ordinary interpreter hooks (python3 x.py) and compound commands
20176
+ // (cd … && tool); 'high' is reserved for egress, which is genuinely
20177
+ // dangerous. Revisit when the known-hooks registry replaces the allowlist.
20178
+ severity: "medium",
20179
+ definition: JSON.stringify({ kind: "hook-unknown", knownTools: [...KNOWN_TOOLS] })
20180
+ },
20181
+ {
20182
+ ruleId: "hook-external-egress",
20183
+ version: RULE_VERSION,
20184
+ name: "Hook command can send data externally",
20185
+ category: "config",
20186
+ severity: "high",
20187
+ definition: JSON.stringify({ kind: "hook-external-egress", pattern: EGRESS_RE.source })
20188
+ }
20189
+ ];
20190
+
20191
+ // ../packages/detections/src/security/redos-probe.ts
20192
+ var EXPONENTIAL_UNITS = [
20193
+ "a",
20194
+ "0",
20195
+ " ",
20196
+ "x",
20197
+ "ab",
20198
+ "a.",
20199
+ "a-",
20200
+ "a_",
20201
+ "a@",
20202
+ "a/",
20203
+ "a:",
20204
+ "a=",
20205
+ "a;",
20206
+ "aA0",
20207
+ " "
20208
+ ];
20209
+ var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
20210
+ (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
20211
+ );
20212
+ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
20213
+ (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
20214
+ );
20215
+
20216
+ // ../packages/persistence/src/database.ts
20217
+ import { randomUUID as randomUUID9 } from "crypto";
20218
+ import { existsSync as existsSync2, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
20219
+ import { join as join2, sep } from "path";
20220
+ import { DatabaseSync } from "node:sqlite";
20221
+
20055
20222
  // ../packages/persistence/src/ids.ts
20056
20223
  import { createHash } from "crypto";
20057
20224
  function sha256Hex(input) {
@@ -21912,6 +22079,9 @@ var AmbiguousExceptionIdError = class extends Error {
21912
22079
  var ACTIVE_PREDICATE = `revoked_at IS NULL
21913
22080
  AND (expires_at IS NULL OR expires_at > :now)
21914
22081
  AND (max_uses IS NULL OR use_count < max_uses)`;
22082
+ var ACTIVE_REVEAL_GRANT_PREDICATE = `capability = 'reveal_to_model'
22083
+ AND conditions IS NULL
22084
+ AND ${ACTIVE_PREDICATE}`;
21915
22085
  var SqliteExceptionsRepository = class {
21916
22086
  constructor(db) {
21917
22087
  this.db = db;
@@ -22003,11 +22173,11 @@ var SqliteExceptionsRepository = class {
22003
22173
  this.db.prepare(
22004
22174
  `INSERT INTO exceptions (
22005
22175
  id, rule_id, category, value_fingerprint, key_version, masked_value,
22006
- scope, expires_at, max_uses, use_count, last_used_at, justification,
22007
- conditions, created_by, created_via, created_at, updated_at
22176
+ capability, scope, expires_at, max_uses, use_count, last_used_at,
22177
+ justification, conditions, created_by, created_via, created_at, updated_at
22008
22178
  ) VALUES (
22009
22179
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
22010
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
22180
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
22011
22181
  :conditions, :createdBy, :createdVia, :now, :now
22012
22182
  )`
22013
22183
  ).run({
@@ -22017,6 +22187,7 @@ var SqliteExceptionsRepository = class {
22017
22187
  valueFingerprint: input.valueFingerprint,
22018
22188
  keyVersion: input.keyVersion,
22019
22189
  maskedValue: input.maskedValue,
22190
+ capability: input.capability ?? "suppress",
22020
22191
  scope: input.scope,
22021
22192
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
22022
22193
  maxUses: input.maxUses,
@@ -22110,6 +22281,7 @@ var SqliteExceptionsRepository = class {
22110
22281
  ruleId: row.rule_id,
22111
22282
  valueFingerprint: row.value_fingerprint,
22112
22283
  keyVersion: row.key_version,
22284
+ capability: row.capability,
22113
22285
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
22114
22286
  maxUses: row.max_uses,
22115
22287
  useCount: row.use_count,
@@ -22164,6 +22336,35 @@ var SqliteExceptionsRepository = class {
22164
22336
  }))
22165
22337
  );
22166
22338
  }
22339
+ /**
22340
+ * The active reveal-to-model grant for a vaulted value's identity, or null.
22341
+ * Matching is exact on (ruleId, valueFingerprint, keyVersion) — the same key
22342
+ * suppression uses — plus the capability: a suppression grant must never
22343
+ * authorize a reveal. Read-only: the caller does NOT consume here, because a
22344
+ * revealed value re-enters the detection scan immediately afterward and the
22345
+ * suppression match there claims the use — one crossing, one use.
22346
+ *
22347
+ * A grant with `conditions` NEVER matches here: the reveal path does not yet
22348
+ * evaluate conditions, and a narrowing clause that is ignored would WIDEN the
22349
+ * grant instead. Fail closed until reveal-side condition evaluation exists.
22350
+ */
22351
+ activeRevealGrant(ruleId, valueFingerprint, keyVersion, now = Date.now()) {
22352
+ try {
22353
+ const row = getRow(
22354
+ this.db.prepare(
22355
+ `SELECT id FROM exceptions
22356
+ WHERE rule_id = :ruleId AND value_fingerprint = :valueFingerprint
22357
+ AND key_version = :keyVersion
22358
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
22359
+ LIMIT 1`
22360
+ ),
22361
+ { ruleId, valueFingerprint, keyVersion, now }
22362
+ );
22363
+ return Promise.resolve(row ?? null);
22364
+ } catch (err) {
22365
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
22366
+ }
22367
+ }
22167
22368
  /**
22168
22369
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
22169
22370
  * exhausted) whose last transition is older than the retention window.
@@ -22191,6 +22392,7 @@ function parseExceptionRow(row) {
22191
22392
  valueFingerprint: row.value_fingerprint,
22192
22393
  keyVersion: row.key_version,
22193
22394
  maskedValue: row.masked_value,
22395
+ capability: row.capability,
22194
22396
  scope: row.scope,
22195
22397
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
22196
22398
  maxUses: row.max_uses,
@@ -24356,29 +24558,310 @@ var SqliteScanLedgerRepository = class {
24356
24558
  }
24357
24559
  };
24358
24560
 
24359
- // ../packages/persistence/src/repositories/security.ts
24360
- var DAY_MS4 = 864e5;
24361
- var SEVERITIES = ["critical", "high", "medium", "low"];
24362
- var ACTION_TO_KIND = {
24363
- block: "blocked",
24364
- redact: "redacted",
24365
- warn: "warned"
24366
- };
24367
- var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
24368
- var SCAN_COVERAGE = [
24369
- { provider: "claudecode", coverage: 100, supported: true },
24370
- { provider: "cursor", coverage: 0, supported: false },
24371
- { provider: "codex", coverage: 0, supported: false },
24372
- { provider: "chatgpt", coverage: 0, supported: false },
24373
- { provider: "copilot", coverage: 0, supported: false },
24374
- { provider: "api", coverage: 0, supported: false }
24375
- ];
24376
- var GRANULARITY = {
24377
- "7d": "day",
24378
- "30d": "day",
24379
- "3m": "week",
24380
- "6m": "week"
24381
- };
24561
+ // ../packages/persistence/src/repositories/secret-vault.ts
24562
+ import { randomUUID as randomUUID7 } from "crypto";
24563
+ var SELECT_COLUMNS = `
24564
+ pointer_id AS pointerId,
24565
+ value_fingerprint AS valueFingerprint,
24566
+ fingerprint_key_version AS fingerprintKeyVersion,
24567
+ key_version AS keyVersion,
24568
+ format_version AS formatVersion,
24569
+ category,
24570
+ rule_id AS ruleId,
24571
+ masked_match AS maskedMatch,
24572
+ provider,
24573
+ ciphertext,
24574
+ nonce,
24575
+ auth_tag AS authTag,
24576
+ occurrence_count AS occurrenceCount,
24577
+ first_seen AS firstSeen,
24578
+ last_seen AS lastSeen`;
24579
+ function toRow(raw) {
24580
+ const { provider, ...rest } = raw;
24581
+ return provider === null ? rest : { ...rest, provider };
24582
+ }
24583
+ var SqliteSecretVaultRepository = class {
24584
+ constructor(db) {
24585
+ this.db = db;
24586
+ this.insertStmt = db.prepare(
24587
+ `INSERT INTO secret_vault (
24588
+ pointer_id, value_fingerprint, fingerprint_key_version, key_version,
24589
+ format_version, category, rule_id, masked_match, provider,
24590
+ ciphertext, nonce, auth_tag,
24591
+ occurrence_count, first_seen, last_seen
24592
+ ) VALUES (
24593
+ :pointerId, :valueFingerprint, :fingerprintKeyVersion, :keyVersion,
24594
+ :formatVersion, :category, :ruleId, :maskedMatch, :provider,
24595
+ :ciphertext, :nonce, :authTag,
24596
+ 1, :now, :now
24597
+ )`
24598
+ );
24599
+ this.bumpStmt = db.prepare(
24600
+ `UPDATE secret_vault
24601
+ SET occurrence_count = occurrence_count + 1, last_seen = :now
24602
+ WHERE value_fingerprint = :valueFingerprint`
24603
+ );
24604
+ this.byPointerStmt = db.prepare(
24605
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE pointer_id = :pointerId`
24606
+ );
24607
+ this.byFingerprintStmt = db.prepare(
24608
+ `SELECT ${SELECT_COLUMNS} FROM secret_vault WHERE value_fingerprint = :valueFingerprint`
24609
+ );
24610
+ this.listStmt = db.prepare(`SELECT ${SELECT_COLUMNS} FROM secret_vault ORDER BY first_seen`);
24611
+ this.replaceCiphertextStmt = db.prepare(
24612
+ `UPDATE secret_vault
24613
+ SET key_version = :keyVersion, ciphertext = :ciphertext, nonce = :nonce, auth_tag = :authTag
24614
+ WHERE pointer_id = :pointerId`
24615
+ );
24616
+ this.refreshFingerprintStmt = db.prepare(
24617
+ `UPDATE secret_vault
24618
+ SET value_fingerprint = :valueFingerprint, fingerprint_key_version = :fingerprintKeyVersion
24619
+ WHERE pointer_id = :pointerId`
24620
+ );
24621
+ this.derefStmt = db.prepare(
24622
+ `INSERT INTO secret_vault_deref (id, pointer_id, at, target, reason, outcome, grant_id, pointer_count)
24623
+ VALUES (:id, :pointerId, :at, :target, :reason, :outcome, :grantId, :pointerCount)`
24624
+ );
24625
+ }
24626
+ db;
24627
+ insertStmt;
24628
+ bumpStmt;
24629
+ byPointerStmt;
24630
+ byFingerprintStmt;
24631
+ listStmt;
24632
+ replaceCiphertextStmt;
24633
+ refreshFingerprintStmt;
24634
+ derefStmt;
24635
+ /**
24636
+ * Vault a value, or record another sighting of one already vaulted. Keyed on
24637
+ * `valueFingerprint`, never on the caller's pointer id: a value seen again
24638
+ * bumps `occurrence_count` and `last_seen` and comes back with its ORIGINAL
24639
+ * pointer, category and ciphertext, so the same secret always resolves to one
24640
+ * wire token. `minted` is true only when this call created the row.
24641
+ *
24642
+ * The read-then-write runs in one IMMEDIATE transaction so two concurrent
24643
+ * writers cannot both decide they are minting.
24644
+ */
24645
+ upsert(input, now) {
24646
+ let minted = false;
24647
+ withTransaction(
24648
+ this.db,
24649
+ () => {
24650
+ const existing = getRow(this.byFingerprintStmt, {
24651
+ valueFingerprint: input.valueFingerprint
24652
+ });
24653
+ if (existing === void 0) {
24654
+ this.insertStmt.run(
24655
+ bindParams({
24656
+ pointerId: input.pointerId,
24657
+ valueFingerprint: input.valueFingerprint,
24658
+ fingerprintKeyVersion: input.fingerprintKeyVersion,
24659
+ keyVersion: input.keyVersion,
24660
+ formatVersion: input.formatVersion ?? POINTER_FORMAT_VERSION,
24661
+ category: input.category,
24662
+ ruleId: input.ruleId,
24663
+ maskedMatch: input.maskedMatch,
24664
+ provider: input.provider,
24665
+ ciphertext: input.ciphertext,
24666
+ nonce: input.nonce,
24667
+ authTag: input.authTag,
24668
+ now
24669
+ })
24670
+ );
24671
+ minted = true;
24672
+ return;
24673
+ }
24674
+ this.bumpStmt.run({ valueFingerprint: input.valueFingerprint, now });
24675
+ },
24676
+ "IMMEDIATE"
24677
+ );
24678
+ const row = getRow(this.byFingerprintStmt, {
24679
+ valueFingerprint: input.valueFingerprint
24680
+ });
24681
+ if (row === void 0) throw new Error("vault: row vanished immediately after write");
24682
+ return { row: toRow(row), minted };
24683
+ }
24684
+ byPointerId(pointerId) {
24685
+ const raw = getRow(this.byPointerStmt, { pointerId });
24686
+ return raw === void 0 ? null : toRow(raw);
24687
+ }
24688
+ byValueFingerprint(fingerprint) {
24689
+ const raw = getRow(this.byFingerprintStmt, { valueFingerprint: fingerprint });
24690
+ return raw === void 0 ? null : toRow(raw);
24691
+ }
24692
+ /** Append one audit row. Carries no raw value and no ciphertext, by shape. */
24693
+ recordDeref(entry) {
24694
+ this.derefStmt.run(
24695
+ bindParams({
24696
+ id: entry.id,
24697
+ pointerId: entry.pointerId,
24698
+ at: entry.at,
24699
+ target: entry.target,
24700
+ reason: entry.reason,
24701
+ outcome: entry.outcome,
24702
+ grantId: entry.grantId,
24703
+ pointerCount: entry.pointerCount ?? 1
24704
+ })
24705
+ );
24706
+ }
24707
+ listAll() {
24708
+ return allRows(this.listStmt).map(toRow);
24709
+ }
24710
+ /** Re-seal an entry under a new key epoch, leaving its identity untouched. */
24711
+ replaceCiphertext(pointerId, next) {
24712
+ this.replaceCiphertextStmt.run({ pointerId, ...next });
24713
+ }
24714
+ /** Re-derive an entry's fingerprint under a new fingerprint-key epoch. */
24715
+ refreshFingerprint(pointerId, next) {
24716
+ this.refreshFingerprintStmt.run({ pointerId, ...next });
24717
+ }
24718
+ /**
24719
+ * Destroy every vaulted value and report how many were destroyed. The deref
24720
+ * audit is left alone on purpose — see the table note above.
24721
+ */
24722
+ purgeAll() {
24723
+ let destroyed = 0;
24724
+ withTransaction(
24725
+ this.db,
24726
+ () => {
24727
+ destroyed = this.countEntries();
24728
+ this.db.exec("DELETE FROM secret_vault");
24729
+ },
24730
+ "IMMEDIATE"
24731
+ );
24732
+ return destroyed;
24733
+ }
24734
+ /**
24735
+ * Record (or re-stamp) one place a pointer has been written. One row per
24736
+ * (pointer, location); a re-sighting bumps last_seen. Best-effort bookkeeping
24737
+ * on hook paths — a failure must never affect the rewrite that triggered it,
24738
+ * so callers wrap this, not the other way around.
24739
+ */
24740
+ recordSighting(entry, now) {
24741
+ this.db.prepare(
24742
+ `INSERT INTO secret_vault_sighting (id, pointer_id, location, kind, first_seen, last_seen)
24743
+ VALUES (:id, :pointerId, :location, :kind, :now, :now)
24744
+ ON CONFLICT (pointer_id, location) DO UPDATE SET last_seen = :now`
24745
+ ).run({
24746
+ id: randomUUID7(),
24747
+ pointerId: entry.pointerId,
24748
+ location: entry.location,
24749
+ kind: entry.kind,
24750
+ now
24751
+ });
24752
+ }
24753
+ listSightings(pointerId) {
24754
+ const rows = allRows(
24755
+ this.db.prepare(
24756
+ `SELECT location, kind, first_seen, last_seen FROM secret_vault_sighting
24757
+ WHERE pointer_id = :pointerId ORDER BY last_seen DESC`
24758
+ ),
24759
+ { pointerId }
24760
+ );
24761
+ return rows.map((r) => ({
24762
+ location: r.location,
24763
+ kind: r.kind,
24764
+ firstSeen: new Date(r.first_seen).toISOString(),
24765
+ lastSeen: new Date(r.last_seen).toISOString()
24766
+ }));
24767
+ }
24768
+ /**
24769
+ * The dashboard inventory: every vaulted value's descriptor data joined with
24770
+ * its sightings and the active reveal-to-model grant when one exists.
24771
+ * Raw-free by construction — neither the fingerprint nor the ciphertext
24772
+ * columns are selected.
24773
+ */
24774
+ listInventory(now = Date.now()) {
24775
+ const rows = allRows(
24776
+ this.db.prepare(
24777
+ `SELECT v.pointer_id, v.category, v.rule_id, v.masked_match, v.provider,
24778
+ v.occurrence_count, v.first_seen, v.last_seen,
24779
+ (SELECT e.id FROM exceptions e
24780
+ WHERE e.rule_id = v.rule_id
24781
+ AND e.value_fingerprint = v.value_fingerprint
24782
+ AND e.key_version = v.fingerprint_key_version
24783
+ AND ${ACTIVE_REVEAL_GRANT_PREDICATE}
24784
+ LIMIT 1) AS grant_id
24785
+ FROM secret_vault v
24786
+ ORDER BY v.last_seen DESC`
24787
+ ),
24788
+ { now }
24789
+ );
24790
+ return rows.map((r) => ({
24791
+ pointerId: r.pointer_id,
24792
+ category: r.category,
24793
+ ...r.provider === null ? {} : { provider: r.provider },
24794
+ maskedMatch: r.masked_match,
24795
+ occurrences: r.occurrence_count,
24796
+ firstSeen: new Date(r.first_seen).toISOString(),
24797
+ lastSeen: new Date(r.last_seen).toISOString(),
24798
+ revealGrantId: r.grant_id,
24799
+ sightings: this.listSightings(r.pointer_id)
24800
+ }));
24801
+ }
24802
+ /**
24803
+ * The de-reference trail, newest first. By default the batched, high-volume
24804
+ * reasons (display, view-render) are hidden and counted instead — the rows
24805
+ * that matter as a signal are the model crossings, and burying them under
24806
+ * render noise would defeat the audit's purpose.
24807
+ */
24808
+ listDerefs(opts) {
24809
+ const limit = opts?.limit ?? 200;
24810
+ const where = opts?.includeBatched === true ? "" : `WHERE reason NOT IN ('display', 'view-render')`;
24811
+ const rows = allRows(
24812
+ this.db.prepare(
24813
+ `SELECT id, pointer_id, at, target, reason, outcome, grant_id, pointer_count
24814
+ FROM secret_vault_deref ${where}
24815
+ ORDER BY at DESC, rowid DESC LIMIT :limit`
24816
+ ),
24817
+ { limit }
24818
+ );
24819
+ const hiddenBatched = opts?.includeBatched === true ? 0 : countScalar(
24820
+ this.db,
24821
+ `SELECT count(*) AS n FROM secret_vault_deref WHERE reason IN ('display', 'view-render')`
24822
+ );
24823
+ return {
24824
+ rows: rows.map((r) => ({
24825
+ id: r.id,
24826
+ pointerId: r.pointer_id,
24827
+ at: new Date(r.at).toISOString(),
24828
+ target: r.target,
24829
+ reason: r.reason,
24830
+ outcome: r.outcome,
24831
+ ...r.grant_id === null ? {} : { grantId: r.grant_id },
24832
+ pointerCount: r.pointer_count
24833
+ })),
24834
+ hiddenBatched
24835
+ };
24836
+ }
24837
+ countEntries() {
24838
+ return countScalar(this.db, "SELECT COUNT(*) AS n FROM secret_vault");
24839
+ }
24840
+ };
24841
+
24842
+ // ../packages/persistence/src/repositories/security.ts
24843
+ var DAY_MS4 = 864e5;
24844
+ var SEVERITIES = ["critical", "high", "medium", "low"];
24845
+ var ACTION_TO_KIND = {
24846
+ block: "blocked",
24847
+ redact: "redacted",
24848
+ warn: "warned"
24849
+ };
24850
+ var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
24851
+ var SCAN_COVERAGE = [
24852
+ { provider: "claudecode", coverage: 100, supported: true },
24853
+ { provider: "cursor", coverage: 0, supported: false },
24854
+ { provider: "codex", coverage: 0, supported: false },
24855
+ { provider: "chatgpt", coverage: 0, supported: false },
24856
+ { provider: "copilot", coverage: 0, supported: false },
24857
+ { provider: "api", coverage: 0, supported: false }
24858
+ ];
24859
+ var GRANULARITY = {
24860
+ "7d": "day",
24861
+ "30d": "day",
24862
+ "3m": "week",
24863
+ "6m": "week"
24864
+ };
24382
24865
  function granularityFor(range) {
24383
24866
  return GRANULARITY[range];
24384
24867
  }
@@ -24701,7 +25184,7 @@ var SqliteSecurityRepository = class {
24701
25184
  };
24702
25185
 
24703
25186
  // ../packages/persistence/src/repositories/shares.ts
24704
- import { randomUUID as randomUUID7 } from "crypto";
25187
+ import { randomUUID as randomUUID8 } from "crypto";
24705
25188
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
24706
25189
  var IN_CHUNK = 500;
24707
25190
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -24957,7 +25440,7 @@ var SqliteSharesRepository = class {
24957
25440
  (id, destination_id, host, decision, created_at, updated_at)
24958
25441
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
24959
25442
  ).run({
24960
- id: randomUUID7(),
25443
+ id: randomUUID8(),
24961
25444
  destinationId,
24962
25445
  host: dest.host,
24963
25446
  decision,
@@ -25106,7 +25589,7 @@ var SqliteSharesRepository = class {
25106
25589
  let destinationId = destIds.get(hit.host);
25107
25590
  if (destinationId === void 0) {
25108
25591
  destStmt.run({
25109
- id: randomUUID7(),
25592
+ id: randomUUID8(),
25110
25593
  kind: hit.kind,
25111
25594
  name: hit.name,
25112
25595
  host: hit.host,
@@ -25122,7 +25605,7 @@ var SqliteSharesRepository = class {
25122
25605
  let endpointId = endpointIds.get(endpointKey);
25123
25606
  if (endpointId === void 0) {
25124
25607
  endpointStmt.run({
25125
- id: randomUUID7(),
25608
+ id: randomUUID8(),
25126
25609
  destinationId,
25127
25610
  method: hit.method,
25128
25611
  transport: hit.transport,
@@ -25135,7 +25618,7 @@ var SqliteSharesRepository = class {
25135
25618
  endpointIds.set(endpointKey, endpointId);
25136
25619
  }
25137
25620
  siteStmt.run({
25138
- id: randomUUID7(),
25621
+ id: randomUUID8(),
25139
25622
  endpointId,
25140
25623
  project: input.project,
25141
25624
  projectKey: input.projectKey,
@@ -25551,6 +26034,7 @@ function openAndInitialize(file2) {
25551
26034
  policies,
25552
26035
  installedPacks,
25553
26036
  scanLedger: new SqliteScanLedgerRepository(db),
26037
+ secretVault: new SqliteSecretVaultRepository(db),
25554
26038
  exceptions: new SqliteExceptionsRepository(db),
25555
26039
  resolutions: new SqliteResolutionsRepository(db),
25556
26040
  ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
@@ -25586,6 +26070,7 @@ function openLocalDatabase(dir) {
25586
26070
  policies,
25587
26071
  installedPacks,
25588
26072
  scanLedger,
26073
+ secretVault,
25589
26074
  exceptions,
25590
26075
  resolutions,
25591
26076
  ruleProbeCache,
@@ -25694,7 +26179,7 @@ function openLocalDatabase(dir) {
25694
26179
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
25695
26180
  if (!definitionId) continue;
25696
26181
  inspectionFindings.insertFinding({
25697
- id: randomUUID8(),
26182
+ id: randomUUID9(),
25698
26183
  auditEventId: record2.scanEvent.id,
25699
26184
  inspectionDefinitionId: definitionId,
25700
26185
  span: finding.span,
@@ -25771,6 +26256,7 @@ function openLocalDatabase(dir) {
25771
26256
  policies,
25772
26257
  installedPacks,
25773
26258
  scanLedger,
26259
+ secretVault,
25774
26260
  exceptions,
25775
26261
  resolutions,
25776
26262
  ruleProbeCache,
@@ -25841,7 +26327,11 @@ function parseKeyFile(raw) {
25841
26327
  }
25842
26328
  return { version: version2, material: bytes };
25843
26329
  }
25844
- var KEY_VERSION_TABLES = ["exceptions", "blocked_detections"];
26330
+ var KEY_VERSION_COLUMNS = {
26331
+ exceptions: "key_version",
26332
+ blocked_detections: "key_version",
26333
+ secret_vault: "fingerprint_key_version"
26334
+ };
25845
26335
  var SQLITE_ERROR = 1;
25846
26336
  var FLOOR_BUSY_TIMEOUT_MS = 250;
25847
26337
  var FloorUnreadableError = class extends Error {
@@ -25862,10 +26352,10 @@ function storedKeyVersionFloor(dataDir2) {
25862
26352
  db = new DatabaseSync2(file2, { readOnly: true });
25863
26353
  db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
25864
26354
  let floor = 0;
25865
- for (const table of KEY_VERSION_TABLES) {
26355
+ for (const [table, column] of Object.entries(KEY_VERSION_COLUMNS)) {
25866
26356
  try {
25867
26357
  const row = getRow(
25868
- db.prepare(`SELECT MAX(key_version) AS v FROM ${table}`)
26358
+ db.prepare(`SELECT MAX(${column}) AS v FROM ${table}`)
25869
26359
  );
25870
26360
  floor = Math.max(floor, row?.v ?? 0);
25871
26361
  } catch (err) {
@@ -25944,6 +26434,9 @@ function dataDir(base = defaultDataDir()) {
25944
26434
  function dbPath(base = defaultDataDir()) {
25945
26435
  return join4(dataDir(base), "aka.db");
25946
26436
  }
26437
+ function keysDir(base = defaultDataDir()) {
26438
+ return join4(base, "keys");
26439
+ }
25947
26440
  function ensureLayoutDirSync(dir = defaultDataDir()) {
25948
26441
  ensureDataDirSync(dir);
25949
26442
  }
@@ -25970,13 +26463,874 @@ function readJson(file2) {
25970
26463
  return parseJsonObject(text) ?? null;
25971
26464
  }
25972
26465
 
25973
- // ../packages/persistence/src/warn-era-cap.ts
25974
- import { existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
26466
+ // ../packages/persistence/src/vault/crypto.ts
26467
+ import {
26468
+ createCipheriv,
26469
+ createDecipheriv,
26470
+ createHmac as createHmac2,
26471
+ hkdfSync,
26472
+ timingSafeEqual
26473
+ } from "crypto";
26474
+ var POINTER_ID_BYTES = 16;
26475
+ var NONCE_BYTES = 12;
26476
+ var TAG_BYTES = 10;
26477
+ var SUBKEY_BYTES = 32;
26478
+ var HKDF_INFO_ENC = "aka:vault:enc:v1";
26479
+ var HKDF_INFO_SIGN = "aka:vault:sign:v1";
26480
+ var HKDF_SALT = "aka:vault:v1";
26481
+ var B32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
26482
+ function base32Encode(bytes) {
26483
+ let out = "";
26484
+ let buffer = 0;
26485
+ let bits = 0;
26486
+ for (const byte of bytes) {
26487
+ buffer = buffer << 8 | byte;
26488
+ bits += 8;
26489
+ while (bits >= 5) {
26490
+ out += B32_ALPHABET.charAt(buffer >>> bits - 5 & 31);
26491
+ bits -= 5;
26492
+ }
26493
+ }
26494
+ if (bits > 0) out += B32_ALPHABET.charAt(buffer << 5 - bits & 31);
26495
+ return out;
26496
+ }
26497
+ function base32Decode(text) {
26498
+ const out = [];
26499
+ let buffer = 0;
26500
+ let bits = 0;
26501
+ for (const char of text) {
26502
+ const value = B32_ALPHABET.indexOf(char);
26503
+ if (value < 0) throw new Error("base32: character outside the alphabet");
26504
+ buffer = buffer << 5 | value;
26505
+ bits += 5;
26506
+ if (bits >= 8) {
26507
+ out.push(buffer >>> bits - 8 & 255);
26508
+ bits -= 8;
26509
+ }
26510
+ }
26511
+ return Buffer.from(out);
26512
+ }
26513
+ function encodeKeyVersion(version2) {
26514
+ if (!Number.isInteger(version2) || version2 < 1 || version2 > 4294967295) {
26515
+ throw new Error("vault: key version out of range");
26516
+ }
26517
+ const bytes = [];
26518
+ let remaining = version2;
26519
+ while (remaining > 0) {
26520
+ bytes.unshift(remaining & 255);
26521
+ remaining = Math.floor(remaining / 256);
26522
+ }
26523
+ return base32Encode(Uint8Array.from(bytes));
26524
+ }
26525
+ function decodeKeyVersion(encoded) {
26526
+ const bytes = base32Decode(encoded);
26527
+ if (bytes.length === 0 || bytes.length > 4) throw new Error("vault: bad key version encoding");
26528
+ let version2 = 0;
26529
+ for (const byte of bytes) version2 = version2 * 256 + byte;
26530
+ if (version2 < 1) throw new Error("vault: bad key version");
26531
+ return version2;
26532
+ }
26533
+ function deriveSubkeys(master) {
26534
+ const derive = (info) => Buffer.from(hkdfSync("sha256", master, HKDF_SALT, info, SUBKEY_BYTES));
26535
+ return { enc: derive(HKDF_INFO_ENC), sign: derive(HKDF_INFO_SIGN) };
26536
+ }
26537
+ function bindingInput(keyVersion, pointerId, category, formatVersion = POINTER_FORMAT_VERSION) {
26538
+ if (pointerId.length !== POINTER_ID_BYTES) {
26539
+ throw new Error("vault: pointer id must be 16 bytes");
26540
+ }
26541
+ const head = Buffer.alloc(6);
26542
+ head.writeUInt16BE(formatVersion, 0);
26543
+ head.writeUInt32BE(keyVersion, 2);
26544
+ return Buffer.concat([head, Buffer.from(pointerId), Buffer.from(category, "utf8")]);
26545
+ }
26546
+ function seal(encKey, plaintext, aad, nonce) {
26547
+ const cipher = createCipheriv("aes-256-gcm", encKey, nonce);
26548
+ cipher.setAAD(aad);
26549
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
26550
+ return { ciphertext, nonce, authTag: cipher.getAuthTag() };
26551
+ }
26552
+ function open(encKey, sealed, aad) {
26553
+ try {
26554
+ const decipher = createDecipheriv("aes-256-gcm", encKey, sealed.nonce);
26555
+ decipher.setAAD(aad);
26556
+ decipher.setAuthTag(sealed.authTag);
26557
+ return Buffer.concat([decipher.update(sealed.ciphertext), decipher.final()]).toString("utf8");
26558
+ } catch {
26559
+ return null;
26560
+ }
26561
+ }
26562
+ function signPointer(signKey, keyVersion, pointerId, category) {
26563
+ return createHmac2("sha256", signKey).update(bindingInput(keyVersion, pointerId, category, POINTER_FORMAT_VERSION)).digest().subarray(0, TAG_BYTES);
26564
+ }
26565
+ function verifyPointerTag(signKey, keyVersion, pointerId, category, tag) {
26566
+ if (tag.length !== TAG_BYTES) return false;
26567
+ const expected = signPointer(signKey, keyVersion, pointerId, category);
26568
+ return timingSafeEqual(expected, Buffer.from(tag));
26569
+ }
26570
+ function formatPointer(category, keyVersion, pointerId, tag) {
26571
+ return `[[aka:${category}:${encodeKeyVersion(keyVersion)}.${base32Encode(pointerId)}.${base32Encode(tag)}]]`;
26572
+ }
26573
+
26574
+ // ../packages/persistence/src/vault/key-provider.ts
26575
+ import { execFileSync as execFileSync2 } from "child_process";
26576
+ import { randomBytes as randomBytes2 } from "crypto";
26577
+ import {
26578
+ chmodSync as chmodSync2,
26579
+ mkdirSync as mkdirSync2,
26580
+ readFileSync as readFileSync4,
26581
+ renameSync as renameSync4,
26582
+ rmSync as rmSync3,
26583
+ statSync,
26584
+ writeFileSync as writeFileSync2
26585
+ } from "fs";
25975
26586
  import { join as join6 } from "path";
26587
+ var VaultKeyEpochMissingError = class extends Error {
26588
+ version;
26589
+ constructor(version2) {
26590
+ super(`vault: key epoch ${String(version2)} is not present in the keyring`);
26591
+ this.name = "VaultKeyEpochMissingError";
26592
+ this.version = version2;
26593
+ }
26594
+ };
26595
+ var VAULT_KEY_FILENAME = "vault.key";
26596
+ var KEY_MATERIAL_BYTES2 = 32;
26597
+ var KEYCHAIN_SERVICE = "aka-vault";
26598
+ var KEYCHAIN_ACCOUNT = "keyring";
26599
+ function parseKeyring(raw) {
26600
+ const parsed = JSON.parse(raw);
26601
+ if (typeof parsed !== "object" || parsed === null) {
26602
+ throw new Error("vault key file is corrupt: not a JSON object");
26603
+ }
26604
+ const { current, keys } = parsed;
26605
+ if (typeof current !== "number" || !Number.isInteger(current) || current < 1) {
26606
+ throw new Error("vault key file is corrupt: bad current version");
26607
+ }
26608
+ if (typeof keys !== "object" || keys === null || Array.isArray(keys)) {
26609
+ throw new Error("vault key file is corrupt: bad keys map");
26610
+ }
26611
+ const map2 = /* @__PURE__ */ new Map();
26612
+ for (const [rawVersion, rawMaterial] of Object.entries(keys)) {
26613
+ const version2 = Number(rawVersion);
26614
+ if (!Number.isInteger(version2) || version2 < 1) {
26615
+ throw new Error("vault key file is corrupt: bad key version");
26616
+ }
26617
+ if (typeof rawMaterial !== "string") {
26618
+ throw new Error("vault key file is corrupt: bad key material");
26619
+ }
26620
+ const bytes = Buffer.from(rawMaterial, "base64");
26621
+ if (bytes.length !== KEY_MATERIAL_BYTES2) {
26622
+ throw new Error("vault key file is corrupt: bad key material length");
26623
+ }
26624
+ map2.set(version2, bytes);
26625
+ }
26626
+ if (!map2.has(current)) {
26627
+ throw new Error("vault key file is corrupt: current version has no material");
26628
+ }
26629
+ return { current, keys: map2 };
26630
+ }
26631
+ function serializeKeyring(keyring) {
26632
+ const keys = {};
26633
+ for (const version2 of [...keyring.keys.keys()].sort((a, b) => a - b)) {
26634
+ const material = keyring.keys.get(version2);
26635
+ if (material) keys[String(version2)] = material.toString("base64");
26636
+ }
26637
+ return JSON.stringify({ current: keyring.current, keys });
26638
+ }
26639
+ function mintKeyring() {
26640
+ return { current: 1, keys: /* @__PURE__ */ new Map([[1, randomBytes2(KEY_MATERIAL_BYTES2)]]) };
26641
+ }
26642
+ function withNextEpoch(keyring) {
26643
+ const next = Math.max(...keyring.keys.keys()) + 1;
26644
+ const keys = new Map(keyring.keys);
26645
+ keys.set(next, randomBytes2(KEY_MATERIAL_BYTES2));
26646
+ return { current: next, keys };
26647
+ }
26648
+ function currentOf(keyring) {
26649
+ const material = keyring.keys.get(keyring.current);
26650
+ if (!material) throw new VaultKeyEpochMissingError(keyring.current);
26651
+ return { material, version: keyring.current };
26652
+ }
26653
+ function epochOf(keyring, version2) {
26654
+ const material = keyring.keys.get(version2);
26655
+ if (!material) throw new VaultKeyEpochMissingError(version2);
26656
+ return { material, version: version2 };
26657
+ }
26658
+ function asAsync(work) {
26659
+ try {
26660
+ return Promise.resolve(work());
26661
+ } catch (err) {
26662
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
26663
+ }
26664
+ }
26665
+ function asError(err) {
26666
+ return err instanceof Error ? err : new Error(String(err));
26667
+ }
26668
+ var ROTATION_LOCK_STALE_MS = 6e4;
26669
+ var LOCK_OWNER_FILE = "owner";
26670
+ var ROTATION_IN_PROGRESS = "vault: a key rotation is already in progress";
26671
+ function claimRotationLock(lock, owner) {
26672
+ try {
26673
+ mkdirSync2(lock);
26674
+ } catch (err) {
26675
+ if (err.code === "EEXIST") return false;
26676
+ throw asError(err);
26677
+ }
26678
+ try {
26679
+ writeFileSync2(join6(lock, LOCK_OWNER_FILE), `${owner}
26680
+ `, { mode: DATA_FILE_MODE });
26681
+ return true;
26682
+ } catch (err) {
26683
+ rmSync3(lock, { recursive: true, force: true });
26684
+ throw asError(err);
26685
+ }
26686
+ }
26687
+ function acquireRotationLock(keysDir2) {
26688
+ const lock = join6(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
26689
+ const owner = randomBytes2(16).toString("hex");
26690
+ if (claimRotationLock(lock, owner)) return { lock, owner };
26691
+ let held;
26692
+ try {
26693
+ held = statSync(lock);
26694
+ } catch {
26695
+ throw new Error(ROTATION_IN_PROGRESS);
26696
+ }
26697
+ if (Date.now() - held.mtimeMs < ROTATION_LOCK_STALE_MS) throw new Error(ROTATION_IN_PROGRESS);
26698
+ const aside = `${lock}.stale.${owner}`;
26699
+ try {
26700
+ const now = statSync(lock);
26701
+ if (now.ino !== held.ino || now.mtimeMs !== held.mtimeMs) {
26702
+ throw new Error(ROTATION_IN_PROGRESS);
26703
+ }
26704
+ renameSync4(lock, aside);
26705
+ } catch (err) {
26706
+ if (err instanceof Error && err.message === ROTATION_IN_PROGRESS) throw err;
26707
+ throw new Error(ROTATION_IN_PROGRESS, { cause: err });
26708
+ }
26709
+ rmSync3(aside, { recursive: true, force: true });
26710
+ if (!claimRotationLock(lock, owner)) throw new Error(ROTATION_IN_PROGRESS);
26711
+ return { lock, owner };
26712
+ }
26713
+ function releaseRotationLock(lease) {
26714
+ try {
26715
+ if (readFileSync4(join6(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
26716
+ } catch {
26717
+ return;
26718
+ }
26719
+ rmSync3(lease.lock, { recursive: true, force: true });
26720
+ }
26721
+ function withRotationLock(keysDir2, work) {
26722
+ ensureDataDirSync(keysDir2);
26723
+ const lease = acquireRotationLock(keysDir2);
26724
+ try {
26725
+ return work();
26726
+ } finally {
26727
+ releaseRotationLock(lease);
26728
+ }
26729
+ }
26730
+ var FileKeyProvider = class {
26731
+ #keysDir;
26732
+ constructor(keysDir2) {
26733
+ this.#keysDir = keysDir2;
26734
+ }
26735
+ get filePath() {
26736
+ return join6(this.#keysDir, VAULT_KEY_FILENAME);
26737
+ }
26738
+ loadOrCreate() {
26739
+ return asAsync(() => {
26740
+ const existing = this.#read();
26741
+ if (!existing) return currentOf(this.#createExclusive());
26742
+ tightenFileMode(this.filePath);
26743
+ return currentOf(existing);
26744
+ });
26745
+ }
26746
+ rotate() {
26747
+ return asAsync(
26748
+ () => withRotationLock(this.#keysDir, () => {
26749
+ const existing = this.#read();
26750
+ if (!existing) return currentOf(this.#createExclusive());
26751
+ return currentOf(this.#write(withNextEpoch(existing)));
26752
+ })
26753
+ );
26754
+ }
26755
+ materialFor(version2) {
26756
+ return asAsync(() => {
26757
+ const existing = this.#read();
26758
+ if (!existing) throw new VaultKeyEpochMissingError(version2);
26759
+ return epochOf(existing, version2);
26760
+ });
26761
+ }
26762
+ /** The keyring, or null when the file is ABSENT. A corrupt file throws. */
26763
+ #read() {
26764
+ let raw;
26765
+ try {
26766
+ raw = readFileSync4(this.filePath, "utf8");
26767
+ } catch (err) {
26768
+ if (err.code === "ENOENT") return null;
26769
+ throw err instanceof Error ? err : new Error(String(err));
26770
+ }
26771
+ return parseKeyring(raw);
26772
+ }
26773
+ /**
26774
+ * First mint: the keyring is created at its FINAL path with a
26775
+ * creation-exclusive write, so two processes racing a fresh machine cannot
26776
+ * each mint a different epoch 1 — with tmp + rename the loser's replace
26777
+ * would orphan everything the winner had already sealed. On EEXIST the
26778
+ * loser re-reads and adopts the winner's keyring; it minted nothing.
26779
+ * Atomic replace is unnecessary here: nothing can be mid-read of a file
26780
+ * that did not exist, and a torn exclusive write parses as corrupt on the
26781
+ * next read and fails secure rather than being re-minted over.
26782
+ */
26783
+ #createExclusive() {
26784
+ ensureDataDirSync(this.#keysDir);
26785
+ const keyring = mintKeyring();
26786
+ try {
26787
+ writeFileSync2(this.filePath, `${serializeKeyring(keyring)}
26788
+ `, {
26789
+ flag: "wx",
26790
+ mode: DATA_FILE_MODE
26791
+ });
26792
+ } catch (err) {
26793
+ if (err.code !== "EEXIST") throw asError(err);
26794
+ const winner = this.#read();
26795
+ if (!winner) {
26796
+ throw new Error("vault: key file vanished during first mint", { cause: err });
26797
+ }
26798
+ return winner;
26799
+ }
26800
+ tightenFileMode(this.filePath);
26801
+ return keyring;
26802
+ }
26803
+ /**
26804
+ * Atomic tmp + rename so a crash mid-write cannot truncate the keyring.
26805
+ * Used only for rotation, under the rotation lock — first creation goes
26806
+ * through the creation-exclusive path instead.
26807
+ */
26808
+ #write(keyring) {
26809
+ ensureDataDirSync(this.#keysDir);
26810
+ const file2 = this.filePath;
26811
+ const tmp = `${file2}.tmp`;
26812
+ writeFileSync2(tmp, `${serializeKeyring(keyring)}
26813
+ `, { mode: DATA_FILE_MODE });
26814
+ renameSync4(tmp, file2);
26815
+ tightenFileMode(file2);
26816
+ return keyring;
26817
+ }
26818
+ };
26819
+ function tightenFileMode(file2) {
26820
+ try {
26821
+ chmodSync2(file2, DATA_FILE_MODE);
26822
+ } catch {
26823
+ }
26824
+ }
26825
+ var runSecurity = (args) => execFileSync2("/usr/bin/security", args, {
26826
+ encoding: "utf8",
26827
+ stdio: ["ignore", "pipe", "ignore"]
26828
+ });
26829
+ var SECURITY_ITEM_NOT_FOUND = 44;
26830
+ var KeychainKeyProvider = class {
26831
+ #keysDir;
26832
+ #exec;
26833
+ constructor(keysDir2, exec = runSecurity) {
26834
+ if (exec === runSecurity && process.platform !== "darwin") {
26835
+ throw new Error(
26836
+ `keychain custody is not available on this platform (${process.platform}); use file custody`
26837
+ );
26838
+ }
26839
+ this.#keysDir = keysDir2;
26840
+ this.#exec = exec;
26841
+ }
26842
+ /** Where a fallback file provider for the same vault would keep its keyring. */
26843
+ get keysDir() {
26844
+ return this.#keysDir;
26845
+ }
26846
+ loadOrCreate() {
26847
+ return asAsync(() => {
26848
+ const existing = this.#read();
26849
+ if (existing) return currentOf(existing);
26850
+ return currentOf(this.#create(mintKeyring()));
26851
+ });
26852
+ }
26853
+ rotate() {
26854
+ return asAsync(
26855
+ () => withRotationLock(this.#keysDir, () => {
26856
+ const existing = this.#read();
26857
+ if (!existing) return currentOf(this.#create(mintKeyring()));
26858
+ return currentOf(this.#replace(withNextEpoch(existing)));
26859
+ })
26860
+ );
26861
+ }
26862
+ materialFor(version2) {
26863
+ return asAsync(() => {
26864
+ const existing = this.#read();
26865
+ if (!existing) throw new VaultKeyEpochMissingError(version2);
26866
+ return epochOf(existing, version2);
26867
+ });
26868
+ }
26869
+ /** The keyring, or null when no item exists yet. A corrupt item throws. */
26870
+ #read() {
26871
+ let raw;
26872
+ try {
26873
+ raw = this.#exec([
26874
+ "find-generic-password",
26875
+ "-s",
26876
+ KEYCHAIN_SERVICE,
26877
+ "-a",
26878
+ KEYCHAIN_ACCOUNT,
26879
+ "-w"
26880
+ ]);
26881
+ } catch (err) {
26882
+ if (err.status === SECURITY_ITEM_NOT_FOUND) return null;
26883
+ throw new Error(
26884
+ `vault: keychain read failed (${err instanceof Error ? err.message : String(err)}); refusing to treat the failure as an absent keyring`,
26885
+ { cause: err }
26886
+ );
26887
+ }
26888
+ const body = raw.trim();
26889
+ if (body.length === 0) return null;
26890
+ return parseKeyring(body);
26891
+ }
26892
+ /**
26893
+ * First mint: a plain `add-generic-password` (no `-U`) fails when an item
26894
+ * already exists, so a concurrent first mint cannot overwrite the winner's
26895
+ * keyring — the loser re-reads and adopts it instead.
26896
+ */
26897
+ #create(keyring) {
26898
+ const args = [
26899
+ "add-generic-password",
26900
+ "-s",
26901
+ KEYCHAIN_SERVICE,
26902
+ "-a",
26903
+ KEYCHAIN_ACCOUNT,
26904
+ "-w",
26905
+ serializeKeyring(keyring)
26906
+ ];
26907
+ try {
26908
+ this.#exec(args);
26909
+ } catch (err) {
26910
+ const winner = this.#read();
26911
+ if (winner) return winner;
26912
+ throw asError(err);
26913
+ }
26914
+ return keyring;
26915
+ }
26916
+ // `-U` updates the item in place, deliberately replacing the stored map with
26917
+ // one that contains it — used only for rotation, under the rotation lock.
26918
+ #replace(keyring) {
26919
+ this.#exec([
26920
+ "add-generic-password",
26921
+ "-U",
26922
+ "-s",
26923
+ KEYCHAIN_SERVICE,
26924
+ "-a",
26925
+ KEYCHAIN_ACCOUNT,
26926
+ "-w",
26927
+ serializeKeyring(keyring)
26928
+ ]);
26929
+ return keyring;
26930
+ }
26931
+ };
26932
+ function createKeyProvider(custody, keysDir2) {
26933
+ if (custody === "keychain") return new KeychainKeyProvider(keysDir2);
26934
+ return new FileKeyProvider(keysDir2);
26935
+ }
26936
+
26937
+ // ../packages/persistence/src/vault/vault.ts
26938
+ import { randomBytes as randomBytes3, randomUUID as randomUUID10 } from "crypto";
26939
+ var CONSENT_ABSENT = /* @__PURE__ */ Symbol("aka.vault.consentAbsent");
26940
+ var UNAVAILABLE = /* @__PURE__ */ Symbol("aka.vault.unavailable");
26941
+ var VAULT_PURGE_POINTER_ID = "*";
26942
+ function parsePointer(token) {
26943
+ if (!POINTER_TOKEN_ANCHORED.test(token)) return null;
26944
+ const body = token.slice("[[aka:".length, -"]]".length);
26945
+ const colon = body.indexOf(":");
26946
+ if (colon < 0) return null;
26947
+ const category = body.slice(0, colon);
26948
+ const [kv, id, tag] = body.slice(colon + 1).split(".");
26949
+ if (kv === void 0 || id === void 0 || tag === void 0) return null;
26950
+ try {
26951
+ const keyVersion = decodeKeyVersion(kv);
26952
+ const pointerId = base32Decode(id);
26953
+ const tagBytes = base32Decode(tag);
26954
+ if (encodeKeyVersion(keyVersion) !== kv || base32Encode(pointerId) !== id || base32Encode(tagBytes) !== tag) {
26955
+ return null;
26956
+ }
26957
+ return { category, keyVersion, pointerId, tag: tagBytes };
26958
+ } catch {
26959
+ return null;
26960
+ }
26961
+ }
26962
+ var SecretVault = class {
26963
+ #repo;
26964
+ #keys;
26965
+ #fingerprintKey;
26966
+ #isConsented;
26967
+ #verifyGrant;
26968
+ #now;
26969
+ constructor(deps) {
26970
+ this.#repo = deps.repo;
26971
+ this.#keys = deps.keys;
26972
+ this.#fingerprintKey = deps.fingerprintKey;
26973
+ this.#isConsented = deps.isConsented;
26974
+ this.#verifyGrant = deps.verifyGrant;
26975
+ this.#now = deps.now ?? (() => Date.now());
26976
+ }
26977
+ /**
26978
+ * Store a value and return the pointer that stands for it. The same value
26979
+ * always yields the same pointer on this machine — one row, one pointer id,
26980
+ * one category — which is what makes dedup and reuse counting work.
26981
+ */
26982
+ async tokenize(raw, meta3) {
26983
+ if (!this.#isConsented()) return CONSENT_ABSENT;
26984
+ const valueFingerprint = fingerprintValue(this.#fingerprintKey, raw);
26985
+ const existing = this.#repo.byValueFingerprint(valueFingerprint);
26986
+ const now = this.#now();
26987
+ if (existing) {
26988
+ this.#repo.upsert({ ...existing, provider: existing.provider ?? void 0 }, now);
26989
+ return await this.#emitToken(existing.keyVersion, existing.pointerId, existing.category);
26990
+ }
26991
+ const { material, version: version2 } = await this.#keys.loadOrCreate();
26992
+ const subkeys = deriveSubkeys(material);
26993
+ const pointerId = randomBytes3(POINTER_ID_BYTES);
26994
+ const aad = bindingInput(version2, pointerId, meta3.category, POINTER_FORMAT_VERSION);
26995
+ const sealed = seal(subkeys.enc, raw, aad, randomBytes3(NONCE_BYTES));
26996
+ const { row } = this.#repo.upsert(
26997
+ {
26998
+ pointerId: base32Encode(pointerId),
26999
+ valueFingerprint,
27000
+ fingerprintKeyVersion: this.#fingerprintKey.version,
27001
+ keyVersion: version2,
27002
+ // Recorded so the row stays OPENABLE if the wire-format constant ever
27003
+ // moves: it is part of this row's AEAD AAD. It is not a tag input —
27004
+ // tags are pinned to the constant on both sides.
27005
+ formatVersion: POINTER_FORMAT_VERSION,
27006
+ category: meta3.category,
27007
+ ruleId: meta3.ruleId,
27008
+ maskedMatch: meta3.maskedMatch,
27009
+ provider: meta3.provider,
27010
+ ciphertext: sealed.ciphertext.toString("base64"),
27011
+ nonce: sealed.nonce.toString("base64"),
27012
+ authTag: sealed.authTag.toString("base64")
27013
+ },
27014
+ now
27015
+ );
27016
+ return await this.#emitToken(row.keyVersion, row.pointerId, row.category);
27017
+ }
27018
+ /**
27019
+ * Resolve a pointer back to its value, for a human or (with a grant) for the
27020
+ * model. Every call that gets as far as an identified row writes an audit row.
27021
+ */
27022
+ async detokenize(token, opts) {
27023
+ const parsed = parsePointer(token);
27024
+ if (!parsed) return UNAVAILABLE;
27025
+ let signKey;
27026
+ try {
27027
+ const epoch = await this.#keys.materialFor(parsed.keyVersion);
27028
+ signKey = deriveSubkeys(epoch.material).sign;
27029
+ } catch {
27030
+ return UNAVAILABLE;
27031
+ }
27032
+ if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
27033
+ return UNAVAILABLE;
27034
+ }
27035
+ const pointerId = base32Encode(parsed.pointerId);
27036
+ const row = this.#repo.byPointerId(pointerId);
27037
+ if (!row) {
27038
+ this.#audit(pointerId, opts, "unavailable");
27039
+ return UNAVAILABLE;
27040
+ }
27041
+ if (row.category !== parsed.category) return UNAVAILABLE;
27042
+ if (opts.target === "model") {
27043
+ const grantId = opts.grantId;
27044
+ const verify = this.#verifyGrant;
27045
+ if (verify === void 0 || grantId === void 0 || grantId === "") {
27046
+ this.#audit(pointerId, opts, "refused");
27047
+ return UNAVAILABLE;
27048
+ }
27049
+ let covered;
27050
+ try {
27051
+ covered = await verify(grantId, {
27052
+ ruleId: row.ruleId,
27053
+ valueFingerprint: row.valueFingerprint,
27054
+ fingerprintKeyVersion: row.fingerprintKeyVersion
27055
+ });
27056
+ } catch {
27057
+ covered = false;
27058
+ }
27059
+ if (!covered) {
27060
+ this.#audit(pointerId, opts, "refused");
27061
+ return UNAVAILABLE;
27062
+ }
27063
+ }
27064
+ let raw;
27065
+ try {
27066
+ const epoch = await this.#keys.materialFor(row.keyVersion);
27067
+ raw = open(
27068
+ deriveSubkeys(epoch.material).enc,
27069
+ {
27070
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
27071
+ nonce: Buffer.from(row.nonce, "base64"),
27072
+ authTag: Buffer.from(row.authTag, "base64")
27073
+ },
27074
+ // Sealed under the ROW's epoch and format version. Rotation may have
27075
+ // moved the epoch past the one this token names, and a format bump may
27076
+ // have moved the constant past the generation this row was sealed
27077
+ // under — the AAD follows the row in both cases, never the token.
27078
+ bindingInput(row.keyVersion, parsed.pointerId, row.category, row.formatVersion)
27079
+ );
27080
+ } catch {
27081
+ raw = null;
27082
+ }
27083
+ if (raw === null) {
27084
+ this.#audit(pointerId, opts, "unavailable");
27085
+ return UNAVAILABLE;
27086
+ }
27087
+ this.#audit(pointerId, opts, "revealed");
27088
+ return raw;
27089
+ }
27090
+ /**
27091
+ * Owner-surface reveal by row id: the dashboard shows a row the owner can
27092
+ * already see and asks for its value. There is no wire token here to verify —
27093
+ * the tag exists to stop FORGED tokens arriving in untrusted text, and a row
27094
+ * id selected server-side from the owner's own store is not that — so this
27095
+ * loads the row directly, opens its ciphertext under the row's epoch, and
27096
+ * audits exactly like a human-target de-reference. Never callable with
27097
+ * target 'model': the wire-token path with its grant gate is the only road
27098
+ * raw travels toward the model.
27099
+ */
27100
+ async revealEntry(pointerId, opts) {
27101
+ const row = this.#repo.byPointerId(pointerId);
27102
+ if (!row) {
27103
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
27104
+ return UNAVAILABLE;
27105
+ }
27106
+ const raw = await this.#openRow(row);
27107
+ if (raw === null) {
27108
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "unavailable");
27109
+ return UNAVAILABLE;
27110
+ }
27111
+ this.#audit(pointerId, { target: "human", reason: opts.reason }, "revealed");
27112
+ return raw;
27113
+ }
27114
+ /** Badge and listing data. No raw value, no fingerprint, and no audit row. */
27115
+ async describePointer(token) {
27116
+ const row = await this.#rowFor(token);
27117
+ if (!row) return null;
27118
+ return {
27119
+ category: row.category,
27120
+ ...row.provider === void 0 ? {} : { provider: row.provider },
27121
+ maskedMatch: row.maskedMatch,
27122
+ occurrences: row.occurrenceCount,
27123
+ firstSeen: new Date(row.firstSeen).toISOString(),
27124
+ lastSeen: new Date(row.lastSeen).toISOString()
27125
+ };
27126
+ }
27127
+ /**
27128
+ * The raw-free row identity a reveal grant matches on. Deliberately not fed to
27129
+ * view surfaces: the keyed fingerprint is a correlation key and must not reach
27130
+ * a presentation layer.
27131
+ */
27132
+ async resolvePointerIdentity(token) {
27133
+ const row = await this.#rowFor(token);
27134
+ if (!row) return null;
27135
+ return {
27136
+ ruleId: row.ruleId,
27137
+ valueFingerprint: row.valueFingerprint,
27138
+ fingerprintKeyVersion: row.fingerprintKeyVersion
27139
+ };
27140
+ }
27141
+ /**
27142
+ * Mint the next vault key epoch and re-encrypt every entry under it. Pointers
27143
+ * already emitted keep verifying: their tag is checked against the historical
27144
+ * epoch they name, which the key provider retains.
27145
+ *
27146
+ * Safe to interrupt — each row carries the epoch its ciphertext is sealed
27147
+ * under, so a half-finished pass leaves every row openable.
27148
+ *
27149
+ * The rotation lock covers only the keyring mint inside `rotate()`; the
27150
+ * re-seal pass below runs unlocked. Two concurrent rotations therefore
27151
+ * serialize on the keyring but interleave over the rows, so a slower pass can
27152
+ * re-seal a row back to an epoch a faster one already moved past, and
27153
+ * `reEncrypted` can double-count. No value is lost either way — every epoch is
27154
+ * retained and every row stays openable — but "after rotation every row sits
27155
+ * at the newest epoch" does not hold under concurrency. Holding the lock
27156
+ * across the whole pass requires an async-aware lock, since a callback that
27157
+ * awaits would release the lock at its first suspension.
27158
+ */
27159
+ async rotateVaultKey() {
27160
+ const next = await this.#keys.rotate();
27161
+ const nextEnc = deriveSubkeys(next.material).enc;
27162
+ let reEncrypted = 0;
27163
+ for (const row of this.#repo.listAll()) {
27164
+ if (row.keyVersion === next.version) continue;
27165
+ const pointerId = base32Decode(row.pointerId);
27166
+ let raw;
27167
+ try {
27168
+ const epoch = await this.#keys.materialFor(row.keyVersion);
27169
+ raw = open(
27170
+ deriveSubkeys(epoch.material).enc,
27171
+ {
27172
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
27173
+ nonce: Buffer.from(row.nonce, "base64"),
27174
+ authTag: Buffer.from(row.authTag, "base64")
27175
+ },
27176
+ bindingInput(row.keyVersion, pointerId, row.category, row.formatVersion)
27177
+ );
27178
+ } catch {
27179
+ raw = null;
27180
+ }
27181
+ if (raw === null) continue;
27182
+ const sealed = seal(
27183
+ nextEnc,
27184
+ raw,
27185
+ bindingInput(next.version, pointerId, row.category, row.formatVersion),
27186
+ randomBytes3(NONCE_BYTES)
27187
+ );
27188
+ this.#repo.replaceCiphertext(row.pointerId, {
27189
+ keyVersion: next.version,
27190
+ ciphertext: sealed.ciphertext.toString("base64"),
27191
+ nonce: sealed.nonce.toString("base64"),
27192
+ authTag: sealed.authTag.toString("base64")
27193
+ });
27194
+ reEncrypted += 1;
27195
+ }
27196
+ return { version: next.version, reEncrypted };
27197
+ }
27198
+ /**
27199
+ * Re-key every entry's value fingerprint after the exception key rotates,
27200
+ * PRESERVING each pointer id. Unlike grants — where rotation is invalidation,
27201
+ * because the raw values are gone — the vault still holds the values, so
27202
+ * determinism, dedup, and every outstanding pointer survive the rotation.
27203
+ *
27204
+ * Every fingerprint-key rotation must run this: a row left at the old epoch
27205
+ * still resolves, but the same value detected again fingerprints under the
27206
+ * NEW key, misses the dedup lookup, and mints a second row and a second
27207
+ * pointer — one value, two tokens in circulation.
27208
+ *
27209
+ * Per-row best-effort: a row that cannot open, or whose refreshed
27210
+ * fingerprint collides with a row already refreshed, is skipped rather than
27211
+ * aborting the pass — one damaged entry must not strand the re-key of every
27212
+ * other. A skipped row keeps resolving under its old fingerprint epoch.
27213
+ */
27214
+ async refreshFingerprints(next) {
27215
+ let refreshed = 0;
27216
+ for (const row of this.#repo.listAll()) {
27217
+ try {
27218
+ const raw = await this.#openRow(row);
27219
+ if (raw === null) continue;
27220
+ this.#repo.refreshFingerprint(row.pointerId, {
27221
+ valueFingerprint: fingerprintValue(next, raw),
27222
+ fingerprintKeyVersion: next.version
27223
+ });
27224
+ refreshed += 1;
27225
+ } catch {
27226
+ continue;
27227
+ }
27228
+ }
27229
+ return refreshed;
27230
+ }
27231
+ /**
27232
+ * Destroy every entry, making all outstanding pointers permanently
27233
+ * unresolvable.
27234
+ *
27235
+ * The count comes from `purgeAll` rather than a separate `countEntries` —
27236
+ * `purgeAll` counts inside the same transaction that deletes, so the audit row
27237
+ * reports what was actually destroyed. Counting beforehand would let a
27238
+ * concurrent write land between the two statements and put a number in the
27239
+ * durable record that never matched reality.
27240
+ */
27241
+ purgeVault() {
27242
+ const destroyed = this.#repo.purgeAll();
27243
+ this.#repo.recordDeref({
27244
+ id: randomUUID10(),
27245
+ pointerId: VAULT_PURGE_POINTER_ID,
27246
+ at: this.#now(),
27247
+ target: "human",
27248
+ reason: "purge",
27249
+ outcome: "unavailable",
27250
+ pointerCount: Math.max(destroyed, 1)
27251
+ });
27252
+ return destroyed;
27253
+ }
27254
+ // Sign under the epoch the token names — which for a re-detected value is the
27255
+ // epoch its row currently sits at rather than whatever is current.
27256
+ //
27257
+ // The row's format version is NOT a tag input. It binds the row's ciphertext
27258
+ // (it is part of the AEAD AAD, so an old row stays openable) but never the
27259
+ // wire tag, which verification checks against POINTER_FORMAT_VERSION without
27260
+ // knowing any row. Signing a token here under a row's own generation is what
27261
+ // would make the vault emit tokens it then refuses.
27262
+ async #emitToken(keyVersion, pointerIdB32, category) {
27263
+ const pointerId = base32Decode(pointerIdB32);
27264
+ const epoch = await this.#keys.materialFor(keyVersion);
27265
+ const signKey = deriveSubkeys(epoch.material).sign;
27266
+ return formatPointer(
27267
+ category,
27268
+ keyVersion,
27269
+ pointerId,
27270
+ signPointer(signKey, keyVersion, pointerId, category)
27271
+ );
27272
+ }
27273
+ async #openRow(row) {
27274
+ try {
27275
+ const epoch = await this.#keys.materialFor(row.keyVersion);
27276
+ return open(
27277
+ deriveSubkeys(epoch.material).enc,
27278
+ {
27279
+ ciphertext: Buffer.from(row.ciphertext, "base64"),
27280
+ nonce: Buffer.from(row.nonce, "base64"),
27281
+ authTag: Buffer.from(row.authTag, "base64")
27282
+ },
27283
+ bindingInput(row.keyVersion, base32Decode(row.pointerId), row.category, row.formatVersion)
27284
+ );
27285
+ } catch {
27286
+ return null;
27287
+ }
27288
+ }
27289
+ // Shared lookup for the read-only surfaces. It verifies the tag exactly as
27290
+ // detokenize does: a descriptor is not raw, but a token nobody can vouch for
27291
+ // should not resolve to anything at all — otherwise a fabricated pointer, or a
27292
+ // lookalike planted in a file, would still yield a category and a masked
27293
+ // preview. Verifying needs the historical epoch's key, which is why these
27294
+ // surfaces are async.
27295
+ async #rowFor(token) {
27296
+ const parsed = parsePointer(token);
27297
+ if (!parsed) return null;
27298
+ try {
27299
+ const epoch = await this.#keys.materialFor(parsed.keyVersion);
27300
+ const signKey = deriveSubkeys(epoch.material).sign;
27301
+ if (!verifyPointerTag(signKey, parsed.keyVersion, parsed.pointerId, parsed.category, parsed.tag)) {
27302
+ return null;
27303
+ }
27304
+ } catch {
27305
+ return null;
27306
+ }
27307
+ const row = this.#repo.byPointerId(base32Encode(parsed.pointerId));
27308
+ if (row?.category !== parsed.category) return null;
27309
+ return row;
27310
+ }
27311
+ #audit(pointerId, opts, outcome) {
27312
+ this.#repo.recordDeref({
27313
+ id: randomUUID10(),
27314
+ pointerId,
27315
+ at: this.#now(),
27316
+ target: opts.target,
27317
+ reason: opts.reason,
27318
+ outcome,
27319
+ ...opts.grantId === void 0 ? {} : { grantId: opts.grantId },
27320
+ // Only the batched reasons carry a count above one; a model crossing is
27321
+ // always its own row.
27322
+ pointerCount: isBatchedDerefReason(opts.reason) ? opts.pointerCount ?? 1 : 1
27323
+ });
27324
+ }
27325
+ };
27326
+
27327
+ // ../packages/persistence/src/warn-era-cap.ts
27328
+ import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "fs";
27329
+ import { join as join7 } from "path";
25976
27330
 
25977
27331
  // ../packages/plugin-sdk/src/config.ts
25978
27332
  import { existsSync as existsSync5 } from "fs";
25979
- import { join as join7 } from "path";
27333
+ import { join as join8 } from "path";
25980
27334
 
25981
27335
  // ../packages/plugin-sdk/src/provider-env.ts
25982
27336
  var booleanish = external_exports.string().optional().transform((v) => {
@@ -25997,9 +27351,9 @@ var providerEnvShape = {
25997
27351
  var ProviderEnvSchema = external_exports.object(providerEnvShape);
25998
27352
 
25999
27353
  // ../packages/plugin-sdk/src/config-inventory.ts
26000
- import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync2 } from "fs";
27354
+ import { readdirSync, readFileSync as readFileSync6, realpathSync, statSync as statSync3 } from "fs";
26001
27355
  import { homedir as homedir4 } from "os";
26002
- import { basename as basename2, join as join9 } from "path";
27356
+ import { basename as basename2, join as join10 } from "path";
26003
27357
 
26004
27358
  // ../rules/code-flaws/auth-jwt-no-verify.json
26005
27359
  var auth_jwt_no_verify_default = {
@@ -28034,8 +29388,8 @@ function bundledDetections() {
28034
29388
  }
28035
29389
 
28036
29390
  // ../packages/plugin-sdk/src/repo.ts
28037
- import { existsSync as existsSync6, readFileSync as readFileSync4, statSync } from "fs";
28038
- import { basename, dirname as dirname2, isAbsolute, join as join8, sep as sep2 } from "path";
29391
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
29392
+ import { basename, dirname as dirname2, isAbsolute, join as join9, sep as sep2 } from "path";
28039
29393
  function resolveRepoIdentity(cwd) {
28040
29394
  try {
28041
29395
  const root = findGitRoot(cwd);
@@ -28073,36 +29427,36 @@ function resolveHeadRoot(cwd) {
28073
29427
  function findGitRoot(start) {
28074
29428
  let dir = start;
28075
29429
  for (; ; ) {
28076
- if (existsSync6(join8(dir, ".git"))) return dir;
29430
+ if (existsSync6(join9(dir, ".git"))) return dir;
28077
29431
  const parent = dirname2(dir);
28078
29432
  if (parent === dir) return void 0;
28079
29433
  dir = parent;
28080
29434
  }
28081
29435
  }
28082
29436
  function resolveGitContext(root) {
28083
- const dotGit = join8(root, ".git");
29437
+ const dotGit = join9(root, ".git");
28084
29438
  try {
28085
- if (statSync(dotGit).isDirectory()) {
28086
- return { configPath: join8(dotGit, "config"), headRoot: root };
29439
+ if (statSync2(dotGit).isDirectory()) {
29440
+ return { configPath: join9(dotGit, "config"), headRoot: root };
28087
29441
  }
28088
29442
  } catch {
28089
29443
  return void 0;
28090
29444
  }
28091
29445
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
28092
29446
  if (!target) return void 0;
28093
- const gitdir = isAbsolute(target) ? target : join8(root, target);
28094
- if (existsSync6(join8(gitdir, "config"))) {
28095
- return { configPath: join8(gitdir, "config"), headRoot: root };
29447
+ const gitdir = isAbsolute(target) ? target : join9(root, target);
29448
+ if (existsSync6(join9(gitdir, "config"))) {
29449
+ return { configPath: join9(gitdir, "config"), headRoot: root };
28096
29450
  }
28097
- const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
29451
+ const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
28098
29452
  if (!commonRaw) return void 0;
28099
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
29453
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join9(gitdir, commonRaw);
28100
29454
  const headRoot = basename(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
28101
- return { configPath: join8(commonGitDir, "config"), headRoot };
29455
+ return { configPath: join9(commonGitDir, "config"), headRoot };
28102
29456
  }
28103
29457
  function safeRead(path) {
28104
29458
  try {
28105
- return readFileSync4(path, "utf8");
29459
+ return readFileSync5(path, "utf8");
28106
29460
  } catch {
28107
29461
  return void 0;
28108
29462
  }
@@ -28140,14 +29494,14 @@ function slugFromUrl(url2) {
28140
29494
  }
28141
29495
 
28142
29496
  // ../packages/plugin-sdk/src/events.ts
28143
- import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
29497
+ import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
28144
29498
 
28145
29499
  // ../packages/plugin-sdk/src/inventory-resolver.ts
28146
29500
  import { arch, hostname as hostname3, platform, release } from "os";
28147
29501
 
28148
29502
  // ../packages/plugin-sdk/src/nudge.ts
28149
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
28150
- import { join as join10 } from "path";
29503
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
29504
+ import { join as join11 } from "path";
28151
29505
 
28152
29506
  // ../packages/plugin-sdk/src/paths.ts
28153
29507
  import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
@@ -28185,8 +29539,8 @@ function resolveNonGitProject(startDir, recognizeMarker) {
28185
29539
 
28186
29540
  // ../packages/plugin-sdk/src/project-files.ts
28187
29541
  var import_ignore = __toESM(require_ignore(), 1);
28188
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28189
- import { basename as basename4, join as join11, relative, sep as sep4 } from "path";
29542
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
29543
+ import { basename as basename4, join as join12, relative, sep as sep4 } from "path";
28190
29544
  var SKIP_DIRS = /* @__PURE__ */ new Set([
28191
29545
  ".git",
28192
29546
  "node_modules",
@@ -28206,7 +29560,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
28206
29560
  var MAX_FILES = 2e4;
28207
29561
  function readIgnoreLayer(dir) {
28208
29562
  try {
28209
- const content = readFileSync7(join11(dir, ".gitignore"), "utf8");
29563
+ const content = readFileSync8(join12(dir, ".gitignore"), "utf8");
28210
29564
  return { base: dir, matcher: (0, import_ignore.default)().add(content) };
28211
29565
  } catch {
28212
29566
  return void 0;
@@ -28284,10 +29638,10 @@ function resolveProjectFiles(cwd) {
28284
29638
  const layer = readIgnoreLayer(dir);
28285
29639
  const dirLayers = layer ? [...layers, layer] : layers;
28286
29640
  for (const entry of dirents) {
28287
- const fullPath = join11(dir, entry.name);
29641
+ const fullPath = join12(dir, entry.name);
28288
29642
  if (entry.isDirectory()) {
28289
29643
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
28290
- if (existsSync7(join11(fullPath, ".git"))) continue;
29644
+ if (existsSync7(join12(fullPath, ".git"))) continue;
28291
29645
  if (visit3(fullPath, dirLayers)) return true;
28292
29646
  continue;
28293
29647
  }
@@ -28322,21 +29676,21 @@ function resolveProjectFiles(cwd) {
28322
29676
  }
28323
29677
 
28324
29678
  // ../packages/plugin-sdk/src/runtime.ts
28325
- import { randomUUID as randomUUID10 } from "crypto";
29679
+ import { randomUUID as randomUUID12 } from "crypto";
28326
29680
 
28327
29681
  // ../packages/plugin-sdk/src/suppressions.ts
28328
29682
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
28329
29683
 
28330
29684
  // ../packages/plugin-sdk/src/throttle.ts
28331
- import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
28332
- import { join as join12 } from "path";
29685
+ import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
29686
+ import { join as join13 } from "path";
28333
29687
 
28334
29688
  // ../packages/local-ops/src/egress-record.ts
28335
29689
  function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
28336
29690
  try {
28337
29691
  if (!readWorkspaceSettings(base).dataSharesInPlace) return null;
28338
29692
  const abs = resolve(target);
28339
- const targetDir = statSync4(abs).isDirectory() ? abs : dirname4(abs);
29693
+ const targetDir = statSync5(abs).isDirectory() ? abs : dirname4(abs);
28340
29694
  const identity = resolveRepoIdentity(abs);
28341
29695
  const worktreeRoot = resolveWorktreeRoot(abs);
28342
29696
  let root;
@@ -28377,9 +29731,9 @@ function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
28377
29731
  }
28378
29732
 
28379
29733
  // ../packages/local-ops/src/fs-scan.ts
28380
- import { createHash as createHash5, randomUUID as randomUUID11 } from "crypto";
28381
- import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
28382
- import { basename as basename5, extname, join as join13, relative as relative3, resolve as resolve2 } from "path";
29734
+ import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
29735
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
29736
+ import { basename as basename5, extname, join as join14, relative as relative3, resolve as resolve2 } from "path";
28383
29737
  var import_ignore2 = __toESM(require_ignore(), 1);
28384
29738
  var AKAIGNORE_FILENAME = ".akaignore";
28385
29739
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
@@ -28395,7 +29749,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
28395
29749
  var MAX_BYTES = 1e6;
28396
29750
  function readIgnoreLayer2(dir, filename) {
28397
29751
  try {
28398
- const content = readFileSync8(join13(dir, filename), "utf8");
29752
+ const content = readFileSync9(join14(dir, filename), "utf8");
28399
29753
  return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
28400
29754
  } catch {
28401
29755
  return void 0;
@@ -28414,7 +29768,7 @@ function evaluate(layers, absPath, isDir) {
28414
29768
  function* collectFiles(target) {
28415
29769
  let st;
28416
29770
  try {
28417
- st = statSync5(target);
29771
+ st = statSync6(target);
28418
29772
  } catch {
28419
29773
  return;
28420
29774
  }
@@ -28431,7 +29785,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
28431
29785
  const skipLayer = readIgnoreLayer2(dir, AKAIGNORE_FILENAME);
28432
29786
  const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
28433
29787
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
28434
- const path = join13(dir, entry.name);
29788
+ const path = join14(dir, entry.name);
28435
29789
  if (entry.isDirectory()) {
28436
29790
  const skipState = evaluate(dirSkipLayers, path, true);
28437
29791
  if (skipState !== "unignored" && (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".") || skipState === "ignored")) {
@@ -28443,7 +29797,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
28443
29797
  if (evaluate(dirSkipLayers, path, false) === "ignored") continue;
28444
29798
  let size;
28445
29799
  try {
28446
- size = statSync5(path).size;
29800
+ size = statSync6(path).size;
28447
29801
  } catch {
28448
29802
  continue;
28449
29803
  }
@@ -28484,16 +29838,17 @@ function scanPathIntoStore(db, target, opts = {}) {
28484
29838
  for (const { path: file2, gitignored } of collectFiles(resolve2(target))) {
28485
29839
  let text;
28486
29840
  try {
28487
- text = readFileSync8(file2, "utf8");
29841
+ text = readFileSync9(file2, "utf8");
28488
29842
  } catch {
28489
29843
  continue;
28490
29844
  }
28491
29845
  scanned++;
28492
29846
  const fileEgress = extractFileEgress(file2, text);
28493
29847
  if (fileEgress) egressFiles.push(fileEgress);
28494
- const matches = scan(text, opts.rules);
29848
+ const shielded = shieldPointers(text);
29849
+ const matches = dropShieldedFindings(scan(shielded.text, opts.rules), shielded.spans);
28495
29850
  if (matches.length === 0) continue;
28496
- const eventId = randomUUID11();
29851
+ const eventId = randomUUID13();
28497
29852
  const metadata = { filePath: file2 };
28498
29853
  if (gitignored) metadata.gitignored = true;
28499
29854
  const event = {
@@ -28511,7 +29866,7 @@ function scanPathIntoStore(db, target, opts = {}) {
28511
29866
  const key = resolveFingerprintKey();
28512
29867
  const valueFingerprint = key ? fingerprintValue(key, m.rawMatch) : maskedMatch;
28513
29868
  return {
28514
- id: randomUUID11(),
29869
+ id: randomUUID13(),
28515
29870
  eventId,
28516
29871
  ruleId: m.ruleId,
28517
29872
  category: m.category,
@@ -28536,12 +29891,12 @@ function scanPathIntoStore(db, target, opts = {}) {
28536
29891
  }
28537
29892
 
28538
29893
  // ../packages/local-ops/src/project-inventory.ts
28539
- import { statSync as statSync6 } from "fs";
29894
+ import { statSync as statSync7 } from "fs";
28540
29895
  import { resolve as resolve3 } from "path";
28541
29896
  function recordProjectInventory(db, target) {
28542
29897
  try {
28543
29898
  const abs = resolve3(target);
28544
- statSync6(abs);
29899
+ statSync7(abs);
28545
29900
  const identity = resolveRepoIdentity(abs);
28546
29901
  if (!identity) return null;
28547
29902
  const sourceProjectId2 = db.sourceProject.upsert({
@@ -28590,17 +29945,17 @@ function reinvokeArgv(subcommand, extraArgs = []) {
28590
29945
 
28591
29946
  // ../packages/local-ops/src/update-cache.ts
28592
29947
  import { spawn } from "child_process";
28593
- import { existsSync as existsSync8, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
28594
- import { join as join14 } from "path";
29948
+ import { existsSync as existsSync8, readFileSync as readFileSync10, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
29949
+ import { join as join15 } from "path";
28595
29950
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
28596
29951
  function cachePath(home) {
28597
- return join14(dataDir(home), "update-check.json");
29952
+ return join15(dataDir(home), "update-check.json");
28598
29953
  }
28599
29954
  function readCache(home) {
28600
29955
  const path = cachePath(home);
28601
29956
  if (!existsSync8(path)) return null;
28602
29957
  try {
28603
- const raw = JSON.parse(readFileSync9(path, "utf8"));
29958
+ const raw = JSON.parse(readFileSync10(path, "utf8"));
28604
29959
  if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
28605
29960
  return raw;
28606
29961
  } catch {
@@ -28610,7 +29965,7 @@ function readCache(home) {
28610
29965
  function writeCache(home, cache) {
28611
29966
  if (!existsSync8(dataDir(home))) return;
28612
29967
  try {
28613
- writeFileSync5(cachePath(home), `${JSON.stringify(cache, null, 2)}
29968
+ writeFileSync6(cachePath(home), `${JSON.stringify(cache, null, 2)}
28614
29969
  `);
28615
29970
  } catch {
28616
29971
  }
@@ -28620,7 +29975,7 @@ function isStale(cache, ttlMs = CHECK_TTL_MS) {
28620
29975
  }
28621
29976
  function clearCache(home) {
28622
29977
  try {
28623
- rmSync3(cachePath(home), { force: true });
29978
+ rmSync4(cachePath(home), { force: true });
28624
29979
  } catch {
28625
29980
  }
28626
29981
  }
@@ -28742,6 +30097,12 @@ var COMMAND_SPECS = [
28742
30097
  summary: "Manage detection exceptions (approve, add, list, show, revoke, rotate-key)",
28743
30098
  args: ["approve", "add", "list", "show", "revoke", "rotate-key"]
28744
30099
  },
30100
+ {
30101
+ name: "vault",
30102
+ argHint: "[show]",
30103
+ summary: "Reveal a vaulted secret by its pointer (every reveal is audited)",
30104
+ args: ["show"]
30105
+ },
28745
30106
  {
28746
30107
  name: "tui",
28747
30108
  argHint: "[view]",
@@ -28961,7 +30322,7 @@ import { spawn as spawn3 } from "child_process";
28961
30322
  import { existsSync as existsSync10 } from "fs";
28962
30323
  import { createRequire as createRequire2 } from "module";
28963
30324
  import { createServer } from "net";
28964
- import { dirname as dirname5, join as join15 } from "path";
30325
+ import { dirname as dirname5, join as join16 } from "path";
28965
30326
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
28966
30327
  import { parseArgs as parseArgs2 } from "util";
28967
30328
 
@@ -28982,7 +30343,7 @@ var require3 = createRequire2(import.meta.url);
28982
30343
  var here = dirname5(fileURLToPath2(import.meta.url));
28983
30344
  var BIND_HOST = "127.0.0.1";
28984
30345
  function pkgBase() {
28985
- return isSea() ? dirname5(process.execPath) : join15(here, "..");
30346
+ return isSea() ? dirname5(process.execPath) : join16(here, "..");
28986
30347
  }
28987
30348
  function isPortFree(port) {
28988
30349
  return new Promise((resolve4) => {
@@ -29036,8 +30397,8 @@ async function runDashboard(argv) {
29036
30397
  refreshUpdateMirror(dataDir());
29037
30398
  const base = pkgBase();
29038
30399
  const bundledServer = [
29039
- join15(base, "web-ui", "server.js"),
29040
- join15(base, "web-ui", "web-ui", "server.js")
30400
+ join16(base, "web-ui", "server.js"),
30401
+ join16(base, "web-ui", "web-ui", "server.js")
29041
30402
  ].find((p) => existsSync10(p));
29042
30403
  if (bundledServer) {
29043
30404
  launchStandalone(bundledServer, port, shouldOpen, url2);
@@ -29057,7 +30418,7 @@ async function runDashboard(argv) {
29057
30418
  onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
29058
30419
  `);
29059
30420
  };
29060
- if (existsSync10(join15(webUiDir, ".next", "BUILD_ID"))) {
30421
+ if (existsSync10(join16(webUiDir, ".next", "BUILD_ID"))) {
29061
30422
  startNext();
29062
30423
  return;
29063
30424
  }
@@ -29395,7 +30756,7 @@ var SCOPE_REQUIRED = `a scope is required \u2014 pick exactly one (there is no d
29395
30756
  --for <duration> time-limited: 30m, 1h, 24h (max 24h)
29396
30757
  --permanent until revoked (typed confirmation required)`;
29397
30758
  var VERB_HELP = {
29398
- approve: `Usage: aka exception approve [reference|value] [flags]
30759
+ approve: `Usage: aka exception approve [reference|value|pointer] [flags]
29399
30760
 
29400
30761
  Grant an exception from a detection blocked in the last 30 minutes. Select the
29401
30762
  block by the reference from the block message, the masked value shown there,
@@ -29403,13 +30764,21 @@ or by pasting the blocked value itself. A pasted value is matched by its keyed
29403
30764
  fingerprint and never stored or echoed \u2014 but it does land in your shell
29404
30765
  history, so prefer the reference where that matters.
29405
30766
 
30767
+ A vault pointer ([[aka:...]] \u2014 quote it in the shell) selects the vaulted
30768
+ value behind it and requires --reveal: the grant lets the model receive that
30769
+ value's RAW form at tool boundaries while it is active, with every crossing
30770
+ audited. Revoke any time with 'aka exception revoke <id>'.
30771
+
29406
30772
  Flags:
30773
+ --reveal-to-model grant reveal-to-model instead of suppression (required with a
30774
+ pointer selector; --reveal is accepted as shorthand)
29407
30775
  --once | --for <30m|1h|24h> | --permanent scope (required, pick one)
29408
30776
  --reason "<why>" required; prompted on a terminal if omitted
29409
30777
  --yes skip confirmations (non-interactive use)
29410
30778
  --home <dir> alternate AKA home (default: ~/.aka)
29411
30779
 
29412
30780
  Example: aka exception approve 3f2a --for 1h --reason "temp deploy creds"
30781
+ Example: aka exception approve '[[aka:secret:...]]' --reveal-to-model --for 1h --reason "agent needs the live key"
29413
30782
  `,
29414
30783
  add: `Usage: aka exception add --rule <ruleId> [--stdin] [flags]
29415
30784
 
@@ -29596,6 +30965,20 @@ ${keyAccessHint(err, dir)}`, {
29596
30965
  });
29597
30966
  }
29598
30967
  }
30968
+ function grantFieldsFrom(entry, key) {
30969
+ if (!isCurrentKeyVersion(key, entry.keyVersion)) {
30970
+ throw new Error(
30971
+ key === null ? `cannot approve ${entry.reference}: the fingerprint key file is missing, so the fingerprint recorded for it can no longer be matched \u2014 trigger the detection again` : `cannot approve ${entry.reference}: it was blocked under fingerprint key v${String(entry.keyVersion)} and the key is now v${String(key.version)}, so a grant from it could never match \u2014 trigger the detection again`
30972
+ );
30973
+ }
30974
+ return {
30975
+ ruleId: entry.ruleId,
30976
+ category: entry.category,
30977
+ valueFingerprint: entry.valueFingerprint,
30978
+ keyVersion: entry.keyVersion,
30979
+ maskedValue: entry.maskedValue
30980
+ };
30981
+ }
29599
30982
  async function pickBlocked(entries, selector, io, key) {
29600
30983
  if (selector !== void 0) {
29601
30984
  const matches = entries.filter(
@@ -29654,20 +31037,90 @@ async function pickBlocked(entries, selector, io, key) {
29654
31037
  const answer = await io.ask(`Which one? [1-${String(entries.length)}]: `);
29655
31038
  const index = Number.parseInt(answer.trim(), 10);
29656
31039
  const picked = Number.isInteger(index) ? entries[index - 1] : void 0;
29657
- if (!picked) throw new Error(`invalid choice '${answer.trim()}'`);
31040
+ if (!picked)
31041
+ throw new Error(`invalid choice \u2014 enter a number between 1 and ${String(entries.length)}`);
29658
31042
  return picked;
29659
31043
  }
31044
+ function pointerCategory(pointer) {
31045
+ const body = pointer.slice("[[aka:".length);
31046
+ return DetectionCategory.parse(body.slice(0, body.indexOf(":")));
31047
+ }
31048
+ async function runApproveReveal(pointer, values, io, base) {
31049
+ const dir = dataDir(base);
31050
+ const db = openLocalDatabase(dir);
31051
+ try {
31052
+ const vault = new SecretVault({
31053
+ repo: db.secretVault,
31054
+ keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
31055
+ fingerprintKey: loadOrCreateFingerprintKey(dir),
31056
+ // Read live so a consent revocation applies to the very next call.
31057
+ isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
31058
+ });
31059
+ const identity = await vault.resolvePointerIdentity(pointer);
31060
+ if (identity === null) {
31061
+ throw new Error(
31062
+ "the pointer is unknown here \u2014 not one this machine issued (forged or tampered), its vault entry was purged, or the key material is unavailable; nothing was granted"
31063
+ );
31064
+ }
31065
+ const descriptor = await vault.describePointer(pointer);
31066
+ const maskedValue = descriptor?.maskedMatch ?? "\xB7\xB7\xB7";
31067
+ const { scope, reason } = await resolveScopeAndReason(values, io);
31068
+ if (scope.scope === "permanent") {
31069
+ await confirmPermanent(io, maskedValue, values.yes === true);
31070
+ }
31071
+ const granted = await createGrant(db, {
31072
+ ruleId: identity.ruleId,
31073
+ category: descriptor?.category ?? pointerCategory(pointer),
31074
+ valueFingerprint: identity.valueFingerprint,
31075
+ keyVersion: identity.fingerprintKeyVersion,
31076
+ maskedValue,
31077
+ capability: "reveal_to_model",
31078
+ ...scope,
31079
+ justification: reason,
31080
+ conditions: null,
31081
+ createdBy: resolveCreatedBy(),
31082
+ createdVia: "cli-approve"
31083
+ });
31084
+ io.out(
31085
+ `Reveal grant ${shortId(granted.id)}: ${granted.ruleId} ${granted.maskedValue} ${scopeSummary(granted)}
31086
+ While this grant is active the model can receive this value's RAW form at
31087
+ tool boundaries. Every crossing is audited. Revoke with: aka exception revoke ${shortId(granted.id)}
31088
+ `
31089
+ );
31090
+ } finally {
31091
+ db.close();
31092
+ }
31093
+ }
29660
31094
  async function runApprove(argv, io) {
29661
31095
  const { values, positionals } = parseArgs4({
29662
31096
  args: argv,
29663
- options: { ...HOME_OPTION, ...SCOPE_OPTIONS },
31097
+ options: {
31098
+ ...HOME_OPTION,
31099
+ ...SCOPE_OPTIONS,
31100
+ "reveal-to-model": { type: "boolean" },
31101
+ // Shorthand for --reveal-to-model.
31102
+ reveal: { type: "boolean" }
31103
+ },
29664
31104
  allowPositionals: true
29665
31105
  });
29666
31106
  if (values.help) {
29667
31107
  io.out(VERB_HELP.approve ?? "");
29668
31108
  return;
29669
31109
  }
29670
- const dir = dataDir(homeBase(values.home));
31110
+ const base = homeBase(values.home);
31111
+ const wantsReveal = values["reveal-to-model"] === true || values.reveal === true;
31112
+ const candidate = positionals[0]?.trim();
31113
+ const pointer = candidate === void 0 ? void 0 : PointerToken.safeParse(candidate);
31114
+ if (pointer?.success === true) {
31115
+ if (!wantsReveal) {
31116
+ throw new Error(
31117
+ "that selector is a vault pointer \u2014 approving one mints a reveal-to-model grant, which must be asked for explicitly: add --reveal-to-model"
31118
+ );
31119
+ }
31120
+ await runApproveReveal(pointer.data, values, io, base);
31121
+ return;
31122
+ }
31123
+ const dir = dataDir(base);
29671
31124
  const db = openLocalDatabase(dir);
29672
31125
  try {
29673
31126
  const entries = await db.exceptions.recentBlocked();
@@ -29679,24 +31132,16 @@ To pre-authorize a value instead: aka exception add --rule <ruleId>
29679
31132
  );
29680
31133
  return;
29681
31134
  }
29682
- const selector = positionals[0]?.trim();
29683
31135
  const key = readKeyForApprove(dir);
29684
- const entry = await pickBlocked(entries, selector === "" ? void 0 : selector, io, key);
29685
- if (!isCurrentKeyVersion(key, entry.keyVersion)) {
29686
- throw new Error(
29687
- key === null ? `cannot approve ${entry.reference}: the fingerprint key file is missing, so the fingerprint recorded for it can no longer be matched \u2014 trigger the detection again` : `cannot approve ${entry.reference}: it was blocked under fingerprint key v${String(entry.keyVersion)} and the key is now v${String(key.version)}, so a grant from it could never match \u2014 trigger the detection again`
29688
- );
29689
- }
31136
+ const entry = await pickBlocked(entries, candidate === "" ? void 0 : candidate, io, key);
31137
+ const grant = grantFieldsFrom(entry, key);
29690
31138
  const { scope, reason } = await resolveScopeAndReason(values, io);
29691
31139
  if (scope.scope === "permanent") {
29692
31140
  await confirmPermanent(io, entry.maskedValue, values.yes === true);
29693
31141
  }
29694
31142
  const granted = await createGrant(db, {
29695
- ruleId: entry.ruleId,
29696
- category: entry.category,
29697
- valueFingerprint: entry.valueFingerprint,
29698
- keyVersion: entry.keyVersion,
29699
- maskedValue: entry.maskedValue,
31143
+ ...grant,
31144
+ ...wantsReveal ? { capability: "reveal_to_model" } : {},
29700
31145
  ...scope,
29701
31146
  justification: reason,
29702
31147
  conditions: null,
@@ -29704,6 +31149,11 @@ To pre-authorize a value instead: aka exception add --rule <ruleId>
29704
31149
  createdVia: "cli-approve"
29705
31150
  });
29706
31151
  printGranted(io, granted);
31152
+ if (wantsReveal) {
31153
+ io.out(
31154
+ "While this grant is active the model can receive this value in RAW form at tool boundaries; every crossing is audited.\n"
31155
+ );
31156
+ }
29707
31157
  io.out("Resubmit the blocked prompt \u2014 it will now pass.\n");
29708
31158
  } finally {
29709
31159
  db.close();
@@ -29840,7 +31290,11 @@ Just got blocked? Run 'aka exception approve'.
29840
31290
  const row = [
29841
31291
  shortId(ex.id),
29842
31292
  ex.ruleId,
29843
- ex.maskedValue,
31293
+ // A reveal grant is strictly stronger than a plain suppression: while
31294
+ // it is active the model can receive this value's RAW form at tool
31295
+ // boundaries. Tag the row so it reads differently from a suppress-only
31296
+ // grant. The value stays masked — the list never shows raw values.
31297
+ ex.capability === "reveal_to_model" ? `${ex.maskedValue} \xB7 REVEALS-TO-MODEL` : ex.maskedValue,
29844
31298
  ex.scope,
29845
31299
  // An expiry countdown is meaningless once the budget/revocation ended
29846
31300
  // the grant ("consumed · expires in 24m" reads as a contradiction);
@@ -29994,19 +31448,39 @@ written under the old key simply stop matching. They remain listed for audit.
29994
31448
  return;
29995
31449
  }
29996
31450
  }
29997
- let version2;
31451
+ let next;
29998
31452
  try {
29999
- version2 = rotateFingerprintKey(dir).version;
31453
+ next = rotateFingerprintKey(dir);
30000
31454
  } catch (err) {
30001
31455
  const message = err instanceof Error ? err.message : String(err);
30002
31456
  throw new Error(`cannot rotate: ${message}
30003
31457
  ${keyAccessHint(err, dir)}`, { cause: err });
30004
31458
  }
30005
31459
  io.out(
30006
- `Fingerprint key rotated \u2014 new key version v${String(version2)}.
31460
+ `Fingerprint key rotated \u2014 new key version v${String(next.version)}.
30007
31461
  Existing grants no longer match; re-approve deliberately where still needed.
30008
31462
  `
30009
31463
  );
31464
+ try {
31465
+ const base = homeBase(values.home);
31466
+ const vault = new SecretVault({
31467
+ repo: db.secretVault,
31468
+ keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
31469
+ fingerprintKey: next,
31470
+ isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
31471
+ });
31472
+ const refreshed = await vault.refreshFingerprints(next);
31473
+ if (refreshed > 0) {
31474
+ io.out(
31475
+ `Vault re-keyed \u2014 ${String(refreshed)} stored ${refreshed === 1 ? "value" : "values"} follow the new key; pointers are unchanged.
31476
+ `
31477
+ );
31478
+ }
31479
+ } catch {
31480
+ io.out(
31481
+ "Warning: the vault could not be re-keyed under the new fingerprint key.\nStored values still resolve, but re-detections may mint new pointers until it succeeds.\n"
31482
+ );
31483
+ }
30010
31484
  } finally {
30011
31485
  db.close();
30012
31486
  }
@@ -30034,8 +31508,8 @@ async function runException(argv, io = terminalPrompter()) {
30034
31508
  }
30035
31509
 
30036
31510
  // src/commands/init.ts
30037
- import { existsSync as existsSync12, statSync as statSync7 } from "fs";
30038
- import { join as join16 } from "path";
31511
+ import { existsSync as existsSync12, statSync as statSync8 } from "fs";
31512
+ import { join as join17 } from "path";
30039
31513
  import * as readline from "readline/promises";
30040
31514
  import { parseArgs as parseArgs6 } from "util";
30041
31515
 
@@ -30147,12 +31621,12 @@ function looseStorePaths(home) {
30147
31621
  home,
30148
31622
  settingsDir(home),
30149
31623
  dataDir(home),
30150
- join16(settingsDir(home), "settings.json"),
31624
+ join17(settingsDir(home), "settings.json"),
30151
31625
  dbPath(home)
30152
31626
  ];
30153
31627
  return targets.filter((p) => {
30154
31628
  try {
30155
- return (statSync7(p).mode & 63) !== 0;
31629
+ return (statSync8(p).mode & 63) !== 0;
30156
31630
  } catch {
30157
31631
  return false;
30158
31632
  }
@@ -30167,7 +31641,7 @@ async function runInit(argv) {
30167
31641
  ensureLayoutDirSync(home);
30168
31642
  const settings = settingsDir(home);
30169
31643
  ensureLayoutDirSync(settings);
30170
- const settingsFile = join16(settings, "settings.json");
31644
+ const settingsFile = join17(settings, "settings.json");
30171
31645
  const settingsCreated = !existsSync12(settingsFile);
30172
31646
  if (settingsCreated) {
30173
31647
  writeOwnerOnlyFileSync(
@@ -30242,7 +31716,7 @@ async function confirm(question) {
30242
31716
  }
30243
31717
 
30244
31718
  // src/commands/scan.ts
30245
- import { statSync as statSync8 } from "fs";
31719
+ import { statSync as statSync9 } from "fs";
30246
31720
  import { parseArgs as parseArgs7 } from "util";
30247
31721
  function severityRank(s) {
30248
31722
  return Severity.options.indexOf(s);
@@ -30289,7 +31763,7 @@ function runScan(argv) {
30289
31763
  failOn = parsed.data;
30290
31764
  }
30291
31765
  try {
30292
- statSync8(target);
31766
+ statSync9(target);
30293
31767
  } catch {
30294
31768
  process.stderr.write(`aka scan: no such file or directory: ${target}
30295
31769
  `);
@@ -31196,6 +32670,92 @@ function applyPluginUpdate2(status) {
31196
32670
  return ok;
31197
32671
  }
31198
32672
 
32673
+ // src/commands/vault.ts
32674
+ import { parseArgs as parseArgs11 } from "util";
32675
+ var VAULT_HELP = `Reveal a vaulted secret by its pointer (every reveal is audited).
32676
+
32677
+ Resolve a pointer: aka vault show '[[aka:...]]' prints the raw value
32678
+
32679
+ The pointer is the [[aka:...]] token that replaced a detected value. Quote it \u2014
32680
+ the double brackets are shell syntax otherwise. Each successful reveal writes an
32681
+ audit row (reason: explicit-reveal) to the local store.
32682
+
32683
+ Flags:
32684
+ --home <dir> alternate AKA home (default: ~/.aka)
32685
+ `;
32686
+ var UNAVAILABLE_MESSAGE = `the pointer could not be resolved.
32687
+ This means one of: the pointer is not one this machine issued (forged or
32688
+ tampered), its entry was purged from the vault, or the vault key material is
32689
+ unavailable. The vault does not distinguish these cases \u2014 a token nobody can
32690
+ vouch for resolves to nothing.`;
32691
+ function descriptorLine(d) {
32692
+ const kind = d.provider === void 0 ? d.category : `${d.category}/${d.provider}`;
32693
+ return `${kind} \xB7 seen ${String(d.occurrences)}\xD7 \xB7 first ${d.firstSeen.slice(0, 10)}`;
32694
+ }
32695
+ async function runShow2(argv, io) {
32696
+ const { values, positionals } = parseArgs11({
32697
+ args: argv,
32698
+ options: { ...HOME_OPTION, help: { type: "boolean", short: "h" } },
32699
+ allowPositionals: true
32700
+ });
32701
+ if (values.help) {
32702
+ io.out(VAULT_HELP);
32703
+ return;
32704
+ }
32705
+ const candidate = positionals[0]?.trim();
32706
+ if (candidate === void 0 || candidate === "") {
32707
+ throw new Error(`usage: aka vault show '[[aka:...]]'`);
32708
+ }
32709
+ const parsed = PointerToken.safeParse(candidate);
32710
+ if (!parsed.success) {
32711
+ throw new Error(
32712
+ "that is not a vault pointer \u2014 expected the full [[aka:...]] token (quote it in the shell)"
32713
+ );
32714
+ }
32715
+ const base = homeBase(values.home);
32716
+ const dir = dataDir(base);
32717
+ const db = openLocalDatabase(dir);
32718
+ try {
32719
+ const vault = new SecretVault({
32720
+ repo: db.secretVault,
32721
+ keys: createKeyProvider(readWorkspaceSettings(base).vaultKeyCustody, keysDir(base)),
32722
+ fingerprintKey: loadOrCreateFingerprintKey(dir),
32723
+ // Read live so a consent revocation applies to the very next call.
32724
+ isConsented: () => isVaultConsentValid(readWorkspaceSettings(base).vaultConsent)
32725
+ });
32726
+ const value = await vault.detokenize(parsed.data, {
32727
+ target: "human",
32728
+ reason: "explicit-reveal"
32729
+ });
32730
+ if (typeof value !== "string") {
32731
+ throw new Error(UNAVAILABLE_MESSAGE);
32732
+ }
32733
+ io.out(`${value}
32734
+ `);
32735
+ const descriptor = await vault.describePointer(parsed.data);
32736
+ if (descriptor) io.out(`${descriptorLine(descriptor)}
32737
+ `);
32738
+ } finally {
32739
+ db.close();
32740
+ }
32741
+ }
32742
+ var VERBS2 = {
32743
+ show: runShow2
32744
+ };
32745
+ async function runVault(argv, io = terminalPrompter()) {
32746
+ const [verb, ...rest] = argv;
32747
+ if (verb === void 0 || verb === "-h" || verb === "--help") {
32748
+ io.out(VAULT_HELP);
32749
+ return;
32750
+ }
32751
+ const handler = VERBS2[verb];
32752
+ if (!handler) {
32753
+ io.out(VAULT_HELP);
32754
+ return;
32755
+ }
32756
+ await handler(rest, io);
32757
+ }
32758
+
31199
32759
  // src/lib/external-dispatch.ts
31200
32760
  import { spawnSync as spawnSync2 } from "child_process";
31201
32761
  import { constants } from "os";
@@ -31248,6 +32808,7 @@ var COMMANDS = {
31248
32808
  plugins: runPlugins,
31249
32809
  dashboard: runDashboard,
31250
32810
  exception: (argv) => runException(argv),
32811
+ vault: (argv) => runVault(argv),
31251
32812
  tui: runTui,
31252
32813
  update: runUpdate,
31253
32814
  "check-updates": runCheckUpdates,