@akasecurity/cli 0.9.2 → 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 (171) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +2020 -279
  3. package/package.json +5 -5
  4. package/web-ui/package.json +6 -2
  5. package/web-ui/packages/dashboard-ui/package.json +2 -1
  6. package/web-ui/packages/detections/package.json +1 -1
  7. package/web-ui/packages/local-ops/package.json +1 -1
  8. package/web-ui/packages/persistence/package.json +1 -1
  9. package/web-ui/packages/plugin-sdk/package.json +1 -1
  10. package/web-ui/packages/schema/package.json +1 -1
  11. package/web-ui/packages/ui-kit/package.json +1 -1
  12. package/web-ui/web-ui/.next/BUILD_ID +1 -1
  13. package/web-ui/web-ui/.next/app-build-manifest.json +135 -126
  14. package/web-ui/web-ui/.next/app-path-routes-manifest.json +6 -5
  15. package/web-ui/web-ui/.next/build-manifest.json +14 -14
  16. package/web-ui/web-ui/.next/prerender-manifest.json +15 -15
  17. package/web-ui/web-ui/.next/required-server-files.json +1 -2
  18. package/web-ui/web-ui/.next/routes-manifest.json +6 -0
  19. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
  20. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js.nft.json +1 -1
  21. package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
  22. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
  23. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js.nft.json +1 -1
  24. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
  25. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
  26. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js.nft.json +1 -1
  27. package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
  28. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
  29. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
  30. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
  31. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +2 -2
  32. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
  33. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
  34. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +2 -2
  35. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
  36. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
  37. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +2 -2
  38. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js.nft.json +1 -1
  39. package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
  40. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +2 -2
  41. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js.nft.json +1 -1
  42. package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
  43. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
  44. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js.nft.json +1 -1
  45. package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
  46. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +2 -2
  47. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
  48. package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
  49. package/web-ui/web-ui/.next/server/app/(app)/security/page.js +2 -2
  50. package/web-ui/web-ui/.next/server/app/(app)/security/page.js.nft.json +1 -1
  51. package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
  52. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
  53. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js.nft.json +1 -1
  54. package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
  55. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
  56. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
  57. package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
  58. package/web-ui/web-ui/.next/server/app/(app)/vault/page.js +2 -0
  59. package/web-ui/web-ui/.next/server/app/(app)/vault/page.js.nft.json +1 -0
  60. package/web-ui/web-ui/.next/server/app/(app)/vault/page_client-reference-manifest.js +1 -0
  61. package/web-ui/web-ui/.next/server/app/_not-found/page.js +2 -2
  62. package/web-ui/web-ui/.next/server/app/_not-found/page.js.nft.json +1 -1
  63. package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  64. package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
  65. package/web-ui/web-ui/.next/server/app/_not-found.rsc +10 -10
  66. package/web-ui/web-ui/.next/server/app/icon.svg/route.js +1 -1
  67. package/web-ui/web-ui/.next/server/app/icon.svg/route.js.nft.json +1 -1
  68. package/web-ui/web-ui/.next/server/app/index.html +1 -1
  69. package/web-ui/web-ui/.next/server/app/index.rsc +10 -10
  70. package/web-ui/web-ui/.next/server/app/page.js +2 -2
  71. package/web-ui/web-ui/.next/server/app/page.js.nft.json +1 -1
  72. package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/web-ui/web-ui/.next/server/app-paths-manifest.json +6 -5
  74. package/web-ui/web-ui/.next/server/chunks/243.js +1 -0
  75. package/web-ui/web-ui/.next/server/chunks/28.js +1 -0
  76. package/web-ui/web-ui/.next/server/chunks/330.js +1 -0
  77. package/web-ui/web-ui/.next/server/chunks/{924.js → 334.js} +1 -1
  78. package/web-ui/web-ui/.next/server/chunks/447.js +6 -0
  79. package/web-ui/web-ui/.next/server/chunks/514.js +755 -0
  80. package/web-ui/web-ui/.next/server/chunks/539.js +22 -0
  81. package/web-ui/web-ui/.next/server/chunks/628.js +84 -0
  82. package/web-ui/web-ui/.next/server/chunks/817.js +1 -0
  83. package/web-ui/web-ui/.next/server/chunks/830.js +750 -123
  84. package/web-ui/web-ui/.next/server/chunks/935.js +11 -0
  85. package/web-ui/web-ui/.next/server/chunks/949.js +123 -0
  86. package/web-ui/web-ui/.next/server/functions-config-manifest.json +5 -4
  87. package/web-ui/web-ui/.next/server/middleware-build-manifest.js +1 -1
  88. package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
  89. package/web-ui/web-ui/.next/server/middleware.js +2 -2
  90. package/web-ui/web-ui/.next/server/pages/404.html +1 -1
  91. package/web-ui/web-ui/.next/server/pages/500.html +1 -1
  92. package/web-ui/web-ui/.next/server/pages/_app.js +1 -1
  93. package/web-ui/web-ui/.next/server/pages/_app.js.nft.json +1 -1
  94. package/web-ui/web-ui/.next/server/pages/_document.js +1 -1
  95. package/web-ui/web-ui/.next/server/pages/_document.js.nft.json +1 -1
  96. package/web-ui/web-ui/.next/server/pages/_error.js +3 -3
  97. package/web-ui/web-ui/.next/server/pages/_error.js.nft.json +1 -1
  98. package/web-ui/web-ui/.next/server/pages-manifest.json +1 -1
  99. package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
  100. package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
  101. package/web-ui/web-ui/.next/static/cVKs25XbQsC1083sU5GOD/_buildManifest.js +1 -0
  102. package/web-ui/web-ui/.next/static/chunks/321-46ce36c1ef95d231.js +1 -0
  103. package/web-ui/web-ui/.next/static/chunks/501-939e9e83b25a6293.js +1 -0
  104. package/web-ui/web-ui/.next/static/chunks/84-8205054e8901dbde.js +62 -0
  105. package/web-ui/web-ui/.next/static/chunks/89-d23f5e27ab3b468f.js +1 -0
  106. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-e3ce4be93d2f8ac5.js +1 -0
  107. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-8bb13b53c7bde504.js +1 -0
  108. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-07307e6d8ddb5cbf.js +1 -0
  109. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-aecbc7b6fbd143c2.js +1 -0
  110. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-1d468a1a0cf12aea.js +1 -0
  111. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-863e759b7e26c759.js +1 -0
  112. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-7636a56ffa40d5c6.js +1 -0
  113. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-c80a692b9d05f321.js +1 -0
  114. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-01e6dceb36cb882f.js +1 -0
  115. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-d98e303b17524dad.js +1 -0
  116. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-178c53b1f56b5e97.js +1 -0
  117. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-7f69b2579337c729.js +1 -0
  118. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-7019a1567cd9aadd.js +1 -0
  119. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-90ce0ad1b9403f48.js +1 -0
  120. package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/page-7ef77f76ff07ea1f.js +1 -0
  121. package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-3cf97f2dd544e8cb.js +1 -0
  122. package/web-ui/web-ui/.next/static/chunks/app/layout-f1727d7b45e0779b.js +1 -0
  123. package/web-ui/web-ui/.next/static/chunks/app/page-3fbfddda391d9939.js +1 -0
  124. package/web-ui/web-ui/.next/static/chunks/{dddf6c0a-f09ab3fc84be1c52.js → d043c60e-31360d0d5cb78983.js} +1 -1
  125. package/web-ui/web-ui/.next/static/chunks/framework-3444de8758288822.js +1 -0
  126. package/web-ui/web-ui/.next/static/chunks/main-app-9e734a14592bc9c4.js +1 -0
  127. package/web-ui/web-ui/.next/static/chunks/main-b13545f01f111855.js +1 -0
  128. package/web-ui/web-ui/.next/static/chunks/pages/_app-7f0d920cc5af1c9f.js +1 -0
  129. package/web-ui/web-ui/.next/static/chunks/pages/_error-3d8b61361c4b2c6b.js +1 -0
  130. package/web-ui/web-ui/.next/static/chunks/{webpack-ea80c9c368a8364e.js → webpack-61986b0a62ba55ed.js} +1 -1
  131. package/web-ui/web-ui/.next/static/css/10425160b1a85ff4.css +2 -0
  132. package/web-ui/web-ui/package.json +1 -1
  133. package/web-ui/web-ui/server.js +1 -1
  134. package/web-ui/web-ui/.next/server/chunks/100.js +0 -1
  135. package/web-ui/web-ui/.next/server/chunks/142.js +0 -700
  136. package/web-ui/web-ui/.next/server/chunks/472.js +0 -84
  137. package/web-ui/web-ui/.next/server/chunks/723.js +0 -22
  138. package/web-ui/web-ui/.next/server/chunks/75.js +0 -6
  139. package/web-ui/web-ui/.next/server/chunks/850.js +0 -1
  140. package/web-ui/web-ui/.next/server/chunks/853.js +0 -698
  141. package/web-ui/web-ui/.next/server/chunks/871.js +0 -1
  142. package/web-ui/web-ui/.next/server/chunks/959.js +0 -9
  143. package/web-ui/web-ui/.next/static/chunks/137-8f757e04d1877a6c.js +0 -1
  144. package/web-ui/web-ui/.next/static/chunks/517-977bc0a7a7be88cc.js +0 -1
  145. package/web-ui/web-ui/.next/static/chunks/613-734dcad67e1fa08c.js +0 -1
  146. package/web-ui/web-ui/.next/static/chunks/784-25fa9bc074877c93.js +0 -62
  147. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-96995843d6cbfb84.js +0 -1
  148. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-28eeea5c4a816f53.js +0 -1
  149. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-0c3888b9381a6e80.js +0 -1
  150. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-3a5138f424e43b6e.js +0 -1
  151. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-32d67b6c8165acda.js +0 -1
  152. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-1c376f3818bfba45.js +0 -1
  153. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-9c9b1d29895a0592.js +0 -1
  154. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-4334a50ed9dcace4.js +0 -1
  155. package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-de6da4ea92784950.js +0 -1
  156. package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-4817c44647618832.js +0 -1
  157. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-5d2aa8c946dd2cae.js +0 -1
  158. package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-c0597d1c1acb1cc4.js +0 -1
  159. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-2a7b0d9dab536ae1.js +0 -1
  160. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-576a976afbe33369.js +0 -1
  161. package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-26662e9d7ee32abc.js +0 -1
  162. package/web-ui/web-ui/.next/static/chunks/app/layout-e531d303d8c8a313.js +0 -1
  163. package/web-ui/web-ui/.next/static/chunks/app/page-3ffe075f223c0af9.js +0 -1
  164. package/web-ui/web-ui/.next/static/chunks/framework-f8855380599951ba.js +0 -1
  165. package/web-ui/web-ui/.next/static/chunks/main-app-7291de73392cac56.js +0 -1
  166. package/web-ui/web-ui/.next/static/chunks/main-ff0082770df07ddc.js +0 -1
  167. package/web-ui/web-ui/.next/static/chunks/pages/_app-56d1709db70ee49f.js +0 -1
  168. package/web-ui/web-ui/.next/static/chunks/pages/_error-63dd320a91a40541.js +0 -1
  169. package/web-ui/web-ui/.next/static/css/1ea302d29fc67ba4.css +0 -2
  170. package/web-ui/web-ui/.next/static/mAOCejrDqkMoDfzjWr6qu/_buildManifest.js +0 -1
  171. /package/web-ui/web-ui/.next/static/{mAOCejrDqkMoDfzjWr6qu → 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,11 +18306,15 @@ 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,
18382
18313
  conditions: true
18383
18314
  });
