@akasecurity/ai-tc-claude-code 0.8.1 → 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.
package/scripts/query.js CHANGED
@@ -15489,6 +15489,11 @@ var AuditEventType = external_exports.enum([
15489
15489
  "prompt",
15490
15490
  "response",
15491
15491
  "code_change",
15492
+ // The events.kind of a scanned tool call, widened in to keep this a
15493
+ // superset. Narrower than 'tool_call' above and not a duplicate of it:
15494
+ // 'tool_call' is the reconciler's structural row for every call, while
15495
+ // 'tool_use' exists only where a hook enforced against the arguments.
15496
+ "tool_use",
15492
15497
  // One row per config-inventory scan, hung off the session root. It is the
15493
15498
  // fact the posture inspection findings reference (findings require an
15494
15499
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -15880,7 +15885,7 @@ var ActivityOverviewResponse = external_exports.object({
15880
15885
  }).meta({ id: "ActivityOverviewResponse" });
15881
15886
 
15882
15887
  // ../../packages/schema/src/zod/event.ts
15883
- var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
15888
+ var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15884
15889
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15885
15890
  var EventMetadata = external_exports.object({
15886
15891
  sessionId: external_exports.string().optional(),
@@ -16193,7 +16198,7 @@ var DetectionException = external_exports.object({
16193
16198
  justification: external_exports.string().min(1),
16194
16199
  conditions: ExceptionConditions.nullable(),
16195
16200
  createdBy: external_exports.string(),
16196
- createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
16201
+ createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
16197
16202
  createdAt: external_exports.iso.datetime(),
16198
16203
  updatedAt: external_exports.iso.datetime(),
16199
16204
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16362,20 +16367,26 @@ var PolicyBundle = external_exports.object({
16362
16367
  customKeywords: external_exports.array(external_exports.string()),
16363
16368
  fetchedAt: external_exports.iso.datetime()
16364
16369
  }).meta({ id: "PolicyBundle" });
16365
- var DEFAULT_ACTIONS = {
16366
- secret: "block",
16367
- pii: "redact",
16368
- financial: "redact",
16369
- phi: "redact",
16370
- code_context: "warn",
16371
- code_flaw: "warn",
16372
- custom: "warn",
16373
- // Config-posture findings only observe today (they land in
16374
- // inspection_findings, outside the live-capture enforcement path).
16375
- config: "warn"
16376
- };
16377
16370
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16378
16371
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
16372
+ var CATEGORY_PEAK_SEVERITY = {
16373
+ secret: "critical",
16374
+ financial: "critical",
16375
+ // core-financial/credit-card
16376
+ code_flaw: "critical",
16377
+ pii: "high",
16378
+ phi: "high",
16379
+ custom: "high",
16380
+ // user-defined; conservative
16381
+ code_context: "low",
16382
+ config: "low"
16383
+ // observe-only; floors to monitor regardless
16384
+ };
16385
+ function severityFloorPolicy(category) {
16386
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
16387
+ const peak = CATEGORY_PEAK_SEVERITY[category];
16388
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
16389
+ }
16379
16390
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
16380
16391
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
16381
16392
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -16402,6 +16413,12 @@ var BUILTIN_POLICY_SPECS = {
16402
16413
  description: "Refuse the request entirely whenever any rule in this detection matches."
16403
16414
  }
16404
16415
  };
16416
+ function builtinPolicyToAction(id) {
16417
+ return BUILTIN_POLICY_SPECS[id].action;
16418
+ }
16419
+ var DEFAULT_ACTIONS = Object.fromEntries(
16420
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
16421
+ );
16405
16422
  var BUILTIN_POLICIES = Object.fromEntries(
16406
16423
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
16407
16424
  );
@@ -16808,10 +16825,8 @@ function toApiProvider(sourceTool) {
16808
16825
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
16809
16826
  }
16810
16827
  var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
16811
- function deriveGroupStatus(instances) {
16812
- const statuses = new Set(
16813
- instances.map((i) => i.status).filter((s) => s !== void 0)
16814
- );
16828
+ function foldGroupStatus(instanceStatuses) {
16829
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
16815
16830
  if (statuses.size === 0) return void 0;
16816
16831
  for (const candidate of STATUS_PRECEDENCE) {
16817
16832
  if (statuses.has(candidate)) return candidate;
@@ -16829,6 +16844,7 @@ function deriveFindingStatus(row) {
16829
16844
  function buildFindingGroups(rows, opts = {}) {
16830
16845
  const overrides = opts.overrides;
16831
16846
  const packNames = opts.packNames;
16847
+ const aggregates = opts.aggregates;
16832
16848
  const byRuleId = /* @__PURE__ */ new Map();
16833
16849
  for (const row of rows) {
16834
16850
  const existing = byRuleId.get(row.ruleId);
@@ -16850,17 +16866,20 @@ function buildFindingGroups(rows, opts = {}) {
16850
16866
  status: r.status
16851
16867
  };
16852
16868
  });
16853
- const latestDetectedAt = ruleRows.reduce(
16869
+ const agg = aggregates?.get(ruleId);
16870
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
16854
16871
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
16855
16872
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
16856
16873
  );
16857
16874
  const seenProviders = /* @__PURE__ */ new Set();
16858
- const providers = instances.map((i) => i.provider).filter((p) => {
16875
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
16859
16876
  if (seenProviders.has(p)) return false;
16860
16877
  seenProviders.add(p);
16861
16878
  return true;
16862
16879
  });
16863
- const actionSet = new Set(instances.map((i) => i.action));
16880
+ const actionSet = new Set(
16881
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
16882
+ );
16864
16883
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
16865
16884
  const severity = ruleRows[0]?.severity ?? "low";
16866
16885
  const detection = {
@@ -16874,8 +16893,10 @@ function buildFindingGroups(rows, opts = {}) {
16874
16893
  contextPrefix: ""
16875
16894
  // empty (pending privacy review)
16876
16895
  };
16877
- const status = deriveGroupStatus(instances);
16878
- groups.push({
16896
+ const status = foldGroupStatus(
16897
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
16898
+ );
16899
+ const group = {
16879
16900
  id: ruleId,
16880
16901
  category: apiCategory,
16881
16902
  subtype: ruleId,
@@ -16884,21 +16905,26 @@ function buildFindingGroups(rows, opts = {}) {
16884
16905
  match,
16885
16906
  detection,
16886
16907
  policy,
16887
- instanceCount: instances.length,
16908
+ instanceCount: agg?.instanceCount ?? instances.length,
16888
16909
  providers,
16889
16910
  aggregateAction,
16890
16911
  latestDetectedAt,
16891
16912
  instances,
16892
16913
  status
16893
- });
16914
+ };
16915
+ if (agg) {
16916
+ actionsCache.set(group, [...actionSet]);
16917
+ if (agg.searchText !== void 0) {
16918
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
16919
+ }
16920
+ }
16921
+ groups.push(group);
16894
16922
  }
16895
16923
  return groups;
16896
16924
  }
16897
16925
  var haystackCache = /* @__PURE__ */ new WeakMap();
16898
- function groupHaystack(g) {
16899
- const cached2 = haystackCache.get(g);
16900
- if (cached2 !== void 0) return cached2;
16901
- const haystack = [
16926
+ function buildHaystack(g, extra) {
16927
+ return [
16902
16928
  g.subtype,
16903
16929
  g.category,
16904
16930
  g.match.maskedValue,
@@ -16906,11 +16932,25 @@ function groupHaystack(g) {
16906
16932
  g.id,
16907
16933
  ...g.instances.map((i) => i.repo),
16908
16934
  ...g.instances.map((i) => i.file),
16909
- ...g.instances.map((i) => i.id)
16935
+ ...g.instances.map((i) => i.id),
16936
+ ...extra === void 0 ? [] : [extra]
16910
16937
  ].join(" ").toLowerCase();
16938
+ }
16939
+ function groupHaystack(g) {
16940
+ const cached2 = haystackCache.get(g);
16941
+ if (cached2 !== void 0) return cached2;
16942
+ const haystack = buildHaystack(g);
16911
16943
  haystackCache.set(g, haystack);
16912
16944
  return haystack;
16913
16945
  }
16946
+ var actionsCache = /* @__PURE__ */ new WeakMap();
16947
+ function groupActions(g) {
16948
+ const cached2 = actionsCache.get(g);
16949
+ if (cached2 !== void 0) return cached2;
16950
+ const actions = [...new Set(g.instances.map((i) => i.action))];
16951
+ actionsCache.set(g, actions);
16952
+ return actions;
16953
+ }
16914
16954
  function applyFindingFilters(groups, opts) {
16915
16955
  let filtered = groups;
16916
16956
  if (opts.severity && opts.severity.length > 0) {
@@ -16923,7 +16963,7 @@ function applyFindingFilters(groups, opts) {
16923
16963
  }
16924
16964
  if (opts.actions && opts.actions.length > 0) {
16925
16965
  const actionSet = new Set(opts.actions);
16926
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
16966
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
16927
16967
  }
16928
16968
  if (opts.subtype && opts.subtype.length > 0) {
16929
16969
  const subtypeSet = new Set(opts.subtype);
@@ -16975,8 +17015,7 @@ function computeFindingFacets(allGroups, opts) {
16975
17015
  });
16976
17016
  const actionMap = /* @__PURE__ */ new Map();
16977
17017
  for (const g of forAction) {
16978
- const actionSet = new Set(g.instances.map((i) => i.action));
16979
- for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
17018
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
16980
17019
  }
16981
17020
  const forSubtype = applyFindingFilters(allGroups, {
16982
17021
  providers: opts.providers,
@@ -17538,6 +17577,132 @@ function reviewSeverityRank(reasons) {
17538
17577
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17539
17578
  }
17540
17579
 
17580
+ // ../../packages/schema/src/zod/triage.ts
17581
+ var TriageHit = external_exports.object({
17582
+ ruleId: external_exports.string(),
17583
+ category: DetectionCategory,
17584
+ severity: Severity,
17585
+ maskedMatch: external_exports.string(),
17586
+ rawMatch: external_exports.string(),
17587
+ context: external_exports.string(),
17588
+ filePath: external_exports.string().optional(),
17589
+ confidence: external_exports.number().min(0).max(1),
17590
+ id: external_exports.string().optional(),
17591
+ valueFingerprint: external_exports.string().optional(),
17592
+ keyVersion: external_exports.number().int().nonnegative().optional()
17593
+ });
17594
+ var TriagePolicy = BuiltinPolicyId;
17595
+ var TriageCategoryRec = external_exports.object({
17596
+ category: DetectionCategory,
17597
+ action: TriagePolicy,
17598
+ reasoning: external_exports.string(),
17599
+ genuineCount: external_exports.number().int().nonnegative(),
17600
+ fpCount: external_exports.number().int().nonnegative(),
17601
+ // TriageHit ids judged false-positive in this category. fpCount must equal
17602
+ // this array's length — enforced by the consumer, not this schema.
17603
+ fpIds: external_exports.array(external_exports.string())
17604
+ });
17605
+ var TriageRecommendation = external_exports.object({
17606
+ perCategory: external_exports.array(TriageCategoryRec),
17607
+ notes: external_exports.string()
17608
+ });
17609
+
17610
+ // ../../packages/persistence/src/internal/sql-text.ts
17611
+ function escapeLikePattern(s) {
17612
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17613
+ }
17614
+ function placeholders(n) {
17615
+ return Array.from({ length: n }, () => "?").join(", ");
17616
+ }
17617
+ function containsPattern(q) {
17618
+ return `%${escapeLikePattern(q)}%`;
17619
+ }
17620
+ function likeAny(exprs) {
17621
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
17622
+ }
17623
+
17624
+ // ../../packages/persistence/src/internal/transactions.ts
17625
+ var savepointSeq = 0;
17626
+ function withTransaction(db, fn, mode = "DEFERRED") {
17627
+ if (db.isTransaction) {
17628
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
17629
+ db.exec(`SAVEPOINT ${savepoint}`);
17630
+ try {
17631
+ fn();
17632
+ db.exec(`RELEASE ${savepoint}`);
17633
+ } catch (error51) {
17634
+ try {
17635
+ db.exec(`ROLLBACK TO ${savepoint}`);
17636
+ db.exec(`RELEASE ${savepoint}`);
17637
+ } catch {
17638
+ }
17639
+ throw error51;
17640
+ }
17641
+ return;
17642
+ }
17643
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
17644
+ try {
17645
+ fn();
17646
+ db.exec("COMMIT");
17647
+ } catch (error51) {
17648
+ try {
17649
+ db.exec("ROLLBACK");
17650
+ } catch {
17651
+ }
17652
+ throw error51;
17653
+ }
17654
+ }
17655
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
17656
+ const nested = db.isTransaction;
17657
+ try {
17658
+ withTransaction(db, fn, mode);
17659
+ return true;
17660
+ } catch (error51) {
17661
+ if (!db.isTransaction && nested) throw error51;
17662
+ return false;
17663
+ }
17664
+ }
17665
+
17666
+ // ../../packages/persistence/src/internal/warn.ts
17667
+ function akaWarn(message) {
17668
+ process.stderr.write(`[aka] ${message}
17669
+ `);
17670
+ }
17671
+
17672
+ // ../../packages/persistence/src/db/migrations/introspection.ts
17673
+ function evidenceObjects(sql) {
17674
+ const objects = [];
17675
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17676
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17677
+ objects.push({ kind: "table", name: m[1] });
17678
+ }
17679
+ }
17680
+ for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17681
+ if (m[1] !== void 0 && m[2] !== void 0) {
17682
+ objects.push({ kind: "column", table: m[1], name: m[2] });
17683
+ }
17684
+ }
17685
+ return objects;
17686
+ }
17687
+ function schemaObjectExists(db, kind, name) {
17688
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
17689
+ return row !== void 0;
17690
+ }
17691
+ function indexExists(db, name) {
17692
+ return schemaObjectExists(db, "index", name);
17693
+ }
17694
+ function columnNames(db, table2, opts) {
17695
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
17696
+ const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
17697
+ return columns.map((c) => c.name);
17698
+ }
17699
+ function evidenceExists(db, object2) {
17700
+ if (object2.kind === "column") {
17701
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
17702
+ }
17703
+ return schemaObjectExists(db, "table", object2.name);
17704
+ }
17705
+
17541
17706
  // ../../packages/persistence/src/ids.ts
17542
17707
  import { createHash } from "crypto";
17543
17708
  function sha256Hex(input) {
@@ -17574,28 +17739,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17574
17739
  }
17575
17740
 
17576
17741
  // ../../packages/persistence/src/migrations.ts
17577
- function evidenceObjects(sql) {
17578
- const objects = [];
17579
- for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17580
- if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17581
- objects.push({ kind: "table", name: m[1] });
17582
- }
17583
- }
17584
- for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17585
- if (m[1] !== void 0 && m[2] !== void 0) {
17586
- objects.push({ kind: "column", table: m[1], name: m[2] });
17587
- }
17588
- }
17589
- return objects;
17590
- }
17591
- function evidenceExists(db, object2) {
17592
- if (object2.kind === "column") {
17593
- const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
17594
- return columns.some((c) => c.name === object2.name);
17595
- }
17596
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
17597
- return row !== void 0;
17598
- }
17599
17742
  function describeObject(object2) {
17600
17743
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
17601
17744
  }
@@ -17606,10 +17749,6 @@ function createdIndexName(statement) {
17606
17749
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17607
17750
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17608
17751
  }
17609
- function indexExists(db, name) {
17610
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
17611
- return row !== void 0;
17612
- }
17613
17752
  function applyMigrations(db) {
17614
17753
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17615
17754
  db.exec(
@@ -17628,44 +17767,39 @@ function applyMigrations(db) {
17628
17767
  const present = evidence.filter((o) => evidenceExists(db, o));
17629
17768
  if (present.length > 0 && present.length < evidence.length) {
17630
17769
  const missing = evidence.filter((o) => !present.includes(o));
17631
- 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.`;
17632
- process.stderr.write(`${message}
17633
- `);
17634
- throw new Error(message);
17770
+ 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.`;
17771
+ akaWarn(message);
17772
+ throw new Error(`[aka] ${message}`);
17635
17773
  }
17636
17774
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
17637
17775
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
17638
17776
  const statements = splitStatements(migration.sql);
17639
17777
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
17640
17778
  try {
17641
- db.exec("BEGIN IMMEDIATE");
17642
- try {
17643
- for (const statement of statements) {
17644
- const indexName = createdIndexName(statement);
17645
- if (indexName === void 0) {
17646
- if (alreadyApplied) continue;
17647
- } else if (indexExists(db, indexName)) {
17648
- continue;
17779
+ withTransaction(
17780
+ db,
17781
+ () => {
17782
+ for (const statement of statements) {
17783
+ const indexName = createdIndexName(statement);
17784
+ if (indexName === void 0) {
17785
+ if (alreadyApplied) continue;
17786
+ } else if (indexExists(db, indexName)) {
17787
+ continue;
17788
+ }
17789
+ db.exec(statement);
17649
17790
  }
17650
- db.exec(statement);
17651
- }
17652
- if (wantsFkOff && !alreadyApplied) {
17653
- const violations = db.prepare("PRAGMA foreign_key_check").all();
17654
- if (violations.length > 0) {
17655
- throw new Error(
17656
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17657
- );
17791
+ if (wantsFkOff && !alreadyApplied) {
17792
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
17793
+ if (violations.length > 0) {
17794
+ throw new Error(
17795
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17796
+ );
17797
+ }
17658
17798
  }
17659
- }
17660
- record2.run(migration.tag, Date.now());
17661
- db.exec("COMMIT");
17662
- } catch (error51) {
17663
- try {
17664
- db.exec("ROLLBACK");
17665
- } catch {
17666
- }
17667
- throw error51;
17668
- }
17799
+ record2.run(migration.tag, Date.now());
17800
+ },
17801
+ "IMMEDIATE"
17802
+ );
17669
17803
  } finally {
17670
17804
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
17671
17805
  }
@@ -17708,8 +17842,7 @@ var TOKEN_USAGE_COLUMNS = [
17708
17842
  }
17709
17843
  ];
17710
17844
  function ensureTokenUsageColumns(db) {
17711
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
17712
- const existing = new Set(columns.map((c) => c.name));
17845
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17713
17846
  for (const column of TOKEN_USAGE_COLUMNS) {
17714
17847
  if (!existing.has(column.name)) {
17715
17848
  db.exec(column.ddl);
@@ -17747,47 +17880,39 @@ function reconcileSourceProjectIds(db) {
17747
17880
  repoint: db.prepare(`UPDATE ${table2} SET project_id = ? WHERE project_id = ?`)
17748
17881
  }));
17749
17882
  const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
17750
- db.exec("BEGIN IMMEDIATE");
17751
- try {
17752
- for (const { row, canonicalId } of legacy) {
17753
- foldProject.run(
17754
- canonicalId,
17755
- row.url,
17756
- row.name,
17757
- row.attributes,
17758
- row.firstSeen,
17759
- row.lastSeen
17760
- );
17761
- repointAudit.run(canonicalId, row.id);
17762
- for (const { dropCollisions, repoint } of pathTables) {
17763
- dropCollisions.run(row.id, canonicalId);
17764
- repoint.run(canonicalId, row.id);
17883
+ withTransaction(
17884
+ db,
17885
+ () => {
17886
+ for (const { row, canonicalId } of legacy) {
17887
+ foldProject.run(
17888
+ canonicalId,
17889
+ row.url,
17890
+ row.name,
17891
+ row.attributes,
17892
+ row.firstSeen,
17893
+ row.lastSeen
17894
+ );
17895
+ repointAudit.run(canonicalId, row.id);
17896
+ for (const { dropCollisions, repoint } of pathTables) {
17897
+ dropCollisions.run(row.id, canonicalId);
17898
+ repoint.run(canonicalId, row.id);
17899
+ }
17900
+ repointCallSite.run(canonicalId, row.id);
17901
+ deleteLegacy.run(row.id);
17765
17902
  }
17766
- repointCallSite.run(canonicalId, row.id);
17767
- deleteLegacy.run(row.id);
17768
- }
17769
- db.exec("COMMIT");
17770
- } catch (error51) {
17771
- try {
17772
- db.exec("ROLLBACK");
17773
- } catch {
17774
- }
17775
- throw error51;
17776
- }
17903
+ },
17904
+ "IMMEDIATE"
17905
+ );
17777
17906
  } catch (error51) {
17778
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
17779
- `);
17907
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
17780
17908
  }
17781
17909
  }
17782
17910
  function isForeignSqliteLineage(db) {
17783
- const tenantsTable = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tenants' LIMIT 1").get();
17784
- if (tenantsTable !== void 0) return true;
17785
- const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
17786
- return eventsColumns.some((c) => c.name === "tenant_id");
17911
+ if (schemaObjectExists(db, "table", "tenants")) return true;
17912
+ return columnNames(db, "events").includes("tenant_id");
17787
17913
  }
17788
17914
  function ensureSyncedAtColumn(db, table2) {
17789
- const columns = db.prepare(`PRAGMA table_info(${table2})`).all();
17790
- if (!columns.some((c) => c.name === "synced_at")) {
17915
+ if (!columnNames(db, table2).includes("synced_at")) {
17791
17916
  db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
17792
17917
  }
17793
17918
  }
@@ -17838,8 +17963,11 @@ function ensureDataDirSync(dir) {
17838
17963
  } catch {
17839
17964
  }
17840
17965
  }
17966
+ function walSidecars(file2) {
17967
+ return [`${file2}-wal`, `${file2}-shm`];
17968
+ }
17841
17969
  function tightenPerms(file2) {
17842
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
17970
+ for (const path of [file2, ...walSidecars(file2)]) {
17843
17971
  try {
17844
17972
  chmodSync(path, DATA_FILE_MODE);
17845
17973
  } catch {
@@ -17847,12 +17975,68 @@ function tightenPerms(file2) {
17847
17975
  }
17848
17976
  }
17849
17977
 
17850
- // ../../packages/persistence/src/repositories/sql-utils.ts
17851
- function escapeLikePattern(s) {
17852
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17978
+ // ../../packages/persistence/src/internal/json.ts
17979
+ function safeJson(s, fallback) {
17980
+ if (s == null) return fallback;
17981
+ try {
17982
+ return JSON.parse(s);
17983
+ } catch {
17984
+ return fallback;
17985
+ }
17853
17986
  }
17854
- function placeholders(n) {
17855
- return Array.from({ length: n }, () => "?").join(", ");
17987
+ function parseJsonObject(s) {
17988
+ if (s == null) return void 0;
17989
+ try {
17990
+ const parsed = JSON.parse(s);
17991
+ if (typeof parsed === "object" && parsed !== null) return parsed;
17992
+ } catch {
17993
+ }
17994
+ return void 0;
17995
+ }
17996
+
17997
+ // ../../packages/persistence/src/internal/rows.ts
17998
+ function allRows(stmt, params) {
17999
+ if (params === void 0) return stmt.all();
18000
+ if (Array.isArray(params)) return stmt.all(...params);
18001
+ return stmt.all(params);
18002
+ }
18003
+ function getRow(stmt, params) {
18004
+ if (params === void 0) return stmt.get();
18005
+ if (Array.isArray(params)) return stmt.get(...params);
18006
+ return stmt.get(params);
18007
+ }
18008
+ function intToBool(raw) {
18009
+ return raw === 1 || raw === true;
18010
+ }
18011
+ function boolToInt(b) {
18012
+ return b ? 1 : 0;
18013
+ }
18014
+ function bindParams(row) {
18015
+ const out = {};
18016
+ for (const [key, value] of Object.entries(row)) {
18017
+ out[key] = value === void 0 ? null : value;
18018
+ }
18019
+ return out;
18020
+ }
18021
+ function countScalar(db, sql, params) {
18022
+ return getRow(db.prepare(sql), params)?.n ?? 0;
18023
+ }
18024
+ function countBy(db, sql, params) {
18025
+ const map2 = /* @__PURE__ */ new Map();
18026
+ for (const row of allRows(db.prepare(sql), params)) {
18027
+ map2.set(row.k, row.n);
18028
+ }
18029
+ return map2;
18030
+ }
18031
+ function mapRowsTolerant(rows, map2) {
18032
+ const out = [];
18033
+ for (const row of rows) {
18034
+ try {
18035
+ out.push(map2(row));
18036
+ } catch {
18037
+ }
18038
+ }
18039
+ return out;
17856
18040
  }
17857
18041
 
17858
18042
  // ../../packages/persistence/src/repositories/activity.ts
@@ -17904,15 +18088,11 @@ function encodeCursor(payload) {
17904
18088
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
17905
18089
  }
17906
18090
  function decodeCursor(cursor) {
17907
- try {
17908
- const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
17909
- if (parsed !== null && typeof parsed === "object" && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
17910
- return parsed;
17911
- }
17912
- return null;
17913
- } catch {
17914
- return null;
18091
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18092
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18093
+ return parsed;
17915
18094
  }
18095
+ return null;
17916
18096
  }
17917
18097
  var DB_EVENT_TYPE_TO_KIND = {
17918
18098
  session: "session",
@@ -17929,15 +18109,8 @@ var DB_EVENT_TYPE_TO_KIND = {
17929
18109
  };
17930
18110
  function safeParseStringArray(raw) {
17931
18111
  if (!raw) return [];
17932
- try {
17933
- const parsed = JSON.parse(raw);
17934
- return Array.isArray(parsed) ? parsed : [];
17935
- } catch {
17936
- return [];
17937
- }
17938
- }
17939
- function toBool(raw) {
17940
- return raw === 1 || raw === true;
18112
+ const parsed = safeJson(raw, null);
18113
+ return Array.isArray(parsed) ? parsed : [];
17941
18114
  }
17942
18115
  function toHarness(raw) {
17943
18116
  const parsed = Harness.safeParse(raw);
@@ -17990,8 +18163,8 @@ function buildAuditEvent(row) {
17990
18163
  severity: severityParsed?.success ? severityParsed.data : null,
17991
18164
  link: linkParsed?.success ? linkParsed.data : null,
17992
18165
  targetId: row.target_id,
17993
- internal: toBool(row.internal),
17994
- flagged: toBool(row.flagged)
18166
+ internal: intToBool(row.internal),
18167
+ flagged: intToBool(row.flagged)
17995
18168
  };
17996
18169
  }
17997
18170
  var TIMELINE_COLUMNS = `
@@ -18017,12 +18190,15 @@ var SqliteActivityRepository = class {
18017
18190
  stats(tz) {
18018
18191
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
18019
18192
  const { startMs, endMs } = window;
18020
- const sessionsToday = this.db.prepare(
18193
+ const sessionsToday = countScalar(
18194
+ this.db,
18021
18195
  `SELECT count(*) AS n FROM audit_events
18022
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
18023
- ).get(startMs, endMs).n;
18196
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
18197
+ [startMs, endMs]
18198
+ );
18024
18199
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
18025
- const liveNow = this.db.prepare(
18200
+ const liveNow = countScalar(
18201
+ this.db,
18026
18202
  `SELECT count(*) AS n FROM audit_events s
18027
18203
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
18028
18204
  AND max(
@@ -18031,22 +18207,29 @@ var SqliteActivityRepository = class {
18031
18207
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
18032
18208
  s.started_at
18033
18209
  )
18034
- ) >= ?`
18035
- ).get(liveThreshold).n;
18036
- const toolCallsToday = this.db.prepare(
18210
+ ) >= ?`,
18211
+ [liveThreshold]
18212
+ );
18213
+ const toolCallsToday = countScalar(
18214
+ this.db,
18037
18215
  `SELECT count(*) AS n FROM audit_events
18038
- WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`
18039
- ).get(startMs, endMs).n;
18040
- const findingsToday = this.db.prepare(
18216
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18217
+ [startMs, endMs]
18218
+ );
18219
+ const findingsToday = countScalar(
18220
+ this.db,
18041
18221
  `SELECT count(*) AS n FROM inspection_findings f
18042
18222
  JOIN audit_events e ON e.id = f.audit_event_id
18043
- WHERE e.started_at >= ? AND e.started_at < ?`
18044
- ).get(startMs, endMs).n;
18045
- const egressToday = this.db.prepare(
18223
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18224
+ [startMs, endMs]
18225
+ );
18226
+ const egressToday = countScalar(
18227
+ this.db,
18046
18228
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
18047
18229
  FROM audit_events
18048
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
18049
- ).get(startMs, endMs).n;
18230
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18231
+ [startMs, endMs]
18232
+ );
18050
18233
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18051
18234
  }
18052
18235
  listSessions(query) {
@@ -18068,7 +18251,7 @@ var SqliteActivityRepository = class {
18068
18251
  conditions.push("started_at <= ?");
18069
18252
  params.push(toMs);
18070
18253
  if (query.q) {
18071
- const pattern = `%${escapeLikePattern(query.q)}%`;
18254
+ const pattern = containsPattern(query.q);
18072
18255
  conditions.push(
18073
18256
  `(content LIKE ? ESCAPE '\\'
18074
18257
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18087,8 +18270,9 @@ var SqliteActivityRepository = class {
18087
18270
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18088
18271
  }
18089
18272
  const limit = query.limit;
18090
- const rows = this.db.prepare(
18091
- `SELECT id,
18273
+ const rows = allRows(
18274
+ this.db.prepare(
18275
+ `SELECT id,
18092
18276
  json_extract(attributes, '$.harness') AS harness,
18093
18277
  content AS title,
18094
18278
  json_extract(attributes, '$.project') AS project,
@@ -18101,7 +18285,9 @@ var SqliteActivityRepository = class {
18101
18285
  WHERE ${conditions.join(" AND ")}
18102
18286
  ORDER BY started_at DESC, id DESC
18103
18287
  LIMIT ?`
18104
- ).all(...params, limit + 1);
18288
+ ),
18289
+ [...params, limit + 1]
18290
+ );
18105
18291
  const hasMore = rows.length > limit;
18106
18292
  const page = hasMore ? rows.slice(0, limit) : rows;
18107
18293
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18118,8 +18304,9 @@ var SqliteActivityRepository = class {
18118
18304
  return Promise.resolve({ items, nextCursor });
18119
18305
  }
18120
18306
  getSession(sessionId) {
18121
- const rootRow = this.db.prepare(
18122
- `SELECT id,
18307
+ const rootRow = getRow(
18308
+ this.db.prepare(
18309
+ `SELECT id,
18123
18310
  json_extract(attributes, '$.harness') AS harness,
18124
18311
  content AS title,
18125
18312
  json_extract(attributes, '$.project') AS project,
@@ -18136,47 +18323,66 @@ var SqliteActivityRepository = class {
18136
18323
  FROM audit_events
18137
18324
  WHERE id = ? AND event_type = 'session'
18138
18325
  LIMIT 1`
18139
- ).get(sessionId);
18326
+ ),
18327
+ [sessionId]
18328
+ );
18140
18329
  if (!rootRow) return Promise.resolve(null);
18141
- const timelineRows = this.db.prepare(
18142
- `SELECT ${TIMELINE_COLUMNS}
18330
+ const timelineRows = allRows(
18331
+ this.db.prepare(
18332
+ `SELECT ${TIMELINE_COLUMNS}
18143
18333
  FROM audit_events
18144
18334
  WHERE id = ? OR root_session_id = ?
18145
18335
  ORDER BY started_at ASC, id ASC`
18146
- ).all(sessionId, sessionId);
18336
+ ),
18337
+ [sessionId, sessionId]
18338
+ );
18147
18339
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18148
- const tokenRow = this.db.prepare(
18149
- `SELECT
18340
+ const tokenRow = getRow(
18341
+ this.db.prepare(
18342
+ `SELECT
18150
18343
  coalesce(sum(input_tokens), 0) AS input,
18151
18344
  coalesce(sum(output_tokens), 0) AS output,
18152
18345
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18153
18346
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18154
18347
  FROM audit_events
18155
18348
  WHERE root_session_id = ? AND event_type = 'llm_call'`
18156
- ).get(sessionId);
18157
- const primaryModel = this.db.prepare(
18158
- `SELECT model, provider FROM audit_events
18349
+ ),
18350
+ [sessionId]
18351
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
18352
+ const primaryModel = getRow(
18353
+ this.db.prepare(
18354
+ `SELECT model, provider FROM audit_events
18159
18355
  WHERE root_session_id = ? AND event_type = 'llm_call'
18160
18356
  ORDER BY started_at ASC, id ASC
18161
18357
  LIMIT 1`
18162
- ).get(sessionId);
18163
- const toolRows = this.db.prepare(
18164
- `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18358
+ ),
18359
+ [sessionId]
18360
+ );
18361
+ const toolRows = allRows(
18362
+ this.db.prepare(
18363
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18165
18364
  count(*) AS n
18166
18365
  FROM audit_events
18167
18366
  WHERE root_session_id = ? AND event_type = 'tool_call'
18168
18367
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
18169
- ).all(sessionId);
18170
- const modelRows = this.db.prepare(
18171
- `SELECT DISTINCT model FROM audit_events
18368
+ ),
18369
+ [sessionId]
18370
+ );
18371
+ const modelRows = allRows(
18372
+ this.db.prepare(
18373
+ `SELECT DISTINCT model FROM audit_events
18172
18374
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18173
18375
  ORDER BY model`
18174
- ).all(sessionId);
18376
+ ),
18377
+ [sessionId]
18378
+ );
18175
18379
  const derivedModels = modelRows.map((r) => r.model);
18176
- const commits = this.db.prepare(
18380
+ const commits = countScalar(
18381
+ this.db,
18177
18382
  `SELECT count(*) AS n FROM audit_events
18178
- WHERE root_session_id = ? AND event_type = 'commit'`
18179
- ).get(sessionId).n;
18383
+ WHERE root_session_id = ? AND event_type = 'commit'`,
18384
+ [sessionId]
18385
+ );
18180
18386
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18181
18387
  turns: 0,
18182
18388
  findings: 0,
@@ -18243,7 +18449,10 @@ var SqliteActivityRepository = class {
18243
18449
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18244
18450
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18245
18451
  );
18246
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
18452
+ const rows = allRows(
18453
+ stmt,
18454
+ fromMs === void 0 ? void 0 : [fromMs]
18455
+ );
18247
18456
  const seen = /* @__PURE__ */ new Set();
18248
18457
  for (const row of rows) seen.add(toHarness(row.harness));
18249
18458
  return Promise.resolve([...seen]);
@@ -18266,23 +18475,23 @@ var SqliteActivityRepository = class {
18266
18475
  conditions.push("started_at >= ?");
18267
18476
  params.push(opts.fromMs);
18268
18477
  }
18269
- const rows = this.db.prepare(
18270
- `SELECT root_session_id AS sessionId, attributes
18478
+ const rows = allRows(
18479
+ this.db.prepare(
18480
+ `SELECT root_session_id AS sessionId, attributes
18271
18481
  FROM audit_events
18272
18482
  WHERE ${conditions.join(" AND ")}`
18273
- ).all(...params);
18274
- const leaves = [];
18275
- for (const row of rows) {
18276
- if (row.sessionId === null) continue;
18277
- try {
18278
- leaves.push({
18279
- sessionId: row.sessionId,
18280
- attributes: JSON.parse(row.attributes)
18281
- });
18282
- } catch {
18283
- }
18284
- }
18285
- return leaves;
18483
+ ),
18484
+ params
18485
+ );
18486
+ return mapRowsTolerant(
18487
+ rows.filter(
18488
+ (row) => row.sessionId !== null
18489
+ ),
18490
+ (row) => ({
18491
+ sessionId: row.sessionId,
18492
+ attributes: JSON.parse(row.attributes)
18493
+ })
18494
+ );
18286
18495
  }
18287
18496
  /**
18288
18497
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -18296,57 +18505,72 @@ var SqliteActivityRepository = class {
18296
18505
  );
18297
18506
  if (sessionIds.length === 0) return result;
18298
18507
  const inClause = placeholders(sessionIds.length);
18299
- const lastActivityRows = this.db.prepare(
18300
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18508
+ const lastActivityRows = allRows(
18509
+ this.db.prepare(
18510
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18301
18511
  WHERE root_session_id IN (${inClause})
18302
18512
  GROUP BY root_session_id`
18303
- ).all(...sessionIds);
18513
+ ),
18514
+ sessionIds
18515
+ );
18304
18516
  for (const row of lastActivityRows) {
18305
18517
  if (row.id === null) continue;
18306
18518
  const entry = result.get(row.id);
18307
18519
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
18308
18520
  }
18309
- const turnsRows = this.db.prepare(
18310
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18521
+ const turnsRows = allRows(
18522
+ this.db.prepare(
18523
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18311
18524
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
18312
18525
  GROUP BY root_session_id`
18313
- ).all(...sessionIds);
18526
+ ),
18527
+ sessionIds
18528
+ );
18314
18529
  for (const row of turnsRows) {
18315
18530
  if (row.id === null) continue;
18316
18531
  const entry = result.get(row.id);
18317
18532
  if (entry) entry.turns = row.n;
18318
18533
  }
18319
- const runKeyRows = this.db.prepare(
18320
- `SELECT root_session_id AS id,
18534
+ const runKeyRows = allRows(
18535
+ this.db.prepare(
18536
+ `SELECT root_session_id AS id,
18321
18537
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
18322
18538
  FROM audit_events
18323
18539
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
18324
18540
  AND json_extract(attributes, '$.run_key') IS NOT NULL
18325
18541
  GROUP BY root_session_id`
18326
- ).all(...sessionIds);
18542
+ ),
18543
+ sessionIds
18544
+ );
18327
18545
  for (const row of runKeyRows) {
18328
18546
  if (row.id === null) continue;
18329
18547
  const entry = result.get(row.id);
18330
18548
  if (entry) entry.turns = Math.max(entry.turns, row.n);
18331
18549
  }
18332
- const findingsRows = this.db.prepare(
18333
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18550
+ const findingsRows = allRows(
18551
+ this.db.prepare(
18552
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18334
18553
  JOIN audit_events e ON e.id = f.audit_event_id
18335
18554
  WHERE e.root_session_id IN (${inClause})
18336
18555
  GROUP BY e.root_session_id`
18337
- ).all(...sessionIds);
18556
+ ),
18557
+ sessionIds
18558
+ );
18338
18559
  for (const row of findingsRows) {
18339
18560
  if (row.id === null) continue;
18340
18561
  const entry = result.get(row.id);
18341
18562
  if (entry) entry.findings = row.n;
18342
18563
  }
18343
- const sharesRows = this.db.prepare(
18344
- `SELECT root_session_id AS id,
18564
+ const sharesRows = allRows(
18565
+ this.db.prepare(
18566
+ `SELECT root_session_id AS id,
18345
18567
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
18346
18568
  FROM audit_events
18347
18569
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
18348
18570
  GROUP BY root_session_id`
18349
- ).all(...sessionIds);
18571
+ ),
18572
+ sessionIds
18573
+ );
18350
18574
  for (const row of sharesRows) {
18351
18575
  if (row.id === null) continue;
18352
18576
  const entry = result.get(row.id);
@@ -18396,33 +18620,28 @@ var SqliteAuditEventsRepository = class {
18396
18620
  // the caller fails open and drops the whole pass — recovered idempotently on the
18397
18621
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
18398
18622
  runInTransaction(fn) {
18399
- this.db.exec("BEGIN");
18400
- try {
18401
- fn();
18402
- this.db.exec("COMMIT");
18403
- } catch (err) {
18404
- this.db.exec("ROLLBACK");
18405
- throw err;
18406
- }
18623
+ withTransaction(this.db, fn);
18407
18624
  }
18408
18625
  insertAuditEvent(input) {
18409
18626
  const row = toAuditEventRow(input);
18410
- this.insertStmt.run({
18411
- id: row.id,
18412
- parentId: row.parentId ?? null,
18413
- rootSessionId: row.rootSessionId ?? null,
18414
- eventType: row.eventType,
18415
- hostId: row.hostId ?? null,
18416
- harnessId: row.harnessId ?? null,
18417
- sourceProjectId: row.sourceProjectId ?? null,
18418
- startedAt: row.startedAt,
18419
- endedAt: row.endedAt ?? null,
18420
- severity: row.severity ?? null,
18421
- priority: row.priority ?? null,
18422
- content: row.content ?? null,
18423
- contentHash: row.contentHash ?? null,
18424
- attributes: row.attributes ?? null
18425
- });
18627
+ this.insertStmt.run(
18628
+ bindParams({
18629
+ id: row.id,
18630
+ parentId: row.parentId,
18631
+ rootSessionId: row.rootSessionId,
18632
+ eventType: row.eventType,
18633
+ hostId: row.hostId,
18634
+ harnessId: row.harnessId,
18635
+ sourceProjectId: row.sourceProjectId,
18636
+ startedAt: row.startedAt,
18637
+ endedAt: row.endedAt,
18638
+ severity: row.severity,
18639
+ priority: row.priority,
18640
+ content: row.content,
18641
+ contentHash: row.contentHash,
18642
+ attributes: row.attributes
18643
+ })
18644
+ );
18426
18645
  }
18427
18646
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18428
18647
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -18436,22 +18655,24 @@ var SqliteAuditEventsRepository = class {
18436
18655
  const startedAt = isoToEpochMillis(input.startedAt);
18437
18656
  if (!Number.isFinite(startedAt)) return;
18438
18657
  const id = llmCallId(input.sessionId, input.messageId);
18439
- this.upsertLlmCallStmt.run({
18440
- id,
18441
- parentId: input.parentId,
18442
- rootSessionId: input.rootSessionId,
18443
- eventType: "llm_call",
18444
- hostId: null,
18445
- harnessId: null,
18446
- sourceProjectId: null,
18447
- startedAt,
18448
- endedAt: null,
18449
- severity: null,
18450
- priority: null,
18451
- content: null,
18452
- contentHash: null,
18453
- attributes: JSON.stringify(input.attributes)
18454
- });
18658
+ this.upsertLlmCallStmt.run(
18659
+ bindParams({
18660
+ id,
18661
+ parentId: input.parentId,
18662
+ rootSessionId: input.rootSessionId,
18663
+ eventType: "llm_call",
18664
+ hostId: null,
18665
+ harnessId: null,
18666
+ sourceProjectId: null,
18667
+ startedAt,
18668
+ endedAt: null,
18669
+ severity: null,
18670
+ priority: null,
18671
+ content: null,
18672
+ contentHash: null,
18673
+ attributes: JSON.stringify(input.attributes)
18674
+ })
18675
+ );
18455
18676
  }
18456
18677
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
18457
18678
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -18475,25 +18696,29 @@ var SqliteAuditEventsRepository = class {
18475
18696
  const startedAt = isoToEpochMillis(input.startedAt);
18476
18697
  if (!Number.isFinite(startedAt)) return;
18477
18698
  const id = toolCallId(input.sessionId, input.toolUseId);
18478
- this.insertStmt.run({
18479
- id,
18480
- parentId: input.parentId,
18481
- rootSessionId: input.rootSessionId,
18482
- eventType: "tool_call",
18483
- hostId: null,
18484
- harnessId: null,
18485
- sourceProjectId: null,
18486
- startedAt,
18487
- endedAt: null,
18488
- severity: null,
18489
- priority: null,
18490
- content: null,
18491
- contentHash: null,
18492
- attributes: JSON.stringify(input.attributes)
18493
- });
18699
+ this.insertStmt.run(
18700
+ bindParams({
18701
+ id,
18702
+ parentId: input.parentId,
18703
+ rootSessionId: input.rootSessionId,
18704
+ eventType: "tool_call",
18705
+ hostId: null,
18706
+ harnessId: null,
18707
+ sourceProjectId: null,
18708
+ startedAt,
18709
+ endedAt: null,
18710
+ severity: null,
18711
+ priority: null,
18712
+ content: null,
18713
+ contentHash: null,
18714
+ attributes: JSON.stringify(input.attributes)
18715
+ })
18716
+ );
18494
18717
  }
18495
18718
  findById(id) {
18496
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
18719
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
18720
+ id
18721
+ });
18497
18722
  }
18498
18723
  // Read the `provider` snapshotted onto a session root's attributes.
18499
18724
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -18503,14 +18728,8 @@ var SqliteAuditEventsRepository = class {
18503
18728
  sessionProvider(sessionId) {
18504
18729
  const row = this.findById(sessionId);
18505
18730
  if (!row?.attributes) return void 0;
18506
- try {
18507
- const parsed = JSON.parse(row.attributes);
18508
- if (typeof parsed === "object" && parsed !== null) {
18509
- const provider = parsed.provider;
18510
- if (typeof provider === "string") return provider;
18511
- }
18512
- } catch {
18513
- }
18731
+ const provider = parseJsonObject(row.attributes)?.provider;
18732
+ if (typeof provider === "string") return provider;
18514
18733
  return void 0;
18515
18734
  }
18516
18735
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -18519,11 +18738,13 @@ var SqliteAuditEventsRepository = class {
18519
18738
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
18520
18739
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
18521
18740
  llmCallLeaves() {
18522
- return this.db.prepare(
18523
- `SELECT root_session_id AS sessionId, attributes
18741
+ return allRows(
18742
+ this.db.prepare(
18743
+ `SELECT root_session_id AS sessionId, attributes
18524
18744
  FROM audit_events
18525
18745
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
18526
- ).all();
18746
+ )
18747
+ );
18527
18748
  }
18528
18749
  };
18529
18750
 
@@ -18542,16 +18763,29 @@ var SqliteClassifiedDataRepository = class {
18542
18763
  upsert(input) {
18543
18764
  const id = classifiedDataId(input.class);
18544
18765
  const row = toClassifiedDataRow(input, id);
18545
- this.insertStmt.run({
18546
- id: row.id,
18547
- class: row.class,
18548
- label: row.label ?? null,
18549
- attributes: row.attributes ?? null
18550
- });
18766
+ this.insertStmt.run(
18767
+ bindParams({
18768
+ id: row.id,
18769
+ class: row.class,
18770
+ label: row.label,
18771
+ attributes: row.attributes
18772
+ })
18773
+ );
18551
18774
  return id;
18552
18775
  }
18553
18776
  };
18554
18777
 
18778
+ // ../../packages/persistence/src/repositories/config-scan.ts
18779
+ function latestConfigScan(db) {
18780
+ return getRow(
18781
+ db.prepare(
18782
+ `SELECT id, started_at, attributes FROM audit_events
18783
+ WHERE event_type = 'config_scan'
18784
+ ORDER BY started_at DESC, id DESC LIMIT 1`
18785
+ )
18786
+ );
18787
+ }
18788
+
18555
18789
  // ../../packages/persistence/src/repositories/config-inventory.ts
18556
18790
  var SqliteConfigInventoryRepository = class {
18557
18791
  constructor(db) {
@@ -18559,7 +18793,7 @@ var SqliteConfigInventoryRepository = class {
18559
18793
  }
18560
18794
  db;
18561
18795
  report() {
18562
- const scan2 = this.latestScan();
18796
+ const scan2 = latestConfigScan(this.db);
18563
18797
  if (!scan2) {
18564
18798
  return {
18565
18799
  scannedAt: null,
@@ -18570,17 +18804,23 @@ var SqliteConfigInventoryRepository = class {
18570
18804
  topics: []
18571
18805
  };
18572
18806
  }
18573
- const rows = this.db.prepare(
18574
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18807
+ const rows = allRows(
18808
+ this.db.prepare(
18809
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18575
18810
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
18576
18811
  ORDER BY object_type, title`
18577
- ).all({ startedAt: scan2.started_at });
18578
- const findings = this.db.prepare(
18579
- `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18812
+ ),
18813
+ { startedAt: scan2.started_at }
18814
+ );
18815
+ const findings = allRows(
18816
+ this.db.prepare(
18817
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18580
18818
  FROM inspection_findings f
18581
18819
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
18582
18820
  WHERE f.audit_event_id = :scanId`
18583
- ).all({ scanId: scan2.id });
18821
+ ),
18822
+ { scanId: scan2.id }
18823
+ );
18584
18824
  const skills = [];
18585
18825
  const hooks = [];
18586
18826
  const mcpServers = [];
@@ -18609,7 +18849,9 @@ var SqliteConfigInventoryRepository = class {
18609
18849
  // schema note); an override whose asset is gone simply never matches. A row
18610
18850
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
18611
18851
  trustOverrides() {
18612
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
18852
+ const rows = allRows(
18853
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
18854
+ );
18613
18855
  const map2 = /* @__PURE__ */ new Map();
18614
18856
  for (const row of rows) {
18615
18857
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -18618,13 +18860,6 @@ var SqliteConfigInventoryRepository = class {
18618
18860
  }
18619
18861
  return map2;
18620
18862
  }
18621
- latestScan() {
18622
- return this.db.prepare(
18623
- `SELECT id, started_at, attributes FROM audit_events
18624
- WHERE event_type = 'config_scan'
18625
- ORDER BY started_at DESC, id DESC LIMIT 1`
18626
- ).get();
18627
- }
18628
18863
  };
18629
18864
  function toSkillItem(row, bag) {
18630
18865
  const item = {
@@ -18735,22 +18970,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
18735
18970
  return topics;
18736
18971
  }
18737
18972
  function countScanErrors(attributes) {
18738
- if (!attributes) return 0;
18739
- try {
18740
- const parsed = JSON.parse(attributes);
18741
- const errors = parsed?.errors;
18742
- return typeof errors === "number" ? errors : 0;
18743
- } catch {
18744
- return 0;
18745
- }
18973
+ const errors = parseJsonObject(attributes)?.errors;
18974
+ return typeof errors === "number" ? errors : 0;
18746
18975
  }
18747
18976
  function parseBag(raw) {
18748
- try {
18749
- const parsed = JSON.parse(raw);
18750
- if (typeof parsed === "object" && parsed !== null) return parsed;
18751
- } catch {
18752
- }
18753
- return void 0;
18977
+ return parseJsonObject(raw);
18754
18978
  }
18755
18979
  function str(value) {
18756
18980
  return typeof value === "string" ? value : void 0;
@@ -18759,12 +18983,7 @@ function str(value) {
18759
18983
  // ../../packages/persistence/src/repositories/detections.ts
18760
18984
  var DAY_MS2 = 864e5;
18761
18985
  function parseRules(rulesJson) {
18762
- let raw;
18763
- try {
18764
- raw = JSON.parse(rulesJson);
18765
- } catch {
18766
- return [];
18767
- }
18986
+ const raw = safeJson(rulesJson, []);
18768
18987
  if (!Array.isArray(raw)) return [];
18769
18988
  const rules = [];
18770
18989
  for (const entry of raw) {
@@ -18783,11 +19002,13 @@ var SqliteDetectionsRepository = class {
18783
19002
  db;
18784
19003
  now;
18785
19004
  listDetections(query) {
18786
- const rows = this.db.prepare(
18787
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19005
+ const rows = allRows(
19006
+ this.db.prepare(
19007
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18788
19008
  rules_json AS rulesJson
18789
19009
  FROM installed_packs`
18790
- ).all();
19010
+ )
19011
+ );
18791
19012
  const available = this.availableByPack();
18792
19013
  const summaries = rows.map((r) => {
18793
19014
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -18796,7 +19017,7 @@ var SqliteDetectionsRepository = class {
18796
19017
  packId: r.packId,
18797
19018
  version: r.version,
18798
19019
  name: r.name,
18799
- enabled: r.enabled === 1,
19020
+ enabled: intToBool(r.enabled),
18800
19021
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
18801
19022
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
18802
19023
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -18813,19 +19034,23 @@ var SqliteDetectionsRepository = class {
18813
19034
  // available_packs keyed by the "namespace/packId" slug (one read per list /
18814
19035
  // detail call; the table is a handful of rows).
18815
19036
  availableByPack() {
18816
- const rows = this.db.prepare(
18817
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
19037
+ const rows = allRows(
19038
+ this.db.prepare(
19039
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18818
19040
  FROM available_packs`
18819
- ).all();
19041
+ )
19042
+ );
18820
19043
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
18821
19044
  }
18822
19045
  getDetectionStats() {
18823
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
19046
+ const rows = allRows(
19047
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
19048
+ );
18824
19049
  let rules = 0;
18825
19050
  let active = 0;
18826
19051
  const ruleIds = /* @__PURE__ */ new Set();
18827
19052
  for (const r of rows) {
18828
- if (r.enabled === 1) active += 1;
19053
+ if (intToBool(r.enabled)) active += 1;
18829
19054
  const parsed = parseRules(r.rulesJson);
18830
19055
  rules += parsed.length;
18831
19056
  for (const rule of parsed) {
@@ -18843,12 +19068,15 @@ var SqliteDetectionsRepository = class {
18843
19068
  const parts = splitDetectionId(id);
18844
19069
  if (!parts) return Promise.resolve(null);
18845
19070
  const { namespace, packId } = parts;
18846
- const row = this.db.prepare(
18847
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19071
+ const row = getRow(
19072
+ this.db.prepare(
19073
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18848
19074
  rules_json AS rulesJson, updated_at AS updatedAt
18849
19075
  FROM installed_packs
18850
19076
  WHERE namespace = ? AND pack_id = ?`
18851
- ).get(namespace, packId);
19077
+ ),
19078
+ [namespace, packId]
19079
+ );
18852
19080
  if (!row) return Promise.resolve(null);
18853
19081
  const rules = parseRules(row.rulesJson);
18854
19082
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -18866,7 +19094,7 @@ var SqliteDetectionsRepository = class {
18866
19094
  packId: row.packId,
18867
19095
  version: row.version,
18868
19096
  name: row.name,
18869
- enabled: row.enabled === 1,
19097
+ enabled: intToBool(row.enabled),
18870
19098
  rules,
18871
19099
  updatedAt: new Date(row.updatedAt),
18872
19100
  policyId: row.policyId
@@ -18881,13 +19109,14 @@ var SqliteDetectionsRepository = class {
18881
19109
  countFindingsLast30d(ruleIds) {
18882
19110
  if (ruleIds.length === 0) return 0;
18883
19111
  const since = this.now() - 30 * DAY_MS2;
18884
- const placeholders2 = ruleIds.map(() => "?").join(", ");
18885
- const row = this.db.prepare(
18886
- `SELECT count(*) AS c
19112
+ const inClause = placeholders(ruleIds.length);
19113
+ return countScalar(
19114
+ this.db,
19115
+ `SELECT count(*) AS n
18887
19116
  FROM findings f JOIN events e ON e.id = f.event_id
18888
- WHERE e.occurred_at >= ? AND f.rule_id IN (${placeholders2})`
18889
- ).get(since, ...ruleIds);
18890
- return row.c;
19117
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19118
+ [since, ...ruleIds]
19119
+ );
18891
19120
  }
18892
19121
  };
18893
19122
 
@@ -18904,16 +19133,17 @@ var SqliteEventsRepository = class {
18904
19133
  insertStmt;
18905
19134
  insertEvent(event) {
18906
19135
  const row = toEventRow(event);
18907
- this.insertStmt.run({
18908
- id: row.id,
18909
- sourceTool: row.sourceTool,
18910
- kind: row.kind,
18911
- occurredAt: row.occurredAt,
18912
- contentHash: row.contentHash,
18913
- content: row.content,
18914
- // exactOptionalPropertyTypes: the column is nullable, never undefined.
18915
- metadata: row.metadata ?? null
18916
- });
19136
+ this.insertStmt.run(
19137
+ bindParams({
19138
+ id: row.id,
19139
+ sourceTool: row.sourceTool,
19140
+ kind: row.kind,
19141
+ occurredAt: row.occurredAt,
19142
+ contentHash: row.contentHash,
19143
+ content: row.content,
19144
+ metadata: row.metadata
19145
+ })
19146
+ );
18917
19147
  }
18918
19148
  // Every recorded event's content hash — the historical backfill loads this once
18919
19149
  // to skip transcript messages it has already stored, so re-running the scan
@@ -18921,13 +19151,23 @@ var SqliteEventsRepository = class {
18921
19151
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
18922
19152
  // async EventsReadPort contract.
18923
19153
  contentHashes() {
18924
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19154
+ const rows = allRows(
19155
+ this.db.prepare("SELECT content_hash FROM events")
19156
+ );
18925
19157
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
18926
19158
  }
18927
19159
  };
18928
19160
 
18929
19161
  // ../../packages/persistence/src/repositories/exceptions.ts
18930
19162
  import { randomUUID } from "crypto";
19163
+
19164
+ // ../../packages/persistence/src/internal/sqlite-errors.ts
19165
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19166
+ function isUniqueConstraintError(err) {
19167
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19168
+ }
19169
+
19170
+ // ../../packages/persistence/src/repositories/exceptions.ts
18931
19171
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
18932
19172
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
18933
19173
  var DuplicateActiveExceptionError = class extends Error {
@@ -18948,10 +19188,6 @@ var AmbiguousExceptionIdError = class extends Error {
18948
19188
  this.name = "AmbiguousExceptionIdError";
18949
19189
  }
18950
19190
  };
18951
- var SQLITE_CONSTRAINT_UNIQUE = 2067;
18952
- function isUniqueConstraintError(err) {
18953
- return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
18954
- }
18955
19191
  var ACTIVE_PREDICATE = `revoked_at IS NULL
18956
19192
  AND (expires_at IS NULL OR expires_at > :now)
18957
19193
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -19007,10 +19243,11 @@ var SqliteExceptionsRepository = class {
19007
19243
  this.insertExceptionRow(id, input, now);
19008
19244
  } catch (err) {
19009
19245
  if (!isUniqueConstraintError(err)) throw err;
19010
- this.db.exec("BEGIN IMMEDIATE");
19011
- try {
19012
- const superseded = this.db.prepare(
19013
- `UPDATE exceptions
19246
+ withTransaction(
19247
+ this.db,
19248
+ () => {
19249
+ const superseded = this.db.prepare(
19250
+ `UPDATE exceptions
19014
19251
  SET revoked_at = :now, revoked_by = :revokedBy,
19015
19252
  revoke_reason = 'superseded by a new grant for the same value',
19016
19253
  updated_at = :now
@@ -19018,24 +19255,27 @@ var SqliteExceptionsRepository = class {
19018
19255
  AND key_version = :keyVersion AND revoked_at IS NULL
19019
19256
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
19020
19257
  OR (max_uses IS NOT NULL AND use_count >= max_uses))`
19021
- ).run({
19022
- now,
19023
- revokedBy: input.createdBy,
19024
- ruleId: input.ruleId,
19025
- valueFingerprint: input.valueFingerprint,
19026
- keyVersion: input.keyVersion
19027
- });
19028
- if (Number(superseded.changes) !== 1) {
19029
- throw new DuplicateActiveExceptionError(input.ruleId);
19030
- }
19031
- this.insertExceptionRow(id, input, now);
19032
- this.db.exec("COMMIT");
19033
- } catch (retryErr) {
19034
- this.db.exec("ROLLBACK");
19035
- throw retryErr;
19036
- }
19258
+ ).run({
19259
+ now,
19260
+ revokedBy: input.createdBy,
19261
+ ruleId: input.ruleId,
19262
+ valueFingerprint: input.valueFingerprint,
19263
+ keyVersion: input.keyVersion
19264
+ });
19265
+ if (Number(superseded.changes) !== 1) {
19266
+ throw new DuplicateActiveExceptionError(input.ruleId);
19267
+ }
19268
+ this.insertExceptionRow(id, input, now);
19269
+ },
19270
+ "IMMEDIATE"
19271
+ );
19272
+ }
19273
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
19274
+ id
19275
+ });
19276
+ if (row === void 0) {
19277
+ throw new Error("exception row not found immediately after insert");
19037
19278
  }
19038
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
19039
19279
  return parseExceptionRow(row);
19040
19280
  }
19041
19281
  insertExceptionRow(id, input, now) {
@@ -19073,14 +19313,11 @@ var SqliteExceptionsRepository = class {
19073
19313
  */
19074
19314
  list(opts) {
19075
19315
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19076
- const rows = this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`).all(opts?.includeTerminal ? {} : { now: Date.now() });
19077
- const exceptions = [];
19078
- for (const row of rows) {
19079
- try {
19080
- exceptions.push(parseExceptionRow(row));
19081
- } catch {
19082
- }
19083
- }
19316
+ const rows = allRows(
19317
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19318
+ opts?.includeTerminal ? {} : { now: Date.now() }
19319
+ );
19320
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19084
19321
  return Promise.resolve(exceptions);
19085
19322
  }
19086
19323
  /**
@@ -19090,7 +19327,12 @@ var SqliteExceptionsRepository = class {
19090
19327
  */
19091
19328
  getByIdPrefix(prefix) {
19092
19329
  if (prefix.length === 0) return Promise.resolve(void 0);
19093
- const rows = this.db.prepare(String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`).all({ pattern: `${escapeLikePattern(prefix)}%` });
19330
+ const rows = allRows(
19331
+ this.db.prepare(
19332
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
19333
+ ),
19334
+ { pattern: `${escapeLikePattern(prefix)}%` }
19335
+ );
19094
19336
  if (rows.length > 1) {
19095
19337
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19096
19338
  }
@@ -19132,30 +19374,27 @@ var SqliteExceptionsRepository = class {
19132
19374
  * a different (rotated-away) key never match, so they are excluded at read.
19133
19375
  */
19134
19376
  activeBundleEntries(keyVersion, now = Date.now()) {
19135
- const rows = this.db.prepare(
19136
- `SELECT * FROM exceptions
19377
+ const rows = allRows(
19378
+ this.db.prepare(
19379
+ `SELECT * FROM exceptions
19137
19380
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19138
19381
  ORDER BY created_at DESC, rowid DESC`
19139
- ).all({ keyVersion, now });
19140
- const entries = [];
19141
- for (const row of rows) {
19142
- try {
19143
- const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19144
- entries.push(
19145
- ExceptionBundleEntry.parse({
19146
- id: row.id,
19147
- ruleId: row.rule_id,
19148
- valueFingerprint: row.value_fingerprint,
19149
- keyVersion: row.key_version,
19150
- expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19151
- maxUses: row.max_uses,
19152
- useCount: row.use_count,
19153
- conditions
19154
- })
19155
- );
19156
- } catch {
19157
- }
19158
- }
19382
+ ),
19383
+ { keyVersion, now }
19384
+ );
19385
+ const entries = mapRowsTolerant(rows, (row) => {
19386
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19387
+ return ExceptionBundleEntry.parse({
19388
+ id: row.id,
19389
+ ruleId: row.rule_id,
19390
+ valueFingerprint: row.value_fingerprint,
19391
+ keyVersion: row.key_version,
19392
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19393
+ maxUses: row.max_uses,
19394
+ useCount: row.use_count,
19395
+ conditions
19396
+ });
19397
+ });
19159
19398
  return Promise.resolve(entries);
19160
19399
  }
19161
19400
  /**
@@ -19182,11 +19421,14 @@ var SqliteExceptionsRepository = class {
19182
19421
  }
19183
19422
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19184
19423
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19185
- const rows = this.db.prepare(
19186
- `SELECT * FROM blocked_detections
19424
+ const rows = allRows(
19425
+ this.db.prepare(
19426
+ `SELECT * FROM blocked_detections
19187
19427
  WHERE blocked_at > :cutoff
19188
19428
  ORDER BY blocked_at DESC, rowid DESC`
19189
- ).all({ cutoff: Date.now() - windowMs });
19429
+ ),
19430
+ { cutoff: Date.now() - windowMs }
19431
+ );
19190
19432
  return Promise.resolve(
19191
19433
  rows.map((row) => ({
19192
19434
  reference: row.reference,
@@ -19266,7 +19508,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
19266
19508
  )`;
19267
19509
 
19268
19510
  // ../../packages/persistence/src/repositories/findings.ts
19269
- var GROUP_ROW_CAP = 2e3;
19511
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
19512
+ var CONCAT_SEP = ",";
19513
+ var TUPLE_SEP = "|";
19514
+ function splitConcat(value) {
19515
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
19516
+ }
19270
19517
  function deriveInstanceStatus(row) {
19271
19518
  return deriveFindingStatus({
19272
19519
  kind: row.kind,
@@ -19334,13 +19581,16 @@ var SqliteFindingsRepository = class {
19334
19581
  }
19335
19582
  recentFindings(opts) {
19336
19583
  const limit = opts?.limit ?? 50;
19337
- const rows = this.db.prepare(
19338
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19584
+ const rows = allRows(
19585
+ this.db.prepare(
19586
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19339
19587
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19340
19588
  FROM findings f JOIN events e ON e.id = f.event_id
19341
19589
  ORDER BY e.occurred_at DESC, f.rowid DESC
19342
19590
  LIMIT :limit`
19343
- ).all({ limit });
19591
+ ),
19592
+ { limit }
19593
+ );
19344
19594
  return Promise.resolve(
19345
19595
  rows.map((r) => ({
19346
19596
  id: r.id,
@@ -19363,20 +19613,47 @@ var SqliteFindingsRepository = class {
19363
19613
  * applies the requested filters, and sorts by severity then recency. Filtering
19364
19614
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19365
19615
  * reflect the full filtered set; `items` is the requested
19366
- * page (default 100); no cursor (nextCursor is always null).
19616
+ * page (default 50); no cursor (nextCursor is always null).
19617
+ *
19618
+ * Two reads, neither of which materializes a row per finding:
19619
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
19620
+ * the group and the filters need (count, providers, actions, statuses,
19621
+ * latest, search text);
19622
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
19623
+ * populate `instances` for the table's expanded rows.
19624
+ * The aggregates carry raw DB values and are translated by the same
19625
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
19626
+ * rule is ever restated in SQL.
19367
19627
  */
19368
19628
  listGroupedFindings(query) {
19369
- const rows = this.db.prepare(
19370
- `SELECT f.id, f.rule_id, f.category, f.severity, f.masked_match,
19371
- f.action_taken, f.confidence, e.occurred_at, e.source_tool,
19372
- json_extract(e.metadata, '$.repo') AS repo,
19373
- json_extract(e.metadata, '$.filePath') AS file,
19374
- e.kind AS kind, f.finding_key AS finding_key,
19375
- ${latestResolutionStatusSql("f")} AS latest_status
19376
- FROM findings f JOIN events e ON e.id = f.event_id
19377
- ORDER BY e.occurred_at DESC, f.id DESC
19378
- LIMIT :cap`
19379
- ).all({ cap: GROUP_ROW_CAP });
19629
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
19630
+ const rows = allRows(
19631
+ this.db.prepare(
19632
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19633
+ occurred_at, source_tool, repo, file, kind, finding_key, latest_status
19634
+ FROM (
19635
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19636
+ f.severity AS severity, f.masked_match AS masked_match,
19637
+ f.action_taken AS action_taken, f.confidence AS confidence,
19638
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19639
+ json_extract(e.metadata, '$.repo') AS repo,
19640
+ json_extract(e.metadata, '$.filePath') AS file,
19641
+ e.kind AS kind, f.finding_key AS finding_key,
19642
+ latest.status AS latest_status,
19643
+ ROW_NUMBER() OVER (
19644
+ PARTITION BY f.rule_id
19645
+ ORDER BY e.occurred_at DESC, f.id DESC
19646
+ ) AS rn
19647
+ FROM findings f
19648
+ JOIN events e ON e.id = f.event_id
19649
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19650
+ ON latest.finding_key = f.finding_key
19651
+ )
19652
+ WHERE rn <= :cap
19653
+ ORDER BY occurred_at DESC, id DESC`
19654
+ ),
19655
+ { cap: PREVIEW_INSTANCES_PER_GROUP }
19656
+ );
19380
19657
  const groupable = rows.map((r) => ({
19381
19658
  id: r.id,
19382
19659
  ruleId: r.rule_id,
@@ -19391,7 +19668,7 @@ var SqliteFindingsRepository = class {
19391
19668
  file: r.file ?? "",
19392
19669
  status: deriveInstanceStatus(r)
19393
19670
  }));
19394
- const allGroups = buildFindingGroups(groupable);
19671
+ const allGroups = buildFindingGroups(groupable, { aggregates });
19395
19672
  const filterOpts = {
19396
19673
  severity: query.severity,
19397
19674
  providers: query.provider,
@@ -19409,42 +19686,122 @@ var SqliteFindingsRepository = class {
19409
19686
  const items = sorted.slice(0, limit);
19410
19687
  return Promise.resolve({ totals, facets, items, nextCursor: null });
19411
19688
  }
19689
+ /**
19690
+ * One row per rule_id, folding EVERY instance of the group into the values
19691
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
19692
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
19693
+ *
19694
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
19695
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19696
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19697
+ * status keeps the classifier itself in @akasecurity/schema, where
19698
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
19699
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
19700
+ * a group's row stays small however many findings it holds.
19701
+ *
19702
+ * `withSearchText` is the exception, and the one column here that does NOT
19703
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
19704
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19705
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
19706
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
19707
+ * match an instance outside the preview, which searching the preview alone
19708
+ * would silently lose, so it is fetched only when the request actually
19709
+ * carries a `q`.
19710
+ */
19711
+ groupAggregates(withSearchText) {
19712
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19713
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
19714
+ const rows = this.db.prepare(
19715
+ `SELECT f.rule_id AS rule_id,
19716
+ count(*) AS instance_count,
19717
+ max(e.occurred_at) AS latest_at,
19718
+ group_concat(DISTINCT e.source_tool) AS source_tools,
19719
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
19720
+ group_concat(DISTINCT (
19721
+ e.kind || '${TUPLE_SEP}' ||
19722
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19723
+ coalesce(latest.status, '')
19724
+ )) AS status_inputs
19725
+ ${searchTextColumns}
19726
+ FROM findings f
19727
+ JOIN events e ON e.id = f.event_id
19728
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19729
+ ON latest.finding_key = f.finding_key
19730
+ GROUP BY f.rule_id`
19731
+ ).all();
19732
+ return new Map(
19733
+ rows.map((r) => [
19734
+ r.rule_id,
19735
+ {
19736
+ instanceCount: r.instance_count,
19737
+ sourceTools: splitConcat(r.source_tools),
19738
+ actionsTaken: splitConcat(r.actions_taken),
19739
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19740
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
19741
+ return {
19742
+ // deriveFindingStatus only distinguishes null from non-null here,
19743
+ // so the marker stands in for the key itself (never rendered).
19744
+ kind,
19745
+ findingKey: keyMarker === "" ? null : keyMarker,
19746
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
19747
+ };
19748
+ }),
19749
+ latestDetectedAt: epochMillisToIso(r.latest_at),
19750
+ // Free text only — joined and substring-matched, so group_concat's
19751
+ // commas need no unpicking (a repo/path containing one still matches).
19752
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
19753
+ // tell "no q this request" from "a group with no repo/file at all"
19754
+ // and skip priming a haystack nothing will read.
19755
+ ...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
19756
+ }
19757
+ ])
19758
+ );
19759
+ }
19412
19760
  healthSummary() {
19413
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
19761
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
19414
19762
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
19415
- const grouped = this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken").all();
19763
+ const grouped = allRows(
19764
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
19765
+ );
19416
19766
  for (const row of grouped) {
19417
19767
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
19418
19768
  }
19419
19769
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
19420
- const sevRows = this.db.prepare(
19421
- `SELECT f.severity AS severity, count(*) AS c
19770
+ const sevRows = allRows(
19771
+ this.db.prepare(
19772
+ `SELECT f.severity AS severity, count(*) AS c
19422
19773
  FROM findings f
19423
19774
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19424
19775
  ON latest.finding_key = f.finding_key
19425
19776
  WHERE latest.status IS NULL OR latest.status != 'resolved'
19426
19777
  GROUP BY f.severity`
19427
- ).all();
19778
+ )
19779
+ );
19428
19780
  for (const row of sevRows) {
19429
19781
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
19430
19782
  }
19431
19783
  const categories = ENFORCEABLE_CATEGORIES;
19432
- const enabledRows = this.db.prepare(
19433
- `SELECT DISTINCT json_extract(target, '$.category') AS category
19784
+ const enabledRows = allRows(
19785
+ this.db.prepare(
19786
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
19434
19787
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
19435
- ).all();
19788
+ )
19789
+ );
19436
19790
  const enabled = new Set(enabledRows.map((r) => r.category));
19437
19791
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
19438
19792
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
19439
19793
  }
19440
19794
  activityByDay(days = 7) {
19441
19795
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
19442
- const rows = this.db.prepare(
19443
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19796
+ const rows = allRows(
19797
+ this.db.prepare(
19798
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19444
19799
  FROM findings f JOIN events e ON e.id = f.event_id
19445
19800
  WHERE e.occurred_at >= :since
19446
19801
  GROUP BY day, f.action_taken`
19447
- ).all({ since });
19802
+ ),
19803
+ { since }
19804
+ );
19448
19805
  const buckets = /* @__PURE__ */ new Map();
19449
19806
  for (let i = 0; i < days; i++) {
19450
19807
  const day = isoDay(since + i * DAY_MS3);
@@ -19516,17 +19873,19 @@ var SqliteInspectionFindingsRepository = class {
19516
19873
  insertStmt;
19517
19874
  insertFinding(input) {
19518
19875
  const row = toInspectionFindingRow(input);
19519
- this.insertStmt.run({
19520
- id: row.id,
19521
- auditEventId: row.auditEventId,
19522
- inspectionDefinitionId: row.inspectionDefinitionId,
19523
- classifiedDataId: row.classifiedDataId ?? null,
19524
- spanStart: row.spanStart,
19525
- spanEnd: row.spanEnd,
19526
- maskedMatch: row.maskedMatch,
19527
- actionTaken: row.actionTaken,
19528
- confidence: row.confidence
19529
- });
19876
+ this.insertStmt.run(
19877
+ bindParams({
19878
+ id: row.id,
19879
+ auditEventId: row.auditEventId,
19880
+ inspectionDefinitionId: row.inspectionDefinitionId,
19881
+ classifiedDataId: row.classifiedDataId,
19882
+ spanStart: row.spanStart,
19883
+ spanEnd: row.spanEnd,
19884
+ maskedMatch: row.maskedMatch,
19885
+ actionTaken: row.actionTaken,
19886
+ confidence: row.confidence
19887
+ })
19888
+ );
19530
19889
  }
19531
19890
  };
19532
19891
 
@@ -19602,12 +19961,7 @@ function isMirrorDowngrade(incoming, stored) {
19602
19961
  }
19603
19962
  function ruleIdsOf(rulesJson) {
19604
19963
  const ids = /* @__PURE__ */ new Set();
19605
- let raw;
19606
- try {
19607
- raw = JSON.parse(rulesJson);
19608
- } catch {
19609
- return ids;
19610
- }
19964
+ const raw = safeJson(rulesJson, []);
19611
19965
  if (!Array.isArray(raw)) return ids;
19612
19966
  for (const entry of raw) {
19613
19967
  if (entry && typeof entry === "object") {
@@ -19679,48 +20033,49 @@ var SqliteInstalledPacksRepository = class {
19679
20033
  ruleIds: new Set(pack.rules.map((r) => r.id))
19680
20034
  }));
19681
20035
  if (this.storedSignature() === inventorySignature(rows)) return;
19682
- const now = Date.now();
19683
- this.db.exec("BEGIN IMMEDIATE");
19684
- try {
19685
- const mirror = this.mirrorState();
19686
- let behind = false;
19687
- for (const row of rows) {
19688
- const params = {
19689
- id: randomUUID2(),
19690
- namespace: row.namespace,
19691
- packId: row.packId,
19692
- version: row.version,
19693
- name: row.name,
19694
- rulesJson: row.rulesJson,
19695
- now
19696
- };
19697
- const stored = mirror.get(`${row.namespace}/${row.packId}`);
19698
- if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
19699
- this.upsertAvailableStmt.run({
19700
- ...params,
20036
+ const now = Date.now();
20037
+ withTransaction(
20038
+ this.db,
20039
+ () => {
20040
+ const mirror = this.mirrorState();
20041
+ let behind = false;
20042
+ for (const row of rows) {
20043
+ const params = {
19701
20044
  id: randomUUID2(),
19702
- recordedBy: meta3?.recordedBy ?? null
19703
- });
19704
- } else {
19705
- behind = true;
20045
+ namespace: row.namespace,
20046
+ packId: row.packId,
20047
+ version: row.version,
20048
+ name: row.name,
20049
+ rulesJson: row.rulesJson,
20050
+ now
20051
+ };
20052
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
20053
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20054
+ this.upsertAvailableStmt.run({
20055
+ ...params,
20056
+ id: randomUUID2(),
20057
+ recordedBy: meta3?.recordedBy ?? null
20058
+ });
20059
+ } else {
20060
+ behind = true;
20061
+ }
20062
+ this.insertMissingStmt.run(params);
19706
20063
  }
19707
- this.insertMissingStmt.run(params);
19708
- }
19709
- if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
19710
- this.db.exec("COMMIT");
19711
- } catch (err) {
19712
- this.db.exec("ROLLBACK");
19713
- throw err;
19714
- }
20064
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20065
+ },
20066
+ "IMMEDIATE"
20067
+ );
19715
20068
  } catch {
19716
20069
  }
19717
20070
  }
19718
20071
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
19719
20072
  // input to the downgrade guard. Read INSIDE the write transaction.
19720
20073
  mirrorState() {
19721
- const rows = this.db.prepare(
19722
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
19723
- ).all();
20074
+ const rows = allRows(
20075
+ this.db.prepare(
20076
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20077
+ )
20078
+ );
19724
20079
  return new Map(
19725
20080
  rows.map((r) => [
19726
20081
  `${r.namespace}/${r.packId}`,
@@ -19732,7 +20087,9 @@ var SqliteInstalledPacksRepository = class {
19732
20087
  // (keys joined with '/', matching the detection id slug encoding — packId may
19733
20088
  // itself contain '/', but namespace may not, so the join is unambiguous).
19734
20089
  pruneAvailable(keep) {
19735
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20090
+ const rows = allRows(
20091
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20092
+ );
19736
20093
  const keepSet = new Set(keep);
19737
20094
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
19738
20095
  for (const r of rows) {
@@ -19759,11 +20116,13 @@ var SqliteInstalledPacksRepository = class {
19759
20116
  if (this.db.isTransaction) {
19760
20117
  throw new Error("applyUpdate must not be called inside an open transaction");
19761
20118
  }
19762
- this.db.exec("BEGIN IMMEDIATE");
19763
- try {
19764
- this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
19765
- const res = this.db.prepare(
19766
- `UPDATE installed_packs SET
20119
+ let changed = false;
20120
+ withTransaction(
20121
+ this.db,
20122
+ () => {
20123
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20124
+ const res = this.db.prepare(
20125
+ `UPDATE installed_packs SET
19767
20126
  version = (SELECT a.version FROM available_packs a
19768
20127
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
19769
20128
  name = (SELECT a.name FROM available_packs a
@@ -19774,17 +20133,13 @@ var SqliteInstalledPacksRepository = class {
19774
20133
  WHERE namespace = :namespace AND pack_id = :packId
19775
20134
  AND EXISTS (SELECT 1 FROM available_packs a
19776
20135
  WHERE a.namespace = :namespace AND a.pack_id = :packId)`
19777
- ).run({ namespace, packId, now: Date.now() });
19778
- this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
19779
- this.db.exec("COMMIT");
19780
- return Number(res.changes) > 0;
19781
- } catch (err) {
19782
- try {
19783
- this.db.exec("ROLLBACK");
19784
- } catch {
19785
- }
19786
- throw err;
19787
- }
20136
+ ).run({ namespace, packId, now: Date.now() });
20137
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20138
+ changed = Number(res.changes) > 0;
20139
+ },
20140
+ "IMMEDIATE"
20141
+ );
20142
+ return changed;
19788
20143
  }
19789
20144
  /**
19790
20145
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -19798,9 +20153,11 @@ var SqliteInstalledPacksRepository = class {
19798
20153
  * JSON-level failure therefore counts as invalid.
19799
20154
  */
19800
20155
  installedRuleset() {
19801
- const rows = this.db.prepare(
19802
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
19803
- ).all();
20156
+ const rows = allRows(
20157
+ this.db.prepare(
20158
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20159
+ )
20160
+ );
19804
20161
  const out = {
19805
20162
  installedPacks: rows.length,
19806
20163
  enabledPacks: 0,
@@ -19809,7 +20166,7 @@ var SqliteInstalledPacksRepository = class {
19809
20166
  ruleActions: /* @__PURE__ */ new Map()
19810
20167
  };
19811
20168
  for (const row of rows) {
19812
- if (row.enabled !== 1) continue;
20169
+ if (!intToBool(row.enabled)) continue;
19813
20170
  out.enabledPacks += 1;
19814
20171
  const action = policyIdToAction(row.policyId);
19815
20172
  let raw;
@@ -19844,9 +20201,11 @@ var SqliteInstalledPacksRepository = class {
19844
20201
  * running max would mask a genuinely-newer parseable stamp.
19845
20202
  */
19846
20203
  newestRecordedBinary() {
19847
- const rows = this.db.prepare(
19848
- `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
19849
- ).all();
20204
+ const rows = allRows(
20205
+ this.db.prepare(
20206
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20207
+ )
20208
+ );
19850
20209
  let newest = null;
19851
20210
  for (const row of rows) {
19852
20211
  const at = row.recordedBy.lastIndexOf("@");
@@ -19861,13 +20220,15 @@ var SqliteInstalledPacksRepository = class {
19861
20220
  return newest;
19862
20221
  }
19863
20222
  counts() {
19864
- const row = this.db.prepare(
19865
- `SELECT count(*) AS packs,
20223
+ const row = getRow(
20224
+ this.db.prepare(
20225
+ `SELECT count(*) AS packs,
19866
20226
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
19867
20227
  coalesce(sum(enabled), 0) AS enabled
19868
20228
  FROM installed_packs`
19869
- ).get();
19870
- return Promise.resolve(row);
20229
+ )
20230
+ );
20231
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
19871
20232
  }
19872
20233
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
19873
20234
  // Back the Policies page's built-in catalog: how many
@@ -19880,26 +20241,29 @@ var SqliteInstalledPacksRepository = class {
19880
20241
  * attributed to Monitor, matching the Detections views.
19881
20242
  */
19882
20243
  countsByPolicyId() {
19883
- const rows = this.db.prepare(
19884
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20244
+ return countBy(
20245
+ this.db,
20246
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
19885
20247
  FROM installed_packs
19886
- GROUP BY pid`
19887
- ).all();
19888
- return new Map(rows.map((r) => [r.pid, r.n]));
20248
+ GROUP BY k`
20249
+ );
19889
20250
  }
19890
20251
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
19891
20252
  listByPolicyId(policyId) {
19892
- const rows = this.db.prepare(
19893
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20253
+ const rows = allRows(
20254
+ this.db.prepare(
20255
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
19894
20256
  FROM installed_packs
19895
20257
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
19896
20258
  ORDER BY name ASC`
19897
- ).all(policyId);
20259
+ ),
20260
+ [policyId]
20261
+ );
19898
20262
  return rows.map((r) => ({
19899
20263
  id: `${r.namespace}/${r.packId}`,
19900
20264
  name: r.name,
19901
20265
  ruleCount: parseRules(r.rulesJson).length,
19902
- enabled: r.enabled === 1
20266
+ enabled: intToBool(r.enabled)
19903
20267
  }));
19904
20268
  }
19905
20269
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -19928,14 +20292,14 @@ var SqliteInstalledPacksRepository = class {
19928
20292
  const res = this.db.prepare(
19929
20293
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
19930
20294
  WHERE namespace = :namespace AND pack_id = :packId`
19931
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
20295
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
19932
20296
  return Number(res.changes) > 0;
19933
20297
  }
19934
20298
  // Fingerprint of the recorded available mirror — compared against the
19935
20299
  // incoming inventory's signature to skip the write entirely when the running
19936
20300
  // binary's inventory hasn't changed since the last record.
19937
20301
  storedSignature() {
19938
- const rows = this.signatureStmt.all();
20302
+ const rows = allRows(this.signatureStmt);
19939
20303
  return inventorySignature(rows);
19940
20304
  }
19941
20305
  };
@@ -19963,42 +20327,48 @@ var SqliteInventoryRepository = class {
19963
20327
  upsert(input, now = Date.now()) {
19964
20328
  const id = inventoryId(input.objectType, input.identityKey);
19965
20329
  const row = toInventoryRow(input, id, now);
19966
- this.upsertStmt.run({
19967
- id: row.id,
19968
- objectType: row.objectType,
19969
- location: row.location ?? null,
19970
- title: row.title ?? null,
19971
- hostId: row.hostId ?? null,
19972
- attributes: row.attributes,
19973
- firstSeen: row.firstSeen,
19974
- lastSeen: row.lastSeen
19975
- });
20330
+ this.upsertStmt.run(
20331
+ bindParams({
20332
+ id: row.id,
20333
+ objectType: row.objectType,
20334
+ location: row.location,
20335
+ title: row.title,
20336
+ hostId: row.hostId,
20337
+ attributes: row.attributes,
20338
+ firstSeen: row.firstSeen,
20339
+ lastSeen: row.lastSeen
20340
+ })
20341
+ );
19976
20342
  return id;
19977
20343
  }
19978
20344
  // The full row, for round-trip assertions.
19979
20345
  findById(id) {
19980
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
19981
- return row;
20346
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
19982
20347
  }
19983
20348
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
19984
20349
  // served from the object_type index, never from audit_events.
19985
20350
  distinctTitles(objectType) {
19986
- const rows = this.db.prepare(
19987
- `SELECT DISTINCT title FROM inventory
20351
+ const rows = allRows(
20352
+ this.db.prepare(
20353
+ `SELECT DISTINCT title FROM inventory
19988
20354
  WHERE object_type = :objectType AND title IS NOT NULL
19989
20355
  ORDER BY title`
19990
- ).all({ objectType });
20356
+ ),
20357
+ { objectType }
20358
+ );
19991
20359
  return rows.map((r) => r.title);
19992
20360
  }
19993
20361
  // Distinct host os_version values — a facet served from an inventory index
19994
20362
  // over the generated column, never from the audit fact (confirm via EXPLAIN
19995
20363
  // QUERY PLAN).
19996
20364
  osVersions() {
19997
- const rows = this.db.prepare(
19998
- `SELECT DISTINCT os_version AS value FROM inventory
20365
+ const rows = allRows(
20366
+ this.db.prepare(
20367
+ `SELECT DISTINCT os_version AS value FROM inventory
19999
20368
  WHERE object_type = 'host' AND os_version IS NOT NULL
20000
20369
  ORDER BY value`
20001
- ).all();
20370
+ )
20371
+ );
20002
20372
  return rows.map((r) => r.value);
20003
20373
  }
20004
20374
  };
@@ -20018,14 +20388,6 @@ var EMPTY_PROJECT_AGG = {
20018
20388
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
20019
20389
  findingsCount: 0
20020
20390
  };
20021
- function safeJson(s, fallback) {
20022
- if (s == null) return fallback;
20023
- try {
20024
- return JSON.parse(s);
20025
- } catch {
20026
- return fallback;
20027
- }
20028
- }
20029
20391
  function resolveHarnessId(attrs, row) {
20030
20392
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
20031
20393
  return attrs.provider;
@@ -20221,30 +20583,49 @@ var SqliteInventoryAssetsRepository = class {
20221
20583
  configRowsCache;
20222
20584
  // ─── stats ─────────────────────────────────────────────────────────────────
20223
20585
  getInventoryStats() {
20224
- const byType = { project: 0, skill: 0, mcp: 0, hook: 0, config: 0 };
20225
- for (const r of this.db.prepare("SELECT asset_type AS t, count(*) AS n FROM inventory_asset GROUP BY asset_type").all()) {
20226
- if (r.t in byType) byType[r.t] = r.n;
20227
- }
20228
- byType.project = this.db.prepare(`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`).get().n;
20229
- const mcpTrust = { "known-good": 0, risky: 0, unapproved: 0 };
20230
- for (const r of this.db.prepare(
20231
- `SELECT coalesce(o.trust, a.trust) AS trust, count(*) AS n
20586
+ const typeCounts = countBy(
20587
+ this.db,
20588
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
20589
+ );
20590
+ const byType = {
20591
+ project: 0,
20592
+ skill: typeCounts.get("skill") ?? 0,
20593
+ mcp: typeCounts.get("mcp") ?? 0,
20594
+ hook: typeCounts.get("hook") ?? 0,
20595
+ config: typeCounts.get("config") ?? 0
20596
+ };
20597
+ byType.project = countScalar(
20598
+ this.db,
20599
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
20600
+ );
20601
+ const mcpTrustCounts = countBy(
20602
+ this.db,
20603
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20232
20604
  FROM inventory_asset a
20233
20605
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20234
20606
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20235
20607
  GROUP BY coalesce(o.trust, a.trust)`
20236
- ).all()) {
20237
- if (r.trust in mcpTrust) mcpTrust[r.trust] = r.n;
20238
- }
20239
- const harnesses = this.db.prepare(
20608
+ );
20609
+ const mcpTrust = {
20610
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
20611
+ risky: mcpTrustCounts.get("risky") ?? 0,
20612
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
20613
+ };
20614
+ const harnesses = countScalar(
20615
+ this.db,
20240
20616
  `SELECT count(*) AS n FROM inventory
20241
20617
  WHERE object_type = 'harness'
20242
- AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20243
- ).get({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }).n;
20244
- const flaggedAssets = this.db.prepare("SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'").get().n;
20245
- const flaggedProjects = this.db.prepare(
20618
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
20619
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20620
+ );
20621
+ const flaggedAssets = countScalar(
20622
+ this.db,
20623
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
20624
+ );
20625
+ const flaggedProjects = countScalar(
20626
+ this.db,
20246
20627
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20247
- ).get().n;
20628
+ );
20248
20629
  const configRows = this.configAssetRows();
20249
20630
  for (const r of configRows) {
20250
20631
  byType[r.assetType] += 1;
@@ -20501,12 +20882,15 @@ var SqliteInventoryAssetsRepository = class {
20501
20882
  }
20502
20883
  // ─── raw fetchers ────────────────────────────────────────────────────────────
20503
20884
  fetchHarnessRows() {
20504
- return this.db.prepare(
20505
- `SELECT id, title, attributes, harness_version AS harnessVersion
20885
+ return allRows(
20886
+ this.db.prepare(
20887
+ `SELECT id, title, attributes, harness_version AS harnessVersion
20506
20888
  FROM inventory
20507
20889
  WHERE object_type = 'harness'
20508
20890
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20509
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
20891
+ ),
20892
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20893
+ );
20510
20894
  }
20511
20895
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
20512
20896
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -20516,12 +20900,13 @@ var SqliteInventoryAssetsRepository = class {
20516
20900
  const params = [...harnessInvIds];
20517
20901
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
20518
20902
  if (q) {
20519
- const pat = `%${escapeLikePattern(q)}%`;
20520
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
20903
+ const pat = containsPattern(q);
20904
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
20521
20905
  params.push(pat, pat);
20522
20906
  }
20523
- const rows = this.db.prepare(
20524
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20907
+ const rows = allRows(
20908
+ this.db.prepare(
20909
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20525
20910
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20526
20911
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20527
20912
  FROM harness_asset ha
@@ -20529,7 +20914,9 @@ var SqliteInventoryAssetsRepository = class {
20529
20914
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20530
20915
  WHERE ${where}
20531
20916
  ORDER BY a.name ASC`
20532
- ).all(...params);
20917
+ ),
20918
+ params
20919
+ );
20533
20920
  for (const raw of rows) {
20534
20921
  const harnessInvId = raw.harnessInvId;
20535
20922
  const [asset] = this.mapAssetRows([raw]);
@@ -20548,21 +20935,24 @@ var SqliteInventoryAssetsRepository = class {
20548
20935
  params.push(...types);
20549
20936
  }
20550
20937
  if (q) {
20551
- const pat = `%${escapeLikePattern(q)}%`;
20552
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
20938
+ const pat = containsPattern(q);
20939
+ conditions.push(likeAny(["a.name", "a.sub"]));
20553
20940
  params.push(pat, pat);
20554
20941
  }
20555
20942
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
20556
20943
  const sampleRows = this.mapAssetRows(
20557
- this.db.prepare(
20558
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20944
+ allRows(
20945
+ this.db.prepare(
20946
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20559
20947
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20560
20948
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20561
20949
  FROM inventory_asset a
20562
20950
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20563
20951
  ${where}
20564
20952
  ORDER BY a.name ASC`
20565
- ).all(...params)
20953
+ ),
20954
+ params
20955
+ )
20566
20956
  );
20567
20957
  const configRows = this.configAssetRows(q).filter(
20568
20958
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -20571,14 +20961,17 @@ var SqliteInventoryAssetsRepository = class {
20571
20961
  }
20572
20962
  fetchAssetById(assetId) {
20573
20963
  const rows = this.mapAssetRows(
20574
- this.db.prepare(
20575
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20964
+ allRows(
20965
+ this.db.prepare(
20966
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20576
20967
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20577
20968
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20578
20969
  FROM inventory_asset a
20579
20970
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20580
20971
  WHERE a.id = ?`
20581
- ).all(assetId)
20972
+ ),
20973
+ [assetId]
20974
+ )
20582
20975
  );
20583
20976
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
20584
20977
  }
@@ -20634,37 +21027,39 @@ var SqliteInventoryAssetsRepository = class {
20634
21027
  return rows;
20635
21028
  }
20636
21029
  latestConfigScanId() {
20637
- const row = this.db.prepare(
20638
- `SELECT id FROM audit_events WHERE event_type = 'config_scan'
20639
- ORDER BY started_at DESC, id DESC LIMIT 1`
20640
- ).get();
20641
- return row?.id ?? null;
21030
+ return latestConfigScan(this.db)?.id ?? null;
20642
21031
  }
20643
21032
  fetchProjects(q) {
20644
21033
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
20645
21034
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
20646
21035
  const params = [];
20647
21036
  if (q) {
20648
- const pat = `%${escapeLikePattern(q)}%`;
20649
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
21037
+ const pat = containsPattern(q);
21038
+ sql += ` AND ${likeAny(["name", "url"])}`;
20650
21039
  params.push(pat, pat);
20651
21040
  }
20652
21041
  sql += " ORDER BY name ASC";
20653
- return this.db.prepare(sql).all(...params);
21042
+ return allRows(this.db.prepare(sql), params);
20654
21043
  }
20655
21044
  fetchProjectById(projectId) {
20656
- return this.db.prepare(
20657
- "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
20658
- ).get(projectId) ?? null;
21045
+ return getRow(
21046
+ this.db.prepare(
21047
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
21048
+ ),
21049
+ [projectId]
21050
+ ) ?? null;
20659
21051
  }
20660
21052
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
20661
21053
  fetchProjectsByIds(projectIds) {
20662
21054
  const map2 = /* @__PURE__ */ new Map();
20663
21055
  if (projectIds.length === 0) return map2;
20664
- const rows = this.db.prepare(
20665
- `SELECT id, url, name, attributes, last_seen AS lastSeen
21056
+ const rows = allRows(
21057
+ this.db.prepare(
21058
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
20666
21059
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
20667
- ).all(...projectIds);
21060
+ ),
21061
+ projectIds
21062
+ );
20668
21063
  for (const r of rows) map2.set(r.id, r);
20669
21064
  return map2;
20670
21065
  }
@@ -20675,8 +21070,9 @@ var SqliteInventoryAssetsRepository = class {
20675
21070
  projectAggregates(projectIds) {
20676
21071
  const map2 = /* @__PURE__ */ new Map();
20677
21072
  if (projectIds.length === 0) return map2;
20678
- const rows = this.db.prepare(
20679
- `SELECT f.project_id AS projectId,
21073
+ const rows = allRows(
21074
+ this.db.prepare(
21075
+ `SELECT f.project_id AS projectId,
20680
21076
  coalesce(o.access, f.default_access) AS eff,
20681
21077
  count(*) AS n,
20682
21078
  coalesce(sum(f.findings_count), 0) AS findings
@@ -20684,7 +21080,9 @@ var SqliteInventoryAssetsRepository = class {
20684
21080
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
20685
21081
  WHERE f.project_id IN (${placeholders(projectIds.length)})
20686
21082
  GROUP BY f.project_id, eff`
20687
- ).all(...projectIds);
21083
+ ),
21084
+ projectIds
21085
+ );
20688
21086
  for (const r of rows) {
20689
21087
  let agg = map2.get(r.projectId);
20690
21088
  if (!agg) {
@@ -20722,37 +21120,52 @@ var SqliteInventoryAssetsRepository = class {
20722
21120
  fetchProjectFilesUnder(projectId, prefix) {
20723
21121
  if (prefix === "") {
20724
21122
  return this.mapFileRows(
20725
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21123
+ allRows(
21124
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21125
+ [projectId]
21126
+ )
20726
21127
  );
20727
21128
  }
20728
21129
  return this.mapFileRows(
20729
- this.db.prepare(
20730
- this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
20731
- ).all(projectId, `${escapeLikePattern(prefix)}/%`)
21130
+ allRows(
21131
+ this.db.prepare(
21132
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21133
+ ),
21134
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21135
+ )
20732
21136
  );
20733
21137
  }
20734
21138
  fetchProjectFilesSearch(projectId, q) {
20735
- const pat = `%${escapeLikePattern(q)}%`;
21139
+ const pat = containsPattern(q);
20736
21140
  return this.mapFileRows(
20737
- this.db.prepare(
20738
- this.fileSelect(
20739
- "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
20740
- )
20741
- ).all(projectId, pat, pat)
21141
+ allRows(
21142
+ this.db.prepare(
21143
+ this.fileSelect(
21144
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21145
+ )
21146
+ ),
21147
+ [projectId, pat, pat]
21148
+ )
20742
21149
  );
20743
21150
  }
20744
21151
  fetchProjectFilesBlocked(projectId) {
20745
21152
  return this.mapFileRows(
20746
- this.db.prepare(
20747
- this.fileSelect(
20748
- "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
20749
- )
20750
- ).all(projectId)
21153
+ allRows(
21154
+ this.db.prepare(
21155
+ this.fileSelect(
21156
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21157
+ )
21158
+ ),
21159
+ [projectId]
21160
+ )
20751
21161
  );
20752
21162
  }
20753
21163
  fetchProjectFile(projectId, path) {
20754
21164
  const rows = this.mapFileRows(
20755
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21165
+ allRows(
21166
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21167
+ [projectId, path]
21168
+ )
20756
21169
  );
20757
21170
  return rows[0] ?? null;
20758
21171
  }
@@ -20766,39 +21179,32 @@ var SqlitePoliciesRepository = class {
20766
21179
  }
20767
21180
  db;
20768
21181
  readPolicies() {
20769
- const rows = this.db.prepare("SELECT * FROM policies").all();
20770
- const policies = [];
20771
- for (const row of rows) {
20772
- try {
20773
- const target = JSON.parse(row.target);
20774
- const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
20775
- policies.push(
20776
- Policy.parse({
20777
- id: row.id,
20778
- scope: row.scope,
20779
- target,
20780
- action: row.action,
20781
- enabled: row.enabled === 1,
20782
- customKeywords
20783
- })
20784
- );
20785
- } catch {
20786
- }
20787
- }
21182
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21183
+ const policies = mapRowsTolerant(rows, (row) => {
21184
+ const target = JSON.parse(row.target);
21185
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21186
+ return Policy.parse({
21187
+ id: row.id,
21188
+ scope: row.scope,
21189
+ target,
21190
+ action: row.action,
21191
+ enabled: intToBool(row.enabled),
21192
+ customKeywords
21193
+ });
21194
+ });
20788
21195
  return Promise.resolve(policies);
20789
21196
  }
20790
21197
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
20791
21198
  // detection-type config exists from first run. Only when the table is empty,
20792
21199
  // so a user's edits are never clobbered.
20793
21200
  seedDefaults() {
20794
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21201
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
20795
21202
  if (count > 0) return;
20796
21203
  const stmt = this.db.prepare(
20797
21204
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
20798
21205
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
20799
21206
  );
20800
- this.db.exec("BEGIN");
20801
- try {
21207
+ failOpenTransaction(this.db, () => {
20802
21208
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
20803
21209
  stmt.run({
20804
21210
  id: randomUUID4(),
@@ -20807,10 +21213,41 @@ var SqlitePoliciesRepository = class {
20807
21213
  now: Date.now()
20808
21214
  });
20809
21215
  }
20810
- this.db.exec("COMMIT");
20811
- } catch {
20812
- this.db.exec("ROLLBACK");
20813
- }
21216
+ });
21217
+ }
21218
+ // Insert-or-update the single global per-category policy row, keyed on the
21219
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21220
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21221
+ // values), so the runtime's resolveAction reads rows written by either path
21222
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21223
+ // `id` and `created_at` are left exactly as they were.
21224
+ upsertCategoryAction(category, action) {
21225
+ const now = Date.now();
21226
+ this.db.prepare(
21227
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21228
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21229
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21230
+ ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
21231
+ }
21232
+ // Caps every global per-category policy currently set to block/redact down
21233
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21234
+ // Returns the number of rows changed.
21235
+ capCategoryActions() {
21236
+ const info = this.db.prepare(
21237
+ `UPDATE policies SET action='warn', updated_at=:now
21238
+ WHERE scope='global' AND action IN ('block','redact')
21239
+ AND json_extract(target,'$.category') IS NOT NULL`
21240
+ ).run({ now: Date.now() });
21241
+ return Number(info.changes);
21242
+ }
21243
+ // Read the current action for a single global per-category policy row, mirroring
21244
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21245
+ // row exists yet, so callers can distinguish an unset category from a set one.
21246
+ getCategoryAction(category) {
21247
+ const row = this.db.prepare(
21248
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21249
+ ).get({ category });
21250
+ return row?.action;
20814
21251
  }
20815
21252
  };
20816
21253
 
@@ -20886,7 +21323,7 @@ var SqliteProjectFilesRepository = class {
20886
21323
  maxStampStmt;
20887
21324
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
20888
21325
  replaceForProject(projectId, scan2, now) {
20889
- const { maxStamp } = this.maxStampStmt.get({ projectId });
21326
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
20890
21327
  const stamp = Math.max(now, maxStamp + 1);
20891
21328
  for (const file2 of scan2.files) {
20892
21329
  this.upsertStmt.run({
@@ -20969,7 +21406,7 @@ var SqliteResolutionsRepository = class {
20969
21406
  }
20970
21407
  /** The newest disposition recorded for a finding key, or undefined if none. */
20971
21408
  latestByKey(key) {
20972
- const row = this.latestStmt.get({ findingKey: key });
21409
+ const row = getRow(this.latestStmt, { findingKey: key });
20973
21410
  if (!row) return void 0;
20974
21411
  return {
20975
21412
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -20986,7 +21423,7 @@ var SqliteResolutionsRepository = class {
20986
21423
  * the CLI) surfaces for that file.
20987
21424
  */
20988
21425
  openAtRestKeysForPath(path) {
20989
- const rows = this.openAtRestStmt.all({ path });
21426
+ const rows = allRows(this.openAtRestStmt, { path });
20990
21427
  return rows.map((r) => r.finding_key);
20991
21428
  }
20992
21429
  /**
@@ -20997,7 +21434,7 @@ var SqliteResolutionsRepository = class {
20997
21434
  * resolution row (see scan.ts).
20998
21435
  */
20999
21436
  resolvedAtRestKeysForPath(path) {
21000
- const rows = this.resolvedAtRestStmt.all({ path });
21437
+ const rows = allRows(this.resolvedAtRestStmt, { path });
21001
21438
  return rows.map((r) => r.finding_key);
21002
21439
  }
21003
21440
  };
@@ -21026,31 +21463,25 @@ var SqliteScanLedgerRepository = class {
21026
21463
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
21027
21464
  // older ruleset are simply absent, which reads as "never scanned".
21028
21465
  entriesForRuleset(rulesetHash) {
21029
- const rows = this.readStmt.all({ rulesetHash });
21466
+ const rows = allRows(this.readStmt, {
21467
+ rulesetHash
21468
+ });
21030
21469
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
21031
21470
  }
21032
21471
  upsertEntries(entries) {
21033
21472
  if (entries.length === 0) return;
21034
21473
  const scannedAt = Date.now();
21035
- try {
21036
- this.db.exec("BEGIN");
21037
- try {
21038
- for (const entry of entries) {
21039
- this.upsertStmt.run({
21040
- path: entry.path,
21041
- mtime: entry.mtime,
21042
- contentHash: entry.contentHash,
21043
- rulesetHash: entry.rulesetHash,
21044
- scannedAt
21045
- });
21046
- }
21047
- this.db.exec("COMMIT");
21048
- } catch (err) {
21049
- this.db.exec("ROLLBACK");
21050
- throw err;
21474
+ failOpenTransaction(this.db, () => {
21475
+ for (const entry of entries) {
21476
+ this.upsertStmt.run({
21477
+ path: entry.path,
21478
+ mtime: entry.mtime,
21479
+ contentHash: entry.contentHash,
21480
+ rulesetHash: entry.rulesetHash,
21481
+ scannedAt
21482
+ });
21051
21483
  }
21052
- } catch {
21053
- }
21484
+ });
21054
21485
  }
21055
21486
  };
21056
21487
 
@@ -21127,8 +21558,9 @@ var SqliteSecurityRepository = class {
21127
21558
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21128
21559
  // double-counting a key that accumulated several append-only rows.
21129
21560
  severitySummary() {
21130
- const rows = this.db.prepare(
21131
- `SELECT f.severity AS severity,
21561
+ const rows = allRows(
21562
+ this.db.prepare(
21563
+ `SELECT f.severity AS severity,
21132
21564
  COUNT(*) AS count,
21133
21565
  SUM(CASE
21134
21566
  WHEN e.kind != 'code_change' THEN 1
@@ -21147,7 +21579,8 @@ var SqliteSecurityRepository = class {
21147
21579
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21148
21580
  ON latest.finding_key = f.finding_key
21149
21581
  GROUP BY f.severity`
21150
- ).all();
21582
+ )
21583
+ );
21151
21584
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21152
21585
  const bySeverity = SEVERITIES.map((severity) => ({
21153
21586
  severity,
@@ -21231,14 +21664,15 @@ var SqliteSecurityRepository = class {
21231
21664
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21232
21665
  const now = this.now();
21233
21666
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
21234
- const rows = this.db.prepare(
21235
- // first_detected_at is the PRESERVED first-detection time (set once on a
21236
- // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21237
- // measures from first sighting not the latest re-scan's event, whose
21238
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21239
- // the parent event's occurred_at defends against any legacy/edge row the
21240
- // backfill left null.
21241
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21667
+ const rows = allRows(
21668
+ this.db.prepare(
21669
+ // first_detected_at is the PRESERVED first-detection time (set once on a
21670
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21671
+ // measures from first sighting not the latest re-scan's event, whose
21672
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21673
+ // the parent event's occurred_at defends against any legacy/edge row the
21674
+ // backfill left null.
21675
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21242
21676
  (
21243
21677
  SELECT fr.status FROM finding_resolution fr
21244
21678
  WHERE fr.finding_key = f.finding_key
@@ -21264,14 +21698,16 @@ var SqliteSecurityRepository = class {
21264
21698
  WHERE fr.finding_key = f.finding_key
21265
21699
  AND fr.resolved_at >= :windowStart
21266
21700
  )`
21267
- // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21268
- // any resolution activity at/after the window start — a row this method
21269
- // ultimately counts has its LATEST resolution inside the window, which
21270
- // implies such a row exists, so nothing wanted is dropped. The exact
21271
- // latest-wins + status/method + window gate stays in JS below,
21272
- // dialect-agnostic. Without this, a
21273
- // 7d request evaluated the store's entire trackable-findings history.
21274
- ).all({ windowStart });
21701
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21702
+ // any resolution activity at/after the window start — a row this method
21703
+ // ultimately counts has its LATEST resolution inside the window, which
21704
+ // implies such a row exists, so nothing wanted is dropped. The exact
21705
+ // latest-wins + status/method + window gate stays in JS below,
21706
+ // dialect-agnostic. Without this, a
21707
+ // 7d request evaluated the store's entire trackable-findings history.
21708
+ ),
21709
+ { windowStart }
21710
+ );
21275
21711
  const sums = /* @__PURE__ */ new Map();
21276
21712
  const counts = /* @__PURE__ */ new Map();
21277
21713
  for (const r of rows) {
@@ -21301,8 +21737,9 @@ var SqliteSecurityRepository = class {
21301
21737
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
21302
21738
  const now = this.now();
21303
21739
  const from = now - RANGE_DAYS[range] * DAY_MS4;
21304
- const rows = this.db.prepare(
21305
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21740
+ const rows = allRows(
21741
+ this.db.prepare(
21742
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21306
21743
  FROM findings f JOIN events e ON e.id = f.event_id
21307
21744
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21308
21745
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -21310,7 +21747,9 @@ var SqliteSecurityRepository = class {
21310
21747
  GROUP BY repo
21311
21748
  ORDER BY c DESC, repo
21312
21749
  LIMIT :limit`
21313
- ).all({ from, to: now, limit });
21750
+ ),
21751
+ { from, to: now, limit }
21752
+ );
21314
21753
  const items = rows.map((r) => ({
21315
21754
  id: `repo_${r.repo}`,
21316
21755
  name: r.repo,
@@ -21332,8 +21771,9 @@ var SqliteSecurityRepository = class {
21332
21771
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21333
21772
  // capped at `limit`.
21334
21773
  recentlyResolved(limit = 20) {
21335
- const rows = this.db.prepare(
21336
- `SELECT f.finding_key AS finding_key,
21774
+ const rows = allRows(
21775
+ this.db.prepare(
21776
+ `SELECT f.finding_key AS finding_key,
21337
21777
  f.rule_id AS rule_id,
21338
21778
  f.severity AS severity,
21339
21779
  json_extract(e.metadata, '$.filePath') AS path,
@@ -21367,7 +21807,9 @@ var SqliteSecurityRepository = class {
21367
21807
  ) IS NOT NULL
21368
21808
  ORDER BY latest_resolved_at DESC
21369
21809
  LIMIT :limit`
21370
- ).all({ limit });
21810
+ ),
21811
+ { limit }
21812
+ );
21371
21813
  const items = rows.map((r) => ({
21372
21814
  findingKey: r.finding_key,
21373
21815
  ruleId: r.rule_id,
@@ -21384,12 +21826,15 @@ var SqliteSecurityRepository = class {
21384
21826
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
21385
21827
  // numeric and the JS aggregations bucket/split on ms directly.
21386
21828
  findingsInRange(fromMs, toMs) {
21387
- const rows = this.db.prepare(
21388
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21829
+ const rows = allRows(
21830
+ this.db.prepare(
21831
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21389
21832
  FROM findings f JOIN events e ON e.id = f.event_id
21390
21833
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21391
21834
  ORDER BY e.occurred_at`
21392
- ).all({ from: fromMs, to: toMs });
21835
+ ),
21836
+ { from: fromMs, to: toMs }
21837
+ );
21393
21838
  return rows.map((r) => ({
21394
21839
  occurredAt: r.occurred_at,
21395
21840
  severity: r.severity,
@@ -21403,12 +21848,7 @@ import { randomUUID as randomUUID7 } from "crypto";
21403
21848
  var KIND_ORDER = ["provider", "internal", "ip"];
21404
21849
  var CALL_SITE_EMBED_CAP = 200;
21405
21850
  function parseNetwork(networkJson) {
21406
- if (!networkJson) return null;
21407
- try {
21408
- return JSON.parse(networkJson);
21409
- } catch {
21410
- return null;
21411
- }
21851
+ return safeJson(networkJson, null);
21412
21852
  }
21413
21853
  function toEndpointSummary(row) {
21414
21854
  return {
@@ -21495,27 +21935,39 @@ var SqliteSharesRepository = class {
21495
21935
  }
21496
21936
  db;
21497
21937
  stats() {
21498
- const scalar = (sql) => this.db.prepare(sql).get()?.n ?? 0;
21499
- const destinations = scalar("SELECT count(*) AS n FROM share_destination");
21500
- const endpoints = scalar("SELECT count(*) AS n FROM share_endpoint");
21501
- const callSites = scalar("SELECT count(*) AS n FROM share_call_site");
21502
- const insecure = scalar(
21938
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
21939
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
21940
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
21941
+ const insecure = countScalar(
21942
+ this.db,
21503
21943
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
21504
21944
  );
21505
- const needsReview = scalar(
21945
+ const needsReview = countScalar(
21946
+ this.db,
21506
21947
  `SELECT count(DISTINCT d.id) AS n
21507
21948
  FROM share_destination d
21508
21949
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
21509
21950
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
21510
21951
  );
21511
- const byKind = { provider: 0, internal: 0, ip: 0 };
21512
- for (const row of this.db.prepare("SELECT kind, count(*) AS n FROM share_destination GROUP BY kind").all()) {
21513
- byKind[row.kind] = row.n;
21514
- }
21515
- const byTrust = { recognized: 0, internal: 0, unverified: 0, ip: 0 };
21516
- for (const row of this.db.prepare("SELECT trust, count(*) AS n FROM share_destination GROUP BY trust").all()) {
21517
- byTrust[row.trust] = row.n;
21518
- }
21952
+ const kindCounts = countBy(
21953
+ this.db,
21954
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
21955
+ );
21956
+ const byKind = {
21957
+ provider: kindCounts.get("provider") ?? 0,
21958
+ internal: kindCounts.get("internal") ?? 0,
21959
+ ip: kindCounts.get("ip") ?? 0
21960
+ };
21961
+ const trustCounts = countBy(
21962
+ this.db,
21963
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
21964
+ );
21965
+ const byTrust = {
21966
+ recognized: trustCounts.get("recognized") ?? 0,
21967
+ internal: trustCounts.get("internal") ?? 0,
21968
+ unverified: trustCounts.get("unverified") ?? 0,
21969
+ ip: trustCounts.get("ip") ?? 0
21970
+ };
21519
21971
  return Promise.resolve({
21520
21972
  destinations,
21521
21973
  endpoints,
@@ -21631,7 +22083,7 @@ var SqliteSharesRepository = class {
21631
22083
  }
21632
22084
  let sql;
21633
22085
  if (q) {
21634
- const pattern = `%${escapeLikePattern(q)}%`;
22086
+ const pattern = containsPattern(q);
21635
22087
  conditions.push(
21636
22088
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
21637
22089
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -21651,24 +22103,31 @@ var SqliteSharesRepository = class {
21651
22103
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
21652
22104
  ORDER BY d.created_at ASC, d.id ASC`;
21653
22105
  }
21654
- const rows = this.db.prepare(sql).all(...params);
22106
+ const rows = allRows(
22107
+ this.db.prepare(sql),
22108
+ params
22109
+ );
21655
22110
  return rows.map((r) => this.mapDestRow(r));
21656
22111
  }
21657
22112
  fetchDestinationById(destinationId) {
21658
- const row = this.db.prepare(
21659
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22113
+ const row = getRow(
22114
+ this.db.prepare(
22115
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
21660
22116
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
21661
22117
  o.decision AS overrideDecision
21662
22118
  FROM share_destination d
21663
22119
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
21664
22120
  WHERE d.id = ?`
21665
- ).get(destinationId);
22121
+ ),
22122
+ [destinationId]
22123
+ );
21666
22124
  return row ? this.mapDestRow(row) : null;
21667
22125
  }
21668
22126
  fetchEndpoints(destinationIds) {
21669
22127
  if (destinationIds.length === 0) return [];
21670
- const rows = this.db.prepare(
21671
- `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
22128
+ const rows = allRows(
22129
+ this.db.prepare(
22130
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
21672
22131
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
21673
22132
  count(c.id) AS callSiteCount
21674
22133
  FROM share_endpoint e
@@ -21676,7 +22135,9 @@ var SqliteSharesRepository = class {
21676
22135
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
21677
22136
  GROUP BY e.id
21678
22137
  ORDER BY e.created_at ASC, e.id ASC`
21679
- ).all(...destinationIds);
22138
+ ),
22139
+ destinationIds
22140
+ );
21680
22141
  return rows.map((r) => ({
21681
22142
  id: r.id,
21682
22143
  destinationId: r.destinationId,
@@ -21701,13 +22162,16 @@ var SqliteSharesRepository = class {
21701
22162
  }
21702
22163
  fetchCallSites(endpointIds) {
21703
22164
  if (endpointIds.length === 0) return [];
21704
- const rows = this.db.prepare(
21705
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22165
+ const rows = allRows(
22166
+ this.db.prepare(
22167
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
21706
22168
  project_id AS projectId
21707
22169
  FROM share_call_site
21708
22170
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
21709
22171
  ORDER BY created_at ASC, id ASC`
21710
- ).all(...endpointIds);
22172
+ ),
22173
+ endpointIds
22174
+ );
21711
22175
  return rows.map((r) => ({
21712
22176
  id: r.id,
21713
22177
  endpointId: r.endpointId,
@@ -21743,27 +22207,36 @@ var SqliteSourceProjectRepository = class {
21743
22207
  upsert(input, now = Date.now()) {
21744
22208
  const id = sourceProjectId(input.url);
21745
22209
  const row = toSourceProjectRow(input, id, now);
21746
- this.upsertStmt.run({
21747
- id: row.id,
21748
- url: row.url ?? null,
21749
- name: row.name ?? null,
21750
- attributes: row.attributes,
21751
- firstSeen: row.firstSeen,
21752
- lastSeen: row.lastSeen
21753
- });
22210
+ this.upsertStmt.run(
22211
+ bindParams({
22212
+ id: row.id,
22213
+ url: row.url,
22214
+ name: row.name,
22215
+ attributes: row.attributes,
22216
+ firstSeen: row.firstSeen,
22217
+ lastSeen: row.lastSeen
22218
+ })
22219
+ );
21754
22220
  return id;
21755
22221
  }
21756
22222
  findById(id) {
21757
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22223
+ return getRow(
22224
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22225
+ {
22226
+ id
22227
+ }
22228
+ );
21758
22229
  }
21759
22230
  // Distinct project names — a filter facet, served from the source_project
21760
22231
  // table, never from the audit fact table.
21761
22232
  distinctNames() {
21762
- const rows = this.db.prepare(
21763
- `SELECT DISTINCT name FROM source_project
22233
+ const rows = allRows(
22234
+ this.db.prepare(
22235
+ `SELECT DISTINCT name FROM source_project
21764
22236
  WHERE name IS NOT NULL
21765
22237
  ORDER BY name`
21766
- ).all();
22238
+ )
22239
+ );
21767
22240
  return rows.map((r) => r.name);
21768
22241
  }
21769
22242
  };
@@ -21782,8 +22255,7 @@ function hasLegacySampleRows(db) {
21782
22255
  function purgeSampleData(db) {
21783
22256
  try {
21784
22257
  if (!hasLegacySampleRows(db)) return;
21785
- db.exec("BEGIN");
21786
- try {
22258
+ withTransaction(db, () => {
21787
22259
  db.exec(
21788
22260
  `DELETE FROM share_call_site WHERE endpoint_id IN (
21789
22261
  SELECT e.id FROM share_endpoint e
@@ -21828,11 +22300,7 @@ function purgeSampleData(db) {
21828
22300
  value TEXT NOT NULL
21829
22301
  )`);
21830
22302
  db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
21831
- db.exec("COMMIT");
21832
- } catch (err) {
21833
- db.exec("ROLLBACK");
21834
- throw err;
21835
- }
22303
+ });
21836
22304
  } catch {
21837
22305
  }
21838
22306
  }
@@ -21851,7 +22319,7 @@ function openWithPragmas(file2) {
21851
22319
  function backupLegacyStore(file2) {
21852
22320
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
21853
22321
  renameSync(file2, backup);
21854
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
22322
+ for (const sidecar of walSidecars(file2)) {
21855
22323
  if (existsSync(sidecar)) rmSync(sidecar);
21856
22324
  }
21857
22325
  return backup;
@@ -21864,9 +22332,8 @@ function openLocalDatabase(dir) {
21864
22332
  db.close();
21865
22333
  const backup = backupLegacyStore(file2);
21866
22334
  db = openWithPragmas(file2);
21867
- process.stderr.write(
21868
- `[aka] Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.
21869
- `
22335
+ akaWarn(
22336
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
21870
22337
  );
21871
22338
  }
21872
22339
  applyMigrations(db);
@@ -21894,96 +22361,77 @@ function openLocalDatabase(dir) {
21894
22361
  const configInventory = new SqliteConfigInventoryRepository(db);
21895
22362
  policies.seedDefaults();
21896
22363
  function recordCapture(event, detected) {
21897
- try {
21898
- db.exec("BEGIN");
21899
- try {
21900
- events.insertEvent(event);
21901
- const sessionId = event.metadata?.sessionId;
21902
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
21903
- db.exec("COMMIT");
21904
- } catch (err) {
21905
- db.exec("ROLLBACK");
21906
- throw err;
21907
- }
21908
- } catch {
21909
- }
22364
+ failOpenTransaction(db, () => {
22365
+ events.insertEvent(event);
22366
+ const sessionId = event.metadata?.sessionId;
22367
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
22368
+ });
21910
22369
  }
21911
22370
  function ensureInventory(ctx) {
21912
22371
  const resolved = {};
21913
- try {
21914
- db.exec("BEGIN");
21915
- try {
21916
- const now = Date.now();
21917
- if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
21918
- if (ctx.harness) {
21919
- resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
21920
- }
21921
- resolved.accountId = inventory.upsert(
21922
- linkHost(
21923
- {
21924
- objectType: "user",
21925
- identityKey: "local",
21926
- attributes: { source: "local" }
21927
- },
21928
- resolved.hostId
21929
- ),
21930
- now
21931
- );
21932
- if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
21933
- db.exec("COMMIT");
21934
- } catch (err) {
21935
- db.exec("ROLLBACK");
21936
- throw err;
21937
- }
21938
- } catch {
21939
- return {};
21940
- }
21941
- return resolved;
22372
+ const committed = failOpenTransaction(db, () => {
22373
+ const now = Date.now();
22374
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22375
+ if (ctx.harness) {
22376
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
22377
+ }
22378
+ resolved.accountId = inventory.upsert(
22379
+ linkHost(
22380
+ {
22381
+ objectType: "user",
22382
+ identityKey: "local",
22383
+ attributes: { source: "local" }
22384
+ },
22385
+ resolved.hostId
22386
+ ),
22387
+ now
22388
+ );
22389
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22390
+ });
22391
+ return committed ? resolved : {};
21942
22392
  }
21943
22393
  function recordConfigScan(record2) {
21944
- try {
21945
- db.exec("BEGIN");
21946
- try {
21947
- const now = isoToEpochMillis(record2.scanEvent.startedAt);
21948
- for (const item of record2.items) inventory.upsert(item, now);
21949
- auditEvents.insertAuditEvent(record2.scanEvent);
21950
- const definitionIds = /* @__PURE__ */ new Map();
21951
- for (const def of record2.definitions ?? []) {
21952
- definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
21953
- }
21954
- for (const finding of record2.findings ?? []) {
21955
- const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
21956
- if (!definitionId) continue;
21957
- inspectionFindings.insertFinding({
21958
- id: randomUUID8(),
21959
- auditEventId: record2.scanEvent.id,
21960
- inspectionDefinitionId: definitionId,
21961
- span: finding.span,
21962
- maskedMatch: finding.maskedMatch,
21963
- actionTaken: finding.actionTaken,
21964
- confidence: finding.confidence
21965
- });
21966
- }
21967
- db.exec("COMMIT");
21968
- } catch (err) {
21969
- db.exec("ROLLBACK");
21970
- throw err;
22394
+ failOpenTransaction(db, () => {
22395
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
22396
+ for (const item of record2.items) inventory.upsert(item, now);
22397
+ auditEvents.insertAuditEvent(record2.scanEvent);
22398
+ const definitionIds = /* @__PURE__ */ new Map();
22399
+ for (const def of record2.definitions ?? []) {
22400
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22401
+ }
22402
+ for (const finding of record2.findings ?? []) {
22403
+ const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
22404
+ if (!definitionId) continue;
22405
+ inspectionFindings.insertFinding({
22406
+ id: randomUUID8(),
22407
+ auditEventId: record2.scanEvent.id,
22408
+ inspectionDefinitionId: definitionId,
22409
+ span: finding.span,
22410
+ maskedMatch: finding.maskedMatch,
22411
+ actionTaken: finding.actionTaken,
22412
+ confidence: finding.confidence
22413
+ });
21971
22414
  }
21972
- } catch {
21973
- }
22415
+ });
21974
22416
  }
21975
22417
  function recordProjectFiles(projectId, scan2) {
21976
22418
  if (scan2.files.length === 0) return;
22419
+ failOpenTransaction(db, () => {
22420
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
22421
+ });
22422
+ }
22423
+ async function transaction(fn) {
22424
+ db.exec("BEGIN");
21977
22425
  try {
21978
- db.exec("BEGIN");
22426
+ const result = await fn();
22427
+ db.exec("COMMIT");
22428
+ return result;
22429
+ } catch (err) {
21979
22430
  try {
21980
- projectFiles.replaceForProject(projectId, scan2, Date.now());
21981
- db.exec("COMMIT");
21982
- } catch (err) {
21983
22431
  db.exec("ROLLBACK");
21984
- throw err;
22432
+ } catch {
21985
22433
  }
21986
- } catch {
22434
+ throw err;
21987
22435
  }
21988
22436
  }
21989
22437
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -22002,8 +22450,7 @@ function openLocalDatabase(dir) {
22002
22450
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
22003
22451
  });
22004
22452
  if (stale.length === 0) return;
22005
- db.exec("BEGIN");
22006
- try {
22453
+ withTransaction(db, () => {
22007
22454
  for (const { id } of stale) {
22008
22455
  db.prepare(
22009
22456
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -22015,11 +22462,7 @@ function openLocalDatabase(dir) {
22015
22462
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
22016
22463
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
22017
22464
  }
22018
- db.exec("COMMIT");
22019
- } catch (err) {
22020
- db.exec("ROLLBACK");
22021
- throw err;
22022
- }
22465
+ });
22023
22466
  } catch {
22024
22467
  }
22025
22468
  }
@@ -22061,6 +22504,7 @@ function openLocalDatabase(dir) {
22061
22504
  purgeSampleData: () => {
22062
22505
  purgeSampleData(db);
22063
22506
  },
22507
+ transaction,
22064
22508
  close: () => {
22065
22509
  db.close();
22066
22510
  }
@@ -22153,12 +22597,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
22153
22597
  }
22154
22598
  }
22155
22599
  function readJson(file2) {
22600
+ let text;
22156
22601
  try {
22157
- const parsed = JSON.parse(readFileSync2(file2, "utf8"));
22158
- return typeof parsed === "object" && parsed !== null ? parsed : null;
22602
+ text = readFileSync2(file2, "utf8");
22159
22603
  } catch {
22160
22604
  return null;
22161
22605
  }
22606
+ return parseJsonObject(text) ?? null;
22607
+ }
22608
+
22609
+ // ../../packages/persistence/src/warn-era-cap.ts
22610
+ import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
22611
+ import { join as join5 } from "path";
22612
+ var MARKER = "warn-era-capped";
22613
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22614
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
22615
+ const marker = join5(dataDir2, MARKER);
22616
+ if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22617
+ const capped = db.policies.capCategoryActions();
22618
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
22619
+ `, { mode: DATA_FILE_MODE });
22620
+ return { capped };
22162
22621
  }
22163
22622
 
22164
22623
  // ../../packages/plugin-sdk/src/provider-env.ts
@@ -22233,7 +22692,7 @@ function resolveProviderSafe() {
22233
22692
  // ../../packages/plugin-sdk/src/config-inventory.ts
22234
22693
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22235
22694
  import { homedir as homedir2 } from "os";
22236
- import { basename as basename2, join as join6 } from "path";
22695
+ import { basename as basename2, join as join7 } from "path";
22237
22696
 
22238
22697
  // ../../packages/detections/src/matchers/keyword.ts
22239
22698
  var KeywordMatcher2 = class {
@@ -24367,8 +24826,8 @@ function bundledDetections() {
24367
24826
  }
24368
24827
 
24369
24828
  // ../../packages/plugin-sdk/src/repo.ts
24370
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
24371
- import { basename, dirname, isAbsolute, join as join5, sep as sep2 } from "path";
24829
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
24830
+ import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
24372
24831
 
24373
24832
  // ../../packages/plugin-sdk/src/events.ts
24374
24833
  import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
@@ -24380,20 +24839,23 @@ import { createHash as createHash4 } from "crypto";
24380
24839
  import { arch, hostname as hostname3, platform, release } from "os";
24381
24840
 
24382
24841
  // ../../packages/plugin-sdk/src/nudge.ts
24383
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
24384
- import { join as join7 } from "path";
24842
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
24843
+ import { join as join8 } from "path";
24385
24844
 
24386
24845
  // ../../packages/plugin-sdk/src/project-files.ts
24387
24846
  var import_ignore = __toESM(require_ignore(), 1);
24388
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
24389
- import { basename as basename3, join as join8, relative, sep as sep3 } from "path";
24847
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
24848
+ import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
24390
24849
 
24391
24850
  // ../../packages/plugin-sdk/src/runtime.ts
24392
24851
  import { randomUUID as randomUUID10 } from "crypto";
24393
24852
 
24853
+ // ../../packages/plugin-sdk/src/suppressions.ts
24854
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
24855
+
24394
24856
  // ../../packages/plugin-sdk/src/throttle.ts
24395
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
24396
- import { join as join9 } from "path";
24857
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
24858
+ import { join as join10 } from "path";
24397
24859
 
24398
24860
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
24399
24861
  import { randomUUID as randomUUID11 } from "crypto";
@@ -24586,6 +25048,14 @@ var StandaloneDataGateway = class {
24586
25048
  sweepTerminalExceptions(retentionMs) {
24587
25049
  return this.db.exceptions.sweepTerminal(retentionMs);
24588
25050
  }
25051
+ // The warn-era enforcement cap, standalone-only store maintenance invoked
25052
+ // from SessionStart, not part of the DataGateway port. Returns the number
25053
+ // of block/redact rows capped to warn (0 for a redact-policy store or an
25054
+ // already-capped one).
25055
+ capWarnEraEnforcement(policyMode) {
25056
+ const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
25057
+ return { capped };
25058
+ }
24589
25059
  // One project-file scan → the local project_file tree (one transaction inside
24590
25060
  // the LocalDatabase, fail-open there). Like the sweep above, this is
24591
25061
  // NOT part of the DataGateway port: the file tree is a local-store read model.
@@ -24825,6 +25295,7 @@ function shortTime(iso) {
24825
25295
  function empty(message) {
24826
25296
  return message;
24827
25297
  }
25298
+ var CATEGORY_ORDER2 = DetectionCategory.options;
24828
25299
  function healthScore(summary) {
24829
25300
  const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
24830
25301
  const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
@@ -24841,7 +25312,7 @@ function renderFindings(findings, status, severity) {
24841
25312
  `${severityGlyph(f.severity)} ${f.severity}`,
24842
25313
  f.category,
24843
25314
  f.ruleId,
24844
- f.actionTaken,
25315
+ toApiAction(f.actionTaken),
24845
25316
  f.maskedMatch
24846
25317
  ]);
24847
25318
  const heading = severity !== void 0 ? `\u25CF Recent ${severity} findings (${String(findings.length)})` : `\u25CF Recent findings (${String(findings.length)})`;
@@ -25047,7 +25518,7 @@ function renderAudit(findings) {
25047
25518
  }
25048
25519
  const rows = findings.map((f) => [
25049
25520
  shortTime(f.occurredAt),
25050
- f.actionTaken,
25521
+ toApiAction(f.actionTaken),
25051
25522
  f.ruleId,
25052
25523
  f.category,
25053
25524
  // Join only the parts that are present so a finding missing sourceTool/kind