@akasecurity/cli 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +65 -0
  2. package/dist/cli.js +2008 -1221
  3. package/package.json +7 -6
  4. package/web-ui/package.json +1 -1
  5. package/web-ui/packages/dashboard-ui/package.json +1 -1
  6. package/web-ui/packages/detections/package.json +2 -2
  7. package/web-ui/packages/local-ops/package.json +3 -2
  8. package/web-ui/packages/persistence/package.json +2 -2
  9. package/web-ui/packages/plugin-sdk/package.json +29 -0
  10. package/web-ui/packages/schema/package.json +2 -2
  11. package/web-ui/rules/code-flaws/auth-jwt-no-verify.json +20 -0
  12. package/web-ui/rules/code-flaws/auth-ssl-verify-false.json +19 -0
  13. package/web-ui/rules/code-flaws/cmd-inject-exec.json +20 -0
  14. package/web-ui/rules/code-flaws/cmd-inject-node-exec.json +20 -0
  15. package/web-ui/rules/code-flaws/cmd-inject-shell.json +20 -0
  16. package/web-ui/rules/code-flaws/crypto-insecure-random.json +20 -0
  17. package/web-ui/rules/code-flaws/crypto-weak-hash-md5.json +16 -0
  18. package/web-ui/rules/code-flaws/crypto-weak-hash-sha1.json +23 -0
  19. package/web-ui/rules/code-flaws/deser-java-ois.json +22 -0
  20. package/web-ui/rules/code-flaws/deser-pickle.json +16 -0
  21. package/web-ui/rules/code-flaws/deser-yaml-unsafe.json +16 -0
  22. package/web-ui/rules/code-flaws/dev-debug-enabled.json +13 -0
  23. package/web-ui/rules/code-flaws/dev-placeholder-secret.json +24 -0
  24. package/web-ui/rules/code-flaws/dev-wildcard-cors.json +17 -0
  25. package/web-ui/rules/code-flaws/eval-dynamic-exec.json +17 -0
  26. package/web-ui/rules/code-flaws/hardcoded-password.json +23 -0
  27. package/web-ui/rules/code-flaws/hardcoded-secret-key.json +17 -0
  28. package/web-ui/rules/code-flaws/path-traversal-join.json +19 -0
  29. package/web-ui/rules/code-flaws/path-traversal-open.json +20 -0
  30. package/web-ui/rules/code-flaws/prototype-pollution-merge.json +16 -0
  31. package/web-ui/rules/code-flaws/regex-redos-backtrack.json +13 -0
  32. package/web-ui/rules/code-flaws/sql-inject-concat-dot.json +16 -0
  33. package/web-ui/rules/code-flaws/sql-inject-concat.json +13 -0
  34. package/web-ui/rules/code-flaws/sql-inject-format.json +16 -0
  35. package/web-ui/rules/code-flaws/sql-inject-interp.json +19 -0
  36. package/web-ui/rules/code-flaws/ssrf-user-url.json +16 -0
  37. package/web-ui/rules/code-flaws/xss-dangerously-set.json +16 -0
  38. package/web-ui/rules/code-flaws/xss-inner-html.json +16 -0
  39. package/web-ui/rules/code-flaws/xss-unescaped-render.json +16 -0
  40. package/web-ui/rules/core-code-context/db-table-name.json +30 -0
  41. package/web-ui/rules/core-code-context/feature-flag.json +24 -0
  42. package/web-ui/rules/core-code-context/file-path.json +17 -0
  43. package/web-ui/rules/core-code-context/internal-domain.json +13 -0
  44. package/web-ui/rules/core-code-context/internal-ip.json +13 -0
  45. package/web-ui/rules/core-code-context/internal-url.json +27 -0
  46. package/web-ui/rules/core-code-context/localhost-ref.json +13 -0
  47. package/web-ui/rules/core-code-context/stack-trace.json +13 -0
  48. package/web-ui/rules/core-financial/credit-card.json +14 -0
  49. package/web-ui/rules/core-financial/cusip.json +14 -0
  50. package/web-ui/rules/core-financial/cvv.json +13 -0
  51. package/web-ui/rules/core-financial/iban.json +17 -0
  52. package/web-ui/rules/core-financial/paypal.json +14 -0
  53. package/web-ui/rules/core-financial/routing-number.json +14 -0
  54. package/web-ui/rules/core-financial/salary.json +23 -0
  55. package/web-ui/rules/core-financial/swift.json +14 -0
  56. package/web-ui/rules/core-phi/biometric-ref.json +23 -0
  57. package/web-ui/rules/core-phi/genetic-data.json +24 -0
  58. package/web-ui/rules/core-phi/group-id.json +19 -0
  59. package/web-ui/rules/core-phi/hipaa-identifier.json +13 -0
  60. package/web-ui/rules/core-phi/icd10-code.json +14 -0
  61. package/web-ui/rules/core-phi/member-id.json +13 -0
  62. package/web-ui/rules/core-phi/mrn.json +13 -0
  63. package/web-ui/rules/core-phi/ndc-code.json +13 -0
  64. package/web-ui/rules/core-pii/dob.json +18 -0
  65. package/web-ui/rules/core-pii/drivers-license-us.json +14 -0
  66. package/web-ui/rules/core-pii/email.json +13 -0
  67. package/web-ui/rules/core-pii/home-address.json +21 -0
  68. package/web-ui/rules/core-pii/ip-address.json +13 -0
  69. package/web-ui/rules/core-pii/mac-address.json +13 -0
  70. package/web-ui/rules/core-pii/name.json +21 -0
  71. package/web-ui/rules/core-pii/national-id.json +27 -0
  72. package/web-ui/rules/core-pii/passport-us.json +17 -0
  73. package/web-ui/rules/core-pii/phone-intl.json +13 -0
  74. package/web-ui/rules/core-pii/phone-us.json +17 -0
  75. package/web-ui/rules/core-pii/ssn.json +13 -0
  76. package/web-ui/rules/core-pii/vin.json +13 -0
  77. package/web-ui/rules/core-pii/zip.json +30 -0
  78. package/web-ui/rules/secrets/anthropic-api-key.json +14 -0
  79. package/web-ui/rules/secrets/aws-access-key.json +14 -0
  80. package/web-ui/rules/secrets/aws-secret-key.json +15 -0
  81. package/web-ui/rules/secrets/azure-connection-string.json +13 -0
  82. package/web-ui/rules/secrets/cloudflare-api-key.json +15 -0
  83. package/web-ui/rules/secrets/datadog-key.json +15 -0
  84. package/web-ui/rules/secrets/digitalocean-token.json +13 -0
  85. package/web-ui/rules/secrets/discord-token.json +14 -0
  86. package/web-ui/rules/secrets/gcp-service-account.json +16 -0
  87. package/web-ui/rules/secrets/github-pat.json +16 -0
  88. package/web-ui/rules/secrets/gitlab-token.json +14 -0
  89. package/web-ui/rules/secrets/heroku-api-key.json +14 -0
  90. package/web-ui/rules/secrets/npm-token.json +14 -0
  91. package/web-ui/rules/secrets/openai-api-key.json +14 -0
  92. package/web-ui/rules/secrets/pulumi-access-token.json +14 -0
  93. package/web-ui/rules/secrets/sendgrid-key.json +14 -0
  94. package/web-ui/rules/secrets/slack-token.json +13 -0
  95. package/web-ui/rules/secrets/stripe-live-key.json +14 -0
  96. package/web-ui/rules/secrets/terraform-cloud-token.json +16 -0
  97. package/web-ui/rules/secrets/twilio-key.json +13 -0
  98. package/web-ui/rules/secrets/vault-token.json +14 -0
  99. package/web-ui/rules/secrets-infra/api-key-header.json +15 -0
  100. package/web-ui/rules/secrets-infra/basic-auth-header.json +13 -0
  101. package/web-ui/rules/secrets-infra/basic-auth-url.json +13 -0
  102. package/web-ui/rules/secrets-infra/bearer-token.json +17 -0
  103. package/web-ui/rules/secrets-infra/db-connection-string.json +13 -0
  104. package/web-ui/rules/secrets-infra/docker-config-auth.json +13 -0
  105. package/web-ui/rules/secrets-infra/env-key-value.json +14 -0
  106. package/web-ui/rules/secrets-infra/generic-high-entropy-secret.json +18 -0
  107. package/web-ui/rules/secrets-infra/jwt-token.json +16 -0
  108. package/web-ui/rules/secrets-infra/kubeconfig-token.json +14 -0
  109. package/web-ui/rules/secrets-infra/password-field.json +14 -0
  110. package/web-ui/rules/secrets-infra/pgp-private-key.json +15 -0
  111. package/web-ui/rules/secrets-infra/ssh-private-key.json +15 -0
  112. package/web-ui/web-ui/.next/BUILD_ID +1 -1
  113. package/web-ui/web-ui/.next/app-build-manifest.json +39 -39
  114. package/web-ui/web-ui/.next/app-path-routes-manifest.json +6 -6
  115. package/web-ui/web-ui/.next/build-manifest.json +2 -2
  116. package/web-ui/web-ui/.next/prerender-manifest.json +3 -3
  117. package/web-ui/web-ui/.next/required-server-files.json +2 -1
  118. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js +2 -2
  119. package/web-ui/web-ui/.next/server/app/(app)/activity/page.js.nft.json +1 -1
  120. package/web-ui/web-ui/.next/server/app/(app)/activity/page_client-reference-manifest.js +1 -1
  121. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js +2 -2
  122. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page.js.nft.json +1 -1
  123. package/web-ui/web-ui/.next/server/app/(app)/data-shares/page_client-reference-manifest.js +1 -1
  124. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js +2 -2
  125. package/web-ui/web-ui/.next/server/app/(app)/detections/page.js.nft.json +1 -1
  126. package/web-ui/web-ui/.next/server/app/(app)/detections/page_client-reference-manifest.js +1 -1
  127. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js +2 -2
  128. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page.js.nft.json +1 -1
  129. package/web-ui/web-ui/.next/server/app/(app)/exceptions/[id]/page_client-reference-manifest.js +1 -1
  130. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js +2 -2
  131. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page.js.nft.json +1 -1
  132. package/web-ui/web-ui/.next/server/app/(app)/exceptions/new/page_client-reference-manifest.js +1 -1
  133. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js +1 -1
  134. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page.js.nft.json +1 -1
  135. package/web-ui/web-ui/.next/server/app/(app)/exceptions/page_client-reference-manifest.js +1 -1
  136. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js +2 -2
  137. package/web-ui/web-ui/.next/server/app/(app)/findings/page.js.nft.json +1 -1
  138. package/web-ui/web-ui/.next/server/app/(app)/findings/page_client-reference-manifest.js +1 -1
  139. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js +1 -1
  140. package/web-ui/web-ui/.next/server/app/(app)/inventory/page.js.nft.json +1 -1
  141. package/web-ui/web-ui/.next/server/app/(app)/inventory/page_client-reference-manifest.js +1 -1
  142. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js +2 -2
  143. package/web-ui/web-ui/.next/server/app/(app)/policies/page.js.nft.json +1 -1
  144. package/web-ui/web-ui/.next/server/app/(app)/policies/page_client-reference-manifest.js +1 -1
  145. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js +2 -2
  146. package/web-ui/web-ui/.next/server/app/(app)/scan/page.js.nft.json +1 -1
  147. package/web-ui/web-ui/.next/server/app/(app)/scan/page_client-reference-manifest.js +1 -1
  148. package/web-ui/web-ui/.next/server/app/(app)/security/page.js +1 -1
  149. package/web-ui/web-ui/.next/server/app/(app)/security/page.js.nft.json +1 -1
  150. package/web-ui/web-ui/.next/server/app/(app)/security/page_client-reference-manifest.js +1 -1
  151. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js +2 -2
  152. package/web-ui/web-ui/.next/server/app/(app)/settings/page.js.nft.json +1 -1
  153. package/web-ui/web-ui/.next/server/app/(app)/settings/page_client-reference-manifest.js +1 -1
  154. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js +2 -2
  155. package/web-ui/web-ui/.next/server/app/(app)/updates/page.js.nft.json +1 -1
  156. package/web-ui/web-ui/.next/server/app/(app)/updates/page_client-reference-manifest.js +1 -1
  157. package/web-ui/web-ui/.next/server/app/_not-found/page.js +1 -1
  158. package/web-ui/web-ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  159. package/web-ui/web-ui/.next/server/app/_not-found.html +1 -1
  160. package/web-ui/web-ui/.next/server/app/_not-found.rsc +2 -2
  161. package/web-ui/web-ui/.next/server/app/index.html +1 -1
  162. package/web-ui/web-ui/.next/server/app/index.rsc +2 -2
  163. package/web-ui/web-ui/.next/server/app/page.js +1 -1
  164. package/web-ui/web-ui/.next/server/app/page_client-reference-manifest.js +1 -1
  165. package/web-ui/web-ui/.next/server/app-paths-manifest.json +6 -6
  166. package/web-ui/web-ui/.next/server/chunks/366.js +566 -0
  167. package/web-ui/web-ui/.next/server/chunks/45.js +1 -1
  168. package/web-ui/web-ui/.next/server/chunks/484.js +564 -0
  169. package/web-ui/web-ui/.next/server/chunks/913.js +2 -0
  170. package/web-ui/web-ui/.next/server/functions-config-manifest.json +7 -7
  171. package/web-ui/web-ui/.next/server/middleware-manifest.json +5 -5
  172. package/web-ui/web-ui/.next/server/pages/404.html +1 -1
  173. package/web-ui/web-ui/.next/server/pages/500.html +1 -1
  174. package/web-ui/web-ui/.next/server/server-reference-manifest.js +1 -1
  175. package/web-ui/web-ui/.next/server/server-reference-manifest.json +1 -1
  176. package/web-ui/web-ui/.next/static/chunks/561-46ea4b9ad0419130.js +1 -0
  177. package/web-ui/web-ui/.next/static/chunks/app/(app)/activity/{page-f6b1b344709ee48e.js → page-49c63d0cec0da80d.js} +1 -1
  178. package/web-ui/web-ui/.next/static/chunks/app/(app)/data-shares/{page-485a76552d6dbabd.js → page-d469bd704615ea76.js} +1 -1
  179. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-07ca4b0ce174ef7a.js +1 -0
  180. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-16d5fd7d373c8f0b.js +1 -0
  181. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/new/{page-e8498a1e48943abf.js → page-d6cfd847bb528eec.js} +1 -1
  182. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-291617f3a4d46a01.js +1 -0
  183. package/web-ui/web-ui/.next/static/chunks/app/(app)/findings/{page-94eb9d838ae00222.js → page-c59135e0cfa8d5c7.js} +1 -1
  184. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-0eb54f5acb30a73e.js +1 -0
  185. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-b00573441ac43bdf.js +1 -0
  186. package/web-ui/web-ui/.next/static/chunks/app/(app)/settings/{page-b9fba600c590896d.js → page-3b5fe92cd995b763.js} +1 -1
  187. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-50f828314c9a5d91.js +1 -0
  188. package/web-ui/web-ui/.next/static/css/dfdcbdce6b7c9eb0.css +2 -0
  189. package/web-ui/web-ui/package.json +1 -1
  190. package/web-ui/web-ui/server.js +1 -1
  191. package/LICENSE +0 -201
  192. package/web-ui/web-ui/.next/server/chunks/116.js +0 -536
  193. package/web-ui/web-ui/.next/server/chunks/223.js +0 -534
  194. package/web-ui/web-ui/.next/server/chunks/226.js +0 -2
  195. package/web-ui/web-ui/.next/static/chunks/561-3a19cd28d2b941fa.js +0 -1
  196. package/web-ui/web-ui/.next/static/chunks/app/(app)/detections/page-8030433ea5c099fa.js +0 -1
  197. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/[id]/page-452be710e9e05ad5.js +0 -1
  198. package/web-ui/web-ui/.next/static/chunks/app/(app)/exceptions/page-1b841b6a60708c6d.js +0 -1
  199. package/web-ui/web-ui/.next/static/chunks/app/(app)/inventory/page-dd61001da0f891d3.js +0 -1
  200. package/web-ui/web-ui/.next/static/chunks/app/(app)/scan/page-158ed3ad2cee998b.js +0 -1
  201. package/web-ui/web-ui/.next/static/chunks/app/(app)/updates/page-241fdcbac6e942aa.js +0 -1
  202. package/web-ui/web-ui/.next/static/css/69dfaec5234c6b99.css +0 -2
  203. /package/web-ui/web-ui/.next/static/{v339u67-B5AsSrzDBfv22 → zNaHfOWePiRki0ugHKYzl}/_buildManifest.js +0 -0
  204. /package/web-ui/web-ui/.next/static/{v339u67-B5AsSrzDBfv22 → zNaHfOWePiRki0ugHKYzl}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -554,10 +554,10 @@ var AGENT_PLUGINS = [
554
554
  name: "Claude Code",
555
555
  sourceTool: "claude-code",
556
556
  description: "Hooks Claude Code sessions to detect + redact sensitive data in prompts, responses, and file writes.",
557
- npmPackage: "@akasecurity/plugin-claude-code",
558
- pluginName: "aka",
559
- marketplace: "ai-tc",
560
- marketplaceSource: "akasecurity/ai-tc"
557
+ npmPackage: "@akasecurity/ai-tc-claude-code",
558
+ pluginName: "ai-tc",
559
+ marketplace: "akasecurity",
560
+ marketplaceSource: "akasecurity/marketplace"
561
561
  }
562
562
  ];