18315
+ function isMatchableUnder(keyVersion, key) {
18316
+ return key.status === "present" && key.version === keyVersion;
18317
+ }
18384
18318
 
18385
18319
  // ../packages/schema/src/zod/rule.ts
18386
18320
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
@@ -19480,8 +19414,128 @@ var PatchInstalledPackRequest = external_exports.object({
19480
19414
  message: "At least one field must be provided"
19481
19415
  }).meta({ id: "PatchInstalledPackRequest" });
19482
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
+
19483
19537
  // ../packages/schema/src/zod/local.ts
19484
- var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
19538
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 5;
19485
19539
  var RunMode = external_exports.enum(["standalone"]);
19486
19540
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
19487
19541
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -19503,6 +19557,16 @@ var WorkspaceSettings = external_exports.object({
19503
19557
  // In-place egress extraction on the scan paths; disable to stop all Data
19504
19558
  // Shares writes.
19505
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"),
19506
19570
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
19507
19571
  onboardedAt: external_exports.iso.datetime().optional(),
19508
19572
  // Records that the user consented to sending findings to the model API for
@@ -20049,6 +20113,112 @@ function reviewSeverityRank(reasons) {
20049
20113
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
20050
20114
  }
20051
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
+
20052
20222
  // ../packages/persistence/src/ids.ts
20053
20223
  import { createHash } from "crypto";
20054
20224
  function sha256Hex(input) {
@@ -21909,6 +22079,9 @@ var AmbiguousExceptionIdError = class extends Error {
21909
22079
  var ACTIVE_PREDICATE = `revoked_at IS NULL
21910
22080
  AND (expires_at IS NULL OR expires_at > :now)
21911
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}`;
21912
22085
  var SqliteExceptionsRepository = class {
21913
22086
  constructor(db) {
21914
22087
  this.db = db;
@@ -22000,11 +22173,11 @@ var SqliteExceptionsRepository = class {
22000
22173
  this.db.prepare(
22001
22174
  `INSERT INTO exceptions (
22002
22175
  id, rule_id, category, value_fingerprint, key_version, masked_value,
22003
- scope, expires_at, max_uses, use_count, last_used_at, justification,
22004
- 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
22005
22178
  ) VALUES (
22006
22179
  :id, :ruleId, :category, :valueFingerprint, :keyVersion, :maskedValue,
22007
- :scope, :expiresAt, :maxUses, 0, NULL, :justification,
22180
+ :capability, :scope, :expiresAt, :maxUses, 0, NULL, :justification,
22008
22181
  :conditions, :createdBy, :createdVia, :now, :now
22009
22182
  )`
22010
22183
  ).run({
@@ -22014,6 +22187,7 @@ var SqliteExceptionsRepository = class {
22014
22187
  valueFingerprint: input.valueFingerprint,
22015
22188
  keyVersion: input.keyVersion,
22016
22189
  maskedValue: input.maskedValue,
22190
+ capability: input.capability ?? "suppress",
22017
22191
  scope: input.scope,
22018
22192
  expiresAt: input.expiresAt === null ? null : isoToEpochMillis(input.expiresAt),
22019
22193
  maxUses: input.maxUses,
@@ -22107,6 +22281,7 @@ var SqliteExceptionsRepository = class {
22107
22281
  ruleId: row.rule_id,
22108
22282
  valueFingerprint: row.value_fingerprint,
22109
22283
  keyVersion: row.key_version,
22284
+ capability: row.capability,
22110
22285
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
22111
22286
  maxUses: row.max_uses,
22112
22287
  useCount: row.use_count,
@@ -22161,6 +22336,35 @@ var SqliteExceptionsRepository = class {
22161
22336
  }))
22162
22337
  );
22163
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
+ }
22164
22368
  /**
22165
22369
  * Retention sweep: delete TERMINAL rows (revoked, expired, or use-budget
22166
22370
  * exhausted) whose last transition is older than the retention window.
@@ -22188,6 +22392,7 @@ function parseExceptionRow(row) {
22188
22392
  valueFingerprint: row.value_fingerprint,
22189
22393
  keyVersion: row.key_version,
22190
22394
  maskedValue: row.masked_value,
22395
+ capability: row.capability,
22191
22396
  scope: row.scope,
22192
22397
  expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
22193
22398
  maxUses: row.max_uses,
@@ -24353,28 +24558,309 @@ var SqliteScanLedgerRepository = class {
24353
24558
  }
24354
24559
  };
24355
24560
 
24356
- // ../packages/persistence/src/repositories/security.ts
24357
- var DAY_MS4 = 864e5;
24358
- var SEVERITIES = ["critical", "high", "medium", "low"];
24359
- var ACTION_TO_KIND = {
24360
- block: "blocked",
24361
- redact: "redacted",
24362
- warn: "warned"
24363
- };
24364
- var ENFORCEMENT_KINDS = ["blocked", "redacted", "warned"];
24365
- var SCAN_COVERAGE = [
24366
- { provider: "claudecode", coverage: 100, supported: true },
24367
- { provider: "cursor", coverage: 0, supported: false },
24368
- { provider: "codex", coverage: 0, supported: false },
24369
- { provider: "chatgpt", coverage: 0, supported: false },
24370
- { provider: "copilot", coverage: 0, supported: false },
24371
- { provider: "api", coverage: 0, supported: false }
24372
- ];
24373
- var GRANULARITY = {
24374
- "7d": "day",
24375
- "30d": "day",
24376
- "3m": "week",
24377
- "6m": "week"
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"
24378
24864
  };
24379
24865
  function granularityFor(range) {
24380
24866
  return GRANULARITY[range];
@@ -24698,7 +25184,7 @@ var SqliteSecurityRepository = class {
24698
25184
  };
24699
25185
 
24700
25186
  // ../packages/persistence/src/repositories/shares.ts
24701
- import { randomUUID as randomUUID7 } from "crypto";
25187
+ import { randomUUID as randomUUID8 } from "crypto";
24702
25188
  var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
24703
25189
  var IN_CHUNK = 500;
24704
25190
  var KIND_ORDER = ["provider", "internal", "external", "ip"];
@@ -24954,7 +25440,7 @@ var SqliteSharesRepository = class {
24954
25440
  (id, destination_id, host, decision, created_at, updated_at)
24955
25441
  VALUES (:id, :destinationId, :host, :decision, :now, :now)`
24956
25442
  ).run({
24957
- id: randomUUID7(),
25443
+ id: randomUUID8(),
24958
25444
  destinationId,
24959
25445
  host: dest.host,
24960
25446
  decision,
@@ -25103,7 +25589,7 @@ var SqliteSharesRepository = class {
25103
25589
  let destinationId = destIds.get(hit.host);
25104
25590
  if (destinationId === void 0) {
25105
25591
  destStmt.run({
25106
- id: randomUUID7(),
25592
+ id: randomUUID8(),
25107
25593
  kind: hit.kind,
25108
25594
  name: hit.name,
25109
25595
  host: hit.host,
@@ -25119,7 +25605,7 @@ var SqliteSharesRepository = class {
25119
25605
  let endpointId = endpointIds.get(endpointKey);
25120
25606
  if (endpointId === void 0) {
25121
25607
  endpointStmt.run({
25122
- id: randomUUID7(),
25608
+ id: randomUUID8(),
25123
25609
  destinationId,
25124
25610
  method: hit.method,
25125
25611
  transport: hit.transport,
@@ -25132,7 +25618,7 @@ var SqliteSharesRepository = class {
25132
25618
  endpointIds.set(endpointKey, endpointId);
25133
25619
  }
25134
25620
  siteStmt.run({
25135
- id: randomUUID7(),
25621
+ id: randomUUID8(),
25136
25622
  endpointId,
25137
25623
  project: input.project,
25138
25624
  projectKey: input.projectKey,
@@ -25500,11 +25986,22 @@ function purgeSampleData(db) {
25500
25986
  function linkHost(input, hostId) {
25501
25987
  return hostId ? { ...input, hostId } : input;
25502
25988
  }
25989
+ function closeQuietly(db) {
25990
+ try {
25991
+ db.close();
25992
+ } catch {
25993
+ }
25994
+ }
25503
25995
  function openWithPragmas(file2) {
25504
25996
  const db = new DatabaseSync(file2);
25505
- db.exec("PRAGMA journal_mode = WAL");
25506
- db.exec("PRAGMA busy_timeout = 2000");
25507
- db.exec("PRAGMA foreign_keys = ON");
25997
+ try {
25998
+ db.exec("PRAGMA journal_mode = WAL");
25999
+ db.exec("PRAGMA busy_timeout = 2000");
26000
+ db.exec("PRAGMA foreign_keys = ON");
26001
+ } catch (err) {
26002
+ closeQuietly(db);
26003
+ throw err;
26004
+ }
25508
26005
  return db;
25509
26006
  }
25510
26007
  function backupLegacyStore(file2) {
@@ -25516,43 +26013,82 @@ function backupLegacyStore(file2) {
25516
26013
  }
25517
26014
  return backup;
25518
26015
  }
26016
+ function openAndInitialize(file2) {
26017
+ let db = openWithPragmas(file2);
26018
+ try {
26019
+ if (isForeignSqliteLineage(db)) {
26020
+ db.close();
26021
+ const backup = backupLegacyStore(file2);
26022
+ db = openWithPragmas(file2);
26023
+ akaWarn(
26024
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
26025
+ );
26026
+ }
26027
+ applyMigrations(db, file2);
26028
+ tightenPerms(file2);
26029
+ const policies = new SqlitePoliciesRepository(db);
26030
+ const installedPacks = new SqliteInstalledPacksRepository(db);
26031
+ const repositories = {
26032
+ events: new SqliteEventsRepository(db),
26033
+ findings: new SqliteFindingsRepository(db),
26034
+ policies,
26035
+ installedPacks,
26036
+ scanLedger: new SqliteScanLedgerRepository(db),
26037
+ secretVault: new SqliteSecretVaultRepository(db),
26038
+ exceptions: new SqliteExceptionsRepository(db),
26039
+ resolutions: new SqliteResolutionsRepository(db),
26040
+ ruleProbeCache: new SqliteRuleProbeCacheRepository(db),
26041
+ security: new SqliteSecurityRepository(db),
26042
+ detections: new SqliteDetectionsRepository(db),
26043
+ shares: new SqliteSharesRepository(db),
26044
+ policyCatalog: new SqlitePolicyCatalogRepository(installedPacks),
26045
+ inventory: new SqliteInventoryRepository(db),
26046
+ inventoryAssets: new SqliteInventoryAssetsRepository(db),
26047
+ projectFiles: new SqliteProjectFilesRepository(db),
26048
+ activity: new SqliteActivityRepository(db),
26049
+ sourceProject: new SqliteSourceProjectRepository(db),
26050
+ auditEvents: new SqliteAuditEventsRepository(db),
26051
+ classifiedData: new SqliteClassifiedDataRepository(db),
26052
+ inspectionDefinitions: new SqliteInspectionDefinitionsRepository(db),
26053
+ inspectionFindings: new SqliteInspectionFindingsRepository(db),
26054
+ configInventory: new SqliteConfigInventoryRepository(db)
26055
+ };
26056
+ policies.seedDefaults();
26057
+ return { db, ...repositories };
26058
+ } catch (err) {
26059
+ closeQuietly(db);
26060
+ throw err;
26061
+ }
26062
+ }
25519
26063
  function openLocalDatabase(dir) {
25520
26064
  ensureDataDirSync(dir);
25521
26065
  const file2 = join2(dir, DB_FILENAME);
25522
- let db = openWithPragmas(file2);
25523
- if (isForeignSqliteLineage(db)) {
25524
- db.close();
25525
- const backup = backupLegacyStore(file2);
25526
- db = openWithPragmas(file2);
25527
- akaWarn(
25528
- `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
25529
- );
25530
- }
25531
- applyMigrations(db, file2);
25532
- tightenPerms(file2);
25533
- const events = new SqliteEventsRepository(db);
25534
- const findings = new SqliteFindingsRepository(db);
25535
- const policies = new SqlitePoliciesRepository(db);
25536
- const installedPacks = new SqliteInstalledPacksRepository(db);
25537
- const scanLedger = new SqliteScanLedgerRepository(db);
25538
- const exceptions = new SqliteExceptionsRepository(db);
25539
- const resolutions = new SqliteResolutionsRepository(db);
25540
- const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
25541
- const security = new SqliteSecurityRepository(db);
25542
- const detections = new SqliteDetectionsRepository(db);
25543
- const shares = new SqliteSharesRepository(db);
25544
- const policyCatalog = new SqlitePolicyCatalogRepository(installedPacks);
25545
- const inventory = new SqliteInventoryRepository(db);
25546
- const inventoryAssets = new SqliteInventoryAssetsRepository(db);
25547
- const projectFiles = new SqliteProjectFilesRepository(db);
25548
- const activity = new SqliteActivityRepository(db);
25549
- const sourceProject = new SqliteSourceProjectRepository(db);
25550
- const auditEvents = new SqliteAuditEventsRepository(db);
25551
- const classifiedData = new SqliteClassifiedDataRepository(db);
25552
- const inspectionDefinitions = new SqliteInspectionDefinitionsRepository(db);
25553
- const inspectionFindings = new SqliteInspectionFindingsRepository(db);
25554
- const configInventory = new SqliteConfigInventoryRepository(db);
25555
- policies.seedDefaults();
26066
+ const {
26067
+ db,
26068
+ events,
26069
+ findings,
26070
+ policies,
26071
+ installedPacks,
26072
+ scanLedger,
26073
+ secretVault,
26074
+ exceptions,
26075
+ resolutions,
26076
+ ruleProbeCache,
26077
+ security,
26078
+ detections,
26079
+ shares,
26080
+ policyCatalog,
26081
+ inventory,
26082
+ inventoryAssets,
26083
+ projectFiles,
26084
+ activity,
26085
+ sourceProject,
26086
+ auditEvents,
26087
+ classifiedData,
26088
+ inspectionDefinitions,
26089
+ inspectionFindings,
26090
+ configInventory
26091
+ } = openAndInitialize(file2);
25556
26092
  function recordCapture(event, detected) {
25557
26093
  failOpenTransaction(db, () => {
25558
26094
  const sessionId = event.metadata?.sessionId;
@@ -25643,7 +26179,7 @@ function openLocalDatabase(dir) {
25643
26179
  const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
25644
26180
  if (!definitionId) continue;
25645
26181
  inspectionFindings.insertFinding({
25646
- id: randomUUID8(),
26182
+ id: randomUUID9(),
25647
26183
  auditEventId: record2.scanEvent.id,
25648
26184
  inspectionDefinitionId: definitionId,
25649
26185
  span: finding.span,
@@ -25720,6 +26256,7 @@ function openLocalDatabase(dir) {
25720
26256
  policies,
25721
26257
  installedPacks,
25722
26258
  scanLedger,
26259
+ secretVault,
25723
26260
  exceptions,
25724
26261
  resolutions,
25725
26262
  ruleProbeCache,
@@ -25764,8 +26301,9 @@ function computeFindingKey(input) {
25764
26301
 
25765
26302
  // ../packages/persistence/src/fingerprint.ts
25766
26303
  import { createHmac, randomBytes } from "crypto";
25767
- import { readFileSync as readFileSync2 } from "fs";
26304
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
25768
26305
  import { join as join3 } from "path";
26306
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
25769
26307
  var KEY_FILENAME = "exception.key";
25770
26308
  var KEY_MATERIAL_BYTES = 32;
25771
26309
  function keyFilePath(dataDir2) {
@@ -25789,6 +26327,50 @@ function parseKeyFile(raw) {
25789
26327
  }
25790
26328
  return { version: version2, material: bytes };
25791
26329
  }
26330
+ var KEY_VERSION_COLUMNS = {
26331
+ exceptions: "key_version",
26332
+ blocked_detections: "key_version",
26333
+ secret_vault: "fingerprint_key_version"
26334
+ };
26335
+ var SQLITE_ERROR = 1;
26336
+ var FLOOR_BUSY_TIMEOUT_MS = 250;
26337
+ var FloorUnreadableError = class extends Error {
26338
+ code = "floor-unreadable";
26339
+ constructor(cause) {
26340
+ super(
26341
+ `cannot read the stored fingerprint key versions: ${cause instanceof Error ? cause.message : String(cause)}`,
26342
+ { cause }
26343
+ );
26344
+ this.name = "FloorUnreadableError";
26345
+ }
26346
+ };
26347
+ function storedKeyVersionFloor(dataDir2) {
26348
+ const file2 = join3(dataDir2, DB_FILENAME);
26349
+ if (!existsSync3(file2)) return 0;
26350
+ let db;
26351
+ try {
26352
+ db = new DatabaseSync2(file2, { readOnly: true });
26353
+ db.exec(`PRAGMA busy_timeout = ${String(FLOOR_BUSY_TIMEOUT_MS)}`);
26354
+ let floor = 0;
26355
+ for (const [table, column] of Object.entries(KEY_VERSION_COLUMNS)) {
26356
+ try {
26357
+ const row = getRow(
26358
+ db.prepare(`SELECT MAX(${column}) AS v FROM ${table}`)
26359
+ );
26360
+ floor = Math.max(floor, row?.v ?? 0);
26361
+ } catch (err) {
26362
+ if (err.errcode !== SQLITE_ERROR) {
26363
+ throw new FloorUnreadableError(err);
26364
+ }
26365
+ }
26366
+ }
26367
+ return floor;
26368
+ } catch (err) {
26369
+ throw err instanceof FloorUnreadableError ? err : new FloorUnreadableError(err);
26370
+ } finally {
26371
+ db?.close();
26372
+ }
26373
+ }
25792
26374
  function writeKeyFile(dataDir2, key) {
25793
26375
  ensureDataDirSync(dataDir2);
25794
26376
  const file2 = keyFilePath(dataDir2);
@@ -25813,18 +26395,27 @@ function loadOrCreateFingerprintKey(dataDir2) {
25813
26395
  tightenFile(keyFilePath(dataDir2));
25814
26396
  return existing;
25815
26397
  }
25816
- return writeKeyFile(dataDir2, { version: 1, material: randomBytes(KEY_MATERIAL_BYTES) });
26398
+ return writeKeyFile(dataDir2, {
26399
+ version: storedKeyVersionFloor(dataDir2) + 1,
26400
+ material: randomBytes(KEY_MATERIAL_BYTES)
26401
+ });
25817
26402
  }
25818
26403
  function rotateFingerprintKey(dataDir2) {
25819
26404
  const existing = readFingerprintKey(dataDir2);
25820
26405
  return writeKeyFile(dataDir2, {
25821
- version: (existing?.version ?? 0) + 1,
26406
+ version: Math.max(existing?.version ?? 0, storedKeyVersionFloor(dataDir2)) + 1,
25822
26407
  material: randomBytes(KEY_MATERIAL_BYTES)
25823
26408
  });
25824
26409
  }
25825
26410
  function fingerprintValue(key, raw) {
25826
26411
  return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
25827
26412
  }
26413
+ function isCurrentKeyVersion(key, keyVersion) {
26414
+ return isMatchableUnder(keyVersion, keyStateOf(key));
26415
+ }
26416
+ function keyStateOf(key) {
26417
+ return key === null ? { status: "absent" } : { status: "present", version: key.version };
26418
+ }
25828
26419
 
25829
26420
  // ../packages/persistence/src/local-layout.ts
25830
26421
  import { renameSync as renameSync3 } from "fs";
@@ -25843,6 +26434,9 @@ function dataDir(base = defaultDataDir()) {
25843
26434
  function dbPath(base = defaultDataDir()) {
25844
26435
  return join4(dataDir(base), "aka.db");
25845
26436
  }
26437
+ function keysDir(base = defaultDataDir()) {
26438
+ return join4(base, "keys");
26439
+ }
25846
26440
  function ensureLayoutDirSync(dir = defaultDataDir()) {
25847
26441
  ensureDataDirSync(dir);
25848
26442
  }
@@ -25856,26 +26450,887 @@ function readWorkspaceSettings(base = defaultDataDir()) {
25856
26450
  try {
25857
26451
  return WorkspaceSettings.parse(record2);
25858
26452
  } catch {
25859
- return defaultWorkspaceSettings();
26453
+ return defaultWorkspaceSettings();
26454
+ }
26455
+ }
26456
+ function readJson(file2) {
26457
+ let text;
26458
+ try {
26459
+ text = readFileSync3(file2, "utf8");
26460
+ } catch {
26461
+ return null;
26462
+ }
26463
+ return parseJsonObject(text) ?? null;
26464
+ }
26465
+
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";
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 {
25860
26823
  }
25861
26824
  }
25862
- function readJson(file2) {
25863
- let text;
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;
25864
26950
  try {
25865
- text = readFileSync3(file2, "utf8");
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 };
25866
26958
  } catch {
25867
26959
  return null;
25868
26960
  }
25869
- return parseJsonObject(text) ?? null;
25870
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
+ };
25871
27326
 
25872
27327
  // ../packages/persistence/src/warn-era-cap.ts
25873
- import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
25874
- import { join as join6 } from "path";
27328
+ import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "fs";
27329
+ import { join as join7 } from "path";
25875
27330
 
25876
27331
  // ../packages/plugin-sdk/src/config.ts
25877
- import { existsSync as existsSync4 } from "fs";
25878
- import { join as join7 } from "path";
27332
+ import { existsSync as existsSync5 } from "fs";
27333
+ import { join as join8 } from "path";
25879
27334
 
25880
27335
  // ../packages/plugin-sdk/src/provider-env.ts
25881
27336
  var booleanish = external_exports.string().optional().transform((v) => {
@@ -25896,9 +27351,9 @@ var providerEnvShape = {
25896
27351
  var ProviderEnvSchema = external_exports.object(providerEnvShape);
25897
27352
 
25898
27353
  // ../packages/plugin-sdk/src/config-inventory.ts
25899
- import { readdirSync, readFileSync as readFileSync5, realpathSync, statSync as statSync2 } from "fs";
27354
+ import { readdirSync, readFileSync as readFileSync6, realpathSync, statSync as statSync3 } from "fs";
25900
27355
  import { homedir as homedir4 } from "os";
25901
- import { basename as basename2, join as join9 } from "path";
27356
+ import { basename as basename2, join as join10 } from "path";
25902
27357
 
25903
27358
  // ../rules/code-flaws/auth-jwt-no-verify.json
25904
27359
  var auth_jwt_no_verify_default = {
@@ -27933,8 +29388,8 @@ function bundledDetections() {
27933
29388
  }
27934
29389
 
27935
29390
  // ../packages/plugin-sdk/src/repo.ts
27936
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync } from "fs";
27937
- 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";
27938
29393
  function resolveRepoIdentity(cwd) {
27939
29394
  try {
27940
29395
  const root = findGitRoot(cwd);
@@ -27972,36 +29427,36 @@ function resolveHeadRoot(cwd) {
27972
29427
  function findGitRoot(start) {
27973
29428
  let dir = start;
27974
29429
  for (; ; ) {
27975
- if (existsSync5(join8(dir, ".git"))) return dir;
29430
+ if (existsSync6(join9(dir, ".git"))) return dir;
27976
29431
  const parent = dirname2(dir);
27977
29432
  if (parent === dir) return void 0;
27978
29433
  dir = parent;
27979
29434
  }
27980
29435
  }
27981
29436
  function resolveGitContext(root) {
27982
- const dotGit = join8(root, ".git");
29437
+ const dotGit = join9(root, ".git");
27983
29438
  try {
27984
- if (statSync(dotGit).isDirectory()) {
27985
- return { configPath: join8(dotGit, "config"), headRoot: root };
29439
+ if (statSync2(dotGit).isDirectory()) {
29440
+ return { configPath: join9(dotGit, "config"), headRoot: root };
27986
29441
  }
27987
29442
  } catch {
27988
29443
  return void 0;
27989
29444
  }
27990
29445
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
27991
29446
  if (!target) return void 0;
27992
- const gitdir = isAbsolute(target) ? target : join8(root, target);
27993
- if (existsSync5(join8(gitdir, "config"))) {
27994
- 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 };
27995
29450
  }
27996
- const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
29451
+ const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
27997
29452
  if (!commonRaw) return void 0;
27998
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
29453
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join9(gitdir, commonRaw);
27999
29454
  const headRoot = basename(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
28000
- return { configPath: join8(commonGitDir, "config"), headRoot };
29455
+ return { configPath: join9(commonGitDir, "config"), headRoot };
28001
29456
  }
28002
29457
  function safeRead(path) {
28003
29458
  try {
28004
- return readFileSync4(path, "utf8");
29459
+ return readFileSync5(path, "utf8");
28005
29460
  } catch {
28006
29461
  return void 0;
28007
29462
  }
@@ -28039,14 +29494,14 @@ function slugFromUrl(url2) {
28039
29494
  }
28040
29495
 
28041
29496
  // ../packages/plugin-sdk/src/events.ts
28042
- import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
29497
+ import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
28043
29498
 
28044
29499
  // ../packages/plugin-sdk/src/inventory-resolver.ts
28045
29500
  import { arch, hostname as hostname3, platform, release } from "os";
28046
29501
 
28047
29502
  // ../packages/plugin-sdk/src/nudge.ts
28048
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
28049
- 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";
28050
29505
 
28051
29506
  // ../packages/plugin-sdk/src/paths.ts
28052
29507
  import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
@@ -28084,8 +29539,8 @@ function resolveNonGitProject(startDir, recognizeMarker) {
28084
29539
 
28085
29540
  // ../packages/plugin-sdk/src/project-files.ts
28086
29541
  var import_ignore = __toESM(require_ignore(), 1);
28087
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
28088
- 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";
28089
29544
  var SKIP_DIRS = /* @__PURE__ */ new Set([
28090
29545
  ".git",
28091
29546
  "node_modules",
@@ -28105,7 +29560,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
28105
29560
  var MAX_FILES = 2e4;
28106
29561
  function readIgnoreLayer(dir) {
28107
29562
  try {
28108
- const content = readFileSync7(join11(dir, ".gitignore"), "utf8");
29563
+ const content = readFileSync8(join12(dir, ".gitignore"), "utf8");
28109
29564
  return { base: dir, matcher: (0, import_ignore.default)().add(content) };
28110
29565
  } catch {
28111
29566
  return void 0;
@@ -28183,10 +29638,10 @@ function resolveProjectFiles(cwd) {
28183
29638
  const layer = readIgnoreLayer(dir);
28184
29639
  const dirLayers = layer ? [...layers, layer] : layers;
28185
29640
  for (const entry of dirents) {
28186
- const fullPath = join11(dir, entry.name);
29641
+ const fullPath = join12(dir, entry.name);
28187
29642
  if (entry.isDirectory()) {
28188
29643
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
28189
- if (existsSync6(join11(fullPath, ".git"))) continue;
29644
+ if (existsSync7(join12(fullPath, ".git"))) continue;
28190
29645
  if (visit3(fullPath, dirLayers)) return true;
28191
29646
  continue;
28192
29647
  }
@@ -28221,21 +29676,21 @@ function resolveProjectFiles(cwd) {
28221
29676
  }
28222
29677
 
28223
29678
  // ../packages/plugin-sdk/src/runtime.ts
28224
- import { randomUUID as randomUUID10 } from "crypto";
29679
+ import { randomUUID as randomUUID12 } from "crypto";
28225
29680
 
28226
29681
  // ../packages/plugin-sdk/src/suppressions.ts
28227
29682
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
28228
29683
 
28229
29684
  // ../packages/plugin-sdk/src/throttle.ts
28230
- import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
28231
- 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";
28232
29687
 
28233
29688
  // ../packages/local-ops/src/egress-record.ts
28234
29689
  function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
28235
29690
  try {
28236
29691
  if (!readWorkspaceSettings(base).dataSharesInPlace) return null;
28237
29692
  const abs = resolve(target);
28238
- const targetDir = statSync4(abs).isDirectory() ? abs : dirname4(abs);
29693
+ const targetDir = statSync5(abs).isDirectory() ? abs : dirname4(abs);
28239
29694
  const identity = resolveRepoIdentity(abs);
28240
29695
  const worktreeRoot = resolveWorktreeRoot(abs);
28241
29696
  let root;
@@ -28276,9 +29731,9 @@ function recordProjectEgress(db, target, egress, base = defaultDataDir()) {
28276
29731
  }
28277
29732
 
28278
29733
  // ../packages/local-ops/src/fs-scan.ts
28279
- import { createHash as createHash5, randomUUID as randomUUID11 } from "crypto";
28280
- import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
28281
- 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";
28282
29737
  var import_ignore2 = __toESM(require_ignore(), 1);
28283
29738
  var AKAIGNORE_FILENAME = ".akaignore";
28284
29739
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
@@ -28294,7 +29749,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
28294
29749
  var MAX_BYTES = 1e6;
28295
29750
  function readIgnoreLayer2(dir, filename) {
28296
29751
  try {
28297
- const content = readFileSync8(join13(dir, filename), "utf8");
29752
+ const content = readFileSync9(join14(dir, filename), "utf8");
28298
29753
  return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
28299
29754
  } catch {
28300
29755
  return void 0;
@@ -28313,7 +29768,7 @@ function evaluate(layers, absPath, isDir) {
28313
29768
  function* collectFiles(target) {
28314
29769
  let st;
28315
29770
  try {
28316
- st = statSync5(target);
29771
+ st = statSync6(target);
28317
29772
  } catch {
28318
29773
  return;
28319
29774
  }
@@ -28330,7 +29785,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
28330
29785
  const skipLayer = readIgnoreLayer2(dir, AKAIGNORE_FILENAME);
28331
29786
  const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
28332
29787
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
28333
- const path = join13(dir, entry.name);
29788
+ const path = join14(dir, entry.name);
28334
29789
  if (entry.isDirectory()) {
28335
29790
  const skipState = evaluate(dirSkipLayers, path, true);
28336
29791
  if (skipState !== "unignored" && (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".") || skipState === "ignored")) {
@@ -28342,7 +29797,7 @@ function* visit(dir, markLayers, skipLayers, inIgnoredDir) {
28342
29797
  if (evaluate(dirSkipLayers, path, false) === "ignored") continue;
28343
29798
  let size;
28344
29799
  try {
28345
- size = statSync5(path).size;
29800
+ size = statSync6(path).size;
28346
29801
  } catch {
28347
29802
  continue;
28348
29803
  }
@@ -28383,16 +29838,17 @@ function scanPathIntoStore(db, target, opts = {}) {
28383
29838
  for (const { path: file2, gitignored } of collectFiles(resolve2(target))) {
28384
29839
  let text;
28385
29840
  try {
28386
- text = readFileSync8(file2, "utf8");
29841
+ text = readFileSync9(file2, "utf8");
28387
29842
  } catch {
28388
29843
  continue;
28389
29844
  }
28390
29845
  scanned++;
28391
29846
  const fileEgress = extractFileEgress(file2, text);
28392
29847
  if (fileEgress) egressFiles.push(fileEgress);
28393
- const matches = scan(text, opts.rules);
29848
+ const shielded = shieldPointers(text);
29849
+ const matches = dropShieldedFindings(scan(shielded.text, opts.rules), shielded.spans);
28394
29850
  if (matches.length === 0) continue;
28395
- const eventId = randomUUID11();
29851
+ const eventId = randomUUID13();
28396
29852
  const metadata = { filePath: file2 };
28397
29853
  if (gitignored) metadata.gitignored = true;
28398
29854
  const event = {
@@ -28410,7 +29866,7 @@ function scanPathIntoStore(db, target, opts = {}) {
28410
29866
  const key = resolveFingerprintKey();
28411
29867
  const valueFingerprint = key ? fingerprintValue(key, m.rawMatch) : maskedMatch;
28412
29868
  return {
28413
- id: randomUUID11(),
29869
+ id: randomUUID13(),
28414
29870
  eventId,
28415
29871
  ruleId: m.ruleId,
28416
29872
  category: m.category,
@@ -28435,12 +29891,12 @@ function scanPathIntoStore(db, target, opts = {}) {
28435
29891
  }
28436
29892
 
28437
29893
  // ../packages/local-ops/src/project-inventory.ts
28438
- import { statSync as statSync6 } from "fs";
29894
+ import { statSync as statSync7 } from "fs";
28439
29895
  import { resolve as resolve3 } from "path";
28440
29896
  function recordProjectInventory(db, target) {
28441
29897
  try {
28442
29898
  const abs = resolve3(target);
28443
- statSync6(abs);
29899
+ statSync7(abs);
28444
29900
  const identity = resolveRepoIdentity(abs);
28445
29901
  if (!identity) return null;
28446
29902
  const sourceProjectId2 = db.sourceProject.upsert({
@@ -28489,17 +29945,17 @@ function reinvokeArgv(subcommand, extraArgs = []) {
28489
29945
 
28490
29946
  // ../packages/local-ops/src/update-cache.ts
28491
29947
  import { spawn } from "child_process";
28492
- import { existsSync as existsSync7, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
28493
- 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";
28494
29950
  var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
28495
29951
  function cachePath(home) {
28496
- return join14(dataDir(home), "update-check.json");
29952
+ return join15(dataDir(home), "update-check.json");
28497
29953
  }
28498
29954
  function readCache(home) {
28499
29955
  const path = cachePath(home);
28500
- if (!existsSync7(path)) return null;
29956
+ if (!existsSync8(path)) return null;
28501
29957
  try {
28502
- const raw = JSON.parse(readFileSync9(path, "utf8"));
29958
+ const raw = JSON.parse(readFileSync10(path, "utf8"));
28503
29959
  if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
28504
29960
  return raw;
28505
29961
  } catch {
@@ -28507,9 +29963,9 @@ function readCache(home) {
28507
29963
  }
28508
29964
  }
28509
29965
  function writeCache(home, cache) {
28510
- if (!existsSync7(dataDir(home))) return;
29966
+ if (!existsSync8(dataDir(home))) return;
28511
29967
  try {
28512
- writeFileSync5(cachePath(home), `${JSON.stringify(cache, null, 2)}
29968
+ writeFileSync6(cachePath(home), `${JSON.stringify(cache, null, 2)}
28513
29969
  `);
28514
29970
  } catch {
28515
29971
  }
@@ -28519,7 +29975,7 @@ function isStale(cache, ttlMs = CHECK_TTL_MS) {
28519
29975
  }
28520
29976
  function clearCache(home) {
28521
29977
  try {
28522
- rmSync3(cachePath(home), { force: true });
29978
+ rmSync4(cachePath(home), { force: true });
28523
29979
  } catch {
28524
29980
  }
28525
29981
  }
@@ -28641,6 +30097,12 @@ var COMMAND_SPECS = [
28641
30097
  summary: "Manage detection exceptions (approve, add, list, show, revoke, rotate-key)",
28642
30098
  args: ["approve", "add", "list", "show", "revoke", "rotate-key"]
28643
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
+ },
28644
30106
  {
28645
30107
  name: "tui",
28646
30108
  argHint: "[view]",
@@ -28672,7 +30134,7 @@ function commandsHelp() {
28672
30134
  }
28673
30135
 
28674
30136
  // src/commands/check-updates.ts
28675
- import { existsSync as existsSync8 } from "fs";
30137
+ import { existsSync as existsSync9 } from "fs";
28676
30138
  import { parseArgs } from "util";
28677
30139
 
28678
30140
  // src/lib/args.ts
@@ -28703,7 +30165,7 @@ function runCheckUpdates(argv) {
28703
30165
  out.write(`${String(ups.length)} update(s) available \u2014 run \`aka update\` to apply.
28704
30166
  `);
28705
30167
  }
28706
- if (existsSync8(dataDir(home))) {
30168
+ if (existsSync9(dataDir(home))) {
28707
30169
  const notifiedPluginIds = readCache(home)?.notifiedPluginIds ?? [];
28708
30170
  writeCache(home, { checkedAt: Date.now(), report, notifiedPluginIds });
28709
30171
  }
@@ -28857,10 +30319,10 @@ function runCompletion(argv) {
28857
30319
 
28858
30320
  // src/commands/dashboard.ts
28859
30321
  import { spawn as spawn3 } from "child_process";
28860
- import { existsSync as existsSync9 } from "fs";
30322
+ import { existsSync as existsSync10 } from "fs";
28861
30323
  import { createRequire as createRequire2 } from "module";
28862
30324
  import { createServer } from "net";
28863
- import { dirname as dirname5, join as join15 } from "path";
30325
+ import { dirname as dirname5, join as join16 } from "path";
28864
30326
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
28865
30327
  import { parseArgs as parseArgs2 } from "util";
28866
30328
 
@@ -28881,7 +30343,7 @@ var require3 = createRequire2(import.meta.url);
28881
30343
  var here = dirname5(fileURLToPath2(import.meta.url));
28882
30344
  var BIND_HOST = "127.0.0.1";
28883
30345
  function pkgBase() {
28884
- return isSea() ? dirname5(process.execPath) : join15(here, "..");
30346
+ return isSea() ? dirname5(process.execPath) : join16(here, "..");
28885
30347
  }
28886
30348
  function isPortFree(port) {
28887
30349
  return new Promise((resolve4) => {
@@ -28935,9 +30397,9 @@ async function runDashboard(argv) {
28935
30397
  refreshUpdateMirror(dataDir());
28936
30398
  const base = pkgBase();
28937
30399
  const bundledServer = [
28938
- join15(base, "web-ui", "server.js"),
28939
- join15(base, "web-ui", "web-ui", "server.js")
28940
- ].find((p) => existsSync9(p));
30400
+ join16(base, "web-ui", "server.js"),
30401
+ join16(base, "web-ui", "web-ui", "server.js")
30402
+ ].find((p) => existsSync10(p));
28941
30403
  if (bundledServer) {
28942
30404
  launchStandalone(bundledServer, port, shouldOpen, url2);
28943
30405
  return;
@@ -28956,7 +30418,7 @@ async function runDashboard(argv) {
28956
30418
  onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
28957
30419
  `);
28958
30420
  };
28959
- if (existsSync9(join15(webUiDir, ".next", "BUILD_ID"))) {
30421
+ if (existsSync10(join16(webUiDir, ".next", "BUILD_ID"))) {
28960
30422
  startNext();
28961
30423
  return;
28962
30424
  }
@@ -29294,7 +30756,7 @@ var SCOPE_REQUIRED = `a scope is required \u2014 pick exactly one (there is no d
29294
30756
  --for <duration> time-limited: 30m, 1h, 24h (max 24h)
29295
30757
  --permanent until revoked (typed confirmation required)`;
29296
30758
  var VERB_HELP = {
29297
- approve: `Usage: aka exception approve [reference|value] [flags]
30759
+ approve: `Usage: aka exception approve [reference|value|pointer] [flags]
29298
30760
 
29299
30761
  Grant an exception from a detection blocked in the last 30 minutes. Select the
29300
30762
  block by the reference from the block message, the masked value shown there,
@@ -29302,13 +30764,21 @@ or by pasting the blocked value itself. A pasted value is matched by its keyed
29302
30764
  fingerprint and never stored or echoed \u2014 but it does land in your shell
29303
30765
  history, so prefer the reference where that matters.
29304
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
+
29305
30772
  Flags:
30773
+ --reveal-to-model grant reveal-to-model instead of suppression (required with a
30774
+ pointer selector; --reveal is accepted as shorthand)
29306
30775
  --once | --for <30m|1h|24h> | --permanent scope (required, pick one)
29307
30776
  --reason "<why>" required; prompted on a terminal if omitted
29308
30777
  --yes skip confirmations (non-interactive use)
29309
30778
  --home <dir> alternate AKA home (default: ~/.aka)
29310
30779
 
29311
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"
29312
30782
  `,
29313
30783
  add: `Usage: aka exception add --rule <ruleId> [--stdin] [flags]
29314
30784
 
@@ -29473,10 +30943,43 @@ function printGranted(io, ex) {
29473
30943
  `
29474
30944
  );
29475
30945
  }
29476
- function blockedLine(entry) {
29477
- return `${entry.reference} ${entry.maskedValue} ${entry.ruleId} ${formatRelative(entry.blockedAt)}`;
30946
+ function blockedLine(entry, key) {
30947
+ const line = `${entry.reference} ${entry.maskedValue} ${entry.ruleId} ${formatRelative(entry.blockedAt)}`;
30948
+ return isCurrentKeyVersion(key, entry.keyVersion) ? line : `${line} [not approvable: recorded under key v${String(entry.keyVersion)}]`;
30949
+ }
30950
+ function keyAccessHint(err, dir) {
30951
+ const code = err?.code;
30952
+ if (code === "floor-unreadable") {
30953
+ return `The local store could not be read, and a key minted without it could reuse a version already in use \u2014 fix the store first (${dir}/aka.db).`;
30954
+ }
30955
+ return typeof code === "string" ? `Check the permissions on ${dir}/exception.key \u2014 do not delete it, that invalidates every existing grant.` : `Delete ${dir}/exception.key to start fresh \u2014 grants written under it already cannot match.`;
30956
+ }
30957
+ function readKeyForApprove(dir) {
30958
+ try {
30959
+ return readFingerprintKey(dir);
30960
+ } catch (err) {
30961
+ const message = err instanceof Error ? err.message : String(err);
30962
+ throw new Error(`cannot read the fingerprint key: ${message}
30963
+ ${keyAccessHint(err, dir)}`, {
30964
+ cause: err
30965
+ });
30966
+ }
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
+ };
29478
30981
  }
29479
- async function pickBlocked(entries, selector, io, dir) {
30982
+ async function pickBlocked(entries, selector, io, key) {
29480
30983
  if (selector !== void 0) {
29481
30984
  const matches = entries.filter(
29482
30985
  (e) => e.reference.startsWith(selector) || e.maskedValue === selector
@@ -29489,10 +30992,14 @@ async function pickBlocked(entries, selector, io, dir) {
29489
30992
  `the selector matches ${String(matches.length)} recent blocks \u2014 run 'aka exception approve' bare to pick from the list`
29490
30993
  );
29491
30994
  }
29492
- const key = loadOrCreateFingerprintKey(dir);
30995
+ if (key === null) {
30996
+ throw new Error(
30997
+ `no blocked detection matches that reference, and matching by value needs the fingerprint key \u2014 none exists on this machine`
30998
+ );
30999
+ }
29493
31000
  const fingerprint = fingerprintValue(key, selector);
29494
31001
  const byValue = entries.filter(
29495
- (e) => e.keyVersion === key.version && e.valueFingerprint === fingerprint
31002
+ (e) => isCurrentKeyVersion(key, e.keyVersion) && e.valueFingerprint === fingerprint
29496
31003
  );
29497
31004
  const newest = byValue[0];
29498
31005
  if (newest) {
@@ -29511,39 +31018,109 @@ async function pickBlocked(entries, selector, io, dir) {
29511
31018
  const only = entries[0];
29512
31019
  if (entries.length === 1 && only) {
29513
31020
  io.out(`Approving the only recent block:
29514
- ${blockedLine(only)}
31021
+ ${blockedLine(only, key)}
29515
31022
  `);
29516
31023
  return only;
29517
31024
  }
29518
31025
  if (!io.isInteractive) {
29519
31026
  io.err(`Blocked in the last 30 minutes:
29520
31027
  `);
29521
- for (const entry of entries) io.err(` ${blockedLine(entry)}
31028
+ for (const entry of entries) io.err(` ${blockedLine(entry, key)}
29522
31029
  `);
29523
31030
  throw new Error("pass the reference to approve: aka exception approve <reference>");
29524
31031
  }
29525
31032
  io.out("Blocked in the last 30 minutes:\n");
29526
31033
  entries.forEach((entry, i) => {
29527
- io.out(` ${String(i + 1)}) ${blockedLine(entry)}
31034
+ io.out(` ${String(i + 1)}) ${blockedLine(entry, key)}
29528
31035
  `);
29529
31036
  });
29530
31037
  const answer = await io.ask(`Which one? [1-${String(entries.length)}]: `);
29531
31038
  const index = Number.parseInt(answer.trim(), 10);
29532
31039
  const picked = Number.isInteger(index) ? entries[index - 1] : void 0;
29533
- 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)}`);
29534
31042
  return picked;
29535
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
+ }
29536
31094
  async function runApprove(argv, io) {
29537
31095
  const { values, positionals } = parseArgs4({
29538
31096
  args: argv,
29539
- 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
+ },
29540
31104
  allowPositionals: true
29541
31105
  });
29542
31106
  if (values.help) {
29543
31107
  io.out(VERB_HELP.approve ?? "");
29544
31108
  return;
29545
31109
  }
29546
- 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);
29547
31124
  const db = openLocalDatabase(dir);
29548
31125
  try {
29549
31126
  const entries = await db.exceptions.recentBlocked();
@@ -29555,18 +31132,16 @@ To pre-authorize a value instead: aka exception add --rule <ruleId>
29555
31132
  );
29556
31133
  return;
29557
31134
  }
29558
- const selector = positionals[0]?.trim();
29559
- const entry = await pickBlocked(entries, selector === "" ? void 0 : selector, io, dir);
31135
+ const key = readKeyForApprove(dir);
31136
+ const entry = await pickBlocked(entries, candidate === "" ? void 0 : candidate, io, key);
31137
+ const grant = grantFieldsFrom(entry, key);
29560
31138
  const { scope, reason } = await resolveScopeAndReason(values, io);
29561
31139
  if (scope.scope === "permanent") {
29562
31140
  await confirmPermanent(io, entry.maskedValue, values.yes === true);
29563
31141
  }
29564
31142
  const granted = await createGrant(db, {
29565
- ruleId: entry.ruleId,
29566
- category: entry.category,
29567
- valueFingerprint: entry.valueFingerprint,
29568
- keyVersion: entry.keyVersion,
29569
- maskedValue: entry.maskedValue,
31143
+ ...grant,
31144
+ ...wantsReveal ? { capability: "reveal_to_model" } : {},
29570
31145
  ...scope,
29571
31146
  justification: reason,
29572
31147
  conditions: null,
@@ -29574,6 +31149,11 @@ To pre-authorize a value instead: aka exception add --rule <ruleId>
29574
31149
  createdVia: "cli-approve"
29575
31150
  });
29576
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
+ }
29577
31157
  io.out("Resubmit the blocked prompt \u2014 it will now pass.\n");
29578
31158
  } finally {
29579
31159
  db.close();
@@ -29710,7 +31290,11 @@ Just got blocked? Run 'aka exception approve'.
29710
31290
  const row = [
29711
31291
  shortId(ex.id),
29712
31292
  ex.ruleId,
29713
- 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,
29714
31298
  ex.scope,
29715
31299
  // An expiry countdown is meaningless once the budget/revocation ended
29716
31300
  // the grant ("consumed · expires in 24m" reads as a contradiction);
@@ -29864,22 +31448,39 @@ written under the old key simply stop matching. They remain listed for audit.
29864
31448
  return;
29865
31449
  }
29866
31450
  }
29867
- let version2;
31451
+ let next;
29868
31452
  try {
29869
- version2 = rotateFingerprintKey(dir).version;
31453
+ next = rotateFingerprintKey(dir);
29870
31454
  } catch (err) {
29871
31455
  const message = err instanceof Error ? err.message : String(err);
29872
- throw new Error(
29873
- `cannot rotate: ${message}
29874
- Delete ${dir}/exception.key to start fresh \u2014 grants written under it already cannot match.`,
29875
- { cause: err }
29876
- );
31456
+ throw new Error(`cannot rotate: ${message}
31457
+ ${keyAccessHint(err, dir)}`, { cause: err });
29877
31458
  }
29878
31459
  io.out(
29879
- `Fingerprint key rotated \u2014 new key version v${String(version2)}.
31460
+ `Fingerprint key rotated \u2014 new key version v${String(next.version)}.
29880
31461
  Existing grants no longer match; re-approve deliberately where still needed.
29881
31462
  `
29882
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
+ }
29883
31484
  } finally {
29884
31485
  db.close();
29885
31486
  }
@@ -29907,13 +31508,13 @@ async function runException(argv, io = terminalPrompter()) {
29907
31508
  }
29908
31509
 
29909
31510
  // src/commands/init.ts
29910
- import { existsSync as existsSync11, statSync as statSync7 } from "fs";
29911
- import { join as join16 } from "path";
31511
+ import { existsSync as existsSync12, statSync as statSync8 } from "fs";
31512
+ import { join as join17 } from "path";
29912
31513
  import * as readline from "readline/promises";
29913
31514
  import { parseArgs as parseArgs6 } from "util";
29914
31515
 
29915
31516
  // src/commands/plugins.ts
29916
- import { existsSync as existsSync10 } from "fs";
31517
+ import { existsSync as existsSync11 } from "fs";
29917
31518
  import { parseArgs as parseArgs5 } from "util";
29918
31519
  function runPlugins(argv) {
29919
31520
  const [sub, ...rest] = argv;
@@ -29930,7 +31531,7 @@ async function listPlugins(argv) {
29930
31531
  const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
29931
31532
  const home = homeBase(values.home);
29932
31533
  const active = /* @__PURE__ */ new Set();
29933
- if (existsSync10(dbPath(home))) {
31534
+ if (existsSync11(dbPath(home))) {
29934
31535
  const db = openLocalDatabase(dataDir(home));
29935
31536
  try {
29936
31537
  const recent = await db.findings.recentFindings({ limit: 1e3 });
@@ -30020,12 +31621,12 @@ function looseStorePaths(home) {
30020
31621
  home,
30021
31622
  settingsDir(home),
30022
31623
  dataDir(home),
30023
- join16(settingsDir(home), "settings.json"),
31624
+ join17(settingsDir(home), "settings.json"),
30024
31625
  dbPath(home)
30025
31626
  ];
30026
31627
  return targets.filter((p) => {
30027
31628
  try {
30028
- return (statSync7(p).mode & 63) !== 0;
31629
+ return (statSync8(p).mode & 63) !== 0;
30029
31630
  } catch {
30030
31631
  return false;
30031
31632
  }
@@ -30040,8 +31641,8 @@ async function runInit(argv) {
30040
31641
  ensureLayoutDirSync(home);
30041
31642
  const settings = settingsDir(home);
30042
31643
  ensureLayoutDirSync(settings);
30043
- const settingsFile = join16(settings, "settings.json");
30044
- const settingsCreated = !existsSync11(settingsFile);
31644
+ const settingsFile = join17(settings, "settings.json");
31645
+ const settingsCreated = !existsSync12(settingsFile);
30045
31646
  if (settingsCreated) {
30046
31647
  writeOwnerOnlyFileSync(
30047
31648
  settingsFile,
@@ -30115,7 +31716,7 @@ async function confirm(question) {
30115
31716
  }
30116
31717
 
30117
31718
  // src/commands/scan.ts
30118
- import { statSync as statSync8 } from "fs";
31719
+ import { statSync as statSync9 } from "fs";
30119
31720
  import { parseArgs as parseArgs7 } from "util";
30120
31721
  function severityRank(s) {
30121
31722
  return Severity.options.indexOf(s);
@@ -30162,7 +31763,7 @@ function runScan(argv) {
30162
31763
  failOn = parsed.data;
30163
31764
  }
30164
31765
  try {
30165
- statSync8(target);
31766
+ statSync9(target);
30166
31767
  } catch {
30167
31768
  process.stderr.write(`aka scan: no such file or directory: ${target}
30168
31769
  `);
@@ -31069,7 +32670,136 @@ function applyPluginUpdate2(status) {
31069
32670
  return ok;
31070
32671
  }
31071
32672
 
31072
- // src/cli.ts
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
+
32759
+ // src/lib/external-dispatch.ts
32760
+ import { spawnSync as spawnSync2 } from "child_process";
32761
+ import { constants } from "os";
32762
+ function commandOnPath(command) {
32763
+ const probe = spawnSync2("/bin/sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", command], {
32764
+ stdio: "ignore"
32765
+ });
32766
+ return probe.status === 0;
32767
+ }
32768
+ function isExternalCommandName(name) {
32769
+ return /^[a-z][a-z0-9-]*$/.test(name);
32770
+ }
32771
+ function shouldDispatchExternal(command, isBuiltin) {
32772
+ return !isBuiltin && isExternalCommandName(command);
32773
+ }
32774
+ function externalDispatchSupported(platform2 = process.platform) {
32775
+ return platform2 !== "win32";
32776
+ }
32777
+ function signalExitCode(signal) {
32778
+ const number4 = signal ? constants.signals[signal] : void 0;
32779
+ return number4 === void 0 ? 1 : 128 + number4;
32780
+ }
32781
+ function dispatchExternal(command, argv, deps = {}) {
32782
+ const spawn4 = deps.spawn ?? spawnSync2;
32783
+ const platform2 = deps.platform ?? process.platform;
32784
+ const exists = deps.exists ?? commandOnPath;
32785
+ if (!externalDispatchSupported(platform2)) return { found: false, status: 1 };
32786
+ const target = `aka-${command}`;
32787
+ const result = spawn4(target, argv, { stdio: "inherit" });
32788
+ if (result.error) {
32789
+ if (result.error.code === "ENOENT" && !exists(target)) return { found: false, status: 1 };
32790
+ process.stderr.write(`aka: ${target}: ${result.error.message}
32791
+ `);
32792
+ return { found: true, status: 1 };
32793
+ }
32794
+ if (result.status === null) {
32795
+ if (result.signal) process.stderr.write(`aka: ${target}: terminated by ${result.signal}
32796
+ `);
32797
+ return { found: true, status: signalExitCode(result.signal) };
32798
+ }
32799
+ return { found: true, status: result.status };
32800
+ }
32801
+
32802
+ // src/main.ts
31073
32803
  var COMMANDS = {
31074
32804
  init: runInit,
31075
32805
  scan: runScan,
@@ -31078,6 +32808,7 @@ var COMMANDS = {
31078
32808
  plugins: runPlugins,
31079
32809
  dashboard: runDashboard,
31080
32810
  exception: (argv) => runException(argv),
32811
+ vault: (argv) => runVault(argv),
31081
32812
  tui: runTui,
31082
32813
  update: runUpdate,
31083
32814
  "check-updates": runCheckUpdates,
@@ -31106,7 +32837,9 @@ Usage: aka <command> [options]
31106
32837
 
31107
32838
  Commands:
31108
32839
  ${commandsHelp()}
31109
-
32840
+ ${externalDispatchSupported() ? `
32841
+ Anything else runs an external 'aka-<command>' from your PATH (so 'aka claude' runs aka-claude).
32842
+ ` : ""}
31110
32843
  Options:
31111
32844
  --home <dir> Use an alternate AKA home (default: ~/.aka)
31112
32845
  --no-update-check Skip the post-command "updates available" notice
@@ -31122,8 +32855,7 @@ function homeFromArgv(argv) {
31122
32855
  }
31123
32856
  return homeBase(void 0);
31124
32857
  }
31125
- async function main() {
31126
- const argv = process.argv.slice(2);
32858
+ async function main(argv = process.argv.slice(2), dispatchDeps = {}) {
31127
32859
  if (argv[0] === "-v" || argv[0] === "--version" || argv[0] === "version") {
31128
32860
  process.stdout.write(`${cliVersion() ?? "unknown"}
31129
32861
  `);
@@ -31136,7 +32868,14 @@ async function main() {
31136
32868
  }
31137
32869
  const noUpdateCheck = rawRest.includes("--no-update-check");
31138
32870
  const rest = rawRest.filter((a) => a !== "--no-update-check");
31139
- const handler = COMMANDS[command];
32871
+ const handler = Object.hasOwn(COMMANDS, command) ? COMMANDS[command] : void 0;
32872
+ if (shouldDispatchExternal(command, handler !== void 0)) {
32873
+ const external = dispatchExternal(command, rawRest, dispatchDeps);
32874
+ if (external.found) {
32875
+ process.exitCode = external.status;
32876
+ return;
32877
+ }
32878
+ }
31140
32879
  if (!handler) {
31141
32880
  process.stderr.write(`aka: unknown command '${command}'
31142
32881
 
@@ -31152,6 +32891,8 @@ ${USAGE}`);
31152
32891
  }
31153
32892
  }
31154
32893
  }
32894
+
32895
+ // src/cli.ts
31155
32896
  main().catch((err) => {
31156
32897
  process.stderr.write(`aka: ${err instanceof Error ? err.message : String(err)}
31157
32898
  `);