@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.
- package/README.md +4 -0
- package/dist/cli.js +2020 -279
- package/package.json +5 -5
- package/web-ui/package.json +6 -2
- package/web-ui/packages/dashboard-ui/package.json +2 -1
- package/web-ui/packages/detections/package.json +1 -1
- package/web-ui/packages/local-ops/package.json +1 -1
- package/web-ui/packages/persistence/package.json +1 -1
- package/web-ui/packages/plugin-sdk/package.json +1 -1
- package/web-ui/packages/schema/package.json +1 -1
- package/web-ui/packages/ui-kit/package.json +1 -1
- package/web-ui/web-ui/.next/BUILD_ID +1 -1
- package/web-ui/web-ui/.next/app-build-manifest.json +135 -126
- package/web-ui/web-ui/.next/app-path-routes-manifest.json +6 -5
- package/web-ui/web-ui/.next/build-manifest.json +14 -14
- package/web-ui/web-ui/.next/prerender-manifest.json +15 -15
- package/web-ui/web-ui/.next/required-server-files.json +1 -2
- package/web-ui/web-ui/.next/routes-manifest.json +6 -0
- package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/activity/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/detections/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/findings/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/policies/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/security/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/security/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/settings/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/(app)/vault/page.js +2 -0
- package/web-ui/web-ui/.next/server/app/(app)/vault/page.js.nft.json +1 -0
- package/web-ui/web-ui/.next/server/app/(app)/vault/page_client-reference-manifest.js +1 -0
- package/web-ui/web-ui/.next/server/app/_not-found/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
- package/web-ui/web-ui/.next/server/app/_not-found.rsc +10 -10
- package/web-ui/web-ui/.next/server/app/icon.svg/route.js +1 -1
- package/web-ui/web-ui/.next/server/app/icon.svg/route.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/index.html +1 -1
- package/web-ui/web-ui/.next/server/app/index.rsc +10 -10
- package/web-ui/web-ui/.next/server/app/page.js +2 -2
- package/web-ui/web-ui/.next/server/app/page.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/app-paths-manifest.json +6 -5
- package/web-ui/web-ui/.next/server/chunks/243.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/28.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/330.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/{924.js → 334.js} +1 -1
- package/web-ui/web-ui/.next/server/chunks/447.js +6 -0
- package/web-ui/web-ui/.next/server/chunks/514.js +755 -0
- package/web-ui/web-ui/.next/server/chunks/539.js +22 -0
- package/web-ui/web-ui/.next/server/chunks/628.js +84 -0
- package/web-ui/web-ui/.next/server/chunks/817.js +1 -0
- package/web-ui/web-ui/.next/server/chunks/830.js +750 -123
- package/web-ui/web-ui/.next/server/chunks/935.js +11 -0
- package/web-ui/web-ui/.next/server/chunks/949.js +123 -0
- package/web-ui/web-ui/.next/server/functions-config-manifest.json +5 -4
- package/web-ui/web-ui/.next/server/middleware-build-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
- package/web-ui/web-ui/.next/server/middleware.js +2 -2
- package/web-ui/web-ui/.next/server/pages/404.html +1 -1
- package/web-ui/web-ui/.next/server/pages/500.html +1 -1
- package/web-ui/web-ui/.next/server/pages/_app.js +1 -1
- package/web-ui/web-ui/.next/server/pages/_app.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/pages/_document.js +1 -1
- package/web-ui/web-ui/.next/server/pages/_document.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/pages/_error.js +3 -3
- package/web-ui/web-ui/.next/server/pages/_error.js.nft.json +1 -1
- package/web-ui/web-ui/.next/server/pages-manifest.json +1 -1
- package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
- package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
- package/web-ui/web-ui/.next/static/cVKs25XbQsC1083sU5GOD/_buildManifest.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/321-46ce36c1ef95d231.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/501-939e9e83b25a6293.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/84-8205054e8901dbde.js +62 -0
- package/web-ui/web-ui/.next/static/chunks/89-d23f5e27ab3b468f.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-e3ce4be93d2f8ac5.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-8bb13b53c7bde504.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-07307e6d8ddb5cbf.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-aecbc7b6fbd143c2.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-1d468a1a0cf12aea.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-863e759b7e26c759.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-7636a56ffa40d5c6.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-c80a692b9d05f321.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-01e6dceb36cb882f.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-d98e303b17524dad.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-178c53b1f56b5e97.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-7f69b2579337c729.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-7019a1567cd9aadd.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-90ce0ad1b9403f48.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/(app)/vault/page-7ef77f76ff07ea1f.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-3cf97f2dd544e8cb.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/layout-f1727d7b45e0779b.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/app/page-3fbfddda391d9939.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/{dddf6c0a-f09ab3fc84be1c52.js → d043c60e-31360d0d5cb78983.js} +1 -1
- package/web-ui/web-ui/.next/static/chunks/framework-3444de8758288822.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/main-app-9e734a14592bc9c4.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/main-b13545f01f111855.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/pages/_app-7f0d920cc5af1c9f.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/pages/_error-3d8b61361c4b2c6b.js +1 -0
- package/web-ui/web-ui/.next/static/chunks/{webpack-ea80c9c368a8364e.js → webpack-61986b0a62ba55ed.js} +1 -1
- package/web-ui/web-ui/.next/static/css/10425160b1a85ff4.css +2 -0
- package/web-ui/web-ui/package.json +1 -1
- package/web-ui/web-ui/server.js +1 -1
- package/web-ui/web-ui/.next/server/chunks/100.js +0 -1
- package/web-ui/web-ui/.next/server/chunks/142.js +0 -700
- package/web-ui/web-ui/.next/server/chunks/472.js +0 -84
- package/web-ui/web-ui/.next/server/chunks/723.js +0 -22
- package/web-ui/web-ui/.next/server/chunks/75.js +0 -6
- package/web-ui/web-ui/.next/server/chunks/850.js +0 -1
- package/web-ui/web-ui/.next/server/chunks/853.js +0 -698
- package/web-ui/web-ui/.next/server/chunks/871.js +0 -1
- package/web-ui/web-ui/.next/server/chunks/959.js +0 -9
- package/web-ui/web-ui/.next/static/chunks/137-8f757e04d1877a6c.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/517-977bc0a7a7be88cc.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/613-734dcad67e1fa08c.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/784-25fa9bc074877c93.js +0 -62
- package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/page-96995843d6cbfb84.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/page-28eeea5c4a816f53.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-0c3888b9381a6e80.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-3a5138f424e43b6e.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/page-32d67b6c8165acda.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-1c376f3818bfba45.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/page-9c9b1d29895a0592.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-4334a50ed9dcace4.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/layout-de6da4ea92784950.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/policies/page-4817c44647618832.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-5d2aa8c946dd2cae.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/security/page-c0597d1c1acb1cc4.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/page-2a7b0d9dab536ae1.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-576a976afbe33369.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/_not-found/page-26662e9d7ee32abc.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/layout-e531d303d8c8a313.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/app/page-3ffe075f223c0af9.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/framework-f8855380599951ba.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/main-app-7291de73392cac56.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/main-ff0082770df07ddc.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/pages/_app-56d1709db70ee49f.js +0 -1
- package/web-ui/web-ui/.next/static/chunks/pages/_error-63dd320a91a40541.js +0 -1
- package/web-ui/web-ui/.next/static/css/1ea302d29fc67ba4.css +0 -2
- package/web-ui/web-ui/.next/static/mAOCejrDqkMoDfzjWr6qu/_buildManifest.js +0 -1
- /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
|
|
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
|
|
1859
|
-
if (
|
|
1860
|
-
blockKeyword =
|
|
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
|
|
2379
|
-
if (
|
|
2380
|
-
|
|
2381
|
-
if (rank >
|
|
2382
|
-
|
|
2383
|
-
|
|
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 =
|
|
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,
|
|
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/
|
|
24357
|
-
|
|
24358
|
-
var
|
|
24359
|
-
|
|
24360
|
-
|
|
24361
|
-
|
|
24362
|
-
|
|
24363
|
-
|
|
24364
|
-
|
|
24365
|
-
|
|
24366
|
-
|
|
24367
|
-
|
|
24368
|
-
|
|
24369
|
-
|
|
24370
|
-
|
|
24371
|
-
|
|
24372
|
-
|
|
24373
|
-
|
|
24374
|
-
|
|
24375
|
-
|
|
24376
|
-
|
|
24377
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
25506
|
-
|
|
25507
|
-
|
|
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
|
-
|
|
25523
|
-
|
|
25524
|
-
|
|
25525
|
-
|
|
25526
|
-
|
|
25527
|
-
|
|
25528
|
-
|
|
25529
|
-
|
|
25530
|
-
|
|
25531
|
-
|
|
25532
|
-
|
|
25533
|
-
|
|
25534
|
-
|
|
25535
|
-
|
|
25536
|
-
|
|
25537
|
-
|
|
25538
|
-
|
|
25539
|
-
|
|
25540
|
-
|
|
25541
|
-
|
|
25542
|
-
|
|
25543
|
-
|
|
25544
|
-
|
|
25545
|
-
|
|
25546
|
-
|
|
25547
|
-
|
|
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:
|
|
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, {
|
|
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
|
-
|
|
25863
|
-
|
|
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
|
-
|
|
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
|
|
25874
|
-
import { join as
|
|
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
|
|
25878
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
|
27937
|
-
import { basename, dirname as dirname2, isAbsolute, join as
|
|
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 (
|
|
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 =
|
|
29437
|
+
const dotGit = join9(root, ".git");
|
|
27983
29438
|
try {
|
|
27984
|
-
if (
|
|
27985
|
-
return { configPath:
|
|
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 :
|
|
27993
|
-
if (
|
|
27994
|
-
return { configPath:
|
|
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(
|
|
29451
|
+
const commonRaw = safeRead(join9(gitdir, "commondir"))?.trim();
|
|
27997
29452
|
if (!commonRaw) return void 0;
|
|
27998
|
-
const commonGitDir = isAbsolute(commonRaw) ? commonRaw :
|
|
29453
|
+
const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join9(gitdir, commonRaw);
|
|
27999
29454
|
const headRoot = basename(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
|
|
28000
|
-
return { configPath:
|
|
29455
|
+
return { configPath: join9(commonGitDir, "config"), headRoot };
|
|
28001
29456
|
}
|
|
28002
29457
|
function safeRead(path) {
|
|
28003
29458
|
try {
|
|
28004
|
-
return
|
|
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
|
|
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
|
|
28049
|
-
import { join as
|
|
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
|
|
28088
|
-
import { basename as basename4, join as
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
|
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
|
|
28231
|
-
import { join as
|
|
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 =
|
|
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
|
|
28280
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
28281
|
-
import { basename as basename5, extname, join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
28493
|
-
import { join as
|
|
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
|
|
29952
|
+
return join15(dataDir(home), "update-check.json");
|
|
28497
29953
|
}
|
|
28498
29954
|
function readCache(home) {
|
|
28499
29955
|
const path = cachePath(home);
|
|
28500
|
-
if (!
|
|
29956
|
+
if (!existsSync8(path)) return null;
|
|
28501
29957
|
try {
|
|
28502
|
-
const raw = JSON.parse(
|
|
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 (!
|
|
29966
|
+
if (!existsSync8(dataDir(home))) return;
|
|
28511
29967
|
try {
|
|
28512
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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) :
|
|
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
|
-
|
|
28939
|
-
|
|
28940
|
-
].find((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 (
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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)
|
|
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: {
|
|
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
|
|
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
|
|
29559
|
-
const entry = await pickBlocked(entries,
|
|
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
|
-
|
|
29566
|
-
|
|
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
|
-
|
|
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
|
|
31451
|
+
let next;
|
|
29868
31452
|
try {
|
|
29869
|
-
|
|
31453
|
+
next = rotateFingerprintKey(dir);
|
|
29870
31454
|
} catch (err) {
|
|
29871
31455
|
const message = err instanceof Error ? err.message : String(err);
|
|
29872
|
-
throw new Error(
|
|
29873
|
-
|
|
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(
|
|
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
|
|
29911
|
-
import { join as
|
|
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
|
|
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 (
|
|
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
|
-
|
|
31624
|
+
join17(settingsDir(home), "settings.json"),
|
|
30024
31625
|
dbPath(home)
|
|
30025
31626
|
];
|
|
30026
31627
|
return targets.filter((p) => {
|
|
30027
31628
|
try {
|
|
30028
|
-
return (
|
|
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 =
|
|
30044
|
-
const settingsCreated = !
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
`);
|