563
563
  function findAgent(id) {
@@ -16109,6 +16109,11 @@ var AuditEventType = external_exports.enum([
16109
16109
  "prompt",
16110
16110
  "response",
16111
16111
  "code_change",
16112
+ // The events.kind of a scanned tool call, widened in to keep this a
16113
+ // superset. Narrower than 'tool_call' above and not a duplicate of it:
16114
+ // 'tool_call' is the reconciler's structural row for every call, while
16115
+ // 'tool_use' exists only where a hook enforced against the arguments.
16116
+ "tool_use",
16112
16117
  // One row per config-inventory scan, hung off the session root. It is the
16113
16118
  // fact the posture inspection findings reference (findings require an
16114
16119
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -16500,7 +16505,7 @@ var ActivityOverviewResponse = external_exports.object({
16500
16505
  }).meta({ id: "ActivityOverviewResponse" });
16501
16506
 
16502
16507
  // ../packages/schema/src/zod/event.ts
16503
- var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
16508
+ var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
16504
16509
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
16505
16510
  var EventMetadata = external_exports.object({
16506
16511
  sessionId: external_exports.string().optional(),
@@ -16813,7 +16818,7 @@ var DetectionException = external_exports.object({
16813
16818
  justification: external_exports.string().min(1),
16814
16819
  conditions: ExceptionConditions.nullable(),
16815
16820
  createdBy: external_exports.string(),
16816
- createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
16821
+ createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
16817
16822
  createdAt: external_exports.iso.datetime(),
16818
16823
  updatedAt: external_exports.iso.datetime(),
16819
16824
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16982,20 +16987,26 @@ var PolicyBundle = external_exports.object({
16982
16987
  customKeywords: external_exports.array(external_exports.string()),
16983
16988
  fetchedAt: external_exports.iso.datetime()
16984
16989
  }).meta({ id: "PolicyBundle" });
16985
- var DEFAULT_ACTIONS = {
16986
- secret: "block",
16987
- pii: "redact",
16988
- financial: "redact",
16989
- phi: "redact",
16990
- code_context: "warn",
16991
- code_flaw: "warn",
16992
- custom: "warn",
16993
- // Config-posture findings only observe today (they land in
16994
- // inspection_findings, outside the live-capture enforcement path).
16995
- config: "warn"
16996
- };
16997
16990
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16998
16991
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
16992
+ var CATEGORY_PEAK_SEVERITY = {
16993
+ secret: "critical",
16994
+ financial: "critical",
16995
+ // core-financial/credit-card
16996
+ code_flaw: "critical",
16997
+ pii: "high",
16998
+ phi: "high",
16999
+ custom: "high",
17000
+ // user-defined; conservative
17001
+ code_context: "low",
17002
+ config: "low"
17003
+ // observe-only; floors to monitor regardless
17004
+ };
17005
+ function severityFloorPolicy(category) {
17006
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
17007
+ const peak = CATEGORY_PEAK_SEVERITY[category];
17008
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
17009
+ }
16999
17010
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
17000
17011
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
17001
17012
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -17022,6 +17033,12 @@ var BUILTIN_POLICY_SPECS = {
17022
17033
  description: "Refuse the request entirely whenever any rule in this detection matches."
17023
17034
  }
17024
17035
  };
17036
+ function builtinPolicyToAction(id) {
17037
+ return BUILTIN_POLICY_SPECS[id].action;
17038
+ }
17039
+ var DEFAULT_ACTIONS = Object.fromEntries(
17040
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
17041
+ );
17025
17042
  var BUILTIN_POLICIES = Object.fromEntries(
17026
17043
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
17027
17044
  );
@@ -17428,10 +17445,8 @@ function toApiProvider(sourceTool) {
17428
17445
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
17429
17446
  }
17430
17447
  var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
17431
- function deriveGroupStatus(instances) {
17432
- const statuses = new Set(
17433
- instances.map((i) => i.status).filter((s) => s !== void 0)
17434
- );
17448
+ function foldGroupStatus(instanceStatuses) {
17449
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
17435
17450
  if (statuses.size === 0) return void 0;
17436
17451
  for (const candidate of STATUS_PRECEDENCE) {
17437
17452
  if (statuses.has(candidate)) return candidate;
@@ -17449,6 +17464,7 @@ function deriveFindingStatus(row) {
17449
17464
  function buildFindingGroups(rows, opts = {}) {
17450
17465
  const overrides = opts.overrides;
17451
17466
  const packNames = opts.packNames;
17467
+ const aggregates = opts.aggregates;
17452
17468
  const byRuleId = /* @__PURE__ */ new Map();
17453
17469
  for (const row of rows) {
17454
17470
  const existing = byRuleId.get(row.ruleId);
@@ -17470,17 +17486,20 @@ function buildFindingGroups(rows, opts = {}) {
17470
17486
  status: r.status
17471
17487
  };
17472
17488
  });
17473
- const latestDetectedAt = ruleRows.reduce(
17489
+ const agg = aggregates?.get(ruleId);
17490
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
17474
17491
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
17475
17492
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
17476
17493
  );
17477
17494
  const seenProviders = /* @__PURE__ */ new Set();
17478
- const providers = instances.map((i) => i.provider).filter((p) => {
17495
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
17479
17496
  if (seenProviders.has(p)) return false;
17480
17497
  seenProviders.add(p);
17481
17498
  return true;
17482
17499
  });
17483
- const actionSet = new Set(instances.map((i) => i.action));
17500
+ const actionSet = new Set(
17501
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
17502
+ );
17484
17503
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
17485
17504
  const severity = ruleRows[0]?.severity ?? "low";
17486
17505
  const detection = {
@@ -17494,8 +17513,10 @@ function buildFindingGroups(rows, opts = {}) {
17494
17513
  contextPrefix: ""
17495
17514
  // empty (pending privacy review)
17496
17515
  };
17497
- const status = deriveGroupStatus(instances);
17498
- groups.push({
17516
+ const status = foldGroupStatus(
17517
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
17518
+ );
17519
+ const group = {
17499
17520
  id: ruleId,
17500
17521
  category: apiCategory,
17501
17522
  subtype: ruleId,
@@ -17504,21 +17525,26 @@ function buildFindingGroups(rows, opts = {}) {
17504
17525
  match,
17505
17526
  detection,
17506
17527
  policy,
17507
- instanceCount: instances.length,
17528
+ instanceCount: agg?.instanceCount ?? instances.length,
17508
17529
  providers,
17509
17530
  aggregateAction,
17510
17531
  latestDetectedAt,
17511
17532
  instances,
17512
17533
  status
17513
- });
17534
+ };
17535
+ if (agg) {
17536
+ actionsCache.set(group, [...actionSet]);
17537
+ if (agg.searchText !== void 0) {
17538
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
17539
+ }
17540
+ }
17541
+ groups.push(group);
17514
17542
  }
17515
17543
  return groups;
17516
17544
  }
17517
17545
  var haystackCache = /* @__PURE__ */ new WeakMap();
17518
- function groupHaystack(g) {
17519
- const cached2 = haystackCache.get(g);
17520
- if (cached2 !== void 0) return cached2;
17521
- const haystack = [
17546
+ function buildHaystack(g, extra) {
17547
+ return [
17522
17548
  g.subtype,
17523
17549
  g.category,
17524
17550
  g.match.maskedValue,
@@ -17526,11 +17552,25 @@ function groupHaystack(g) {
17526
17552
  g.id,
17527
17553
  ...g.instances.map((i) => i.repo),
17528
17554
  ...g.instances.map((i) => i.file),
17529
- ...g.instances.map((i) => i.id)
17555
+ ...g.instances.map((i) => i.id),
17556
+ ...extra === void 0 ? [] : [extra]
17530
17557
  ].join(" ").toLowerCase();
17558
+ }
17559
+ function groupHaystack(g) {
17560
+ const cached2 = haystackCache.get(g);
17561
+ if (cached2 !== void 0) return cached2;
17562
+ const haystack = buildHaystack(g);
17531
17563
  haystackCache.set(g, haystack);
17532
17564
  return haystack;
17533
17565
  }
17566
+ var actionsCache = /* @__PURE__ */ new WeakMap();
17567
+ function groupActions(g) {
17568
+ const cached2 = actionsCache.get(g);
17569
+ if (cached2 !== void 0) return cached2;
17570
+ const actions = [...new Set(g.instances.map((i) => i.action))];
17571
+ actionsCache.set(g, actions);
17572
+ return actions;
17573
+ }
17534
17574
  function applyFindingFilters(groups, opts) {
17535
17575
  let filtered = groups;
17536
17576
  if (opts.severity && opts.severity.length > 0) {
@@ -17543,7 +17583,7 @@ function applyFindingFilters(groups, opts) {
17543
17583
  }
17544
17584
  if (opts.actions && opts.actions.length > 0) {
17545
17585
  const actionSet = new Set(opts.actions);
17546
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
17586
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
17547
17587
  }
17548
17588
  if (opts.subtype && opts.subtype.length > 0) {
17549
17589
  const subtypeSet = new Set(opts.subtype);
@@ -17595,8 +17635,7 @@ function computeFindingFacets(allGroups, opts) {
17595
17635
  });
17596
17636
  const actionMap = /* @__PURE__ */ new Map();
17597
17637
  for (const g of forAction) {
17598
- const actionSet = new Set(g.instances.map((i) => i.action));
17599
- for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
17638
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
17600
17639
  }
17601
17640
  const forSubtype = applyFindingFilters(allGroups, {
17602
17641
  providers: opts.providers,
@@ -18158,6 +18197,36 @@ function reviewSeverityRank(reasons) {
18158
18197
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
18159
18198
  }
18160
18199
 
18200
+ // ../packages/schema/src/zod/triage.ts
18201
+ var TriageHit = external_exports.object({
18202
+ ruleId: external_exports.string(),
18203
+ category: DetectionCategory,
18204
+ severity: Severity,
18205
+ maskedMatch: external_exports.string(),
18206
+ rawMatch: external_exports.string(),
18207
+ context: external_exports.string(),
18208
+ filePath: external_exports.string().optional(),
18209
+ confidence: external_exports.number().min(0).max(1),
18210
+ id: external_exports.string().optional(),
18211
+ valueFingerprint: external_exports.string().optional(),
18212
+ keyVersion: external_exports.number().int().nonnegative().optional()
18213
+ });
18214
+ var TriagePolicy = BuiltinPolicyId;
18215
+ var TriageCategoryRec = external_exports.object({
18216
+ category: DetectionCategory,
18217
+ action: TriagePolicy,
18218
+ reasoning: external_exports.string(),
18219
+ genuineCount: external_exports.number().int().nonnegative(),
18220
+ fpCount: external_exports.number().int().nonnegative(),
18221
+ // TriageHit ids judged false-positive in this category. fpCount must equal
18222
+ // this array's length — enforced by the consumer, not this schema.
18223
+ fpIds: external_exports.array(external_exports.string())
18224
+ });
18225
+ var TriageRecommendation = external_exports.object({
18226
+ perCategory: external_exports.array(TriageCategoryRec),
18227
+ notes: external_exports.string()
18228
+ });
18229
+
18161
18230
  // ../packages/local-ops/src/fs-scan.ts
18162
18231
  var import_ignore = __toESM(require_ignore(), 1);
18163
18232
  var AKAIGNORE_FILENAME = ".akaignore";
@@ -18281,10 +18350,9 @@ function scanPathIntoStore(db, target, opts = {}) {
18281
18350
  return { scanned, findings: findingCount, files };
18282
18351
  }
18283
18352
 
18284
- // ../packages/local-ops/src/update-cache.ts
18285
- import { spawn } from "child_process";
18286
- import { existsSync as existsSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
18287
- import { join as join7 } from "path";
18353
+ // ../packages/local-ops/src/project-inventory.ts
18354
+ import { statSync as statSync5 } from "fs";
18355
+ import { resolve } from "path";
18288
18356
 
18289
18357
  // ../packages/persistence/src/database.ts
18290
18358
  import { randomUUID as randomUUID9 } from "crypto";
@@ -18292,31 +18360,69 @@ import { existsSync as existsSync2, renameSync, rmSync } from "fs";
18292
18360
  import { join as join3, sep as sep2 } from "path";
18293
18361
  import { DatabaseSync } from "node:sqlite";
18294
18362
 
18295
- // ../packages/persistence/src/ids.ts
18296
- import { createHash as createHash2 } from "crypto";
18297
- function sha256Hex(input) {
18298
- return createHash2("sha256").update(input).digest("hex");
18363
+ // ../packages/persistence/src/internal/sql-text.ts
18364
+ function escapeLikePattern(s) {
18365
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
18299
18366
  }
18300
- function inventoryId(objectType, identityKey) {
18301
- return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
18367
+ function placeholders(n) {
18368
+ return Array.from({ length: n }, () => "?").join(", ");
18302
18369
  }
18303
- function sourceProjectId(url2) {
18304
- return sha256Hex(canonicalIdentity(["source_project", url2]));
18370
+ function containsPattern(q) {
18371
+ return `%${escapeLikePattern(q)}%`;
18305
18372
  }
18306
- function classifiedDataId(cls) {
18307
- return sha256Hex(canonicalIdentity(["classified_data", cls]));
18373
+ function likeAny(exprs) {
18374
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
18308
18375
  }
18309
- function inspectionDefinitionId(ruleId, version2) {
18310
- return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
18376
+
18377
+ // ../packages/persistence/src/internal/transactions.ts
18378
+ var savepointSeq = 0;
18379
+ function withTransaction(db, fn, mode = "DEFERRED") {
18380
+ if (db.isTransaction) {
18381
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
18382
+ db.exec(`SAVEPOINT ${savepoint}`);
18383
+ try {
18384
+ fn();
18385
+ db.exec(`RELEASE ${savepoint}`);
18386
+ } catch (error51) {
18387
+ try {
18388
+ db.exec(`ROLLBACK TO ${savepoint}`);
18389
+ db.exec(`RELEASE ${savepoint}`);
18390
+ } catch {
18391
+ }
18392
+ throw error51;
18393
+ }
18394
+ return;
18395
+ }
18396
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
18397
+ try {
18398
+ fn();
18399
+ db.exec("COMMIT");
18400
+ } catch (error51) {
18401
+ try {
18402
+ db.exec("ROLLBACK");
18403
+ } catch {
18404
+ }
18405
+ throw error51;
18406
+ }
18311
18407
  }
18312
- function llmCallId(sessionId, messageId) {
18313
- return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
18408
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
18409
+ const nested = db.isTransaction;
18410
+ try {
18411
+ withTransaction(db, fn, mode);
18412
+ return true;
18413
+ } catch (error51) {
18414
+ if (!db.isTransaction && nested) throw error51;
18415
+ return false;
18416
+ }
18314
18417
  }
18315
- function toolCallId(sessionId, toolUseId) {
18316
- return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
18418
+
18419
+ // ../packages/persistence/src/internal/warn.ts
18420
+ function akaWarn(message) {
18421
+ process.stderr.write(`[aka] ${message}
18422
+ `);
18317
18423
  }
18318
18424
 
18319
- // ../packages/persistence/src/migrations.ts
18425
+ // ../packages/persistence/src/db/migrations/introspection.ts
18320
18426
  function evidenceObjects(sql) {
18321
18427
  const objects = [];
18322
18428
  for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
@@ -18331,14 +18437,50 @@ function evidenceObjects(sql) {
18331
18437
  }
18332
18438
  return objects;
18333
18439
  }
18440
+ function schemaObjectExists(db, kind, name) {
18441
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
18442
+ return row !== void 0;
18443
+ }
18444
+ function indexExists(db, name) {
18445
+ return schemaObjectExists(db, "index", name);
18446
+ }
18447
+ function columnNames(db, table, opts) {
18448
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
18449
+ const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
18450
+ return columns.map((c) => c.name);
18451
+ }
18334
18452
  function evidenceExists(db, object2) {
18335
18453
  if (object2.kind === "column") {
18336
- const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
18337
- return columns.some((c) => c.name === object2.name);
18454
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
18338
18455
  }
18339
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
18340
- return row !== void 0;
18456
+ return schemaObjectExists(db, "table", object2.name);
18457
+ }
18458
+
18459
+ // ../packages/persistence/src/ids.ts
18460
+ import { createHash as createHash2 } from "crypto";
18461
+ function sha256Hex(input) {
18462
+ return createHash2("sha256").update(input).digest("hex");
18463
+ }
18464
+ function inventoryId(objectType, identityKey) {
18465
+ return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
18466
+ }
18467
+ function sourceProjectId(url2) {
18468
+ return sha256Hex(canonicalIdentity(["source_project", url2]));
18469
+ }
18470
+ function classifiedDataId(cls) {
18471
+ return sha256Hex(canonicalIdentity(["classified_data", cls]));
18472
+ }
18473
+ function inspectionDefinitionId(ruleId, version2) {
18474
+ return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
18341
18475
  }
18476
+ function llmCallId(sessionId, messageId) {
18477
+ return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
18478
+ }
18479
+ function toolCallId(sessionId, toolUseId) {
18480
+ return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
18481
+ }
18482
+
18483
+ // ../packages/persistence/src/migrations.ts
18342
18484
  function describeObject(object2) {
18343
18485
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
18344
18486
  }
@@ -18349,10 +18491,6 @@ function createdIndexName(statement) {
18349
18491
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
18350
18492
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
18351
18493
  }
18352
- function indexExists(db, name) {
18353
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
18354
- return row !== void 0;
18355
- }
18356
18494
  function applyMigrations(db) {
18357
18495
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
18358
18496
  db.exec(
@@ -18371,44 +18509,39 @@ function applyMigrations(db) {
18371
18509
  const present = evidence.filter((o) => evidenceExists(db, o));
18372
18510
  if (present.length > 0 && present.length < evidence.length) {
18373
18511
  const missing = evidence.filter((o) => !present.includes(o));
18374
- const message = `[aka] sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
18375
- process.stderr.write(`${message}
18376
- `);
18377
- throw new Error(message);
18512
+ const message = `sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
18513
+ akaWarn(message);
18514
+ throw new Error(`[aka] ${message}`);
18378
18515
  }
18379
18516
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
18380
18517
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
18381
18518
  const statements = splitStatements(migration.sql);
18382
18519
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
18383
18520
  try {
18384
- db.exec("BEGIN IMMEDIATE");
18385
- try {
18386
- for (const statement of statements) {
18387
- const indexName = createdIndexName(statement);
18388
- if (indexName === void 0) {
18389
- if (alreadyApplied) continue;
18390
- } else if (indexExists(db, indexName)) {
18391
- continue;
18521
+ withTransaction(
18522
+ db,
18523
+ () => {
18524
+ for (const statement of statements) {
18525
+ const indexName = createdIndexName(statement);
18526
+ if (indexName === void 0) {
18527
+ if (alreadyApplied) continue;
18528
+ } else if (indexExists(db, indexName)) {
18529
+ continue;
18530
+ }
18531
+ db.exec(statement);
18392
18532
  }
18393
- db.exec(statement);
18394
- }
18395
- if (wantsFkOff && !alreadyApplied) {
18396
- const violations = db.prepare("PRAGMA foreign_key_check").all();
18397
- if (violations.length > 0) {
18398
- throw new Error(
18399
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
18400
- );
18533
+ if (wantsFkOff && !alreadyApplied) {
18534
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
18535
+ if (violations.length > 0) {
18536
+ throw new Error(
18537
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
18538
+ );
18539
+ }
18401
18540
  }
18402
- }
18403
- record2.run(migration.tag, Date.now());
18404
- db.exec("COMMIT");
18405
- } catch (error51) {
18406
- try {
18407
- db.exec("ROLLBACK");
18408
- } catch {
18409
- }
18410
- throw error51;
18411
- }
18541
+ record2.run(migration.tag, Date.now());
18542
+ },
18543
+ "IMMEDIATE"
18544
+ );
18412
18545
  } finally {
18413
18546
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
18414
18547
  }
@@ -18451,8 +18584,7 @@ var TOKEN_USAGE_COLUMNS = [
18451
18584
  }
18452
18585
  ];
18453
18586
  function ensureTokenUsageColumns(db) {
18454
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
18455
- const existing = new Set(columns.map((c) => c.name));
18587
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
18456
18588
  for (const column of TOKEN_USAGE_COLUMNS) {
18457
18589
  if (!existing.has(column.name)) {
18458
18590
  db.exec(column.ddl);
@@ -18490,47 +18622,39 @@ function reconcileSourceProjectIds(db) {
18490
18622
  repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
18491
18623
  }));
18492
18624
  const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
18493
- db.exec("BEGIN IMMEDIATE");
18494
- try {
18495
- for (const { row, canonicalId } of legacy) {
18496
- foldProject.run(
18497
- canonicalId,
18498
- row.url,
18499
- row.name,
18500
- row.attributes,
18501
- row.firstSeen,
18502
- row.lastSeen
18503
- );
18504
- repointAudit.run(canonicalId, row.id);
18505
- for (const { dropCollisions, repoint } of pathTables) {
18506
- dropCollisions.run(row.id, canonicalId);
18507
- repoint.run(canonicalId, row.id);
18625
+ withTransaction(
18626
+ db,
18627
+ () => {
18628
+ for (const { row, canonicalId } of legacy) {
18629
+ foldProject.run(
18630
+ canonicalId,
18631
+ row.url,
18632
+ row.name,
18633
+ row.attributes,
18634
+ row.firstSeen,
18635
+ row.lastSeen
18636
+ );
18637
+ repointAudit.run(canonicalId, row.id);
18638
+ for (const { dropCollisions, repoint } of pathTables) {
18639
+ dropCollisions.run(row.id, canonicalId);
18640
+ repoint.run(canonicalId, row.id);
18641
+ }
18642
+ repointCallSite.run(canonicalId, row.id);
18643
+ deleteLegacy.run(row.id);
18508
18644
  }
18509
- repointCallSite.run(canonicalId, row.id);
18510
- deleteLegacy.run(row.id);
18511
- }
18512
- db.exec("COMMIT");
18513
- } catch (error51) {
18514
- try {
18515
- db.exec("ROLLBACK");
18516
- } catch {
18517
- }
18518
- throw error51;
18519
- }
18645
+ },
18646
+ "IMMEDIATE"
18647
+ );
18520
18648
  } catch (error51) {
18521
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
18522
- `);
18649
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
18523
18650
  }
18524
18651
  }
18525
18652
  function isForeignSqliteLineage(db) {
18526
- const tenantsTable = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tenants' LIMIT 1").get();
18527
- if (tenantsTable !== void 0) return true;
18528
- const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
18529
- return eventsColumns.some((c) => c.name === "tenant_id");
18653
+ if (schemaObjectExists(db, "table", "tenants")) return true;
18654
+ return columnNames(db, "events").includes("tenant_id");
18530
18655
  }
18531
18656
  function ensureSyncedAtColumn(db, table) {
18532
- const columns = db.prepare(`PRAGMA table_info(${table})`).all();
18533
- if (!columns.some((c) => c.name === "synced_at")) {
18657
+ if (!columnNames(db, table).includes("synced_at")) {
18534
18658
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
18535
18659
  }
18536
18660
  }
@@ -18581,8 +18705,11 @@ function ensureDataDirSync(dir) {
18581
18705
  } catch {
18582
18706
  }
18583
18707
  }
18708
+ function walSidecars(file2) {
18709
+ return [`${file2}-wal`, `${file2}-shm`];
18710
+ }
18584
18711
  function tightenPerms(file2) {
18585
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
18712
+ for (const path of [file2, ...walSidecars(file2)]) {
18586
18713
  try {
18587
18714
  chmodSync(path, DATA_FILE_MODE);
18588
18715
  } catch {
@@ -18590,12 +18717,68 @@ function tightenPerms(file2) {
18590
18717
  }
18591
18718
  }
18592
18719
 
18593
- // ../packages/persistence/src/repositories/sql-utils.ts
18594
- function escapeLikePattern(s) {
18595
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
18720
+ // ../packages/persistence/src/internal/json.ts
18721
+ function safeJson(s, fallback) {
18722
+ if (s == null) return fallback;
18723
+ try {
18724
+ return JSON.parse(s);
18725
+ } catch {
18726
+ return fallback;
18727
+ }
18596
18728
  }
18597
- function placeholders(n) {
18598
- return Array.from({ length: n }, () => "?").join(", ");
18729
+ function parseJsonObject(s) {
18730
+ if (s == null) return void 0;
18731
+ try {
18732
+ const parsed = JSON.parse(s);
18733
+ if (typeof parsed === "object" && parsed !== null) return parsed;
18734
+ } catch {
18735
+ }
18736
+ return void 0;
18737
+ }
18738
+
18739
+ // ../packages/persistence/src/internal/rows.ts
18740
+ function allRows(stmt, params) {
18741
+ if (params === void 0) return stmt.all();
18742
+ if (Array.isArray(params)) return stmt.all(...params);
18743
+ return stmt.all(params);
18744
+ }
18745
+ function getRow(stmt, params) {
18746
+ if (params === void 0) return stmt.get();
18747
+ if (Array.isArray(params)) return stmt.get(...params);
18748
+ return stmt.get(params);
18749
+ }
18750
+ function intToBool(raw) {
18751
+ return raw === 1 || raw === true;
18752
+ }
18753
+ function boolToInt(b) {
18754
+ return b ? 1 : 0;
18755
+ }
18756
+ function bindParams(row) {
18757
+ const out = {};
18758
+ for (const [key, value] of Object.entries(row)) {
18759
+ out[key] = value === void 0 ? null : value;
18760
+ }
18761
+ return out;
18762
+ }
18763
+ function countScalar(db, sql, params) {
18764
+ return getRow(db.prepare(sql), params)?.n ?? 0;
18765
+ }
18766
+ function countBy(db, sql, params) {
18767
+ const map2 = /* @__PURE__ */ new Map();
18768
+ for (const row of allRows(db.prepare(sql), params)) {
18769
+ map2.set(row.k, row.n);
18770
+ }
18771
+ return map2;
18772
+ }
18773
+ function mapRowsTolerant(rows, map2) {
18774
+ const out = [];
18775
+ for (const row of rows) {
18776
+ try {
18777
+ out.push(map2(row));
18778
+ } catch {
18779
+ }
18780
+ }
18781
+ return out;
18599
18782
  }
18600
18783
 
18601
18784
  // ../packages/persistence/src/repositories/activity.ts
@@ -18647,15 +18830,11 @@ function encodeCursor(payload) {
18647
18830
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
18648
18831
  }
18649
18832
  function decodeCursor(cursor) {
18650
- try {
18651
- const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
18652
- if (parsed !== null && typeof parsed === "object" && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18653
- return parsed;
18654
- }
18655
- return null;
18656
- } catch {
18657
- return null;
18833
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18834
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18835
+ return parsed;
18658
18836
  }
18837
+ return null;
18659
18838
  }
18660
18839
  var DB_EVENT_TYPE_TO_KIND = {
18661
18840
  session: "session",
@@ -18672,15 +18851,8 @@ var DB_EVENT_TYPE_TO_KIND = {
18672
18851
  };
18673
18852
  function safeParseStringArray(raw) {
18674
18853
  if (!raw) return [];
18675
- try {
18676
- const parsed = JSON.parse(raw);
18677
- return Array.isArray(parsed) ? parsed : [];
18678
- } catch {
18679
- return [];
18680
- }
18681
- }
18682
- function toBool(raw) {
18683
- return raw === 1 || raw === true;
18854
+ const parsed = safeJson(raw, null);
18855
+ return Array.isArray(parsed) ? parsed : [];
18684
18856
  }
18685
18857
  function toHarness(raw) {
18686
18858
  const parsed = Harness.safeParse(raw);
@@ -18733,8 +18905,8 @@ function buildAuditEvent(row) {
18733
18905
  severity: severityParsed?.success ? severityParsed.data : null,
18734
18906
  link: linkParsed?.success ? linkParsed.data : null,
18735
18907
  targetId: row.target_id,
18736
- internal: toBool(row.internal),
18737
- flagged: toBool(row.flagged)
18908
+ internal: intToBool(row.internal),
18909
+ flagged: intToBool(row.flagged)
18738
18910
  };
18739
18911
  }
18740
18912
  var TIMELINE_COLUMNS = `
@@ -18760,12 +18932,15 @@ var SqliteActivityRepository = class {
18760
18932
  stats(tz) {
18761
18933
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
18762
18934
  const { startMs, endMs } = window;
18763
- const sessionsToday = this.db.prepare(
18935
+ const sessionsToday = countScalar(
18936
+ this.db,
18764
18937
  `SELECT count(*) AS n FROM audit_events
18765
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
18766
- ).get(startMs, endMs).n;
18938
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
18939
+ [startMs, endMs]
18940
+ );
18767
18941
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
18768
- const liveNow = this.db.prepare(
18942
+ const liveNow = countScalar(
18943
+ this.db,
18769
18944
  `SELECT count(*) AS n FROM audit_events s
18770
18945
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
18771
18946
  AND max(
@@ -18774,22 +18949,29 @@ var SqliteActivityRepository = class {
18774
18949
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
18775
18950
  s.started_at
18776
18951
  )
18777
- ) >= ?`
18778
- ).get(liveThreshold).n;
18779
- const toolCallsToday = this.db.prepare(
18952
+ ) >= ?`,
18953
+ [liveThreshold]
18954
+ );
18955
+ const toolCallsToday = countScalar(
18956
+ this.db,
18780
18957
  `SELECT count(*) AS n FROM audit_events
18781
- WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`
18782
- ).get(startMs, endMs).n;
18783
- const findingsToday = this.db.prepare(
18958
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18959
+ [startMs, endMs]
18960
+ );
18961
+ const findingsToday = countScalar(
18962
+ this.db,
18784
18963
  `SELECT count(*) AS n FROM inspection_findings f
18785
18964
  JOIN audit_events e ON e.id = f.audit_event_id
18786
- WHERE e.started_at >= ? AND e.started_at < ?`
18787
- ).get(startMs, endMs).n;
18788
- const egressToday = this.db.prepare(
18965
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18966
+ [startMs, endMs]
18967
+ );
18968
+ const egressToday = countScalar(
18969
+ this.db,
18789
18970
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
18790
18971
  FROM audit_events
18791
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
18792
- ).get(startMs, endMs).n;
18972
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18973
+ [startMs, endMs]
18974
+ );
18793
18975
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18794
18976
  }
18795
18977
  listSessions(query) {
@@ -18811,7 +18993,7 @@ var SqliteActivityRepository = class {
18811
18993
  conditions.push("started_at <= ?");
18812
18994
  params.push(toMs);
18813
18995
  if (query.q) {
18814
- const pattern = `%${escapeLikePattern(query.q)}%`;
18996
+ const pattern = containsPattern(query.q);
18815
18997
  conditions.push(
18816
18998
  `(content LIKE ? ESCAPE '\\'
18817
18999
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18830,8 +19012,9 @@ var SqliteActivityRepository = class {
18830
19012
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18831
19013
  }
18832
19014
  const limit = query.limit;
18833
- const rows = this.db.prepare(
18834
- `SELECT id,
19015
+ const rows = allRows(
19016
+ this.db.prepare(
19017
+ `SELECT id,
18835
19018
  json_extract(attributes, '$.harness') AS harness,
18836
19019
  content AS title,
18837
19020
  json_extract(attributes, '$.project') AS project,
@@ -18844,7 +19027,9 @@ var SqliteActivityRepository = class {
18844
19027
  WHERE ${conditions.join(" AND ")}
18845
19028
  ORDER BY started_at DESC, id DESC
18846
19029
  LIMIT ?`
18847
- ).all(...params, limit + 1);
19030
+ ),
19031
+ [...params, limit + 1]
19032
+ );
18848
19033
  const hasMore = rows.length > limit;
18849
19034
  const page = hasMore ? rows.slice(0, limit) : rows;
18850
19035
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18861,8 +19046,9 @@ var SqliteActivityRepository = class {
18861
19046
  return Promise.resolve({ items, nextCursor });
18862
19047
  }
18863
19048
  getSession(sessionId) {
18864
- const rootRow = this.db.prepare(
18865
- `SELECT id,
19049
+ const rootRow = getRow(
19050
+ this.db.prepare(
19051
+ `SELECT id,
18866
19052
  json_extract(attributes, '$.harness') AS harness,
18867
19053
  content AS title,
18868
19054
  json_extract(attributes, '$.project') AS project,
@@ -18879,47 +19065,66 @@ var SqliteActivityRepository = class {
18879
19065
  FROM audit_events
18880
19066
  WHERE id = ? AND event_type = 'session'
18881
19067
  LIMIT 1`
18882
- ).get(sessionId);
19068
+ ),
19069
+ [sessionId]
19070
+ );
18883
19071
  if (!rootRow) return Promise.resolve(null);
18884
- const timelineRows = this.db.prepare(
18885
- `SELECT ${TIMELINE_COLUMNS}
19072
+ const timelineRows = allRows(
19073
+ this.db.prepare(
19074
+ `SELECT ${TIMELINE_COLUMNS}
18886
19075
  FROM audit_events
18887
19076
  WHERE id = ? OR root_session_id = ?
18888
19077
  ORDER BY started_at ASC, id ASC`
18889
- ).all(sessionId, sessionId);
19078
+ ),
19079
+ [sessionId, sessionId]
19080
+ );
18890
19081
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18891
- const tokenRow = this.db.prepare(
18892
- `SELECT
19082
+ const tokenRow = getRow(
19083
+ this.db.prepare(
19084
+ `SELECT
18893
19085
  coalesce(sum(input_tokens), 0) AS input,
18894
19086
  coalesce(sum(output_tokens), 0) AS output,
18895
19087
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18896
19088
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18897
19089
  FROM audit_events
18898
19090
  WHERE root_session_id = ? AND event_type = 'llm_call'`
18899
- ).get(sessionId);
18900
- const primaryModel = this.db.prepare(
18901
- `SELECT model, provider FROM audit_events
19091
+ ),
19092
+ [sessionId]
19093
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
19094
+ const primaryModel = getRow(
19095
+ this.db.prepare(
19096
+ `SELECT model, provider FROM audit_events
18902
19097
  WHERE root_session_id = ? AND event_type = 'llm_call'
18903
19098
  ORDER BY started_at ASC, id ASC
18904
19099
  LIMIT 1`
18905
- ).get(sessionId);
18906
- const toolRows = this.db.prepare(
18907
- `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
19100
+ ),
19101
+ [sessionId]
19102
+ );
19103
+ const toolRows = allRows(
19104
+ this.db.prepare(
19105
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18908
19106
  count(*) AS n
18909
19107
  FROM audit_events
18910
19108
  WHERE root_session_id = ? AND event_type = 'tool_call'
18911
19109
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
18912
- ).all(sessionId);
18913
- const modelRows = this.db.prepare(
18914
- `SELECT DISTINCT model FROM audit_events
19110
+ ),
19111
+ [sessionId]
19112
+ );
19113
+ const modelRows = allRows(
19114
+ this.db.prepare(
19115
+ `SELECT DISTINCT model FROM audit_events
18915
19116
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18916
19117
  ORDER BY model`
18917
- ).all(sessionId);
19118
+ ),
19119
+ [sessionId]
19120
+ );
18918
19121
  const derivedModels = modelRows.map((r) => r.model);
18919
- const commits = this.db.prepare(
19122
+ const commits = countScalar(
19123
+ this.db,
18920
19124
  `SELECT count(*) AS n FROM audit_events
18921
- WHERE root_session_id = ? AND event_type = 'commit'`
18922
- ).get(sessionId).n;
19125
+ WHERE root_session_id = ? AND event_type = 'commit'`,
19126
+ [sessionId]
19127
+ );
18923
19128
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18924
19129
  turns: 0,
18925
19130
  findings: 0,
@@ -18986,7 +19191,10 @@ var SqliteActivityRepository = class {
18986
19191
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18987
19192
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18988
19193
  );
18989
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
19194
+ const rows = allRows(
19195
+ stmt,
19196
+ fromMs === void 0 ? void 0 : [fromMs]
19197
+ );
18990
19198
  const seen = /* @__PURE__ */ new Set();
18991
19199
  for (const row of rows) seen.add(toHarness(row.harness));
18992
19200
  return Promise.resolve([...seen]);
@@ -19009,23 +19217,23 @@ var SqliteActivityRepository = class {
19009
19217
  conditions.push("started_at >= ?");
19010
19218
  params.push(opts.fromMs);
19011
19219
  }
19012
- const rows = this.db.prepare(
19013
- `SELECT root_session_id AS sessionId, attributes
19220
+ const rows = allRows(
19221
+ this.db.prepare(
19222
+ `SELECT root_session_id AS sessionId, attributes
19014
19223
  FROM audit_events
19015
19224
  WHERE ${conditions.join(" AND ")}`
19016
- ).all(...params);
19017
- const leaves = [];
19018
- for (const row of rows) {
19019
- if (row.sessionId === null) continue;
19020
- try {
19021
- leaves.push({
19022
- sessionId: row.sessionId,
19023
- attributes: JSON.parse(row.attributes)
19024
- });
19025
- } catch {
19026
- }
19027
- }
19028
- return leaves;
19225
+ ),
19226
+ params
19227
+ );
19228
+ return mapRowsTolerant(
19229
+ rows.filter(
19230
+ (row) => row.sessionId !== null
19231
+ ),
19232
+ (row) => ({
19233
+ sessionId: row.sessionId,
19234
+ attributes: JSON.parse(row.attributes)
19235
+ })
19236
+ );
19029
19237
  }
19030
19238
  /**
19031
19239
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -19039,57 +19247,72 @@ var SqliteActivityRepository = class {
19039
19247
  );
19040
19248
  if (sessionIds.length === 0) return result;
19041
19249
  const inClause = placeholders(sessionIds.length);
19042
- const lastActivityRows = this.db.prepare(
19043
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
19250
+ const lastActivityRows = allRows(
19251
+ this.db.prepare(
19252
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
19044
19253
  WHERE root_session_id IN (${inClause})
19045
19254
  GROUP BY root_session_id`
19046
- ).all(...sessionIds);
19255
+ ),
19256
+ sessionIds
19257
+ );
19047
19258
  for (const row of lastActivityRows) {
19048
19259
  if (row.id === null) continue;
19049
19260
  const entry = result.get(row.id);
19050
19261
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
19051
19262
  }
19052
- const turnsRows = this.db.prepare(
19053
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
19263
+ const turnsRows = allRows(
19264
+ this.db.prepare(
19265
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
19054
19266
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
19055
19267
  GROUP BY root_session_id`
19056
- ).all(...sessionIds);
19268
+ ),
19269
+ sessionIds
19270
+ );
19057
19271
  for (const row of turnsRows) {
19058
19272
  if (row.id === null) continue;
19059
19273
  const entry = result.get(row.id);
19060
19274
  if (entry) entry.turns = row.n;
19061
19275
  }
19062
- const runKeyRows = this.db.prepare(
19063
- `SELECT root_session_id AS id,
19276
+ const runKeyRows = allRows(
19277
+ this.db.prepare(
19278
+ `SELECT root_session_id AS id,
19064
19279
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
19065
19280
  FROM audit_events
19066
19281
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
19067
19282
  AND json_extract(attributes, '$.run_key') IS NOT NULL
19068
19283
  GROUP BY root_session_id`
19069
- ).all(...sessionIds);
19284
+ ),
19285
+ sessionIds
19286
+ );
19070
19287
  for (const row of runKeyRows) {
19071
19288
  if (row.id === null) continue;
19072
19289
  const entry = result.get(row.id);
19073
19290
  if (entry) entry.turns = Math.max(entry.turns, row.n);
19074
19291
  }
19075
- const findingsRows = this.db.prepare(
19076
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
19292
+ const findingsRows = allRows(
19293
+ this.db.prepare(
19294
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
19077
19295
  JOIN audit_events e ON e.id = f.audit_event_id
19078
19296
  WHERE e.root_session_id IN (${inClause})
19079
19297
  GROUP BY e.root_session_id`
19080
- ).all(...sessionIds);
19298
+ ),
19299
+ sessionIds
19300
+ );
19081
19301
  for (const row of findingsRows) {
19082
19302
  if (row.id === null) continue;
19083
19303
  const entry = result.get(row.id);
19084
19304
  if (entry) entry.findings = row.n;
19085
19305
  }
19086
- const sharesRows = this.db.prepare(
19087
- `SELECT root_session_id AS id,
19306
+ const sharesRows = allRows(
19307
+ this.db.prepare(
19308
+ `SELECT root_session_id AS id,
19088
19309
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
19089
19310
  FROM audit_events
19090
19311
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
19091
19312
  GROUP BY root_session_id`
19092
- ).all(...sessionIds);
19313
+ ),
19314
+ sessionIds
19315
+ );
19093
19316
  for (const row of sharesRows) {
19094
19317
  if (row.id === null) continue;
19095
19318
  const entry = result.get(row.id);
@@ -19139,33 +19362,28 @@ var SqliteAuditEventsRepository = class {
19139
19362
  // the caller fails open and drops the whole pass — recovered idempotently on the
19140
19363
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
19141
19364
  runInTransaction(fn) {
19142
- this.db.exec("BEGIN");
19143
- try {
19144
- fn();
19145
- this.db.exec("COMMIT");
19146
- } catch (err) {
19147
- this.db.exec("ROLLBACK");
19148
- throw err;
19149
- }
19365
+ withTransaction(this.db, fn);
19150
19366
  }
19151
19367
  insertAuditEvent(input) {
19152
19368
  const row = toAuditEventRow(input);
19153
- this.insertStmt.run({
19154
- id: row.id,
19155
- parentId: row.parentId ?? null,
19156
- rootSessionId: row.rootSessionId ?? null,
19157
- eventType: row.eventType,
19158
- hostId: row.hostId ?? null,
19159
- harnessId: row.harnessId ?? null,
19160
- sourceProjectId: row.sourceProjectId ?? null,
19161
- startedAt: row.startedAt,
19162
- endedAt: row.endedAt ?? null,
19163
- severity: row.severity ?? null,
19164
- priority: row.priority ?? null,
19165
- content: row.content ?? null,
19166
- contentHash: row.contentHash ?? null,
19167
- attributes: row.attributes ?? null
19168
- });
19369
+ this.insertStmt.run(
19370
+ bindParams({
19371
+ id: row.id,
19372
+ parentId: row.parentId,
19373
+ rootSessionId: row.rootSessionId,
19374
+ eventType: row.eventType,
19375
+ hostId: row.hostId,
19376
+ harnessId: row.harnessId,
19377
+ sourceProjectId: row.sourceProjectId,
19378
+ startedAt: row.startedAt,
19379
+ endedAt: row.endedAt,
19380
+ severity: row.severity,
19381
+ priority: row.priority,
19382
+ content: row.content,
19383
+ contentHash: row.contentHash,
19384
+ attributes: row.attributes
19385
+ })
19386
+ );
19169
19387
  }
19170
19388
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
19171
19389
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -19179,22 +19397,24 @@ var SqliteAuditEventsRepository = class {
19179
19397
  const startedAt = isoToEpochMillis(input.startedAt);
19180
19398
  if (!Number.isFinite(startedAt)) return;
19181
19399
  const id = llmCallId(input.sessionId, input.messageId);
19182
- this.upsertLlmCallStmt.run({
19183
- id,
19184
- parentId: input.parentId,
19185
- rootSessionId: input.rootSessionId,
19186
- eventType: "llm_call",
19187
- hostId: null,
19188
- harnessId: null,
19189
- sourceProjectId: null,
19190
- startedAt,
19191
- endedAt: null,
19192
- severity: null,
19193
- priority: null,
19194
- content: null,
19195
- contentHash: null,
19196
- attributes: JSON.stringify(input.attributes)
19197
- });
19400
+ this.upsertLlmCallStmt.run(
19401
+ bindParams({
19402
+ id,
19403
+ parentId: input.parentId,
19404
+ rootSessionId: input.rootSessionId,
19405
+ eventType: "llm_call",
19406
+ hostId: null,
19407
+ harnessId: null,
19408
+ sourceProjectId: null,
19409
+ startedAt,
19410
+ endedAt: null,
19411
+ severity: null,
19412
+ priority: null,
19413
+ content: null,
19414
+ contentHash: null,
19415
+ attributes: JSON.stringify(input.attributes)
19416
+ })
19417
+ );
19198
19418
  }
19199
19419
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
19200
19420
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -19218,25 +19438,29 @@ var SqliteAuditEventsRepository = class {
19218
19438
  const startedAt = isoToEpochMillis(input.startedAt);
19219
19439
  if (!Number.isFinite(startedAt)) return;
19220
19440
  const id = toolCallId(input.sessionId, input.toolUseId);
19221
- this.insertStmt.run({
19222
- id,
19223
- parentId: input.parentId,
19224
- rootSessionId: input.rootSessionId,
19225
- eventType: "tool_call",
19226
- hostId: null,
19227
- harnessId: null,
19228
- sourceProjectId: null,
19229
- startedAt,
19230
- endedAt: null,
19231
- severity: null,
19232
- priority: null,
19233
- content: null,
19234
- contentHash: null,
19235
- attributes: JSON.stringify(input.attributes)
19236
- });
19441
+ this.insertStmt.run(
19442
+ bindParams({
19443
+ id,
19444
+ parentId: input.parentId,
19445
+ rootSessionId: input.rootSessionId,
19446
+ eventType: "tool_call",
19447
+ hostId: null,
19448
+ harnessId: null,
19449
+ sourceProjectId: null,
19450
+ startedAt,
19451
+ endedAt: null,
19452
+ severity: null,
19453
+ priority: null,
19454
+ content: null,
19455
+ contentHash: null,
19456
+ attributes: JSON.stringify(input.attributes)
19457
+ })
19458
+ );
19237
19459
  }
19238
19460
  findById(id) {
19239
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
19461
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
19462
+ id
19463
+ });
19240
19464
  }
19241
19465
  // Read the `provider` snapshotted onto a session root's attributes.
19242
19466
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -19246,14 +19470,8 @@ var SqliteAuditEventsRepository = class {
19246
19470
  sessionProvider(sessionId) {
19247
19471
  const row = this.findById(sessionId);
19248
19472
  if (!row?.attributes) return void 0;
19249
- try {
19250
- const parsed = JSON.parse(row.attributes);
19251
- if (typeof parsed === "object" && parsed !== null) {
19252
- const provider = parsed.provider;
19253
- if (typeof provider === "string") return provider;
19254
- }
19255
- } catch {
19256
- }
19473
+ const provider = parseJsonObject(row.attributes)?.provider;
19474
+ if (typeof provider === "string") return provider;
19257
19475
  return void 0;
19258
19476
  }
19259
19477
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -19262,11 +19480,13 @@ var SqliteAuditEventsRepository = class {
19262
19480
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
19263
19481
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
19264
19482
  llmCallLeaves() {
19265
- return this.db.prepare(
19266
- `SELECT root_session_id AS sessionId, attributes
19483
+ return allRows(
19484
+ this.db.prepare(
19485
+ `SELECT root_session_id AS sessionId, attributes
19267
19486
  FROM audit_events
19268
19487
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
19269
- ).all();
19488
+ )
19489
+ );
19270
19490
  }
19271
19491
  };
19272
19492
 
@@ -19285,16 +19505,29 @@ var SqliteClassifiedDataRepository = class {
19285
19505
  upsert(input) {
19286
19506
  const id = classifiedDataId(input.class);
19287
19507
  const row = toClassifiedDataRow(input, id);
19288
- this.insertStmt.run({
19289
- id: row.id,
19290
- class: row.class,
19291
- label: row.label ?? null,
19292
- attributes: row.attributes ?? null
19293
- });
19508
+ this.insertStmt.run(
19509
+ bindParams({
19510
+ id: row.id,
19511
+ class: row.class,
19512
+ label: row.label,
19513
+ attributes: row.attributes
19514
+ })
19515
+ );
19294
19516
  return id;
19295
19517
  }
19296
19518
  };
19297
19519
 
19520
+ // ../packages/persistence/src/repositories/config-scan.ts
19521
+ function latestConfigScan(db) {
19522
+ return getRow(
19523
+ db.prepare(
19524
+ `SELECT id, started_at, attributes FROM audit_events
19525
+ WHERE event_type = 'config_scan'
19526
+ ORDER BY started_at DESC, id DESC LIMIT 1`
19527
+ )
19528
+ );
19529
+ }
19530
+
19298
19531
  // ../packages/persistence/src/repositories/config-inventory.ts
19299
19532
  var SqliteConfigInventoryRepository = class {
19300
19533
  constructor(db) {
@@ -19302,7 +19535,7 @@ var SqliteConfigInventoryRepository = class {
19302
19535
  }
19303
19536
  db;
19304
19537
  report() {
19305
- const scan2 = this.latestScan();
19538
+ const scan2 = latestConfigScan(this.db);
19306
19539
  if (!scan2) {
19307
19540
  return {
19308
19541
  scannedAt: null,
@@ -19313,17 +19546,23 @@ var SqliteConfigInventoryRepository = class {
19313
19546
  topics: []
19314
19547
  };
19315
19548
  }
19316
- const rows = this.db.prepare(
19317
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
19549
+ const rows = allRows(
19550
+ this.db.prepare(
19551
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
19318
19552
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
19319
19553
  ORDER BY object_type, title`
19320
- ).all({ startedAt: scan2.started_at });
19321
- const findings = this.db.prepare(
19322
- `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
19554
+ ),
19555
+ { startedAt: scan2.started_at }
19556
+ );
19557
+ const findings = allRows(
19558
+ this.db.prepare(
19559
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
19323
19560
  FROM inspection_findings f
19324
19561
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19325
19562
  WHERE f.audit_event_id = :scanId`
19326
- ).all({ scanId: scan2.id });
19563
+ ),
19564
+ { scanId: scan2.id }
19565
+ );
19327
19566
  const skills = [];
19328
19567
  const hooks = [];
19329
19568
  const mcpServers = [];
@@ -19352,7 +19591,9 @@ var SqliteConfigInventoryRepository = class {
19352
19591
  // schema note); an override whose asset is gone simply never matches. A row
19353
19592
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
19354
19593
  trustOverrides() {
19355
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
19594
+ const rows = allRows(
19595
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
19596
+ );
19356
19597
  const map2 = /* @__PURE__ */ new Map();
19357
19598
  for (const row of rows) {
19358
19599
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -19361,13 +19602,6 @@ var SqliteConfigInventoryRepository = class {
19361
19602
  }
19362
19603
  return map2;
19363
19604
  }
19364
- latestScan() {
19365
- return this.db.prepare(
19366
- `SELECT id, started_at, attributes FROM audit_events
19367
- WHERE event_type = 'config_scan'
19368
- ORDER BY started_at DESC, id DESC LIMIT 1`
19369
- ).get();
19370
- }
19371
19605
  };
19372
19606
  function toSkillItem(row, bag) {
19373
19607
  const item = {
@@ -19478,22 +19712,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
19478
19712
  return topics;
19479
19713
  }
19480
19714
  function countScanErrors(attributes) {
19481
- if (!attributes) return 0;
19482
- try {
19483
- const parsed = JSON.parse(attributes);
19484
- const errors = parsed?.errors;
19485
- return typeof errors === "number" ? errors : 0;
19486
- } catch {
19487
- return 0;
19488
- }
19715
+ const errors = parseJsonObject(attributes)?.errors;
19716
+ return typeof errors === "number" ? errors : 0;
19489
19717
  }
19490
19718
  function parseBag(raw) {
19491
- try {
19492
- const parsed = JSON.parse(raw);
19493
- if (typeof parsed === "object" && parsed !== null) return parsed;
19494
- } catch {
19495
- }
19496
- return void 0;
19719
+ return parseJsonObject(raw);
19497
19720
  }
19498
19721
  function str(value) {
19499
19722
  return typeof value === "string" ? value : void 0;
@@ -19502,12 +19725,7 @@ function str(value) {
19502
19725
  // ../packages/persistence/src/repositories/detections.ts
19503
19726
  var DAY_MS2 = 864e5;
19504
19727
  function parseRules(rulesJson) {
19505
- let raw;
19506
- try {
19507
- raw = JSON.parse(rulesJson);
19508
- } catch {
19509
- return [];
19510
- }
19728
+ const raw = safeJson(rulesJson, []);
19511
19729
  if (!Array.isArray(raw)) return [];
19512
19730
  const rules = [];
19513
19731
  for (const entry of raw) {
@@ -19526,11 +19744,13 @@ var SqliteDetectionsRepository = class {
19526
19744
  db;
19527
19745
  now;
19528
19746
  listDetections(query) {
19529
- const rows = this.db.prepare(
19530
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19747
+ const rows = allRows(
19748
+ this.db.prepare(
19749
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19531
19750
  rules_json AS rulesJson
19532
19751
  FROM installed_packs`
19533
- ).all();
19752
+ )
19753
+ );
19534
19754
  const available = this.availableByPack();
19535
19755
  const summaries = rows.map((r) => {
19536
19756
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -19539,7 +19759,7 @@ var SqliteDetectionsRepository = class {
19539
19759
  packId: r.packId,
19540
19760
  version: r.version,
19541
19761
  name: r.name,
19542
- enabled: r.enabled === 1,
19762
+ enabled: intToBool(r.enabled),
19543
19763
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
19544
19764
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
19545
19765
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -19556,19 +19776,23 @@ var SqliteDetectionsRepository = class {
19556
19776
  // available_packs keyed by the "namespace/packId" slug (one read per list /
19557
19777
  // detail call; the table is a handful of rows).
19558
19778
  availableByPack() {
19559
- const rows = this.db.prepare(
19560
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
19779
+ const rows = allRows(
19780
+ this.db.prepare(
19781
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
19561
19782
  FROM available_packs`
19562
- ).all();
19783
+ )
19784
+ );
19563
19785
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
19564
19786
  }
19565
19787
  getDetectionStats() {
19566
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
19788
+ const rows = allRows(
19789
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
19790
+ );
19567
19791
  let rules = 0;
19568
19792
  let active = 0;
19569
19793
  const ruleIds = /* @__PURE__ */ new Set();
19570
19794
  for (const r of rows) {
19571
- if (r.enabled === 1) active += 1;
19795
+ if (intToBool(r.enabled)) active += 1;
19572
19796
  const parsed = parseRules(r.rulesJson);
19573
19797
  rules += parsed.length;
19574
19798
  for (const rule of parsed) {
@@ -19586,12 +19810,15 @@ var SqliteDetectionsRepository = class {
19586
19810
  const parts = splitDetectionId(id);
19587
19811
  if (!parts) return Promise.resolve(null);
19588
19812
  const { namespace, packId } = parts;
19589
- const row = this.db.prepare(
19590
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19813
+ const row = getRow(
19814
+ this.db.prepare(
19815
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19591
19816
  rules_json AS rulesJson, updated_at AS updatedAt
19592
19817
  FROM installed_packs
19593
19818
  WHERE namespace = ? AND pack_id = ?`
19594
- ).get(namespace, packId);
19819
+ ),
19820
+ [namespace, packId]
19821
+ );
19595
19822
  if (!row) return Promise.resolve(null);
19596
19823
  const rules = parseRules(row.rulesJson);
19597
19824
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -19609,7 +19836,7 @@ var SqliteDetectionsRepository = class {
19609
19836
  packId: row.packId,
19610
19837
  version: row.version,
19611
19838
  name: row.name,
19612
- enabled: row.enabled === 1,
19839
+ enabled: intToBool(row.enabled),
19613
19840
  rules,
19614
19841
  updatedAt: new Date(row.updatedAt),
19615
19842
  policyId: row.policyId
@@ -19624,13 +19851,14 @@ var SqliteDetectionsRepository = class {
19624
19851
  countFindingsLast30d(ruleIds) {
19625
19852
  if (ruleIds.length === 0) return 0;
19626
19853
  const since = this.now() - 30 * DAY_MS2;
19627
- const placeholders2 = ruleIds.map(() => "?").join(", ");
19628
- const row = this.db.prepare(
19629
- `SELECT count(*) AS c
19854
+ const inClause = placeholders(ruleIds.length);
19855
+ return countScalar(
19856
+ this.db,
19857
+ `SELECT count(*) AS n
19630
19858
  FROM findings f JOIN events e ON e.id = f.event_id
19631
- WHERE e.occurred_at >= ? AND f.rule_id IN (${placeholders2})`
19632
- ).get(since, ...ruleIds);
19633
- return row.c;
19859
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19860
+ [since, ...ruleIds]
19861
+ );
19634
19862
  }
19635
19863
  };
19636
19864
 
@@ -19647,16 +19875,17 @@ var SqliteEventsRepository = class {
19647
19875
  insertStmt;
19648
19876
  insertEvent(event) {
19649
19877
  const row = toEventRow(event);
19650
- this.insertStmt.run({
19651
- id: row.id,
19652
- sourceTool: row.sourceTool,
19653
- kind: row.kind,
19654
- occurredAt: row.occurredAt,
19655
- contentHash: row.contentHash,
19656
- content: row.content,
19657
- // exactOptionalPropertyTypes: the column is nullable, never undefined.
19658
- metadata: row.metadata ?? null
19659
- });
19878
+ this.insertStmt.run(
19879
+ bindParams({
19880
+ id: row.id,
19881
+ sourceTool: row.sourceTool,
19882
+ kind: row.kind,
19883
+ occurredAt: row.occurredAt,
19884
+ contentHash: row.contentHash,
19885
+ content: row.content,
19886
+ metadata: row.metadata
19887
+ })
19888
+ );
19660
19889
  }
19661
19890
  // Every recorded event's content hash — the historical backfill loads this once
19662
19891
  // to skip transcript messages it has already stored, so re-running the scan
@@ -19664,13 +19893,23 @@ var SqliteEventsRepository = class {
19664
19893
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
19665
19894
  // async EventsReadPort contract.
19666
19895
  contentHashes() {
19667
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19896
+ const rows = allRows(
19897
+ this.db.prepare("SELECT content_hash FROM events")
19898
+ );
19668
19899
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
19669
19900
  }
19670
19901
  };
19671
19902
 
19672
19903
  // ../packages/persistence/src/repositories/exceptions.ts
19673
19904
  import { randomUUID as randomUUID2 } from "crypto";
19905
+
19906
+ // ../packages/persistence/src/internal/sqlite-errors.ts
19907
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19908
+ function isUniqueConstraintError(err) {
19909
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19910
+ }
19911
+
19912
+ // ../packages/persistence/src/repositories/exceptions.ts
19674
19913
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
19675
19914
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
19676
19915
  var DuplicateActiveExceptionError = class extends Error {
@@ -19691,10 +19930,6 @@ var AmbiguousExceptionIdError = class extends Error {
19691
19930
  this.name = "AmbiguousExceptionIdError";
19692
19931
  }
19693
19932
  };
19694
- var SQLITE_CONSTRAINT_UNIQUE = 2067;
19695
- function isUniqueConstraintError(err) {
19696
- return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19697
- }
19698
19933
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19699
19934
  AND (expires_at IS NULL OR expires_at > :now)
19700
19935
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -19750,10 +19985,11 @@ var SqliteExceptionsRepository = class {
19750
19985
  this.insertExceptionRow(id, input, now);
19751
19986
  } catch (err) {
19752
19987
  if (!isUniqueConstraintError(err)) throw err;
19753
- this.db.exec("BEGIN IMMEDIATE");
19754
- try {
19755
- const superseded = this.db.prepare(
19756
- `UPDATE exceptions
19988
+ withTransaction(
19989
+ this.db,
19990
+ () => {
19991
+ const superseded = this.db.prepare(
19992
+ `UPDATE exceptions
19757
19993
  SET revoked_at = :now, revoked_by = :revokedBy,
19758
19994
  revoke_reason = 'superseded by a new grant for the same value',
19759
19995
  updated_at = :now
@@ -19761,24 +19997,27 @@ var SqliteExceptionsRepository = class {
19761
19997
  AND key_version = :keyVersion AND revoked_at IS NULL
19762
19998
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
19763
19999
  OR (max_uses IS NOT NULL AND use_count >= max_uses))`
19764
- ).run({
19765
- now,
19766
- revokedBy: input.createdBy,
19767
- ruleId: input.ruleId,
19768
- valueFingerprint: input.valueFingerprint,
19769
- keyVersion: input.keyVersion
19770
- });
19771
- if (Number(superseded.changes) !== 1) {
19772
- throw new DuplicateActiveExceptionError(input.ruleId);
19773
- }
19774
- this.insertExceptionRow(id, input, now);
19775
- this.db.exec("COMMIT");
19776
- } catch (retryErr) {
19777
- this.db.exec("ROLLBACK");
19778
- throw retryErr;
19779
- }
20000
+ ).run({
20001
+ now,
20002
+ revokedBy: input.createdBy,
20003
+ ruleId: input.ruleId,
20004
+ valueFingerprint: input.valueFingerprint,
20005
+ keyVersion: input.keyVersion
20006
+ });
20007
+ if (Number(superseded.changes) !== 1) {
20008
+ throw new DuplicateActiveExceptionError(input.ruleId);
20009
+ }
20010
+ this.insertExceptionRow(id, input, now);
20011
+ },
20012
+ "IMMEDIATE"
20013
+ );
20014
+ }
20015
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
20016
+ id
20017
+ });
20018
+ if (row === void 0) {
20019
+ throw new Error("exception row not found immediately after insert");
19780
20020
  }
19781
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
19782
20021
  return parseExceptionRow(row);
19783
20022
  }
19784
20023
  insertExceptionRow(id, input, now) {
@@ -19816,14 +20055,11 @@ var SqliteExceptionsRepository = class {
19816
20055
  */
19817
20056
  list(opts) {
19818
20057
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19819
- const rows = this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`).all(opts?.includeTerminal ? {} : { now: Date.now() });
19820
- const exceptions = [];
19821
- for (const row of rows) {
19822
- try {
19823
- exceptions.push(parseExceptionRow(row));
19824
- } catch {
19825
- }
19826
- }
20058
+ const rows = allRows(
20059
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
20060
+ opts?.includeTerminal ? {} : { now: Date.now() }
20061
+ );
20062
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19827
20063
  return Promise.resolve(exceptions);
19828
20064
  }
19829
20065
  /**
@@ -19833,7 +20069,12 @@ var SqliteExceptionsRepository = class {
19833
20069
  */
19834
20070
  getByIdPrefix(prefix) {
19835
20071
  if (prefix.length === 0) return Promise.resolve(void 0);
19836
- const rows = this.db.prepare(String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`).all({ pattern: `${escapeLikePattern(prefix)}%` });
20072
+ const rows = allRows(
20073
+ this.db.prepare(
20074
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
20075
+ ),
20076
+ { pattern: `${escapeLikePattern(prefix)}%` }
20077
+ );
19837
20078
  if (rows.length > 1) {
19838
20079
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19839
20080
  }
@@ -19875,30 +20116,27 @@ var SqliteExceptionsRepository = class {
19875
20116
  * a different (rotated-away) key never match, so they are excluded at read.
19876
20117
  */
19877
20118
  activeBundleEntries(keyVersion, now = Date.now()) {
19878
- const rows = this.db.prepare(
19879
- `SELECT * FROM exceptions
20119
+ const rows = allRows(
20120
+ this.db.prepare(
20121
+ `SELECT * FROM exceptions
19880
20122
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19881
20123
  ORDER BY created_at DESC, rowid DESC`
19882
- ).all({ keyVersion, now });
19883
- const entries = [];
19884
- for (const row of rows) {
19885
- try {
19886
- const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19887
- entries.push(
19888
- ExceptionBundleEntry.parse({
19889
- id: row.id,
19890
- ruleId: row.rule_id,
19891
- valueFingerprint: row.value_fingerprint,
19892
- keyVersion: row.key_version,
19893
- expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19894
- maxUses: row.max_uses,
19895
- useCount: row.use_count,
19896
- conditions
19897
- })
19898
- );
19899
- } catch {
19900
- }
19901
- }
20124
+ ),
20125
+ { keyVersion, now }
20126
+ );
20127
+ const entries = mapRowsTolerant(rows, (row) => {
20128
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
20129
+ return ExceptionBundleEntry.parse({
20130
+ id: row.id,
20131
+ ruleId: row.rule_id,
20132
+ valueFingerprint: row.value_fingerprint,
20133
+ keyVersion: row.key_version,
20134
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
20135
+ maxUses: row.max_uses,
20136
+ useCount: row.use_count,
20137
+ conditions
20138
+ });
20139
+ });
19902
20140
  return Promise.resolve(entries);
19903
20141
  }
19904
20142
  /**
@@ -19925,11 +20163,14 @@ var SqliteExceptionsRepository = class {
19925
20163
  }
19926
20164
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19927
20165
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19928
- const rows = this.db.prepare(
19929
- `SELECT * FROM blocked_detections
20166
+ const rows = allRows(
20167
+ this.db.prepare(
20168
+ `SELECT * FROM blocked_detections
19930
20169
  WHERE blocked_at > :cutoff
19931
20170
  ORDER BY blocked_at DESC, rowid DESC`
19932
- ).all({ cutoff: Date.now() - windowMs });
20171
+ ),
20172
+ { cutoff: Date.now() - windowMs }
20173
+ );
19933
20174
  return Promise.resolve(
19934
20175
  rows.map((row) => ({
19935
20176
  reference: row.reference,
@@ -20009,7 +20250,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
20009
20250
  )`;
20010
20251
 
20011
20252
  // ../packages/persistence/src/repositories/findings.ts
20012
- var GROUP_ROW_CAP = 2e3;
20253
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
20254
+ var CONCAT_SEP = ",";
20255
+ var TUPLE_SEP = "|";
20256
+ function splitConcat(value) {
20257
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
20258
+ }
20013
20259
  function deriveInstanceStatus(row) {
20014
20260
  return deriveFindingStatus({
20015
20261
  kind: row.kind,
@@ -20077,13 +20323,16 @@ var SqliteFindingsRepository = class {
20077
20323
  }
20078
20324
  recentFindings(opts) {
20079
20325
  const limit = opts?.limit ?? 50;
20080
- const rows = this.db.prepare(
20081
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
20326
+ const rows = allRows(
20327
+ this.db.prepare(
20328
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
20082
20329
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
20083
20330
  FROM findings f JOIN events e ON e.id = f.event_id
20084
20331
  ORDER BY e.occurred_at DESC, f.rowid DESC
20085
20332
  LIMIT :limit`
20086
- ).all({ limit });
20333
+ ),
20334
+ { limit }
20335
+ );
20087
20336
  return Promise.resolve(
20088
20337
  rows.map((r) => ({
20089
20338
  id: r.id,
@@ -20106,20 +20355,47 @@ var SqliteFindingsRepository = class {
20106
20355
  * applies the requested filters, and sorts by severity then recency. Filtering
20107
20356
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
20108
20357
  * reflect the full filtered set; `items` is the requested
20109
- * page (default 100); no cursor (nextCursor is always null).
20358
+ * page (default 50); no cursor (nextCursor is always null).
20359
+ *
20360
+ * Two reads, neither of which materializes a row per finding:
20361
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
20362
+ * the group and the filters need (count, providers, actions, statuses,
20363
+ * latest, search text);
20364
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
20365
+ * populate `instances` for the table's expanded rows.
20366
+ * The aggregates carry raw DB values and are translated by the same
20367
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
20368
+ * rule is ever restated in SQL.
20110
20369
  */
20111
20370
  listGroupedFindings(query) {
20112
- const rows = this.db.prepare(
20113
- `SELECT f.id, f.rule_id, f.category, f.severity, f.masked_match,
20114
- f.action_taken, f.confidence, e.occurred_at, e.source_tool,
20115
- json_extract(e.metadata, '$.repo') AS repo,
20116
- json_extract(e.metadata, '$.filePath') AS file,
20117
- e.kind AS kind, f.finding_key AS finding_key,
20118
- ${latestResolutionStatusSql("f")} AS latest_status
20119
- FROM findings f JOIN events e ON e.id = f.event_id
20120
- ORDER BY e.occurred_at DESC, f.id DESC
20121
- LIMIT :cap`
20122
- ).all({ cap: GROUP_ROW_CAP });
20371
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
20372
+ const rows = allRows(
20373
+ this.db.prepare(
20374
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
20375
+ occurred_at, source_tool, repo, file, kind, finding_key, latest_status
20376
+ FROM (
20377
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
20378
+ f.severity AS severity, f.masked_match AS masked_match,
20379
+ f.action_taken AS action_taken, f.confidence AS confidence,
20380
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
20381
+ json_extract(e.metadata, '$.repo') AS repo,
20382
+ json_extract(e.metadata, '$.filePath') AS file,
20383
+ e.kind AS kind, f.finding_key AS finding_key,
20384
+ latest.status AS latest_status,
20385
+ ROW_NUMBER() OVER (
20386
+ PARTITION BY f.rule_id
20387
+ ORDER BY e.occurred_at DESC, f.id DESC
20388
+ ) AS rn
20389
+ FROM findings f
20390
+ JOIN events e ON e.id = f.event_id
20391
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20392
+ ON latest.finding_key = f.finding_key
20393
+ )
20394
+ WHERE rn <= :cap
20395
+ ORDER BY occurred_at DESC, id DESC`
20396
+ ),
20397
+ { cap: PREVIEW_INSTANCES_PER_GROUP }
20398
+ );
20123
20399
  const groupable = rows.map((r) => ({
20124
20400
  id: r.id,
20125
20401
  ruleId: r.rule_id,
@@ -20134,7 +20410,7 @@ var SqliteFindingsRepository = class {
20134
20410
  file: r.file ?? "",
20135
20411
  status: deriveInstanceStatus(r)
20136
20412
  }));
20137
- const allGroups = buildFindingGroups(groupable);
20413
+ const allGroups = buildFindingGroups(groupable, { aggregates });
20138
20414
  const filterOpts = {
20139
20415
  severity: query.severity,
20140
20416
  providers: query.provider,
@@ -20152,42 +20428,122 @@ var SqliteFindingsRepository = class {
20152
20428
  const items = sorted.slice(0, limit);
20153
20429
  return Promise.resolve({ totals, facets, items, nextCursor: null });
20154
20430
  }
20431
+ /**
20432
+ * One row per rule_id, folding EVERY instance of the group into the values
20433
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
20434
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
20435
+ *
20436
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
20437
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
20438
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
20439
+ * status keeps the classifier itself in @akasecurity/schema, where
20440
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
20441
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
20442
+ * a group's row stays small however many findings it holds.
20443
+ *
20444
+ * `withSearchText` is the exception, and the one column here that does NOT
20445
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
20446
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
20447
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
20448
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
20449
+ * match an instance outside the preview, which searching the preview alone
20450
+ * would silently lose, so it is fetched only when the request actually
20451
+ * carries a `q`.
20452
+ */
20453
+ groupAggregates(withSearchText) {
20454
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
20455
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
20456
+ const rows = this.db.prepare(
20457
+ `SELECT f.rule_id AS rule_id,
20458
+ count(*) AS instance_count,
20459
+ max(e.occurred_at) AS latest_at,
20460
+ group_concat(DISTINCT e.source_tool) AS source_tools,
20461
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
20462
+ group_concat(DISTINCT (
20463
+ e.kind || '${TUPLE_SEP}' ||
20464
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
20465
+ coalesce(latest.status, '')
20466
+ )) AS status_inputs
20467
+ ${searchTextColumns}
20468
+ FROM findings f
20469
+ JOIN events e ON e.id = f.event_id
20470
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20471
+ ON latest.finding_key = f.finding_key
20472
+ GROUP BY f.rule_id`
20473
+ ).all();
20474
+ return new Map(
20475
+ rows.map((r) => [
20476
+ r.rule_id,
20477
+ {
20478
+ instanceCount: r.instance_count,
20479
+ sourceTools: splitConcat(r.source_tools),
20480
+ actionsTaken: splitConcat(r.actions_taken),
20481
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
20482
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
20483
+ return {
20484
+ // deriveFindingStatus only distinguishes null from non-null here,
20485
+ // so the marker stands in for the key itself (never rendered).
20486
+ kind,
20487
+ findingKey: keyMarker === "" ? null : keyMarker,
20488
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
20489
+ };
20490
+ }),
20491
+ latestDetectedAt: epochMillisToIso(r.latest_at),
20492
+ // Free text only — joined and substring-matched, so group_concat's
20493
+ // commas need no unpicking (a repo/path containing one still matches).
20494
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
20495
+ // tell "no q this request" from "a group with no repo/file at all"
20496
+ // and skip priming a haystack nothing will read.
20497
+ ...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
20498
+ }
20499
+ ])
20500
+ );
20501
+ }
20155
20502
  healthSummary() {
20156
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
20503
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
20157
20504
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
20158
- const grouped = this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken").all();
20505
+ const grouped = allRows(
20506
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
20507
+ );
20159
20508
  for (const row of grouped) {
20160
20509
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
20161
20510
  }
20162
20511
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
20163
- const sevRows = this.db.prepare(
20164
- `SELECT f.severity AS severity, count(*) AS c
20512
+ const sevRows = allRows(
20513
+ this.db.prepare(
20514
+ `SELECT f.severity AS severity, count(*) AS c
20165
20515
  FROM findings f
20166
20516
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20167
20517
  ON latest.finding_key = f.finding_key
20168
20518
  WHERE latest.status IS NULL OR latest.status != 'resolved'
20169
20519
  GROUP BY f.severity`
20170
- ).all();
20520
+ )
20521
+ );
20171
20522
  for (const row of sevRows) {
20172
20523
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
20173
20524
  }
20174
20525
  const categories = ENFORCEABLE_CATEGORIES;
20175
- const enabledRows = this.db.prepare(
20176
- `SELECT DISTINCT json_extract(target, '$.category') AS category
20526
+ const enabledRows = allRows(
20527
+ this.db.prepare(
20528
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
20177
20529
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
20178
- ).all();
20530
+ )
20531
+ );
20179
20532
  const enabled = new Set(enabledRows.map((r) => r.category));
20180
20533
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
20181
20534
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
20182
20535
  }
20183
20536
  activityByDay(days = 7) {
20184
20537
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
20185
- const rows = this.db.prepare(
20186
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
20538
+ const rows = allRows(
20539
+ this.db.prepare(
20540
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
20187
20541
  FROM findings f JOIN events e ON e.id = f.event_id
20188
20542
  WHERE e.occurred_at >= :since
20189
20543
  GROUP BY day, f.action_taken`
20190
- ).all({ since });
20544
+ ),
20545
+ { since }
20546
+ );
20191
20547
  const buckets = /* @__PURE__ */ new Map();
20192
20548
  for (let i = 0; i < days; i++) {
20193
20549
  const day = isoDay(since + i * DAY_MS3);
@@ -20259,17 +20615,19 @@ var SqliteInspectionFindingsRepository = class {
20259
20615
  insertStmt;
20260
20616
  insertFinding(input) {
20261
20617
  const row = toInspectionFindingRow(input);
20262
- this.insertStmt.run({
20263
- id: row.id,
20264
- auditEventId: row.auditEventId,
20265
- inspectionDefinitionId: row.inspectionDefinitionId,
20266
- classifiedDataId: row.classifiedDataId ?? null,
20267
- spanStart: row.spanStart,
20268
- spanEnd: row.spanEnd,
20269
- maskedMatch: row.maskedMatch,
20270
- actionTaken: row.actionTaken,
20271
- confidence: row.confidence
20272
- });
20618
+ this.insertStmt.run(
20619
+ bindParams({
20620
+ id: row.id,
20621
+ auditEventId: row.auditEventId,
20622
+ inspectionDefinitionId: row.inspectionDefinitionId,
20623
+ classifiedDataId: row.classifiedDataId,
20624
+ spanStart: row.spanStart,
20625
+ spanEnd: row.spanEnd,
20626
+ maskedMatch: row.maskedMatch,
20627
+ actionTaken: row.actionTaken,
20628
+ confidence: row.confidence
20629
+ })
20630
+ );
20273
20631
  }
20274
20632
  };
20275
20633
 
@@ -20345,12 +20703,7 @@ function isMirrorDowngrade(incoming, stored) {
20345
20703
  }
20346
20704
  function ruleIdsOf(rulesJson) {
20347
20705
  const ids = /* @__PURE__ */ new Set();
20348
- let raw;
20349
- try {
20350
- raw = JSON.parse(rulesJson);
20351
- } catch {
20352
- return ids;
20353
- }
20706
+ const raw = safeJson(rulesJson, []);
20354
20707
  if (!Array.isArray(raw)) return ids;
20355
20708
  for (const entry of raw) {
20356
20709
  if (entry && typeof entry === "object") {
@@ -20423,47 +20776,48 @@ var SqliteInstalledPacksRepository = class {
20423
20776
  }));
20424
20777
  if (this.storedSignature() === inventorySignature(rows)) return;
20425
20778
  const now = Date.now();
20426
- this.db.exec("BEGIN IMMEDIATE");
20427
- try {
20428
- const mirror = this.mirrorState();
20429
- let behind = false;
20430
- for (const row of rows) {
20431
- const params = {
20432
- id: randomUUID3(),
20433
- namespace: row.namespace,
20434
- packId: row.packId,
20435
- version: row.version,
20436
- name: row.name,
20437
- rulesJson: row.rulesJson,
20438
- now
20439
- };
20440
- const stored = mirror.get(`${row.namespace}/${row.packId}`);
20441
- if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20442
- this.upsertAvailableStmt.run({
20443
- ...params,
20779
+ withTransaction(
20780
+ this.db,
20781
+ () => {
20782
+ const mirror = this.mirrorState();
20783
+ let behind = false;
20784
+ for (const row of rows) {
20785
+ const params = {
20444
20786
  id: randomUUID3(),
20445
- recordedBy: meta3?.recordedBy ?? null
20446
- });
20447
- } else {
20448
- behind = true;
20787
+ namespace: row.namespace,
20788
+ packId: row.packId,
20789
+ version: row.version,
20790
+ name: row.name,
20791
+ rulesJson: row.rulesJson,
20792
+ now
20793
+ };
20794
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
20795
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20796
+ this.upsertAvailableStmt.run({
20797
+ ...params,
20798
+ id: randomUUID3(),
20799
+ recordedBy: meta3?.recordedBy ?? null
20800
+ });
20801
+ } else {
20802
+ behind = true;
20803
+ }
20804
+ this.insertMissingStmt.run(params);
20449
20805
  }
20450
- this.insertMissingStmt.run(params);
20451
- }
20452
- if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20453
- this.db.exec("COMMIT");
20454
- } catch (err) {
20455
- this.db.exec("ROLLBACK");
20456
- throw err;
20457
- }
20806
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20807
+ },
20808
+ "IMMEDIATE"
20809
+ );
20458
20810
  } catch {
20459
20811
  }
20460
20812
  }
20461
20813
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
20462
20814
  // input to the downgrade guard. Read INSIDE the write transaction.
20463
20815
  mirrorState() {
20464
- const rows = this.db.prepare(
20465
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20466
- ).all();
20816
+ const rows = allRows(
20817
+ this.db.prepare(
20818
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20819
+ )
20820
+ );
20467
20821
  return new Map(
20468
20822
  rows.map((r) => [
20469
20823
  `${r.namespace}/${r.packId}`,
@@ -20475,7 +20829,9 @@ var SqliteInstalledPacksRepository = class {
20475
20829
  // (keys joined with '/', matching the detection id slug encoding — packId may
20476
20830
  // itself contain '/', but namespace may not, so the join is unambiguous).
20477
20831
  pruneAvailable(keep) {
20478
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20832
+ const rows = allRows(
20833
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20834
+ );
20479
20835
  const keepSet = new Set(keep);
20480
20836
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
20481
20837
  for (const r of rows) {
@@ -20502,11 +20858,13 @@ var SqliteInstalledPacksRepository = class {
20502
20858
  if (this.db.isTransaction) {
20503
20859
  throw new Error("applyUpdate must not be called inside an open transaction");
20504
20860
  }
20505
- this.db.exec("BEGIN IMMEDIATE");
20506
- try {
20507
- this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20508
- const res = this.db.prepare(
20509
- `UPDATE installed_packs SET
20861
+ let changed = false;
20862
+ withTransaction(
20863
+ this.db,
20864
+ () => {
20865
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20866
+ const res = this.db.prepare(
20867
+ `UPDATE installed_packs SET
20510
20868
  version = (SELECT a.version FROM available_packs a
20511
20869
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
20512
20870
  name = (SELECT a.name FROM available_packs a
@@ -20517,17 +20875,13 @@ var SqliteInstalledPacksRepository = class {
20517
20875
  WHERE namespace = :namespace AND pack_id = :packId
20518
20876
  AND EXISTS (SELECT 1 FROM available_packs a
20519
20877
  WHERE a.namespace = :namespace AND a.pack_id = :packId)`
20520
- ).run({ namespace, packId, now: Date.now() });
20521
- this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20522
- this.db.exec("COMMIT");
20523
- return Number(res.changes) > 0;
20524
- } catch (err) {
20525
- try {
20526
- this.db.exec("ROLLBACK");
20527
- } catch {
20528
- }
20529
- throw err;
20530
- }
20878
+ ).run({ namespace, packId, now: Date.now() });
20879
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20880
+ changed = Number(res.changes) > 0;
20881
+ },
20882
+ "IMMEDIATE"
20883
+ );
20884
+ return changed;
20531
20885
  }
20532
20886
  /**
20533
20887
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -20541,9 +20895,11 @@ var SqliteInstalledPacksRepository = class {
20541
20895
  * JSON-level failure therefore counts as invalid.
20542
20896
  */
20543
20897
  installedRuleset() {
20544
- const rows = this.db.prepare(
20545
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20546
- ).all();
20898
+ const rows = allRows(
20899
+ this.db.prepare(
20900
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20901
+ )
20902
+ );
20547
20903
  const out = {
20548
20904
  installedPacks: rows.length,
20549
20905
  enabledPacks: 0,
@@ -20552,7 +20908,7 @@ var SqliteInstalledPacksRepository = class {
20552
20908
  ruleActions: /* @__PURE__ */ new Map()
20553
20909
  };
20554
20910
  for (const row of rows) {
20555
- if (row.enabled !== 1) continue;
20911
+ if (!intToBool(row.enabled)) continue;
20556
20912
  out.enabledPacks += 1;
20557
20913
  const action = policyIdToAction(row.policyId);
20558
20914
  let raw;
@@ -20587,9 +20943,11 @@ var SqliteInstalledPacksRepository = class {
20587
20943
  * running max would mask a genuinely-newer parseable stamp.
20588
20944
  */
20589
20945
  newestRecordedBinary() {
20590
- const rows = this.db.prepare(
20591
- `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20592
- ).all();
20946
+ const rows = allRows(
20947
+ this.db.prepare(
20948
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20949
+ )
20950
+ );
20593
20951
  let newest = null;
20594
20952
  for (const row of rows) {
20595
20953
  const at = row.recordedBy.lastIndexOf("@");
@@ -20604,13 +20962,15 @@ var SqliteInstalledPacksRepository = class {
20604
20962
  return newest;
20605
20963
  }
20606
20964
  counts() {
20607
- const row = this.db.prepare(
20608
- `SELECT count(*) AS packs,
20965
+ const row = getRow(
20966
+ this.db.prepare(
20967
+ `SELECT count(*) AS packs,
20609
20968
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
20610
20969
  coalesce(sum(enabled), 0) AS enabled
20611
20970
  FROM installed_packs`
20612
- ).get();
20613
- return Promise.resolve(row);
20971
+ )
20972
+ );
20973
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
20614
20974
  }
20615
20975
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
20616
20976
  // Back the Policies page's built-in catalog: how many
@@ -20623,26 +20983,29 @@ var SqliteInstalledPacksRepository = class {
20623
20983
  * attributed to Monitor, matching the Detections views.
20624
20984
  */
20625
20985
  countsByPolicyId() {
20626
- const rows = this.db.prepare(
20627
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20986
+ return countBy(
20987
+ this.db,
20988
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
20628
20989
  FROM installed_packs
20629
- GROUP BY pid`
20630
- ).all();
20631
- return new Map(rows.map((r) => [r.pid, r.n]));
20990
+ GROUP BY k`
20991
+ );
20632
20992
  }
20633
20993
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
20634
20994
  listByPolicyId(policyId) {
20635
- const rows = this.db.prepare(
20636
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20995
+ const rows = allRows(
20996
+ this.db.prepare(
20997
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20637
20998
  FROM installed_packs
20638
20999
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
20639
21000
  ORDER BY name ASC`
20640
- ).all(policyId);
21001
+ ),
21002
+ [policyId]
21003
+ );
20641
21004
  return rows.map((r) => ({
20642
21005
  id: `${r.namespace}/${r.packId}`,
20643
21006
  name: r.name,
20644
21007
  ruleCount: parseRules(r.rulesJson).length,
20645
- enabled: r.enabled === 1
21008
+ enabled: intToBool(r.enabled)
20646
21009
  }));
20647
21010
  }
20648
21011
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -20671,14 +21034,14 @@ var SqliteInstalledPacksRepository = class {
20671
21034
  const res = this.db.prepare(
20672
21035
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
20673
21036
  WHERE namespace = :namespace AND pack_id = :packId`
20674
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
21037
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
20675
21038
  return Number(res.changes) > 0;
20676
21039
  }
20677
21040
  // Fingerprint of the recorded available mirror — compared against the
20678
21041
  // incoming inventory's signature to skip the write entirely when the running
20679
21042
  // binary's inventory hasn't changed since the last record.
20680
21043
  storedSignature() {
20681
- const rows = this.signatureStmt.all();
21044
+ const rows = allRows(this.signatureStmt);
20682
21045
  return inventorySignature(rows);
20683
21046
  }
20684
21047
  };
@@ -20706,42 +21069,48 @@ var SqliteInventoryRepository = class {
20706
21069
  upsert(input, now = Date.now()) {
20707
21070
  const id = inventoryId(input.objectType, input.identityKey);
20708
21071
  const row = toInventoryRow(input, id, now);
20709
- this.upsertStmt.run({
20710
- id: row.id,
20711
- objectType: row.objectType,
20712
- location: row.location ?? null,
20713
- title: row.title ?? null,
20714
- hostId: row.hostId ?? null,
20715
- attributes: row.attributes,
20716
- firstSeen: row.firstSeen,
20717
- lastSeen: row.lastSeen
20718
- });
21072
+ this.upsertStmt.run(
21073
+ bindParams({
21074
+ id: row.id,
21075
+ objectType: row.objectType,
21076
+ location: row.location,
21077
+ title: row.title,
21078
+ hostId: row.hostId,
21079
+ attributes: row.attributes,
21080
+ firstSeen: row.firstSeen,
21081
+ lastSeen: row.lastSeen
21082
+ })
21083
+ );
20719
21084
  return id;
20720
21085
  }
20721
21086
  // The full row, for round-trip assertions.
20722
21087
  findById(id) {
20723
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
20724
- return row;
21088
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
20725
21089
  }
20726
21090
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
20727
21091
  // served from the object_type index, never from audit_events.
20728
21092
  distinctTitles(objectType) {
20729
- const rows = this.db.prepare(
20730
- `SELECT DISTINCT title FROM inventory
21093
+ const rows = allRows(
21094
+ this.db.prepare(
21095
+ `SELECT DISTINCT title FROM inventory
20731
21096
  WHERE object_type = :objectType AND title IS NOT NULL
20732
21097
  ORDER BY title`
20733
- ).all({ objectType });
21098
+ ),
21099
+ { objectType }
21100
+ );
20734
21101
  return rows.map((r) => r.title);
20735
21102
  }
20736
21103
  // Distinct host os_version values — a facet served from an inventory index
20737
21104
  // over the generated column, never from the audit fact (confirm via EXPLAIN
20738
21105
  // QUERY PLAN).
20739
21106
  osVersions() {
20740
- const rows = this.db.prepare(
20741
- `SELECT DISTINCT os_version AS value FROM inventory
21107
+ const rows = allRows(
21108
+ this.db.prepare(
21109
+ `SELECT DISTINCT os_version AS value FROM inventory
20742
21110
  WHERE object_type = 'host' AND os_version IS NOT NULL
20743
21111
  ORDER BY value`
20744
- ).all();
21112
+ )
21113
+ );
20745
21114
  return rows.map((r) => r.value);
20746
21115
  }
20747
21116
  };
@@ -20761,14 +21130,6 @@ var EMPTY_PROJECT_AGG = {
20761
21130
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
20762
21131
  findingsCount: 0
20763
21132
  };
20764
- function safeJson(s, fallback) {
20765
- if (s == null) return fallback;
20766
- try {
20767
- return JSON.parse(s);
20768
- } catch {
20769
- return fallback;
20770
- }
20771
- }
20772
21133
  function resolveHarnessId(attrs, row) {
20773
21134
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
20774
21135
  return attrs.provider;
@@ -20964,30 +21325,49 @@ var SqliteInventoryAssetsRepository = class {
20964
21325
  configRowsCache;
20965
21326
  // ─── stats ─────────────────────────────────────────────────────────────────
20966
21327
  getInventoryStats() {
20967
- const byType = { project: 0, skill: 0, mcp: 0, hook: 0, config: 0 };
20968
- for (const r of this.db.prepare("SELECT asset_type AS t, count(*) AS n FROM inventory_asset GROUP BY asset_type").all()) {
20969
- if (r.t in byType) byType[r.t] = r.n;
20970
- }
20971
- byType.project = this.db.prepare(`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`).get().n;
20972
- const mcpTrust = { "known-good": 0, risky: 0, unapproved: 0 };
20973
- for (const r of this.db.prepare(
20974
- `SELECT coalesce(o.trust, a.trust) AS trust, count(*) AS n
21328
+ const typeCounts = countBy(
21329
+ this.db,
21330
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
21331
+ );
21332
+ const byType = {
21333
+ project: 0,
21334
+ skill: typeCounts.get("skill") ?? 0,
21335
+ mcp: typeCounts.get("mcp") ?? 0,
21336
+ hook: typeCounts.get("hook") ?? 0,
21337
+ config: typeCounts.get("config") ?? 0
21338
+ };
21339
+ byType.project = countScalar(
21340
+ this.db,
21341
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
21342
+ );
21343
+ const mcpTrustCounts = countBy(
21344
+ this.db,
21345
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20975
21346
  FROM inventory_asset a
20976
21347
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20977
21348
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20978
21349
  GROUP BY coalesce(o.trust, a.trust)`
20979
- ).all()) {
20980
- if (r.trust in mcpTrust) mcpTrust[r.trust] = r.n;
20981
- }
20982
- const harnesses = this.db.prepare(
21350
+ );
21351
+ const mcpTrust = {
21352
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
21353
+ risky: mcpTrustCounts.get("risky") ?? 0,
21354
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
21355
+ };
21356
+ const harnesses = countScalar(
21357
+ this.db,
20983
21358
  `SELECT count(*) AS n FROM inventory
20984
21359
  WHERE object_type = 'harness'
20985
- AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20986
- ).get({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }).n;
20987
- const flaggedAssets = this.db.prepare("SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'").get().n;
20988
- const flaggedProjects = this.db.prepare(
21360
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
21361
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
21362
+ );
21363
+ const flaggedAssets = countScalar(
21364
+ this.db,
21365
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
21366
+ );
21367
+ const flaggedProjects = countScalar(
21368
+ this.db,
20989
21369
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20990
- ).get().n;
21370
+ );
20991
21371
  const configRows = this.configAssetRows();
20992
21372
  for (const r of configRows) {
20993
21373
  byType[r.assetType] += 1;
@@ -21244,12 +21624,15 @@ var SqliteInventoryAssetsRepository = class {
21244
21624
  }
21245
21625
  // ─── raw fetchers ────────────────────────────────────────────────────────────
21246
21626
  fetchHarnessRows() {
21247
- return this.db.prepare(
21248
- `SELECT id, title, attributes, harness_version AS harnessVersion
21627
+ return allRows(
21628
+ this.db.prepare(
21629
+ `SELECT id, title, attributes, harness_version AS harnessVersion
21249
21630
  FROM inventory
21250
21631
  WHERE object_type = 'harness'
21251
21632
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
21252
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
21633
+ ),
21634
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
21635
+ );
21253
21636
  }
21254
21637
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
21255
21638
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -21259,12 +21642,13 @@ var SqliteInventoryAssetsRepository = class {
21259
21642
  const params = [...harnessInvIds];
21260
21643
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
21261
21644
  if (q) {
21262
- const pat = `%${escapeLikePattern(q)}%`;
21263
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
21645
+ const pat = containsPattern(q);
21646
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
21264
21647
  params.push(pat, pat);
21265
21648
  }
21266
- const rows = this.db.prepare(
21267
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
21649
+ const rows = allRows(
21650
+ this.db.prepare(
21651
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
21268
21652
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
21269
21653
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
21270
21654
  FROM harness_asset ha
@@ -21272,7 +21656,9 @@ var SqliteInventoryAssetsRepository = class {
21272
21656
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
21273
21657
  WHERE ${where}
21274
21658
  ORDER BY a.name ASC`
21275
- ).all(...params);
21659
+ ),
21660
+ params
21661
+ );
21276
21662
  for (const raw of rows) {
21277
21663
  const harnessInvId = raw.harnessInvId;
21278
21664
  const [asset] = this.mapAssetRows([raw]);
@@ -21291,21 +21677,24 @@ var SqliteInventoryAssetsRepository = class {
21291
21677
  params.push(...types);
21292
21678
  }
21293
21679
  if (q) {
21294
- const pat = `%${escapeLikePattern(q)}%`;
21295
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
21680
+ const pat = containsPattern(q);
21681
+ conditions.push(likeAny(["a.name", "a.sub"]));
21296
21682
  params.push(pat, pat);
21297
21683
  }
21298
21684
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
21299
21685
  const sampleRows = this.mapAssetRows(
21300
- this.db.prepare(
21301
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21686
+ allRows(
21687
+ this.db.prepare(
21688
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21302
21689
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
21303
21690
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
21304
21691
  FROM inventory_asset a
21305
21692
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
21306
21693
  ${where}
21307
21694
  ORDER BY a.name ASC`
21308
- ).all(...params)
21695
+ ),
21696
+ params
21697
+ )
21309
21698
  );
21310
21699
  const configRows = this.configAssetRows(q).filter(
21311
21700
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -21314,14 +21703,17 @@ var SqliteInventoryAssetsRepository = class {
21314
21703
  }
21315
21704
  fetchAssetById(assetId) {
21316
21705
  const rows = this.mapAssetRows(
21317
- this.db.prepare(
21318
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21706
+ allRows(
21707
+ this.db.prepare(
21708
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21319
21709
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
21320
21710
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
21321
21711
  FROM inventory_asset a
21322
21712
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
21323
21713
  WHERE a.id = ?`
21324
- ).all(assetId)
21714
+ ),
21715
+ [assetId]
21716
+ )
21325
21717
  );
21326
21718
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
21327
21719
  }
@@ -21377,37 +21769,39 @@ var SqliteInventoryAssetsRepository = class {
21377
21769
  return rows;
21378
21770
  }
21379
21771
  latestConfigScanId() {
21380
- const row = this.db.prepare(
21381
- `SELECT id FROM audit_events WHERE event_type = 'config_scan'
21382
- ORDER BY started_at DESC, id DESC LIMIT 1`
21383
- ).get();
21384
- return row?.id ?? null;
21772
+ return latestConfigScan(this.db)?.id ?? null;
21385
21773
  }
21386
21774
  fetchProjects(q) {
21387
21775
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
21388
21776
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
21389
21777
  const params = [];
21390
21778
  if (q) {
21391
- const pat = `%${escapeLikePattern(q)}%`;
21392
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
21779
+ const pat = containsPattern(q);
21780
+ sql += ` AND ${likeAny(["name", "url"])}`;
21393
21781
  params.push(pat, pat);
21394
21782
  }
21395
21783
  sql += " ORDER BY name ASC";
21396
- return this.db.prepare(sql).all(...params);
21784
+ return allRows(this.db.prepare(sql), params);
21397
21785
  }
21398
21786
  fetchProjectById(projectId) {
21399
- return this.db.prepare(
21400
- "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
21401
- ).get(projectId) ?? null;
21787
+ return getRow(
21788
+ this.db.prepare(
21789
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
21790
+ ),
21791
+ [projectId]
21792
+ ) ?? null;
21402
21793
  }
21403
21794
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
21404
21795
  fetchProjectsByIds(projectIds) {
21405
21796
  const map2 = /* @__PURE__ */ new Map();
21406
21797
  if (projectIds.length === 0) return map2;
21407
- const rows = this.db.prepare(
21408
- `SELECT id, url, name, attributes, last_seen AS lastSeen
21798
+ const rows = allRows(
21799
+ this.db.prepare(
21800
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
21409
21801
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
21410
- ).all(...projectIds);
21802
+ ),
21803
+ projectIds
21804
+ );
21411
21805
  for (const r of rows) map2.set(r.id, r);
21412
21806
  return map2;
21413
21807
  }
@@ -21418,8 +21812,9 @@ var SqliteInventoryAssetsRepository = class {
21418
21812
  projectAggregates(projectIds) {
21419
21813
  const map2 = /* @__PURE__ */ new Map();
21420
21814
  if (projectIds.length === 0) return map2;
21421
- const rows = this.db.prepare(
21422
- `SELECT f.project_id AS projectId,
21815
+ const rows = allRows(
21816
+ this.db.prepare(
21817
+ `SELECT f.project_id AS projectId,
21423
21818
  coalesce(o.access, f.default_access) AS eff,
21424
21819
  count(*) AS n,
21425
21820
  coalesce(sum(f.findings_count), 0) AS findings
@@ -21427,7 +21822,9 @@ var SqliteInventoryAssetsRepository = class {
21427
21822
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
21428
21823
  WHERE f.project_id IN (${placeholders(projectIds.length)})
21429
21824
  GROUP BY f.project_id, eff`
21430
- ).all(...projectIds);
21825
+ ),
21826
+ projectIds
21827
+ );
21431
21828
  for (const r of rows) {
21432
21829
  let agg = map2.get(r.projectId);
21433
21830
  if (!agg) {
@@ -21465,37 +21862,52 @@ var SqliteInventoryAssetsRepository = class {
21465
21862
  fetchProjectFilesUnder(projectId, prefix) {
21466
21863
  if (prefix === "") {
21467
21864
  return this.mapFileRows(
21468
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21865
+ allRows(
21866
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21867
+ [projectId]
21868
+ )
21469
21869
  );
21470
21870
  }
21471
21871
  return this.mapFileRows(
21472
- this.db.prepare(
21473
- this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21474
- ).all(projectId, `${escapeLikePattern(prefix)}/%`)
21872
+ allRows(
21873
+ this.db.prepare(
21874
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21875
+ ),
21876
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21877
+ )
21475
21878
  );
21476
21879
  }
21477
21880
  fetchProjectFilesSearch(projectId, q) {
21478
- const pat = `%${escapeLikePattern(q)}%`;
21881
+ const pat = containsPattern(q);
21479
21882
  return this.mapFileRows(
21480
- this.db.prepare(
21481
- this.fileSelect(
21482
- "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21483
- )
21484
- ).all(projectId, pat, pat)
21883
+ allRows(
21884
+ this.db.prepare(
21885
+ this.fileSelect(
21886
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21887
+ )
21888
+ ),
21889
+ [projectId, pat, pat]
21890
+ )
21485
21891
  );
21486
21892
  }
21487
21893
  fetchProjectFilesBlocked(projectId) {
21488
21894
  return this.mapFileRows(
21489
- this.db.prepare(
21490
- this.fileSelect(
21491
- "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21492
- )
21493
- ).all(projectId)
21895
+ allRows(
21896
+ this.db.prepare(
21897
+ this.fileSelect(
21898
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21899
+ )
21900
+ ),
21901
+ [projectId]
21902
+ )
21494
21903
  );
21495
21904
  }
21496
21905
  fetchProjectFile(projectId, path) {
21497
21906
  const rows = this.mapFileRows(
21498
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21907
+ allRows(
21908
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21909
+ [projectId, path]
21910
+ )
21499
21911
  );
21500
21912
  return rows[0] ?? null;
21501
21913
  }
@@ -21509,39 +21921,32 @@ var SqlitePoliciesRepository = class {
21509
21921
  }
21510
21922
  db;
21511
21923
  readPolicies() {
21512
- const rows = this.db.prepare("SELECT * FROM policies").all();
21513
- const policies = [];
21514
- for (const row of rows) {
21515
- try {
21516
- const target = JSON.parse(row.target);
21517
- const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21518
- policies.push(
21519
- Policy.parse({
21520
- id: row.id,
21521
- scope: row.scope,
21522
- target,
21523
- action: row.action,
21524
- enabled: row.enabled === 1,
21525
- customKeywords
21526
- })
21527
- );
21528
- } catch {
21529
- }
21530
- }
21924
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21925
+ const policies = mapRowsTolerant(rows, (row) => {
21926
+ const target = JSON.parse(row.target);
21927
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21928
+ return Policy.parse({
21929
+ id: row.id,
21930
+ scope: row.scope,
21931
+ target,
21932
+ action: row.action,
21933
+ enabled: intToBool(row.enabled),
21934
+ customKeywords
21935
+ });
21936
+ });
21531
21937
  return Promise.resolve(policies);
21532
21938
  }
21533
21939
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
21534
21940
  // detection-type config exists from first run. Only when the table is empty,
21535
21941
  // so a user's edits are never clobbered.
21536
21942
  seedDefaults() {
21537
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21943
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
21538
21944
  if (count > 0) return;
21539
21945
  const stmt = this.db.prepare(
21540
21946
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21541
21947
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
21542
21948
  );
21543
- this.db.exec("BEGIN");
21544
- try {
21949
+ failOpenTransaction(this.db, () => {
21545
21950
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
21546
21951
  stmt.run({
21547
21952
  id: randomUUID5(),
@@ -21550,10 +21955,41 @@ var SqlitePoliciesRepository = class {
21550
21955
  now: Date.now()
21551
21956
  });
21552
21957
  }
21553
- this.db.exec("COMMIT");
21554
- } catch {
21555
- this.db.exec("ROLLBACK");
21556
- }
21958
+ });
21959
+ }
21960
+ // Insert-or-update the single global per-category policy row, keyed on the
21961
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21962
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21963
+ // values), so the runtime's resolveAction reads rows written by either path
21964
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21965
+ // `id` and `created_at` are left exactly as they were.
21966
+ upsertCategoryAction(category, action) {
21967
+ const now = Date.now();
21968
+ this.db.prepare(
21969
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21970
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21971
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21972
+ ).run({ id: randomUUID5(), target: JSON.stringify({ category }), action, now });
21973
+ }
21974
+ // Caps every global per-category policy currently set to block/redact down
21975
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21976
+ // Returns the number of rows changed.
21977
+ capCategoryActions() {
21978
+ const info = this.db.prepare(
21979
+ `UPDATE policies SET action='warn', updated_at=:now
21980
+ WHERE scope='global' AND action IN ('block','redact')
21981
+ AND json_extract(target,'$.category') IS NOT NULL`
21982
+ ).run({ now: Date.now() });
21983
+ return Number(info.changes);
21984
+ }
21985
+ // Read the current action for a single global per-category policy row, mirroring
21986
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21987
+ // row exists yet, so callers can distinguish an unset category from a set one.
21988
+ getCategoryAction(category) {
21989
+ const row = this.db.prepare(
21990
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21991
+ ).get({ category });
21992
+ return row?.action;
21557
21993
  }
21558
21994
  };
21559
21995
 
@@ -21629,7 +22065,7 @@ var SqliteProjectFilesRepository = class {
21629
22065
  maxStampStmt;
21630
22066
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
21631
22067
  replaceForProject(projectId, scan2, now) {
21632
- const { maxStamp } = this.maxStampStmt.get({ projectId });
22068
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
21633
22069
  const stamp = Math.max(now, maxStamp + 1);
21634
22070
  for (const file2 of scan2.files) {
21635
22071
  this.upsertStmt.run({
@@ -21712,7 +22148,7 @@ var SqliteResolutionsRepository = class {
21712
22148
  }
21713
22149
  /** The newest disposition recorded for a finding key, or undefined if none. */
21714
22150
  latestByKey(key) {
21715
- const row = this.latestStmt.get({ findingKey: key });
22151
+ const row = getRow(this.latestStmt, { findingKey: key });
21716
22152
  if (!row) return void 0;
21717
22153
  return {
21718
22154
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -21729,7 +22165,7 @@ var SqliteResolutionsRepository = class {
21729
22165
  * the CLI) surfaces for that file.
21730
22166
  */
21731
22167
  openAtRestKeysForPath(path) {
21732
- const rows = this.openAtRestStmt.all({ path });
22168
+ const rows = allRows(this.openAtRestStmt, { path });
21733
22169
  return rows.map((r) => r.finding_key);
21734
22170
  }
21735
22171
  /**
@@ -21740,7 +22176,7 @@ var SqliteResolutionsRepository = class {
21740
22176
  * resolution row (see scan.ts).
21741
22177
  */
21742
22178
  resolvedAtRestKeysForPath(path) {
21743
- const rows = this.resolvedAtRestStmt.all({ path });
22179
+ const rows = allRows(this.resolvedAtRestStmt, { path });
21744
22180
  return rows.map((r) => r.finding_key);
21745
22181
  }
21746
22182
  };
@@ -21769,31 +22205,25 @@ var SqliteScanLedgerRepository = class {
21769
22205
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
21770
22206
  // older ruleset are simply absent, which reads as "never scanned".
21771
22207
  entriesForRuleset(rulesetHash) {
21772
- const rows = this.readStmt.all({ rulesetHash });
22208
+ const rows = allRows(this.readStmt, {
22209
+ rulesetHash
22210
+ });
21773
22211
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
21774
22212
  }
21775
22213
  upsertEntries(entries) {
21776
22214
  if (entries.length === 0) return;
21777
22215
  const scannedAt = Date.now();
21778
- try {
21779
- this.db.exec("BEGIN");
21780
- try {
21781
- for (const entry of entries) {
21782
- this.upsertStmt.run({
21783
- path: entry.path,
21784
- mtime: entry.mtime,
21785
- contentHash: entry.contentHash,
21786
- rulesetHash: entry.rulesetHash,
21787
- scannedAt
21788
- });
21789
- }
21790
- this.db.exec("COMMIT");
21791
- } catch (err) {
21792
- this.db.exec("ROLLBACK");
21793
- throw err;
22216
+ failOpenTransaction(this.db, () => {
22217
+ for (const entry of entries) {
22218
+ this.upsertStmt.run({
22219
+ path: entry.path,
22220
+ mtime: entry.mtime,
22221
+ contentHash: entry.contentHash,
22222
+ rulesetHash: entry.rulesetHash,
22223
+ scannedAt
22224
+ });
21794
22225
  }
21795
- } catch {
21796
- }
22226
+ });
21797
22227
  }
21798
22228
  };
21799
22229
 
@@ -21870,8 +22300,9 @@ var SqliteSecurityRepository = class {
21870
22300
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21871
22301
  // double-counting a key that accumulated several append-only rows.
21872
22302
  severitySummary() {
21873
- const rows = this.db.prepare(
21874
- `SELECT f.severity AS severity,
22303
+ const rows = allRows(
22304
+ this.db.prepare(
22305
+ `SELECT f.severity AS severity,
21875
22306
  COUNT(*) AS count,
21876
22307
  SUM(CASE
21877
22308
  WHEN e.kind != 'code_change' THEN 1
@@ -21890,7 +22321,8 @@ var SqliteSecurityRepository = class {
21890
22321
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21891
22322
  ON latest.finding_key = f.finding_key
21892
22323
  GROUP BY f.severity`
21893
- ).all();
22324
+ )
22325
+ );
21894
22326
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21895
22327
  const bySeverity = SEVERITIES.map((severity) => ({
21896
22328
  severity,
@@ -21974,14 +22406,15 @@ var SqliteSecurityRepository = class {
21974
22406
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21975
22407
  const now = this.now();
21976
22408
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
21977
- const rows = this.db.prepare(
21978
- // first_detected_at is the PRESERVED first-detection time (set once on a
21979
- // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21980
- // measures from first sighting — not the latest re-scan's event, whose
21981
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21982
- // the parent event's occurred_at defends against any legacy/edge row the
21983
- // backfill left null.
21984
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
22409
+ const rows = allRows(
22410
+ this.db.prepare(
22411
+ // first_detected_at is the PRESERVED first-detection time (set once on a
22412
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
22413
+ // measures from first sighting — not the latest re-scan's event, whose
22414
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
22415
+ // the parent event's occurred_at defends against any legacy/edge row the
22416
+ // backfill left null.
22417
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21985
22418
  (
21986
22419
  SELECT fr.status FROM finding_resolution fr
21987
22420
  WHERE fr.finding_key = f.finding_key
@@ -22007,14 +22440,16 @@ var SqliteSecurityRepository = class {
22007
22440
  WHERE fr.finding_key = f.finding_key
22008
22441
  AND fr.resolved_at >= :windowStart
22009
22442
  )`
22010
- // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
22011
- // any resolution activity at/after the window start — a row this method
22012
- // ultimately counts has its LATEST resolution inside the window, which
22013
- // implies such a row exists, so nothing wanted is dropped. The exact
22014
- // latest-wins + status/method + window gate stays in JS below,
22015
- // dialect-agnostic. Without this, a
22016
- // 7d request evaluated the store's entire trackable-findings history.
22017
- ).all({ windowStart });
22443
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
22444
+ // any resolution activity at/after the window start — a row this method
22445
+ // ultimately counts has its LATEST resolution inside the window, which
22446
+ // implies such a row exists, so nothing wanted is dropped. The exact
22447
+ // latest-wins + status/method + window gate stays in JS below,
22448
+ // dialect-agnostic. Without this, a
22449
+ // 7d request evaluated the store's entire trackable-findings history.
22450
+ ),
22451
+ { windowStart }
22452
+ );
22018
22453
  const sums = /* @__PURE__ */ new Map();
22019
22454
  const counts = /* @__PURE__ */ new Map();
22020
22455
  for (const r of rows) {
@@ -22044,8 +22479,9 @@ var SqliteSecurityRepository = class {
22044
22479
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
22045
22480
  const now = this.now();
22046
22481
  const from = now - RANGE_DAYS[range] * DAY_MS4;
22047
- const rows = this.db.prepare(
22048
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
22482
+ const rows = allRows(
22483
+ this.db.prepare(
22484
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
22049
22485
  FROM findings f JOIN events e ON e.id = f.event_id
22050
22486
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
22051
22487
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -22053,7 +22489,9 @@ var SqliteSecurityRepository = class {
22053
22489
  GROUP BY repo
22054
22490
  ORDER BY c DESC, repo
22055
22491
  LIMIT :limit`
22056
- ).all({ from, to: now, limit });
22492
+ ),
22493
+ { from, to: now, limit }
22494
+ );
22057
22495
  const items = rows.map((r) => ({
22058
22496
  id: `repo_${r.repo}`,
22059
22497
  name: r.repo,
@@ -22075,8 +22513,9 @@ var SqliteSecurityRepository = class {
22075
22513
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
22076
22514
  // capped at `limit`.
22077
22515
  recentlyResolved(limit = 20) {
22078
- const rows = this.db.prepare(
22079
- `SELECT f.finding_key AS finding_key,
22516
+ const rows = allRows(
22517
+ this.db.prepare(
22518
+ `SELECT f.finding_key AS finding_key,
22080
22519
  f.rule_id AS rule_id,
22081
22520
  f.severity AS severity,
22082
22521
  json_extract(e.metadata, '$.filePath') AS path,
@@ -22110,7 +22549,9 @@ var SqliteSecurityRepository = class {
22110
22549
  ) IS NOT NULL
22111
22550
  ORDER BY latest_resolved_at DESC
22112
22551
  LIMIT :limit`
22113
- ).all({ limit });
22552
+ ),
22553
+ { limit }
22554
+ );
22114
22555
  const items = rows.map((r) => ({
22115
22556
  findingKey: r.finding_key,
22116
22557
  ruleId: r.rule_id,
@@ -22127,12 +22568,15 @@ var SqliteSecurityRepository = class {
22127
22568
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
22128
22569
  // numeric and the JS aggregations bucket/split on ms directly.
22129
22570
  findingsInRange(fromMs, toMs) {
22130
- const rows = this.db.prepare(
22131
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
22571
+ const rows = allRows(
22572
+ this.db.prepare(
22573
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
22132
22574
  FROM findings f JOIN events e ON e.id = f.event_id
22133
22575
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
22134
22576
  ORDER BY e.occurred_at`
22135
- ).all({ from: fromMs, to: toMs });
22577
+ ),
22578
+ { from: fromMs, to: toMs }
22579
+ );
22136
22580
  return rows.map((r) => ({
22137
22581
  occurredAt: r.occurred_at,
22138
22582
  severity: r.severity,
@@ -22146,12 +22590,7 @@ import { randomUUID as randomUUID8 } from "crypto";
22146
22590
  var KIND_ORDER = ["provider", "internal", "ip"];
22147
22591
  var CALL_SITE_EMBED_CAP = 200;
22148
22592
  function parseNetwork(networkJson) {
22149
- if (!networkJson) return null;
22150
- try {
22151
- return JSON.parse(networkJson);
22152
- } catch {
22153
- return null;
22154
- }
22593
+ return safeJson(networkJson, null);
22155
22594
  }
22156
22595
  function toEndpointSummary(row) {
22157
22596
  return {
@@ -22238,27 +22677,39 @@ var SqliteSharesRepository = class {
22238
22677
  }
22239
22678
  db;
22240
22679
  stats() {
22241
- const scalar = (sql) => this.db.prepare(sql).get()?.n ?? 0;
22242
- const destinations = scalar("SELECT count(*) AS n FROM share_destination");
22243
- const endpoints = scalar("SELECT count(*) AS n FROM share_endpoint");
22244
- const callSites = scalar("SELECT count(*) AS n FROM share_call_site");
22245
- const insecure = scalar(
22680
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
22681
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
22682
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22683
+ const insecure = countScalar(
22684
+ this.db,
22246
22685
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
22247
22686
  );
22248
- const needsReview = scalar(
22687
+ const needsReview = countScalar(
22688
+ this.db,
22249
22689
  `SELECT count(DISTINCT d.id) AS n
22250
22690
  FROM share_destination d
22251
22691
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
22252
22692
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
22253
22693
  );
22254
- const byKind = { provider: 0, internal: 0, ip: 0 };
22255
- for (const row of this.db.prepare("SELECT kind, count(*) AS n FROM share_destination GROUP BY kind").all()) {
22256
- byKind[row.kind] = row.n;
22257
- }
22258
- const byTrust = { recognized: 0, internal: 0, unverified: 0, ip: 0 };
22259
- for (const row of this.db.prepare("SELECT trust, count(*) AS n FROM share_destination GROUP BY trust").all()) {
22260
- byTrust[row.trust] = row.n;
22261
- }
22694
+ const kindCounts = countBy(
22695
+ this.db,
22696
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
22697
+ );
22698
+ const byKind = {
22699
+ provider: kindCounts.get("provider") ?? 0,
22700
+ internal: kindCounts.get("internal") ?? 0,
22701
+ ip: kindCounts.get("ip") ?? 0
22702
+ };
22703
+ const trustCounts = countBy(
22704
+ this.db,
22705
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
22706
+ );
22707
+ const byTrust = {
22708
+ recognized: trustCounts.get("recognized") ?? 0,
22709
+ internal: trustCounts.get("internal") ?? 0,
22710
+ unverified: trustCounts.get("unverified") ?? 0,
22711
+ ip: trustCounts.get("ip") ?? 0
22712
+ };
22262
22713
  return Promise.resolve({
22263
22714
  destinations,
22264
22715
  endpoints,
@@ -22374,7 +22825,7 @@ var SqliteSharesRepository = class {
22374
22825
  }
22375
22826
  let sql;
22376
22827
  if (q) {
22377
- const pattern = `%${escapeLikePattern(q)}%`;
22828
+ const pattern = containsPattern(q);
22378
22829
  conditions.push(
22379
22830
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
22380
22831
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -22394,24 +22845,31 @@ var SqliteSharesRepository = class {
22394
22845
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
22395
22846
  ORDER BY d.created_at ASC, d.id ASC`;
22396
22847
  }
22397
- const rows = this.db.prepare(sql).all(...params);
22848
+ const rows = allRows(
22849
+ this.db.prepare(sql),
22850
+ params
22851
+ );
22398
22852
  return rows.map((r) => this.mapDestRow(r));
22399
22853
  }
22400
22854
  fetchDestinationById(destinationId) {
22401
- const row = this.db.prepare(
22402
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22855
+ const row = getRow(
22856
+ this.db.prepare(
22857
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22403
22858
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22404
22859
  o.decision AS overrideDecision
22405
22860
  FROM share_destination d
22406
22861
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22407
22862
  WHERE d.id = ?`
22408
- ).get(destinationId);
22863
+ ),
22864
+ [destinationId]
22865
+ );
22409
22866
  return row ? this.mapDestRow(row) : null;
22410
22867
  }
22411
22868
  fetchEndpoints(destinationIds) {
22412
22869
  if (destinationIds.length === 0) return [];
22413
- const rows = this.db.prepare(
22414
- `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
22870
+ const rows = allRows(
22871
+ this.db.prepare(
22872
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
22415
22873
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
22416
22874
  count(c.id) AS callSiteCount
22417
22875
  FROM share_endpoint e
@@ -22419,7 +22877,9 @@ var SqliteSharesRepository = class {
22419
22877
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
22420
22878
  GROUP BY e.id
22421
22879
  ORDER BY e.created_at ASC, e.id ASC`
22422
- ).all(...destinationIds);
22880
+ ),
22881
+ destinationIds
22882
+ );
22423
22883
  return rows.map((r) => ({
22424
22884
  id: r.id,
22425
22885
  destinationId: r.destinationId,
@@ -22444,13 +22904,16 @@ var SqliteSharesRepository = class {
22444
22904
  }
22445
22905
  fetchCallSites(endpointIds) {
22446
22906
  if (endpointIds.length === 0) return [];
22447
- const rows = this.db.prepare(
22448
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22907
+ const rows = allRows(
22908
+ this.db.prepare(
22909
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22449
22910
  project_id AS projectId
22450
22911
  FROM share_call_site
22451
22912
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
22452
22913
  ORDER BY created_at ASC, id ASC`
22453
- ).all(...endpointIds);
22914
+ ),
22915
+ endpointIds
22916
+ );
22454
22917
  return rows.map((r) => ({
22455
22918
  id: r.id,
22456
22919
  endpointId: r.endpointId,
@@ -22486,27 +22949,36 @@ var SqliteSourceProjectRepository = class {
22486
22949
  upsert(input, now = Date.now()) {
22487
22950
  const id = sourceProjectId(input.url);
22488
22951
  const row = toSourceProjectRow(input, id, now);
22489
- this.upsertStmt.run({
22490
- id: row.id,
22491
- url: row.url ?? null,
22492
- name: row.name ?? null,
22493
- attributes: row.attributes,
22494
- firstSeen: row.firstSeen,
22495
- lastSeen: row.lastSeen
22496
- });
22952
+ this.upsertStmt.run(
22953
+ bindParams({
22954
+ id: row.id,
22955
+ url: row.url,
22956
+ name: row.name,
22957
+ attributes: row.attributes,
22958
+ firstSeen: row.firstSeen,
22959
+ lastSeen: row.lastSeen
22960
+ })
22961
+ );
22497
22962
  return id;
22498
22963
  }
22499
22964
  findById(id) {
22500
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22965
+ return getRow(
22966
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22967
+ {
22968
+ id
22969
+ }
22970
+ );
22501
22971
  }
22502
22972
  // Distinct project names — a filter facet, served from the source_project
22503
22973
  // table, never from the audit fact table.
22504
22974
  distinctNames() {
22505
- const rows = this.db.prepare(
22506
- `SELECT DISTINCT name FROM source_project
22975
+ const rows = allRows(
22976
+ this.db.prepare(
22977
+ `SELECT DISTINCT name FROM source_project
22507
22978
  WHERE name IS NOT NULL
22508
22979
  ORDER BY name`
22509
- ).all();
22980
+ )
22981
+ );
22510
22982
  return rows.map((r) => r.name);
22511
22983
  }
22512
22984
  };
@@ -22525,8 +22997,7 @@ function hasLegacySampleRows(db) {
22525
22997
  function purgeSampleData(db) {
22526
22998
  try {
22527
22999
  if (!hasLegacySampleRows(db)) return;
22528
- db.exec("BEGIN");
22529
- try {
23000
+ withTransaction(db, () => {
22530
23001
  db.exec(
22531
23002
  `DELETE FROM share_call_site WHERE endpoint_id IN (
22532
23003
  SELECT e.id FROM share_endpoint e
@@ -22571,11 +23042,7 @@ function purgeSampleData(db) {
22571
23042
  value TEXT NOT NULL
22572
23043
  )`);
22573
23044
  db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
22574
- db.exec("COMMIT");
22575
- } catch (err) {
22576
- db.exec("ROLLBACK");
22577
- throw err;
22578
- }
23045
+ });
22579
23046
  } catch {
22580
23047
  }
22581
23048
  }
@@ -22594,7 +23061,7 @@ function openWithPragmas(file2) {
22594
23061
  function backupLegacyStore(file2) {
22595
23062
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
22596
23063
  renameSync(file2, backup);
22597
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
23064
+ for (const sidecar of walSidecars(file2)) {
22598
23065
  if (existsSync2(sidecar)) rmSync(sidecar);
22599
23066
  }
22600
23067
  return backup;
@@ -22607,9 +23074,8 @@ function openLocalDatabase(dir) {
22607
23074
  db.close();
22608
23075
  const backup = backupLegacyStore(file2);
22609
23076
  db = openWithPragmas(file2);
22610
- process.stderr.write(
22611
- `[aka] Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.
22612
- `
23077
+ akaWarn(
23078
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
22613
23079
  );
22614
23080
  }
22615
23081
  applyMigrations(db);
@@ -22637,96 +23103,77 @@ function openLocalDatabase(dir) {
22637
23103
  const configInventory = new SqliteConfigInventoryRepository(db);
22638
23104
  policies.seedDefaults();
22639
23105
  function recordCapture(event, detected) {
22640
- try {
22641
- db.exec("BEGIN");
22642
- try {
22643
- events.insertEvent(event);
22644
- const sessionId = event.metadata?.sessionId;
22645
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
22646
- db.exec("COMMIT");
22647
- } catch (err) {
22648
- db.exec("ROLLBACK");
22649
- throw err;
22650
- }
22651
- } catch {
22652
- }
23106
+ failOpenTransaction(db, () => {
23107
+ events.insertEvent(event);
23108
+ const sessionId = event.metadata?.sessionId;
23109
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
23110
+ });
22653
23111
  }
22654
23112
  function ensureInventory(ctx) {
22655
23113
  const resolved = {};
22656
- try {
22657
- db.exec("BEGIN");
22658
- try {
22659
- const now = Date.now();
22660
- if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22661
- if (ctx.harness) {
22662
- resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
22663
- }
22664
- resolved.accountId = inventory.upsert(
22665
- linkHost(
22666
- {
22667
- objectType: "user",
22668
- identityKey: "local",
22669
- attributes: { source: "local" }
22670
- },
22671
- resolved.hostId
22672
- ),
22673
- now
22674
- );
22675
- if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22676
- db.exec("COMMIT");
22677
- } catch (err) {
22678
- db.exec("ROLLBACK");
22679
- throw err;
22680
- }
22681
- } catch {
22682
- return {};
22683
- }
22684
- return resolved;
23114
+ const committed = failOpenTransaction(db, () => {
23115
+ const now = Date.now();
23116
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
23117
+ if (ctx.harness) {
23118
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
23119
+ }
23120
+ resolved.accountId = inventory.upsert(
23121
+ linkHost(
23122
+ {
23123
+ objectType: "user",
23124
+ identityKey: "local",
23125
+ attributes: { source: "local" }
23126
+ },
23127
+ resolved.hostId
23128
+ ),
23129
+ now
23130
+ );
23131
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
23132
+ });
23133
+ return committed ? resolved : {};
22685
23134
  }
22686
23135
  function recordConfigScan(record2) {
22687
- try {
22688
- db.exec("BEGIN");
22689
- try {
22690
- const now = isoToEpochMillis(record2.scanEvent.startedAt);
22691
- for (const item of record2.items) inventory.upsert(item, now);
22692
- auditEvents.insertAuditEvent(record2.scanEvent);
22693
- const definitionIds = /* @__PURE__ */ new Map();
22694
- for (const def of record2.definitions ?? []) {
22695
- definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22696
- }
22697
- for (const finding of record2.findings ?? []) {
22698
- const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
22699
- if (!definitionId) continue;
22700
- inspectionFindings.insertFinding({
22701
- id: randomUUID9(),
22702
- auditEventId: record2.scanEvent.id,
22703
- inspectionDefinitionId: definitionId,
22704
- span: finding.span,
22705
- maskedMatch: finding.maskedMatch,
22706
- actionTaken: finding.actionTaken,
22707
- confidence: finding.confidence
22708
- });
22709
- }
22710
- db.exec("COMMIT");
22711
- } catch (err) {
22712
- db.exec("ROLLBACK");
22713
- throw err;
23136
+ failOpenTransaction(db, () => {
23137
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
23138
+ for (const item of record2.items) inventory.upsert(item, now);
23139
+ auditEvents.insertAuditEvent(record2.scanEvent);
23140
+ const definitionIds = /* @__PURE__ */ new Map();
23141
+ for (const def of record2.definitions ?? []) {
23142
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
23143
+ }
23144
+ for (const finding of record2.findings ?? []) {
23145
+ const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
23146
+ if (!definitionId) continue;
23147
+ inspectionFindings.insertFinding({
23148
+ id: randomUUID9(),
23149
+ auditEventId: record2.scanEvent.id,
23150
+ inspectionDefinitionId: definitionId,
23151
+ span: finding.span,
23152
+ maskedMatch: finding.maskedMatch,
23153
+ actionTaken: finding.actionTaken,
23154
+ confidence: finding.confidence
23155
+ });
22714
23156
  }
22715
- } catch {
22716
- }
23157
+ });
22717
23158
  }
22718
23159
  function recordProjectFiles(projectId, scan2) {
22719
23160
  if (scan2.files.length === 0) return;
23161
+ failOpenTransaction(db, () => {
23162
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
23163
+ });
23164
+ }
23165
+ async function transaction(fn) {
23166
+ db.exec("BEGIN");
22720
23167
  try {
22721
- db.exec("BEGIN");
23168
+ const result = await fn();
23169
+ db.exec("COMMIT");
23170
+ return result;
23171
+ } catch (err) {
22722
23172
  try {
22723
- projectFiles.replaceForProject(projectId, scan2, Date.now());
22724
- db.exec("COMMIT");
22725
- } catch (err) {
22726
23173
  db.exec("ROLLBACK");
22727
- throw err;
23174
+ } catch {
22728
23175
  }
22729
- } catch {
23176
+ throw err;
22730
23177
  }
22731
23178
  }
22732
23179
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -22745,8 +23192,7 @@ function openLocalDatabase(dir) {
22745
23192
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
22746
23193
  });
22747
23194
  if (stale.length === 0) return;
22748
- db.exec("BEGIN");
22749
- try {
23195
+ withTransaction(db, () => {
22750
23196
  for (const { id } of stale) {
22751
23197
  db.prepare(
22752
23198
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -22758,11 +23204,7 @@ function openLocalDatabase(dir) {
22758
23204
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
22759
23205
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
22760
23206
  }
22761
- db.exec("COMMIT");
22762
- } catch (err) {
22763
- db.exec("ROLLBACK");
22764
- throw err;
22765
- }
23207
+ });
22766
23208
  } catch {
22767
23209
  }
22768
23210
  }
@@ -22804,6 +23246,7 @@ function openLocalDatabase(dir) {
22804
23246
  purgeSampleData: () => {
22805
23247
  purgeSampleData(db);
22806
23248
  },
23249
+ transaction,
22807
23250
  close: () => {
22808
23251
  db.close();
22809
23252
  }
@@ -22908,190 +23351,9 @@ function ensureLayoutDirSync(dir = defaultDataDir()) {
22908
23351
  import { readFileSync as readFileSync4, renameSync as renameSync4, writeFileSync as writeFileSync2 } from "fs";
22909
23352
  import { join as join6 } from "path";
22910
23353
 
22911
- // ../packages/local-ops/src/update-cache.ts
22912
- var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
22913
- function cachePath(home) {
22914
- return join7(dataDir(home), "update-check.json");
22915
- }
22916
- function readCache(home) {
22917
- const path = cachePath(home);
22918
- if (!existsSync3(path)) return null;
22919
- try {
22920
- const raw = JSON.parse(readFileSync5(path, "utf8"));
22921
- if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
22922
- return raw;
22923
- } catch {
22924
- return null;
22925
- }
22926
- }
22927
- function writeCache(home, cache) {
22928
- if (!existsSync3(dataDir(home))) return;
22929
- try {
22930
- writeFileSync3(cachePath(home), `${JSON.stringify(cache, null, 2)}
22931
- `);
22932
- } catch {
22933
- }
22934
- }
22935
- function isStale(cache, ttlMs = CHECK_TTL_MS) {
22936
- return Date.now() - cache.checkedAt >= ttlMs;
22937
- }
22938
- function clearCache(home) {
22939
- try {
22940
- rmSync2(cachePath(home), { force: true });
22941
- } catch {
22942
- }
22943
- }
22944
- function refreshCache(home) {
22945
- const previous = readCache(home);
22946
- writeCache(home, {
22947
- checkedAt: Date.now(),
22948
- report: gatherReportLive(),
22949
- notifiedPluginIds: previous?.notifiedPluginIds ?? []
22950
- });
22951
- }
22952
- function triggerBackgroundRefresh(home) {
22953
- const entry = process.argv[1];
22954
- if (!entry) return;
22955
- try {
22956
- spawn(process.execPath, [entry, "__update-refresh", "--home", home], {
22957
- detached: true,
22958
- stdio: "ignore"
22959
- }).unref();
22960
- } catch {
22961
- }
22962
- }
22963
- function notifyFromCache(home, opts) {
22964
- if (!opts.isTty) return;
22965
- const cache = readCache(home);
22966
- if (cache) {
22967
- const lines = [];
22968
- for (const s of cache.report.statuses) {
22969
- if (!s.updateAvailable || !s.latest) continue;
22970
- const cmd = s.kind === "cli" ? "aka update" : `aka update ${s.id}`;
22971
- lines.push(` \u2B06 ${s.name}: ${s.installed ?? "\u2014"} \u2192 ${s.latest} available. Run \`${cmd}\`.`);
22972
- }
22973
- const alreadyShown = new Set(cache.notifiedPluginIds);
22974
- const freshlyShown = [];
22975
- for (const p of cache.report.availablePlugins) {
22976
- if (alreadyShown.has(p.id)) continue;
22977
- lines.push(
22978
- ` \u2728 New plugin available: ${p.name}. Install with \`aka plugins install ${p.id}\`.`
22979
- );
22980
- freshlyShown.push(p.id);
22981
- }
22982
- if (lines.length > 0) {
22983
- process.stderr.write(`
22984
- ${lines.join("\n")}
22985
- `);
22986
- }
22987
- if (freshlyShown.length > 0) {
22988
- writeCache(home, {
22989
- ...cache,
22990
- notifiedPluginIds: [...cache.notifiedPluginIds, ...freshlyShown]
22991
- });
22992
- }
22993
- }
22994
- if (!cache || isStale(cache)) triggerBackgroundRefresh(home);
22995
- }
22996
-
22997
- // ../packages/local-ops/src/update-render.ts
22998
- function statusLabel(s) {
22999
- if (s.installed === null || s.latest === null) return "unknown";
23000
- return s.updateAvailable ? "update available" : "up to date";
23001
- }
23002
- function renderReport(report) {
23003
- const rows = report.statuses.map((s) => ({
23004
- name: s.name,
23005
- installed: s.installed ?? "\u2014",
23006
- latest: s.latest ?? "unknown",
23007
- status: statusLabel(s)
23008
- }));
23009
- const nameW = Math.max(9, ...rows.map((r) => r.name.length));
23010
- const instW = Math.max(9, ...rows.map((r) => r.installed.length));
23011
- const latW = Math.max(6, ...rows.map((r) => r.latest.length));
23012
- const lines = [];
23013
- lines.push(
23014
- ` ${"Component".padEnd(nameW)} ${"Installed".padEnd(instW)} ${"Latest".padEnd(latW)} Status`
23015
- );
23016
- for (const r of rows) {
23017
- lines.push(
23018
- ` ${r.name.padEnd(nameW)} ${r.installed.padEnd(instW)} ${r.latest.padEnd(latW)} ${r.status}`
23019
- );
23020
- }
23021
- if (report.availablePlugins.length > 0) {
23022
- lines.push("");
23023
- lines.push(" Available plugins (not installed):");
23024
- for (const p of report.availablePlugins) {
23025
- const version2 = p.latest ? ` v${p.latest}` : "";
23026
- lines.push(` ${p.name} (${p.id})${version2} \u2014 install: aka plugins install ${p.id}`);
23027
- }
23028
- }
23029
- return lines.join("\n");
23030
- }
23031
- function outdated(report) {
23032
- return report.statuses.filter((s) => s.updateAvailable);
23033
- }
23034
-
23035
- // src/command-manifest.ts
23036
- var COMMAND_SPECS = [
23037
- { name: "init", summary: "Scaffold ~/.aka (settings + local database)" },
23038
- {
23039
- name: "scan",
23040
- argHint: "[path]",
23041
- summary: "Scan a file or directory and record findings (default: .)",
23042
- completesFiles: true
23043
- },
23044
- { name: "stats", summary: "Print findings / enforcement / detections from the local store" },
23045
- {
23046
- name: "detections",
23047
- argHint: "[update]",
23048
- summary: "List installed detection packs + available updates (updates are manual)",
23049
- args: ["update"]
23050
- },
23051
- {
23052
- name: "plugins",
23053
- summary: "List / install agent plugins (the CLI is an optional hub)",
23054
- args: ["list", "install"]
23055
- },
23056
- { name: "dashboard", summary: "Launch the local web dashboard and open it in the browser" },
23057
- {
23058
- name: "exception",
23059
- summary: "Manage detection exceptions (approve, add, list, show, revoke, rotate-key)",
23060
- args: ["approve", "add", "list", "show", "revoke", "rotate-key"]
23061
- },
23062
- {
23063
- name: "tui",
23064
- argHint: "[view]",
23065
- summary: "Interactive colour dashboard \u2014 health|findings|recommend|audit (needs a TTY)",
23066
- args: ["health", "findings", "recommend", "audit"]
23067
- },
23068
- {
23069
- name: "check-updates",
23070
- summary: "Show which components (CLI + plugins) have updates available"
23071
- },
23072
- {
23073
- name: "update",
23074
- argHint: "[what]",
23075
- summary: "Update the CLI and/or plugins (what: cli | <plugin-id> | all)",
23076
- args: ["cli", "all"]
23077
- },
23078
- {
23079
- name: "completion",
23080
- argHint: "<sh>",
23081
- summary: "Emit a shell completion script (sh: zsh | bash)",
23082
- args: ["zsh", "bash"]
23083
- }
23084
- ];
23085
- var GLOBAL_FLAGS = ["--home", "--no-update-check", "--version", "--help"];
23086
- function commandsHelp() {
23087
- const label = (s) => s.argHint ? `${s.name} ${s.argHint}` : s.name;
23088
- const width = Math.max(...COMMAND_SPECS.map((s) => label(s).length)) + 1;
23089
- return COMMAND_SPECS.map((s) => ` ${label(s).padEnd(width)}${s.summary}`).join("\n");
23090
- }
23091
-
23092
- // src/commands/check-updates.ts
23093
- import { existsSync as existsSync6 } from "fs";
23094
- import { parseArgs } from "util";
23354
+ // ../packages/persistence/src/warn-era-cap.ts
23355
+ import { existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
23356
+ import { join as join7 } from "path";
23095
23357
 
23096
23358
  // ../packages/plugin-sdk/src/provider-env.ts
23097
23359
  var booleanish = external_exports.string().optional().transform((v) => {
@@ -23112,7 +23374,7 @@ var providerEnvShape = {
23112
23374
  var ProviderEnvSchema = external_exports.object(providerEnvShape);
23113
23375
 
23114
23376
  // ../packages/plugin-sdk/src/config-inventory.ts
23115
- import { readdirSync as readdirSync2, readFileSync as readFileSync7, realpathSync, statSync as statSync3 } from "fs";
23377
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, realpathSync, statSync as statSync3 } from "fs";
23116
23378
  import { homedir as homedir4 } from "os";
23117
23379
  import { basename as basename2, join as join9 } from "path";
23118
23380
 
@@ -25043,140 +25305,597 @@ var BUNDLED_PACKS = [
25043
25305
  ]
25044
25306
  },
25045
25307
  {
25046
- packId: "core-phi",
25047
- name: "Protected Health Information",
25048
- version: "0.1.0",
25049
- rawRules: [
25050
- mrn_default,
25051
- hipaa_identifier_default,
25052
- member_id_default,
25053
- group_id_default,
25054
- icd10_code_default,
25055
- ndc_code_default,
25056
- genetic_data_default,
25057
- biometric_ref_default
25058
- ]
25308
+ packId: "core-phi",
25309
+ name: "Protected Health Information",
25310
+ version: "0.1.0",
25311
+ rawRules: [
25312
+ mrn_default,
25313
+ hipaa_identifier_default,
25314
+ member_id_default,
25315
+ group_id_default,
25316
+ icd10_code_default,
25317
+ ndc_code_default,
25318
+ genetic_data_default,
25319
+ biometric_ref_default
25320
+ ]
25321
+ },
25322
+ {
25323
+ packId: "core-pii",
25324
+ name: "Core PII",
25325
+ version: "0.2.0",
25326
+ rawRules: [
25327
+ email_default,
25328
+ ssn_default,
25329
+ phone_us_default,
25330
+ phone_intl_default,
25331
+ ip_address_default,
25332
+ dob_default,
25333
+ drivers_license_us_default,
25334
+ passport_us_default,
25335
+ home_address_default,
25336
+ name_default,
25337
+ vin_default,
25338
+ mac_address_default,
25339
+ national_id_default,
25340
+ zip_default
25341
+ ]
25342
+ },
25343
+ {
25344
+ packId: "secrets",
25345
+ name: "Secrets & Credentials",
25346
+ version: "0.1.0",
25347
+ rawRules: [
25348
+ aws_access_key_default,
25349
+ aws_secret_key_default,
25350
+ gcp_service_account_default,
25351
+ azure_connection_string_default,
25352
+ openai_api_key_default,
25353
+ anthropic_api_key_default,
25354
+ stripe_live_key_default,
25355
+ slack_token_default,
25356
+ discord_token_default,
25357
+ sendgrid_key_default,
25358
+ twilio_key_default,
25359
+ gitlab_token_default,
25360
+ digitalocean_token_default,
25361
+ datadog_key_default,
25362
+ npm_token_default,
25363
+ heroku_api_key_default,
25364
+ cloudflare_api_key_default,
25365
+ pulumi_access_token_default,
25366
+ terraform_cloud_token_default,
25367
+ vault_token_default,
25368
+ github_pat_default
25369
+ ]
25370
+ },
25371
+ {
25372
+ packId: "secrets-infra",
25373
+ name: "Infrastructure Secrets",
25374
+ version: "0.1.0",
25375
+ rawRules: [
25376
+ ssh_private_key_default,
25377
+ db_connection_string_default,
25378
+ jwt_token_default,
25379
+ basic_auth_url_default,
25380
+ basic_auth_header_default,
25381
+ pgp_private_key_default,
25382
+ bearer_token_default,
25383
+ env_key_value_default,
25384
+ password_field_default,
25385
+ docker_config_auth_default,
25386
+ kubeconfig_token_default,
25387
+ api_key_header_default,
25388
+ generic_high_entropy_secret_default
25389
+ ]
25390
+ }
25391
+ ];
25392
+
25393
+ // ../packages/plugin-sdk/src/rule-packs.ts
25394
+ var BUNDLED_NAMESPACE = "aka";
25395
+ function registerRulePack(packId, rawRules) {
25396
+ const rules = rawRules.map((raw) => Rule.parse(raw));
25397
+ registerPack({ id: packId, rules });
25398
+ }
25399
+ function registerBundledPacks() {
25400
+ for (const pack of BUNDLED_PACKS) registerRulePack(pack.packId, pack.rawRules);
25401
+ }
25402
+ var cachedDetections;
25403
+ function bundledDetections() {
25404
+ cachedDetections ??= BUNDLED_PACKS.map((pack) => ({
25405
+ namespace: BUNDLED_NAMESPACE,
25406
+ packId: pack.packId,
25407
+ version: pack.version,
25408
+ name: pack.name,
25409
+ rules: pack.rawRules.map((raw) => Rule.parse(raw))
25410
+ }));
25411
+ return cachedDetections;
25412
+ }
25413
+
25414
+ // ../packages/plugin-sdk/src/repo.ts
25415
+ import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
25416
+ import { basename, dirname as dirname2, isAbsolute, join as join8, sep as sep3 } from "path";
25417
+ function resolveRepoIdentity(cwd) {
25418
+ try {
25419
+ const root = findGitRoot(cwd);
25420
+ if (!root) return void 0;
25421
+ const ctx = resolveGitContext(root);
25422
+ const headRoot = ctx?.headRoot ?? root;
25423
+ const url2 = ctx ? remoteUrl(ctx) : void 0;
25424
+ return {
25425
+ // The path fallback is normalized to posix separators (a no-op outside
25426
+ // win32) so the persistence layer's `/`-separated checkout-path patterns
25427
+ // (the ghost sweep + the read-side worktree filter) match it as written.
25428
+ url: url2 ?? headRoot.split(sep3).join("/"),
25429
+ name: (url2 ? slugFromUrl(url2) : void 0) ?? basename(headRoot)
25430
+ };
25431
+ } catch {
25432
+ return void 0;
25433
+ }
25434
+ }
25435
+ function resolveWorktreeRoot(cwd) {
25436
+ try {
25437
+ return findGitRoot(cwd);
25438
+ } catch {
25439
+ return void 0;
25440
+ }
25441
+ }
25442
+ function resolveHeadRoot(cwd) {
25443
+ try {
25444
+ const root = findGitRoot(cwd);
25445
+ if (!root) return void 0;
25446
+ return resolveGitContext(root)?.headRoot ?? root;
25447
+ } catch {
25448
+ return void 0;
25449
+ }
25450
+ }
25451
+ function findGitRoot(start) {
25452
+ let dir = start;
25453
+ for (; ; ) {
25454
+ if (existsSync4(join8(dir, ".git"))) return dir;
25455
+ const parent = dirname2(dir);
25456
+ if (parent === dir) return void 0;
25457
+ dir = parent;
25458
+ }
25459
+ }
25460
+ function resolveGitContext(root) {
25461
+ const dotGit = join8(root, ".git");
25462
+ try {
25463
+ if (statSync2(dotGit).isDirectory()) {
25464
+ return { configPath: join8(dotGit, "config"), headRoot: root };
25465
+ }
25466
+ } catch {
25467
+ return void 0;
25468
+ }
25469
+ const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
25470
+ if (!target) return void 0;
25471
+ const gitdir = isAbsolute(target) ? target : join8(root, target);
25472
+ if (existsSync4(join8(gitdir, "config"))) {
25473
+ return { configPath: join8(gitdir, "config"), headRoot: root };
25474
+ }
25475
+ const commonRaw = safeRead(join8(gitdir, "commondir"))?.trim();
25476
+ if (!commonRaw) return void 0;
25477
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join8(gitdir, commonRaw);
25478
+ const headRoot = basename(commonGitDir) === ".git" ? dirname2(commonGitDir) : root;
25479
+ return { configPath: join8(commonGitDir, "config"), headRoot };
25480
+ }
25481
+ function safeRead(path) {
25482
+ try {
25483
+ return readFileSync5(path, "utf8");
25484
+ } catch {
25485
+ return void 0;
25486
+ }
25487
+ }
25488
+ function remoteUrl(ctx) {
25489
+ const config2 = safeRead(ctx.configPath);
25490
+ if (config2 === void 0) return void 0;
25491
+ const remotes = parseRemoteUrls(config2);
25492
+ return remotes.origin ?? Object.values(remotes)[0];
25493
+ }
25494
+ function parseRemoteUrls(config2) {
25495
+ const remotes = {};
25496
+ let current;
25497
+ for (const raw of config2.split("\n")) {
25498
+ const line = raw.trim();
25499
+ const header = /^\[remote "([^"]+)"\]$/.exec(line);
25500
+ if (header) {
25501
+ current = header[1];
25502
+ continue;
25503
+ }
25504
+ if (line.startsWith("[")) {
25505
+ current = void 0;
25506
+ continue;
25507
+ }
25508
+ if (current && !(current in remotes)) {
25509
+ const url2 = /^url\s*=\s*(.+)$/.exec(line)?.[1];
25510
+ if (url2) remotes[current] = url2.trim();
25511
+ }
25512
+ }
25513
+ return remotes;
25514
+ }
25515
+ function slugFromUrl(url2) {
25516
+ const segment = url2.replace(/\.git$/, "").split(/[/:]/).filter(Boolean).pop();
25517
+ return segment && segment.length > 0 ? segment : void 0;
25518
+ }
25519
+
25520
+ // ../packages/plugin-sdk/src/events.ts
25521
+ import { createHash as createHash4, randomUUID as randomUUID10 } from "crypto";
25522
+
25523
+ // ../packages/plugin-sdk/src/finding-key.ts
25524
+ import { createHash as createHash5 } from "crypto";
25525
+
25526
+ // ../packages/plugin-sdk/src/inventory-resolver.ts
25527
+ import { arch, hostname as hostname3, platform, release } from "os";
25528
+
25529
+ // ../packages/plugin-sdk/src/nudge.ts
25530
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
25531
+ import { join as join10 } from "path";
25532
+
25533
+ // ../packages/plugin-sdk/src/project-files.ts
25534
+ var import_ignore2 = __toESM(require_ignore(), 1);
25535
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
25536
+ import { basename as basename3, join as join11, relative as relative2, sep as sep4 } from "path";
25537
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
25538
+ ".git",
25539
+ "node_modules",
25540
+ ".pnpm-store",
25541
+ ".venv",
25542
+ "venv",
25543
+ "__pycache__",
25544
+ ".next",
25545
+ ".turbo",
25546
+ "dist",
25547
+ "build",
25548
+ "out",
25549
+ "coverage",
25550
+ "target",
25551
+ ".cache"
25552
+ ]);
25553
+ var MAX_FILES = 2e4;
25554
+ function readIgnoreLayer2(dir) {
25555
+ try {
25556
+ const content = readFileSync8(join11(dir, ".gitignore"), "utf8");
25557
+ return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
25558
+ } catch {
25559
+ return void 0;
25560
+ }
25561
+ }
25562
+ function isIgnored(layers, absPath, isDir) {
25563
+ let ignored = false;
25564
+ for (const layer of layers) {
25565
+ const rel = relative2(layer.base, absPath).split(sep4).join("/") + (isDir ? "/" : "");
25566
+ const verdict = layer.matcher.test(rel);
25567
+ if (verdict.ignored) ignored = true;
25568
+ else if (verdict.unignored) ignored = false;
25569
+ }
25570
+ return ignored;
25571
+ }
25572
+ var GENERATED_FILES = /* @__PURE__ */ new Set([
25573
+ "pnpm-lock.yaml",
25574
+ "package-lock.json",
25575
+ "yarn.lock",
25576
+ "cargo.lock",
25577
+ "poetry.lock",
25578
+ "gemfile.lock",
25579
+ "composer.lock",
25580
+ "go.sum"
25581
+ ]);
25582
+ var CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([".yml", ".yaml", ".toml", ".ini", ".properties", ".conf"]);
25583
+ var DOCS_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".rst", ".adoc", ".txt"]);
25584
+ var CONFIG_TXT_BASENAMES = /* @__PURE__ */ new Set(["cmakelists.txt", "constraints.txt", "robots.txt"]);
25585
+ var DATA_EXTENSIONS = /* @__PURE__ */ new Set([".csv", ".tsv", ".parquet", ".sql", ".jsonl", ".ndjson"]);
25586
+ var CONFIG_BASENAMES = /* @__PURE__ */ new Set([
25587
+ "package.json",
25588
+ "dockerfile",
25589
+ "makefile",
25590
+ "justfile",
25591
+ ".gitignore",
25592
+ ".gitattributes",
25593
+ ".npmrc",
25594
+ ".nvmrc",
25595
+ ".editorconfig",
25596
+ ".akaignore"
25597
+ ]);
25598
+ function classifyOrigin(relPath, name) {
25599
+ const lowerName = name.toLowerCase();
25600
+ const dot = lowerName.lastIndexOf(".");
25601
+ const ext = dot > 0 ? lowerName.slice(dot) : "";
25602
+ const segments = relPath.split("/");
25603
+ if (GENERATED_FILES.has(lowerName) || lowerName.includes(".generated.") || segments.includes("__generated__") || segments.includes("generated") || ext === ".map" || lowerName.endsWith(".min.js") || lowerName.endsWith(".min.css")) {
25604
+ return "generated";
25605
+ }
25606
+ if (segments.includes("vendor") || segments.includes("third_party")) return "vendored";
25607
+ if (CONFIG_TXT_BASENAMES.has(lowerName) || ext === ".txt" && lowerName.startsWith("requirements")) {
25608
+ return "config";
25609
+ }
25610
+ if (DOCS_EXTENSIONS.has(ext) || lowerName.startsWith("license") || segments[0] === "docs") {
25611
+ return "docs";
25612
+ }
25613
+ if (DATA_EXTENSIONS.has(ext) || segments.includes("fixtures") || segments.includes("__fixtures__")) {
25614
+ return "data";
25615
+ }
25616
+ if (CONFIG_BASENAMES.has(lowerName) || CONFIG_EXTENSIONS.has(ext) || lowerName.startsWith(".") || lowerName.includes(".config.") || lowerName.startsWith("tsconfig")) {
25617
+ return "config";
25618
+ }
25619
+ return "source";
25620
+ }
25621
+ function resolveProjectFiles(cwd) {
25622
+ try {
25623
+ let visit3 = function(dir, layers) {
25624
+ let dirents;
25625
+ try {
25626
+ dirents = readdirSync3(dir, { withFileTypes: true, encoding: "utf8" });
25627
+ } catch {
25628
+ walk.lostSubtree = true;
25629
+ return false;
25630
+ }
25631
+ const layer = readIgnoreLayer2(dir);
25632
+ const dirLayers = layer ? [...layers, layer] : layers;
25633
+ for (const entry of dirents) {
25634
+ const fullPath = join11(dir, entry.name);
25635
+ if (entry.isDirectory()) {
25636
+ if (SKIP_DIRS2.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
25637
+ if (existsSync5(join11(fullPath, ".git"))) continue;
25638
+ if (visit3(fullPath, dirLayers)) return true;
25639
+ continue;
25640
+ }
25641
+ if (!entry.isFile()) continue;
25642
+ if (entry.name === ".git") continue;
25643
+ if (isIgnored(dirLayers, fullPath, false)) continue;
25644
+ if (files.length >= MAX_FILES) return true;
25645
+ const relPath = relative2(root, fullPath).split(sep4).join("/");
25646
+ files.push({
25647
+ path: relPath,
25648
+ name: basename3(entry.name),
25649
+ origin: classifyOrigin(relPath, entry.name),
25650
+ defaultAccess: "approved"
25651
+ });
25652
+ }
25653
+ return false;
25654
+ };
25655
+ var visit2 = visit3;
25656
+ const worktree = resolveWorktreeRoot(cwd);
25657
+ if (!worktree) return void 0;
25658
+ const root = worktree;
25659
+ const isLinkedCheckout = resolveHeadRoot(cwd) !== worktree;
25660
+ const files = [];
25661
+ const walk = { lostSubtree: false };
25662
+ const truncated = visit3(root, []) || walk.lostSubtree || isLinkedCheckout;
25663
+ if (files.length === 0) return void 0;
25664
+ files.sort((a, b) => a.path.localeCompare(b.path));
25665
+ return { files, truncated, scannedAt: (/* @__PURE__ */ new Date()).toISOString() };
25666
+ } catch {
25667
+ return void 0;
25668
+ }
25669
+ }
25670
+
25671
+ // ../packages/plugin-sdk/src/runtime.ts
25672
+ import { randomUUID as randomUUID11 } from "crypto";
25673
+
25674
+ // ../packages/plugin-sdk/src/suppressions.ts
25675
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
25676
+
25677
+ // ../packages/plugin-sdk/src/throttle.ts
25678
+ import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
25679
+ import { join as join12 } from "path";
25680
+
25681
+ // ../packages/local-ops/src/project-inventory.ts
25682
+ function recordProjectInventory(db, target) {
25683
+ try {
25684
+ const abs = resolve(target);
25685
+ statSync5(abs);
25686
+ const identity = resolveRepoIdentity(abs);
25687
+ if (!identity) return null;
25688
+ const sourceProjectId2 = db.sourceProject.upsert({
25689
+ url: identity.url,
25690
+ name: identity.name,
25691
+ attributes: {}
25692
+ });
25693
+ const scan2 = resolveProjectFiles(abs);
25694
+ if (scan2) db.recordProjectFiles(sourceProjectId2, scan2);
25695
+ const headRoot = resolveHeadRoot(abs);
25696
+ const worktreeRoot = resolveWorktreeRoot(abs);
25697
+ if (headRoot && worktreeRoot) {
25698
+ db.reconcileWorktreeProjects(sourceProjectId2, headRoot, worktreeRoot);
25699
+ }
25700
+ return {
25701
+ projectId: sourceProjectId2,
25702
+ name: identity.name,
25703
+ url: identity.url,
25704
+ fileCount: scan2?.files.length ?? 0,
25705
+ truncated: scan2?.truncated ?? false
25706
+ };
25707
+ } catch {
25708
+ return null;
25709
+ }
25710
+ }
25711
+
25712
+ // ../packages/local-ops/src/update-cache.ts
25713
+ import { spawn } from "child_process";
25714
+ import { existsSync as existsSync6, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
25715
+ import { join as join13 } from "path";
25716
+ var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
25717
+ function cachePath(home) {
25718
+ return join13(dataDir(home), "update-check.json");
25719
+ }
25720
+ function readCache(home) {
25721
+ const path = cachePath(home);
25722
+ if (!existsSync6(path)) return null;
25723
+ try {
25724
+ const raw = JSON.parse(readFileSync9(path, "utf8"));
25725
+ if (!isRecord(raw) || typeof raw.checkedAt !== "number" || !isRecord(raw.report)) return null;
25726
+ return raw;
25727
+ } catch {
25728
+ return null;
25729
+ }
25730
+ }
25731
+ function writeCache(home, cache) {
25732
+ if (!existsSync6(dataDir(home))) return;
25733
+ try {
25734
+ writeFileSync6(cachePath(home), `${JSON.stringify(cache, null, 2)}
25735
+ `);
25736
+ } catch {
25737
+ }
25738
+ }
25739
+ function isStale(cache, ttlMs = CHECK_TTL_MS) {
25740
+ return Date.now() - cache.checkedAt >= ttlMs;
25741
+ }
25742
+ function clearCache(home) {
25743
+ try {
25744
+ rmSync2(cachePath(home), { force: true });
25745
+ } catch {
25746
+ }
25747
+ }
25748
+ function refreshCache(home) {
25749
+ const previous = readCache(home);
25750
+ writeCache(home, {
25751
+ checkedAt: Date.now(),
25752
+ report: gatherReportLive(),
25753
+ notifiedPluginIds: previous?.notifiedPluginIds ?? []
25754
+ });
25755
+ }
25756
+ function triggerBackgroundRefresh(home) {
25757
+ const entry = process.argv[1];
25758
+ if (!entry) return;
25759
+ try {
25760
+ spawn(process.execPath, [entry, "__update-refresh", "--home", home], {
25761
+ detached: true,
25762
+ stdio: "ignore"
25763
+ }).unref();
25764
+ } catch {
25765
+ }
25766
+ }
25767
+ function notifyFromCache(home, opts) {
25768
+ if (!opts.isTty) return;
25769
+ const cache = readCache(home);
25770
+ if (cache) {
25771
+ const lines = [];
25772
+ for (const s of cache.report.statuses) {
25773
+ if (!s.updateAvailable || !s.latest) continue;
25774
+ const cmd = s.kind === "cli" ? "aka update" : `aka update ${s.id}`;
25775
+ lines.push(` \u2B06 ${s.name}: ${s.installed ?? "\u2014"} \u2192 ${s.latest} available. Run \`${cmd}\`.`);
25776
+ }
25777
+ const alreadyShown = new Set(cache.notifiedPluginIds);
25778
+ const freshlyShown = [];
25779
+ for (const p of cache.report.availablePlugins) {
25780
+ if (alreadyShown.has(p.id)) continue;
25781
+ lines.push(
25782
+ ` \u2728 New plugin available: ${p.name}. Install with \`aka plugins install ${p.id}\`.`
25783
+ );
25784
+ freshlyShown.push(p.id);
25785
+ }
25786
+ if (lines.length > 0) {
25787
+ process.stderr.write(`
25788
+ ${lines.join("\n")}
25789
+ `);
25790
+ }
25791
+ if (freshlyShown.length > 0) {
25792
+ writeCache(home, {
25793
+ ...cache,
25794
+ notifiedPluginIds: [...cache.notifiedPluginIds, ...freshlyShown]
25795
+ });
25796
+ }
25797
+ }
25798
+ if (!cache || isStale(cache)) triggerBackgroundRefresh(home);
25799
+ }
25800
+
25801
+ // ../packages/local-ops/src/update-render.ts
25802
+ function statusLabel(s) {
25803
+ if (s.installed === null || s.latest === null) return "unknown";
25804
+ return s.updateAvailable ? "update available" : "up to date";
25805
+ }
25806
+ function renderReport(report) {
25807
+ const rows = report.statuses.map((s) => ({
25808
+ name: s.name,
25809
+ installed: s.installed ?? "\u2014",
25810
+ latest: s.latest ?? "unknown",
25811
+ status: statusLabel(s)
25812
+ }));
25813
+ const nameW = Math.max(9, ...rows.map((r) => r.name.length));
25814
+ const instW = Math.max(9, ...rows.map((r) => r.installed.length));
25815
+ const latW = Math.max(6, ...rows.map((r) => r.latest.length));
25816
+ const lines = [];
25817
+ lines.push(
25818
+ ` ${"Component".padEnd(nameW)} ${"Installed".padEnd(instW)} ${"Latest".padEnd(latW)} Status`
25819
+ );
25820
+ for (const r of rows) {
25821
+ lines.push(
25822
+ ` ${r.name.padEnd(nameW)} ${r.installed.padEnd(instW)} ${r.latest.padEnd(latW)} ${r.status}`
25823
+ );
25824
+ }
25825
+ if (report.availablePlugins.length > 0) {
25826
+ lines.push("");
25827
+ lines.push(" Available plugins (not installed):");
25828
+ for (const p of report.availablePlugins) {
25829
+ const version2 = p.latest ? ` v${p.latest}` : "";
25830
+ lines.push(` ${p.name} (${p.id})${version2} \u2014 install: aka plugins install ${p.id}`);
25831
+ }
25832
+ }
25833
+ return lines.join("\n");
25834
+ }
25835
+ function outdated(report) {
25836
+ return report.statuses.filter((s) => s.updateAvailable);
25837
+ }
25838
+
25839
+ // src/command-manifest.ts
25840
+ var COMMAND_SPECS = [
25841
+ { name: "init", summary: "Scaffold ~/.aka (settings + local database)" },
25842
+ {
25843
+ name: "scan",
25844
+ argHint: "[path]",
25845
+ summary: "Scan a file or directory and record findings (default: .)",
25846
+ completesFiles: true
25847
+ },
25848
+ { name: "stats", summary: "Print findings / enforcement / detections from the local store" },
25849
+ {
25850
+ name: "detections",
25851
+ argHint: "[update]",
25852
+ summary: "List installed detection packs + available updates (updates are manual)",
25853
+ args: ["update"]
25854
+ },
25855
+ {
25856
+ name: "plugins",
25857
+ summary: "List / install agent plugins (the CLI is an optional hub)",
25858
+ args: ["list", "install"]
25859
+ },
25860
+ { name: "dashboard", summary: "Launch the local web dashboard and open it in the browser" },
25861
+ {
25862
+ name: "exception",
25863
+ summary: "Manage detection exceptions (approve, add, list, show, revoke, rotate-key)",
25864
+ args: ["approve", "add", "list", "show", "revoke", "rotate-key"]
25865
+ },
25866
+ {
25867
+ name: "tui",
25868
+ argHint: "[view]",
25869
+ summary: "Interactive colour dashboard \u2014 health|findings|recommend|audit (needs a TTY)",
25870
+ args: ["health", "findings", "recommend", "audit"]
25059
25871
  },
25060
25872
  {
25061
- packId: "core-pii",
25062
- name: "Core PII",
25063
- version: "0.2.0",
25064
- rawRules: [
25065
- email_default,
25066
- ssn_default,
25067
- phone_us_default,
25068
- phone_intl_default,
25069
- ip_address_default,
25070
- dob_default,
25071
- drivers_license_us_default,
25072
- passport_us_default,
25073
- home_address_default,
25074
- name_default,
25075
- vin_default,
25076
- mac_address_default,
25077
- national_id_default,
25078
- zip_default
25079
- ]
25873
+ name: "check-updates",
25874
+ summary: "Show which components (CLI + plugins) have updates available"
25080
25875
  },
25081
25876
  {
25082
- packId: "secrets",
25083
- name: "Secrets & Credentials",
25084
- version: "0.1.0",
25085
- rawRules: [
25086
- aws_access_key_default,
25087
- aws_secret_key_default,
25088
- gcp_service_account_default,
25089
- azure_connection_string_default,
25090
- openai_api_key_default,
25091
- anthropic_api_key_default,
25092
- stripe_live_key_default,
25093
- slack_token_default,
25094
- discord_token_default,
25095
- sendgrid_key_default,
25096
- twilio_key_default,
25097
- gitlab_token_default,
25098
- digitalocean_token_default,
25099
- datadog_key_default,
25100
- npm_token_default,
25101
- heroku_api_key_default,
25102
- cloudflare_api_key_default,
25103
- pulumi_access_token_default,
25104
- terraform_cloud_token_default,
25105
- vault_token_default,
25106
- github_pat_default
25107
- ]
25877
+ name: "update",
25878
+ argHint: "[what]",
25879
+ summary: "Update the CLI and/or plugins (what: cli | <plugin-id> | all)",
25880
+ args: ["cli", "all"]
25108
25881
  },
25109
25882
  {
25110
- packId: "secrets-infra",
25111
- name: "Infrastructure Secrets",
25112
- version: "0.1.0",
25113
- rawRules: [
25114
- ssh_private_key_default,
25115
- db_connection_string_default,
25116
- jwt_token_default,
25117
- basic_auth_url_default,
25118
- basic_auth_header_default,
25119
- pgp_private_key_default,
25120
- bearer_token_default,
25121
- env_key_value_default,
25122
- password_field_default,
25123
- docker_config_auth_default,
25124
- kubeconfig_token_default,
25125
- api_key_header_default,
25126
- generic_high_entropy_secret_default
25127
- ]
25883
+ name: "completion",
25884
+ argHint: "<sh>",
25885
+ summary: "Emit a shell completion script (sh: zsh | bash)",
25886
+ args: ["zsh", "bash"]
25128
25887
  }
25129
25888
  ];
25130
-
25131
- // ../packages/plugin-sdk/src/rule-packs.ts
25132
- var BUNDLED_NAMESPACE = "aka";
25133
- function registerRulePack(packId, rawRules) {
25134
- const rules = rawRules.map((raw) => Rule.parse(raw));
25135
- registerPack({ id: packId, rules });
25136
- }
25137
- function registerBundledPacks() {
25138
- for (const pack of BUNDLED_PACKS) registerRulePack(pack.packId, pack.rawRules);
25139
- }
25140
- var cachedDetections;
25141
- function bundledDetections() {
25142
- cachedDetections ??= BUNDLED_PACKS.map((pack) => ({
25143
- namespace: BUNDLED_NAMESPACE,
25144
- packId: pack.packId,
25145
- version: pack.version,
25146
- name: pack.name,
25147
- rules: pack.rawRules.map((raw) => Rule.parse(raw))
25148
- }));
25149
- return cachedDetections;
25889
+ var GLOBAL_FLAGS = ["--home", "--no-update-check", "--version", "--help"];
25890
+ function commandsHelp() {
25891
+ const label = (s) => s.argHint ? `${s.name} ${s.argHint}` : s.name;
25892
+ const width = Math.max(...COMMAND_SPECS.map((s) => label(s).length)) + 1;
25893
+ return COMMAND_SPECS.map((s) => ` ${label(s).padEnd(width)}${s.summary}`).join("\n");
25150
25894
  }
25151
25895
 
25152
- // ../packages/plugin-sdk/src/repo.ts
25153
- import { existsSync as existsSync4, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
25154
- import { basename, dirname as dirname2, isAbsolute, join as join8, sep as sep3 } from "path";
25155
-
25156
- // ../packages/plugin-sdk/src/events.ts
25157
- import { createHash as createHash4, randomUUID as randomUUID10 } from "crypto";
25158
-
25159
- // ../packages/plugin-sdk/src/finding-key.ts
25160
- import { createHash as createHash5 } from "crypto";
25161
-
25162
- // ../packages/plugin-sdk/src/inventory-resolver.ts
25163
- import { arch, hostname as hostname3, platform, release } from "os";
25164
-
25165
- // ../packages/plugin-sdk/src/nudge.ts
25166
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
25167
- import { join as join10 } from "path";
25168
-
25169
- // ../packages/plugin-sdk/src/project-files.ts
25170
- var import_ignore2 = __toESM(require_ignore(), 1);
25171
- import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
25172
- import { basename as basename3, join as join11, relative as relative2, sep as sep4 } from "path";
25173
-
25174
- // ../packages/plugin-sdk/src/runtime.ts
25175
- import { randomUUID as randomUUID11 } from "crypto";
25176
-
25177
- // ../packages/plugin-sdk/src/throttle.ts
25178
- import { mkdirSync as mkdirSync4, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
25179
- import { join as join12 } from "path";
25896
+ // src/commands/check-updates.ts
25897
+ import { existsSync as existsSync7 } from "fs";
25898
+ import { parseArgs } from "util";
25180
25899
 
25181
25900
  // src/lib/args.ts
25182
25901
  var HOME_OPTION = { home: { type: "string" } };
@@ -25206,7 +25925,7 @@ function runCheckUpdates(argv) {
25206
25925
  out.write(`${String(ups.length)} update(s) available \u2014 run \`aka update\` to apply.
25207
25926
  `);
25208
25927
  }
25209
- if (existsSync6(dataDir(home))) {
25928
+ if (existsSync7(dataDir(home))) {
25210
25929
  const notifiedPluginIds = readCache(home)?.notifiedPluginIds ?? [];
25211
25930
  writeCache(home, { checkedAt: Date.now(), report, notifiedPluginIds });
25212
25931
  }
@@ -25360,10 +26079,10 @@ function runCompletion(argv) {
25360
26079
 
25361
26080
  // src/commands/dashboard.ts
25362
26081
  import { spawn as spawn3 } from "child_process";
25363
- import { existsSync as existsSync7 } from "fs";
26082
+ import { existsSync as existsSync8 } from "fs";
25364
26083
  import { createRequire } from "module";
25365
26084
  import { createServer } from "net";
25366
- import { dirname as dirname3, join as join13 } from "path";
26085
+ import { dirname as dirname3, join as join14 } from "path";
25367
26086
  import { fileURLToPath as fileURLToPath2 } from "url";
25368
26087
  import { parseArgs as parseArgs2 } from "util";
25369
26088
 
@@ -25383,14 +26102,14 @@ function openUrl(url2) {
25383
26102
  var require2 = createRequire(import.meta.url);
25384
26103
  var here = dirname3(fileURLToPath2(import.meta.url));
25385
26104
  function isPortFree(port) {
25386
- return new Promise((resolve) => {
26105
+ return new Promise((resolve2) => {
25387
26106
  const probe = createServer();
25388
26107
  probe.once("error", () => {
25389
- resolve(false);
26108
+ resolve2(false);
25390
26109
  });
25391
26110
  probe.once("listening", () => {
25392
26111
  probe.close(() => {
25393
- resolve(true);
26112
+ resolve2(true);
25394
26113
  });
25395
26114
  });
25396
26115
  probe.listen(port, "127.0.0.1");
@@ -25433,9 +26152,9 @@ async function runDashboard(argv) {
25433
26152
  }
25434
26153
  refreshUpdateMirror(dataDir());
25435
26154
  const bundledServer = [
25436
- join13(here, "..", "web-ui", "server.js"),
25437
- join13(here, "..", "web-ui", "web-ui", "server.js")
25438
- ].find((p) => existsSync7(p));
26155
+ join14(here, "..", "web-ui", "server.js"),
26156
+ join14(here, "..", "web-ui", "web-ui", "server.js")
26157
+ ].find((p) => existsSync8(p));
25439
26158
  if (bundledServer) {
25440
26159
  launchStandalone(bundledServer, port, shouldOpen, url2);
25441
26160
  return;
@@ -25454,7 +26173,7 @@ async function runDashboard(argv) {
25454
26173
  onReadyOpen(child, shouldOpen, url2, `Starting the AKA dashboard at ${url2}
25455
26174
  `);
25456
26175
  };
25457
- if (existsSync7(join13(webUiDir, ".next", "BUILD_ID"))) {
26176
+ if (existsSync8(join14(webUiDir, ".next", "BUILD_ID"))) {
25458
26177
  startNext();
25459
26178
  return;
25460
26179
  }
@@ -25680,11 +26399,11 @@ function terminalPrompter() {
25680
26399
  },
25681
26400
  isInteractive: process.stdin.isTTY && process.stdout.isTTY,
25682
26401
  ask(question) {
25683
- return new Promise((resolve) => {
26402
+ return new Promise((resolve2) => {
25684
26403
  const rl = createInterface({ input: process.stdin, output: process.stdout });
25685
26404
  rl.question(question, (answer) => {
25686
26405
  rl.close();
25687
- resolve(answer);
26406
+ resolve2(answer);
25688
26407
  });
25689
26408
  });
25690
26409
  },
@@ -25695,7 +26414,7 @@ function terminalPrompter() {
25695
26414
  // fatal signals: a security tool must never brick the terminal it was
25696
26415
  // invoked from.
25697
26416
  askHidden(question) {
25698
- return new Promise((resolve, reject) => {
26417
+ return new Promise((resolve2, reject) => {
25699
26418
  const stdin = process.stdin;
25700
26419
  process.stdout.write(question);
25701
26420
  stdin.setRawMode(true);
@@ -25718,7 +26437,7 @@ function terminalPrompter() {
25718
26437
  restore();
25719
26438
  process.stdout.write("\n");
25720
26439
  if (err) reject(err);
25721
- else resolve(state.value);
26440
+ else resolve2(state.value);
25722
26441
  };
25723
26442
  const onError = (err) => {
25724
26443
  finish(err);
@@ -25881,6 +26600,8 @@ function viaLabel(via) {
25881
26600
  return "web add";
25882
26601
  case "api":
25883
26602
  return "api";
26603
+ case "setup-triage":
26604
+ return "setup triage";
25884
26605
  }
25885
26606
  }
25886
26607
  function scopeSummary(ex) {
@@ -26387,50 +27108,14 @@ async function runException(argv, io = terminalPrompter()) {
26387
27108
  }
26388
27109
 
26389
27110
  // src/commands/init.ts
26390
- import { existsSync as existsSync8, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "fs";
26391
- import { join as join14 } from "path";
26392
- import { parseArgs as parseArgs5 } from "util";
26393
- async function runInit(argv) {
26394
- const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
26395
- const home = homeBase(values.home);
26396
- ensureLayoutDirSync(home);
26397
- const settings = settingsDir(home);
26398
- ensureLayoutDirSync(settings);
26399
- const settingsFile = join14(settings, "settings.json");
26400
- const settingsCreated = !existsSync8(settingsFile);
26401
- if (settingsCreated) {
26402
- const tmp = `${settingsFile}.tmp`;
26403
- writeFileSync6(tmp, `${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
26404
- `, {
26405
- mode: DATA_FILE_MODE
26406
- });
26407
- renameSync5(tmp, settingsFile);
26408
- }
26409
- const db = openLocalDatabase(dataDir(home));
26410
- let policyCount;
26411
- let packCount;
26412
- let updatesAvailable;
26413
- try {
26414
- db.installedPacks.recordInventory(bundledDetections(), cliRecordedBy());
26415
- policyCount = (await db.policies.readPolicies()).length;
26416
- packCount = (await db.installedPacks.counts()).packs;
26417
- updatesAvailable = (await db.detections.listDetections({ filter: "all" })).counts.updates;
26418
- } finally {
26419
- db.close();
26420
- }
26421
- process.stdout.write(
26422
- `\u2713 Initialized AKA at ${home}
26423
- settings: ${settingsFile}${settingsCreated ? "" : " (kept existing)"}
26424
- database: ${dbPath(home)}
26425
- seeded ${String(policyCount)} default policies, ${String(packCount)} detection pack(s)
26426
- ` + (updatesAvailable > 0 ? ` \u2B06 ${String(updatesAvailable)} detection pack update(s) available \u2014 review with \`aka detections\`, apply with \`aka detections update --all\`
26427
- ` : "")
26428
- );
26429
- }
27111
+ import { existsSync as existsSync10, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
27112
+ import { join as join15 } from "path";
27113
+ import * as readline from "readline/promises";
27114
+ import { parseArgs as parseArgs6 } from "util";
26430
27115
 
26431
27116
  // src/commands/plugins.ts
26432
27117
  import { existsSync as existsSync9 } from "fs";
26433
- import { parseArgs as parseArgs6 } from "util";
27118
+ import { parseArgs as parseArgs5 } from "util";
26434
27119
  function runPlugins(argv) {
26435
27120
  const [sub, ...rest] = argv;
26436
27121
  if (!sub || sub === "list") return listPlugins(rest);
@@ -26443,7 +27128,7 @@ function runPlugins(argv) {
26443
27128
  process.exitCode = 1;
26444
27129
  }
26445
27130
  async function listPlugins(argv) {
26446
- const { values } = parseArgs6({ args: argv, options: HOME_OPTION });
27131
+ const { values } = parseArgs5({ args: argv, options: HOME_OPTION });
26447
27132
  const home = homeBase(values.home);
26448
27133
  const active = /* @__PURE__ */ new Set();
26449
27134
  if (existsSync9(dbPath(home))) {
@@ -26471,7 +27156,7 @@ async function listPlugins(argv) {
26471
27156
  out.write("Update: aka update (or: aka check-updates)\n");
26472
27157
  }
26473
27158
  function installPlugin(argv) {
26474
- const { positionals } = parseArgs6({ args: argv, options: HOME_OPTION, allowPositionals: true });
27159
+ const { positionals } = parseArgs5({ args: argv, options: HOME_OPTION, allowPositionals: true });
26475
27160
  const id = positionals[0];
26476
27161
  if (!id) {
26477
27162
  process.stderr.write("aka plugins install: missing <agent> (try: aka plugins list)\n");
@@ -26528,11 +27213,95 @@ Then run \`aka init\` to set up the local store.
26528
27213
  }
26529
27214
  }
26530
27215
 
27216
+ // src/commands/init.ts
27217
+ async function runInit(argv) {
27218
+ const { values } = parseArgs6({
27219
+ args: argv,
27220
+ options: { ...HOME_OPTION, yes: { type: "boolean", short: "y" } }
27221
+ });
27222
+ const home = homeBase(values.home);
27223
+ ensureLayoutDirSync(home);
27224
+ const settings = settingsDir(home);
27225
+ ensureLayoutDirSync(settings);
27226
+ const settingsFile = join15(settings, "settings.json");
27227
+ const settingsCreated = !existsSync10(settingsFile);
27228
+ if (settingsCreated) {
27229
+ const tmp = `${settingsFile}.tmp`;
27230
+ writeFileSync7(tmp, `${JSON.stringify(defaultWorkspaceSettings(), null, 2)}
27231
+ `, {
27232
+ mode: DATA_FILE_MODE
27233
+ });
27234
+ renameSync5(tmp, settingsFile);
27235
+ }
27236
+ const db = openLocalDatabase(dataDir(home));
27237
+ let policyCount;
27238
+ let packCount;
27239
+ let updatesAvailable;
27240
+ try {
27241
+ db.installedPacks.recordInventory(bundledDetections(), cliRecordedBy());
27242
+ policyCount = (await db.policies.readPolicies()).length;
27243
+ packCount = (await db.installedPacks.counts()).packs;
27244
+ updatesAvailable = (await db.detections.listDetections({ filter: "all" })).counts.updates;
27245
+ } finally {
27246
+ db.close();
27247
+ }
27248
+ process.stdout.write(
27249
+ `\u2713 Initialized AKA at ${home}
27250
+ settings: ${settingsFile}${settingsCreated ? "" : " (kept existing)"}
27251
+ database: ${dbPath(home)}
27252
+ seeded ${String(policyCount)} default policies, ${String(packCount)} detection pack(s)
27253
+ ` + (updatesAvailable > 0 ? ` \u2B06 ${String(updatesAvailable)} detection pack update(s) available \u2014 review with \`aka detections\`, apply with \`aka detections update --all\`
27254
+ ` : "")
27255
+ );
27256
+ await offerPluginInstall(values.yes === true);
27257
+ }
27258
+ async function offerPluginInstall(autoYes) {
27259
+ const agent = findAgent("claude-code");
27260
+ const ref = agent ? pluginRef(agent) : void 0;
27261
+ if (!agent || !ref) return;
27262
+ if (installedPluginVersions().has(ref)) return;
27263
+ const out = process.stdout;
27264
+ if (!autoYes) {
27265
+ if (!process.stdin.isTTY) {
27266
+ out.write(
27267
+ `
27268
+ No Claude Code plugin detected. Install it via the marketplace:
27269
+ /plugin marketplace add ${agent.marketplaceSource ?? ""}
27270
+ /plugin install ${ref}
27271
+ Or re-run \`aka init --yes\` to install it automatically.
27272
+ `
27273
+ );
27274
+ return;
27275
+ }
27276
+ if (!await confirm(`
27277
+ Install the AKA Claude Code plugin now (via the marketplace)? [y/N] `)) {
27278
+ out.write("Skipped. Install anytime with `aka plugins install claude-code`.\n");
27279
+ return;
27280
+ }
27281
+ }
27282
+ await runPlugins(["install", "claude-code"]);
27283
+ }
27284
+ async function confirm(question) {
27285
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
27286
+ try {
27287
+ const answer = (await rl.question(question)).trim().toLowerCase();
27288
+ return answer === "y" || answer === "yes";
27289
+ } finally {
27290
+ rl.close();
27291
+ }
27292
+ }
27293
+
26531
27294
  // src/commands/scan.ts
27295
+ import { statSync as statSync6 } from "fs";
26532
27296
  import { parseArgs as parseArgs7 } from "util";
26533
27297
  function severityRank(s) {
26534
27298
  return Severity.options.indexOf(s);
26535
27299
  }
27300
+ function renderInventoryLine(inv) {
27301
+ if (inv.fileCount === 0) return `Inventory: ${inv.name} \xB7 file tree unchanged`;
27302
+ const partial2 = inv.truncated ? " (partial walk)" : "";
27303
+ return `Inventory: ${inv.name} \xB7 ${String(inv.fileCount)} project file(s) recorded${partial2}`;
27304
+ }
26536
27305
  function runScan(argv) {
26537
27306
  const { values, positionals } = parseArgs7({
26538
27307
  args: argv,
@@ -26565,12 +27334,22 @@ function runScan(argv) {
26565
27334
  }
26566
27335
  failOn = parsed.data;
26567
27336
  }
27337
+ try {
27338
+ statSync6(target);
27339
+ } catch {
27340
+ process.stderr.write(`aka scan: no such file or directory: ${target}
27341
+ `);
27342
+ process.exitCode = 1;
27343
+ return;
27344
+ }
26568
27345
  registerBundledPacks();
26569
27346
  const db = openLocalDatabase(dataDir(home));
26570
27347
  let result;
27348
+ let inventory;
26571
27349
  try {
26572
27350
  const { ruleActions } = db.installedPacks.installedRuleset();
26573
27351
  result = scanPathIntoStore(db, target, { ruleActions, sourceTool: "cli" });
27352
+ inventory = recordProjectInventory(db, target);
26574
27353
  } finally {
26575
27354
  db.close();
26576
27355
  }
@@ -26588,8 +27367,14 @@ function runScan(argv) {
26588
27367
  confidence: d.confidence
26589
27368
  }))
26590
27369
  );
27370
+ const inventoryJson = inventory ? {
27371
+ name: inventory.name,
27372
+ url: inventory.url,
27373
+ fileCount: inventory.fileCount,
27374
+ truncated: inventory.truncated
27375
+ } : null;
26591
27376
  process.stdout.write(
26592
- `${JSON.stringify({ target, scanned: result.scanned, findings }, null, 2)}
27377
+ `${JSON.stringify({ target, scanned: result.scanned, findings, inventory: inventoryJson }, null, 2)}
26593
27378
  `
26594
27379
  );
26595
27380
  } else {
@@ -26597,6 +27382,8 @@ function runScan(argv) {
26597
27382
  `Scanned ${String(result.scanned)} file(s) under ${target} \xB7 ${String(result.findings)} finding(s) recorded
26598
27383
  `
26599
27384
  );
27385
+ if (inventory) process.stdout.write(`${renderInventoryLine(inventory)}
27386
+ `);
26600
27387
  }
26601
27388
  if (failOn !== void 0) {
26602
27389
  const threshold = severityRank(failOn);
@@ -27335,7 +28122,7 @@ async function runTui(argv) {
27335
28122
  }
27336
28123
 
27337
28124
  // src/commands/update.ts
27338
- import * as readline from "readline/promises";
28125
+ import * as readline2 from "readline/promises";
27339
28126
  import { parseArgs as parseArgs10 } from "util";
27340
28127
  async function runUpdate(argv) {
27341
28128
  const { values, positionals } = parseArgs10({
@@ -27385,7 +28172,7 @@ async function runUpdate(argv) {
27385
28172
  out.write("Re-run with --yes to apply (no interactive terminal detected).\n");
27386
28173
  return;
27387
28174
  }
27388
- if (!await confirm("Apply these updates? [y/N] ")) {
28175
+ if (!await confirm2("Apply these updates? [y/N] ")) {
27389
28176
  out.write("Aborted \u2014 nothing changed.\n");
27390
28177
  return;
27391
28178
  }
@@ -27404,8 +28191,8 @@ async function runUpdate(argv) {
27404
28191
  }
27405
28192
  if (anyFailed) process.exitCode = 1;
27406
28193
  }
27407
- async function confirm(question) {
27408
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
28194
+ async function confirm2(question) {
28195
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
27409
28196
  try {
27410
28197
  const answer = (await rl.question(question)).trim().toLowerCase();
27411
28198
  return answer === "y" || answer === "yes";