@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.
@@ -15427,6 +15427,11 @@ var AuditEventType = external_exports.enum([
15427
15427
  "prompt",
15428
15428
  "response",
15429
15429
  "code_change",
15430
+ // The events.kind of a scanned tool call, widened in to keep this a
15431
+ // superset. Narrower than 'tool_call' above and not a duplicate of it:
15432
+ // 'tool_call' is the reconciler's structural row for every call, while
15433
+ // 'tool_use' exists only where a hook enforced against the arguments.
15434
+ "tool_use",
15430
15435
  // One row per config-inventory scan, hung off the session root. It is the
15431
15436
  // fact the posture inspection findings reference (findings require an
15432
15437
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -15818,7 +15823,7 @@ var ActivityOverviewResponse = external_exports.object({
15818
15823
  }).meta({ id: "ActivityOverviewResponse" });
15819
15824
 
15820
15825
  // ../../packages/schema/src/zod/event.ts
15821
- var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
15826
+ var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15822
15827
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15823
15828
  var EventMetadata = external_exports.object({
15824
15829
  sessionId: external_exports.string().optional(),
@@ -16131,7 +16136,7 @@ var DetectionException = external_exports.object({
16131
16136
  justification: external_exports.string().min(1),
16132
16137
  conditions: ExceptionConditions.nullable(),
16133
16138
  createdBy: external_exports.string(),
16134
- createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
16139
+ createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
16135
16140
  createdAt: external_exports.iso.datetime(),
16136
16141
  updatedAt: external_exports.iso.datetime(),
16137
16142
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16300,20 +16305,26 @@ var PolicyBundle = external_exports.object({
16300
16305
  customKeywords: external_exports.array(external_exports.string()),
16301
16306
  fetchedAt: external_exports.iso.datetime()
16302
16307
  }).meta({ id: "PolicyBundle" });
16303
- var DEFAULT_ACTIONS = {
16304
- secret: "block",
16305
- pii: "redact",
16306
- financial: "redact",
16307
- phi: "redact",
16308
- code_context: "warn",
16309
- code_flaw: "warn",
16310
- custom: "warn",
16311
- // Config-posture findings only observe today (they land in
16312
- // inspection_findings, outside the live-capture enforcement path).
16313
- config: "warn"
16314
- };
16315
16308
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16316
16309
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
16310
+ var CATEGORY_PEAK_SEVERITY = {
16311
+ secret: "critical",
16312
+ financial: "critical",
16313
+ // core-financial/credit-card
16314
+ code_flaw: "critical",
16315
+ pii: "high",
16316
+ phi: "high",
16317
+ custom: "high",
16318
+ // user-defined; conservative
16319
+ code_context: "low",
16320
+ config: "low"
16321
+ // observe-only; floors to monitor regardless
16322
+ };
16323
+ function severityFloorPolicy(category) {
16324
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
16325
+ const peak = CATEGORY_PEAK_SEVERITY[category];
16326
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
16327
+ }
16317
16328
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
16318
16329
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
16319
16330
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -16340,6 +16351,12 @@ var BUILTIN_POLICY_SPECS = {
16340
16351
  description: "Refuse the request entirely whenever any rule in this detection matches."
16341
16352
  }
16342
16353
  };
16354
+ function builtinPolicyToAction(id) {
16355
+ return BUILTIN_POLICY_SPECS[id].action;
16356
+ }
16357
+ var DEFAULT_ACTIONS = Object.fromEntries(
16358
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
16359
+ );
16343
16360
  var BUILTIN_POLICIES = Object.fromEntries(
16344
16361
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
16345
16362
  );
@@ -16746,10 +16763,8 @@ function toApiProvider(sourceTool) {
16746
16763
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
16747
16764
  }
16748
16765
  var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
16749
- function deriveGroupStatus(instances) {
16750
- const statuses = new Set(
16751
- instances.map((i) => i.status).filter((s) => s !== void 0)
16752
- );
16766
+ function foldGroupStatus(instanceStatuses) {
16767
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
16753
16768
  if (statuses.size === 0) return void 0;
16754
16769
  for (const candidate of STATUS_PRECEDENCE) {
16755
16770
  if (statuses.has(candidate)) return candidate;
@@ -16767,6 +16782,7 @@ function deriveFindingStatus(row) {
16767
16782
  function buildFindingGroups(rows, opts = {}) {
16768
16783
  const overrides = opts.overrides;
16769
16784
  const packNames = opts.packNames;
16785
+ const aggregates = opts.aggregates;
16770
16786
  const byRuleId = /* @__PURE__ */ new Map();
16771
16787
  for (const row of rows) {
16772
16788
  const existing = byRuleId.get(row.ruleId);
@@ -16788,17 +16804,20 @@ function buildFindingGroups(rows, opts = {}) {
16788
16804
  status: r.status
16789
16805
  };
16790
16806
  });
16791
- const latestDetectedAt = ruleRows.reduce(
16807
+ const agg = aggregates?.get(ruleId);
16808
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
16792
16809
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
16793
16810
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
16794
16811
  );
16795
16812
  const seenProviders = /* @__PURE__ */ new Set();
16796
- const providers = instances.map((i) => i.provider).filter((p) => {
16813
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
16797
16814
  if (seenProviders.has(p)) return false;
16798
16815
  seenProviders.add(p);
16799
16816
  return true;
16800
16817
  });
16801
- const actionSet = new Set(instances.map((i) => i.action));
16818
+ const actionSet = new Set(
16819
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
16820
+ );
16802
16821
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
16803
16822
  const severity = ruleRows[0]?.severity ?? "low";
16804
16823
  const detection = {
@@ -16812,8 +16831,10 @@ function buildFindingGroups(rows, opts = {}) {
16812
16831
  contextPrefix: ""
16813
16832
  // empty (pending privacy review)
16814
16833
  };
16815
- const status = deriveGroupStatus(instances);
16816
- groups.push({
16834
+ const status = foldGroupStatus(
16835
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
16836
+ );
16837
+ const group = {
16817
16838
  id: ruleId,
16818
16839
  category: apiCategory,
16819
16840
  subtype: ruleId,
@@ -16822,21 +16843,26 @@ function buildFindingGroups(rows, opts = {}) {
16822
16843
  match,
16823
16844
  detection,
16824
16845
  policy,
16825
- instanceCount: instances.length,
16846
+ instanceCount: agg?.instanceCount ?? instances.length,
16826
16847
  providers,
16827
16848
  aggregateAction,
16828
16849
  latestDetectedAt,
16829
16850
  instances,
16830
16851
  status
16831
- });
16852
+ };
16853
+ if (agg) {
16854
+ actionsCache.set(group, [...actionSet]);
16855
+ if (agg.searchText !== void 0) {
16856
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
16857
+ }
16858
+ }
16859
+ groups.push(group);
16832
16860
  }
16833
16861
  return groups;
16834
16862
  }
16835
16863
  var haystackCache = /* @__PURE__ */ new WeakMap();
16836
- function groupHaystack(g) {
16837
- const cached2 = haystackCache.get(g);
16838
- if (cached2 !== void 0) return cached2;
16839
- const haystack = [
16864
+ function buildHaystack(g, extra) {
16865
+ return [
16840
16866
  g.subtype,
16841
16867
  g.category,
16842
16868
  g.match.maskedValue,
@@ -16844,11 +16870,25 @@ function groupHaystack(g) {
16844
16870
  g.id,
16845
16871
  ...g.instances.map((i) => i.repo),
16846
16872
  ...g.instances.map((i) => i.file),
16847
- ...g.instances.map((i) => i.id)
16873
+ ...g.instances.map((i) => i.id),
16874
+ ...extra === void 0 ? [] : [extra]
16848
16875
  ].join(" ").toLowerCase();
16876
+ }
16877
+ function groupHaystack(g) {
16878
+ const cached2 = haystackCache.get(g);
16879
+ if (cached2 !== void 0) return cached2;
16880
+ const haystack = buildHaystack(g);
16849
16881
  haystackCache.set(g, haystack);
16850
16882
  return haystack;
16851
16883
  }
16884
+ var actionsCache = /* @__PURE__ */ new WeakMap();
16885
+ function groupActions(g) {
16886
+ const cached2 = actionsCache.get(g);
16887
+ if (cached2 !== void 0) return cached2;
16888
+ const actions = [...new Set(g.instances.map((i) => i.action))];
16889
+ actionsCache.set(g, actions);
16890
+ return actions;
16891
+ }
16852
16892
  function applyFindingFilters(groups, opts) {
16853
16893
  let filtered = groups;
16854
16894
  if (opts.severity && opts.severity.length > 0) {
@@ -16861,7 +16901,7 @@ function applyFindingFilters(groups, opts) {
16861
16901
  }
16862
16902
  if (opts.actions && opts.actions.length > 0) {
16863
16903
  const actionSet = new Set(opts.actions);
16864
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
16904
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
16865
16905
  }
16866
16906
  if (opts.subtype && opts.subtype.length > 0) {
16867
16907
  const subtypeSet = new Set(opts.subtype);
@@ -16913,8 +16953,7 @@ function computeFindingFacets(allGroups, opts) {
16913
16953
  });
16914
16954
  const actionMap = /* @__PURE__ */ new Map();
16915
16955
  for (const g of forAction) {
16916
- const actionSet = new Set(g.instances.map((i) => i.action));
16917
- for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
16956
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
16918
16957
  }
16919
16958
  const forSubtype = applyFindingFilters(allGroups, {
16920
16959
  providers: opts.providers,
@@ -17476,6 +17515,132 @@ function reviewSeverityRank(reasons) {
17476
17515
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17477
17516
  }
17478
17517
 
17518
+ // ../../packages/schema/src/zod/triage.ts
17519
+ var TriageHit = external_exports.object({
17520
+ ruleId: external_exports.string(),
17521
+ category: DetectionCategory,
17522
+ severity: Severity,
17523
+ maskedMatch: external_exports.string(),
17524
+ rawMatch: external_exports.string(),
17525
+ context: external_exports.string(),
17526
+ filePath: external_exports.string().optional(),
17527
+ confidence: external_exports.number().min(0).max(1),
17528
+ id: external_exports.string().optional(),
17529
+ valueFingerprint: external_exports.string().optional(),
17530
+ keyVersion: external_exports.number().int().nonnegative().optional()
17531
+ });
17532
+ var TriagePolicy = BuiltinPolicyId;
17533
+ var TriageCategoryRec = external_exports.object({
17534
+ category: DetectionCategory,
17535
+ action: TriagePolicy,
17536
+ reasoning: external_exports.string(),
17537
+ genuineCount: external_exports.number().int().nonnegative(),
17538
+ fpCount: external_exports.number().int().nonnegative(),
17539
+ // TriageHit ids judged false-positive in this category. fpCount must equal
17540
+ // this array's length — enforced by the consumer, not this schema.
17541
+ fpIds: external_exports.array(external_exports.string())
17542
+ });
17543
+ var TriageRecommendation = external_exports.object({
17544
+ perCategory: external_exports.array(TriageCategoryRec),
17545
+ notes: external_exports.string()
17546
+ });
17547
+
17548
+ // ../../packages/persistence/src/internal/sql-text.ts
17549
+ function escapeLikePattern(s) {
17550
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17551
+ }
17552
+ function placeholders(n) {
17553
+ return Array.from({ length: n }, () => "?").join(", ");
17554
+ }
17555
+ function containsPattern(q) {
17556
+ return `%${escapeLikePattern(q)}%`;
17557
+ }
17558
+ function likeAny(exprs) {
17559
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
17560
+ }
17561
+
17562
+ // ../../packages/persistence/src/internal/transactions.ts
17563
+ var savepointSeq = 0;
17564
+ function withTransaction(db, fn, mode = "DEFERRED") {
17565
+ if (db.isTransaction) {
17566
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
17567
+ db.exec(`SAVEPOINT ${savepoint}`);
17568
+ try {
17569
+ fn();
17570
+ db.exec(`RELEASE ${savepoint}`);
17571
+ } catch (error51) {
17572
+ try {
17573
+ db.exec(`ROLLBACK TO ${savepoint}`);
17574
+ db.exec(`RELEASE ${savepoint}`);
17575
+ } catch {
17576
+ }
17577
+ throw error51;
17578
+ }
17579
+ return;
17580
+ }
17581
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
17582
+ try {
17583
+ fn();
17584
+ db.exec("COMMIT");
17585
+ } catch (error51) {
17586
+ try {
17587
+ db.exec("ROLLBACK");
17588
+ } catch {
17589
+ }
17590
+ throw error51;
17591
+ }
17592
+ }
17593
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
17594
+ const nested = db.isTransaction;
17595
+ try {
17596
+ withTransaction(db, fn, mode);
17597
+ return true;
17598
+ } catch (error51) {
17599
+ if (!db.isTransaction && nested) throw error51;
17600
+ return false;
17601
+ }
17602
+ }
17603
+
17604
+ // ../../packages/persistence/src/internal/warn.ts
17605
+ function akaWarn(message) {
17606
+ process.stderr.write(`[aka] ${message}
17607
+ `);
17608
+ }
17609
+
17610
+ // ../../packages/persistence/src/db/migrations/introspection.ts
17611
+ function evidenceObjects(sql) {
17612
+ const objects = [];
17613
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17614
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17615
+ objects.push({ kind: "table", name: m[1] });
17616
+ }
17617
+ }
17618
+ for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17619
+ if (m[1] !== void 0 && m[2] !== void 0) {
17620
+ objects.push({ kind: "column", table: m[1], name: m[2] });
17621
+ }
17622
+ }
17623
+ return objects;
17624
+ }
17625
+ function schemaObjectExists(db, kind, name) {
17626
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
17627
+ return row !== void 0;
17628
+ }
17629
+ function indexExists(db, name) {
17630
+ return schemaObjectExists(db, "index", name);
17631
+ }
17632
+ function columnNames(db, table2, opts) {
17633
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
17634
+ const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
17635
+ return columns.map((c) => c.name);
17636
+ }
17637
+ function evidenceExists(db, object2) {
17638
+ if (object2.kind === "column") {
17639
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
17640
+ }
17641
+ return schemaObjectExists(db, "table", object2.name);
17642
+ }
17643
+
17479
17644
  // ../../packages/persistence/src/ids.ts
17480
17645
  import { createHash } from "crypto";
17481
17646
  function sha256Hex(input) {
@@ -17512,28 +17677,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17512
17677
  }
17513
17678
 
17514
17679
  // ../../packages/persistence/src/migrations.ts
17515
- function evidenceObjects(sql) {
17516
- const objects = [];
17517
- for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17518
- if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17519
- objects.push({ kind: "table", name: m[1] });
17520
- }
17521
- }
17522
- for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17523
- if (m[1] !== void 0 && m[2] !== void 0) {
17524
- objects.push({ kind: "column", table: m[1], name: m[2] });
17525
- }
17526
- }
17527
- return objects;
17528
- }
17529
- function evidenceExists(db, object2) {
17530
- if (object2.kind === "column") {
17531
- const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
17532
- return columns.some((c) => c.name === object2.name);
17533
- }
17534
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
17535
- return row !== void 0;
17536
- }
17537
17680
  function describeObject(object2) {
17538
17681
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
17539
17682
  }
@@ -17544,10 +17687,6 @@ function createdIndexName(statement) {
17544
17687
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17545
17688
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17546
17689
  }
17547
- function indexExists(db, name) {
17548
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
17549
- return row !== void 0;
17550
- }
17551
17690
  function applyMigrations(db) {
17552
17691
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17553
17692
  db.exec(
@@ -17566,44 +17705,39 @@ function applyMigrations(db) {
17566
17705
  const present = evidence.filter((o) => evidenceExists(db, o));
17567
17706
  if (present.length > 0 && present.length < evidence.length) {
17568
17707
  const missing = evidence.filter((o) => !present.includes(o));
17569
- 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.`;
17570
- process.stderr.write(`${message}
17571
- `);
17572
- throw new Error(message);
17708
+ 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.`;
17709
+ akaWarn(message);
17710
+ throw new Error(`[aka] ${message}`);
17573
17711
  }
17574
17712
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
17575
17713
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
17576
17714
  const statements = splitStatements(migration.sql);
17577
17715
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
17578
17716
  try {
17579
- db.exec("BEGIN IMMEDIATE");
17580
- try {
17581
- for (const statement of statements) {
17582
- const indexName = createdIndexName(statement);
17583
- if (indexName === void 0) {
17584
- if (alreadyApplied) continue;
17585
- } else if (indexExists(db, indexName)) {
17586
- continue;
17717
+ withTransaction(
17718
+ db,
17719
+ () => {
17720
+ for (const statement of statements) {
17721
+ const indexName = createdIndexName(statement);
17722
+ if (indexName === void 0) {
17723
+ if (alreadyApplied) continue;
17724
+ } else if (indexExists(db, indexName)) {
17725
+ continue;
17726
+ }
17727
+ db.exec(statement);
17587
17728
  }
17588
- db.exec(statement);
17589
- }
17590
- if (wantsFkOff && !alreadyApplied) {
17591
- const violations = db.prepare("PRAGMA foreign_key_check").all();
17592
- if (violations.length > 0) {
17593
- throw new Error(
17594
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17595
- );
17729
+ if (wantsFkOff && !alreadyApplied) {
17730
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
17731
+ if (violations.length > 0) {
17732
+ throw new Error(
17733
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17734
+ );
17735
+ }
17596
17736
  }
17597
- }
17598
- record2.run(migration.tag, Date.now());
17599
- db.exec("COMMIT");
17600
- } catch (error51) {
17601
- try {
17602
- db.exec("ROLLBACK");
17603
- } catch {
17604
- }
17605
- throw error51;
17606
- }
17737
+ record2.run(migration.tag, Date.now());
17738
+ },
17739
+ "IMMEDIATE"
17740
+ );
17607
17741
  } finally {
17608
17742
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
17609
17743
  }
@@ -17646,8 +17780,7 @@ var TOKEN_USAGE_COLUMNS = [
17646
17780
  }
17647
17781
  ];
17648
17782
  function ensureTokenUsageColumns(db) {
17649
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
17650
- const existing = new Set(columns.map((c) => c.name));
17783
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17651
17784
  for (const column of TOKEN_USAGE_COLUMNS) {
17652
17785
  if (!existing.has(column.name)) {
17653
17786
  db.exec(column.ddl);
@@ -17685,47 +17818,39 @@ function reconcileSourceProjectIds(db) {
17685
17818
  repoint: db.prepare(`UPDATE ${table2} SET project_id = ? WHERE project_id = ?`)
17686
17819
  }));
17687
17820
  const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
17688
- db.exec("BEGIN IMMEDIATE");
17689
- try {
17690
- for (const { row, canonicalId } of legacy) {
17691
- foldProject.run(
17692
- canonicalId,
17693
- row.url,
17694
- row.name,
17695
- row.attributes,
17696
- row.firstSeen,
17697
- row.lastSeen
17698
- );
17699
- repointAudit.run(canonicalId, row.id);
17700
- for (const { dropCollisions, repoint } of pathTables) {
17701
- dropCollisions.run(row.id, canonicalId);
17702
- repoint.run(canonicalId, row.id);
17821
+ withTransaction(
17822
+ db,
17823
+ () => {
17824
+ for (const { row, canonicalId } of legacy) {
17825
+ foldProject.run(
17826
+ canonicalId,
17827
+ row.url,
17828
+ row.name,
17829
+ row.attributes,
17830
+ row.firstSeen,
17831
+ row.lastSeen
17832
+ );
17833
+ repointAudit.run(canonicalId, row.id);
17834
+ for (const { dropCollisions, repoint } of pathTables) {
17835
+ dropCollisions.run(row.id, canonicalId);
17836
+ repoint.run(canonicalId, row.id);
17837
+ }
17838
+ repointCallSite.run(canonicalId, row.id);
17839
+ deleteLegacy.run(row.id);
17703
17840
  }
17704
- repointCallSite.run(canonicalId, row.id);
17705
- deleteLegacy.run(row.id);
17706
- }
17707
- db.exec("COMMIT");
17708
- } catch (error51) {
17709
- try {
17710
- db.exec("ROLLBACK");
17711
- } catch {
17712
- }
17713
- throw error51;
17714
- }
17841
+ },
17842
+ "IMMEDIATE"
17843
+ );
17715
17844
  } catch (error51) {
17716
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
17717
- `);
17845
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
17718
17846
  }
17719
17847
  }
17720
17848
  function isForeignSqliteLineage(db) {
17721
- const tenantsTable = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tenants' LIMIT 1").get();
17722
- if (tenantsTable !== void 0) return true;
17723
- const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
17724
- return eventsColumns.some((c) => c.name === "tenant_id");
17849
+ if (schemaObjectExists(db, "table", "tenants")) return true;
17850
+ return columnNames(db, "events").includes("tenant_id");
17725
17851
  }
17726
17852
  function ensureSyncedAtColumn(db, table2) {
17727
- const columns = db.prepare(`PRAGMA table_info(${table2})`).all();
17728
- if (!columns.some((c) => c.name === "synced_at")) {
17853
+ if (!columnNames(db, table2).includes("synced_at")) {
17729
17854
  db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
17730
17855
  }
17731
17856
  }
@@ -17776,8 +17901,11 @@ function ensureDataDirSync(dir) {
17776
17901
  } catch {
17777
17902
  }
17778
17903
  }
17904
+ function walSidecars(file2) {
17905
+ return [`${file2}-wal`, `${file2}-shm`];
17906
+ }
17779
17907
  function tightenPerms(file2) {
17780
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
17908
+ for (const path of [file2, ...walSidecars(file2)]) {
17781
17909
  try {
17782
17910
  chmodSync(path, DATA_FILE_MODE);
17783
17911
  } catch {
@@ -17785,12 +17913,68 @@ function tightenPerms(file2) {
17785
17913
  }
17786
17914
  }
17787
17915
 
17788
- // ../../packages/persistence/src/repositories/sql-utils.ts
17789
- function escapeLikePattern(s) {
17790
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17916
+ // ../../packages/persistence/src/internal/json.ts
17917
+ function safeJson(s, fallback) {
17918
+ if (s == null) return fallback;
17919
+ try {
17920
+ return JSON.parse(s);
17921
+ } catch {
17922
+ return fallback;
17923
+ }
17791
17924
  }
17792
- function placeholders(n) {
17793
- return Array.from({ length: n }, () => "?").join(", ");
17925
+ function parseJsonObject(s) {
17926
+ if (s == null) return void 0;
17927
+ try {
17928
+ const parsed = JSON.parse(s);
17929
+ if (typeof parsed === "object" && parsed !== null) return parsed;
17930
+ } catch {
17931
+ }
17932
+ return void 0;
17933
+ }
17934
+
17935
+ // ../../packages/persistence/src/internal/rows.ts
17936
+ function allRows(stmt, params) {
17937
+ if (params === void 0) return stmt.all();
17938
+ if (Array.isArray(params)) return stmt.all(...params);
17939
+ return stmt.all(params);
17940
+ }
17941
+ function getRow(stmt, params) {
17942
+ if (params === void 0) return stmt.get();
17943
+ if (Array.isArray(params)) return stmt.get(...params);
17944
+ return stmt.get(params);
17945
+ }
17946
+ function intToBool(raw) {
17947
+ return raw === 1 || raw === true;
17948
+ }
17949
+ function boolToInt(b) {
17950
+ return b ? 1 : 0;
17951
+ }
17952
+ function bindParams(row) {
17953
+ const out = {};
17954
+ for (const [key, value] of Object.entries(row)) {
17955
+ out[key] = value === void 0 ? null : value;
17956
+ }
17957
+ return out;
17958
+ }
17959
+ function countScalar(db, sql, params) {
17960
+ return getRow(db.prepare(sql), params)?.n ?? 0;
17961
+ }
17962
+ function countBy(db, sql, params) {
17963
+ const map2 = /* @__PURE__ */ new Map();
17964
+ for (const row of allRows(db.prepare(sql), params)) {
17965
+ map2.set(row.k, row.n);
17966
+ }
17967
+ return map2;
17968
+ }
17969
+ function mapRowsTolerant(rows, map2) {
17970
+ const out = [];
17971
+ for (const row of rows) {
17972
+ try {
17973
+ out.push(map2(row));
17974
+ } catch {
17975
+ }
17976
+ }
17977
+ return out;
17794
17978
  }
17795
17979
 
17796
17980
  // ../../packages/persistence/src/repositories/activity.ts
@@ -17842,15 +18026,11 @@ function encodeCursor(payload) {
17842
18026
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
17843
18027
  }
17844
18028
  function decodeCursor(cursor) {
17845
- try {
17846
- const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
17847
- if (parsed !== null && typeof parsed === "object" && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
17848
- return parsed;
17849
- }
17850
- return null;
17851
- } catch {
17852
- return null;
18029
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18030
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18031
+ return parsed;
17853
18032
  }
18033
+ return null;
17854
18034
  }
17855
18035
  var DB_EVENT_TYPE_TO_KIND = {
17856
18036
  session: "session",
@@ -17867,15 +18047,8 @@ var DB_EVENT_TYPE_TO_KIND = {
17867
18047
  };
17868
18048
  function safeParseStringArray(raw) {
17869
18049
  if (!raw) return [];
17870
- try {
17871
- const parsed = JSON.parse(raw);
17872
- return Array.isArray(parsed) ? parsed : [];
17873
- } catch {
17874
- return [];
17875
- }
17876
- }
17877
- function toBool(raw) {
17878
- return raw === 1 || raw === true;
18050
+ const parsed = safeJson(raw, null);
18051
+ return Array.isArray(parsed) ? parsed : [];
17879
18052
  }
17880
18053
  function toHarness(raw) {
17881
18054
  const parsed = Harness.safeParse(raw);
@@ -17928,8 +18101,8 @@ function buildAuditEvent(row) {
17928
18101
  severity: severityParsed?.success ? severityParsed.data : null,
17929
18102
  link: linkParsed?.success ? linkParsed.data : null,
17930
18103
  targetId: row.target_id,
17931
- internal: toBool(row.internal),
17932
- flagged: toBool(row.flagged)
18104
+ internal: intToBool(row.internal),
18105
+ flagged: intToBool(row.flagged)
17933
18106
  };
17934
18107
  }
17935
18108
  var TIMELINE_COLUMNS = `
@@ -17955,12 +18128,15 @@ var SqliteActivityRepository = class {
17955
18128
  stats(tz) {
17956
18129
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
17957
18130
  const { startMs, endMs } = window;
17958
- const sessionsToday = this.db.prepare(
18131
+ const sessionsToday = countScalar(
18132
+ this.db,
17959
18133
  `SELECT count(*) AS n FROM audit_events
17960
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
17961
- ).get(startMs, endMs).n;
18134
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
18135
+ [startMs, endMs]
18136
+ );
17962
18137
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
17963
- const liveNow = this.db.prepare(
18138
+ const liveNow = countScalar(
18139
+ this.db,
17964
18140
  `SELECT count(*) AS n FROM audit_events s
17965
18141
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
17966
18142
  AND max(
@@ -17969,22 +18145,29 @@ var SqliteActivityRepository = class {
17969
18145
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
17970
18146
  s.started_at
17971
18147
  )
17972
- ) >= ?`
17973
- ).get(liveThreshold).n;
17974
- const toolCallsToday = this.db.prepare(
18148
+ ) >= ?`,
18149
+ [liveThreshold]
18150
+ );
18151
+ const toolCallsToday = countScalar(
18152
+ this.db,
17975
18153
  `SELECT count(*) AS n FROM audit_events
17976
- WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`
17977
- ).get(startMs, endMs).n;
17978
- const findingsToday = this.db.prepare(
18154
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18155
+ [startMs, endMs]
18156
+ );
18157
+ const findingsToday = countScalar(
18158
+ this.db,
17979
18159
  `SELECT count(*) AS n FROM inspection_findings f
17980
18160
  JOIN audit_events e ON e.id = f.audit_event_id
17981
- WHERE e.started_at >= ? AND e.started_at < ?`
17982
- ).get(startMs, endMs).n;
17983
- const egressToday = this.db.prepare(
18161
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18162
+ [startMs, endMs]
18163
+ );
18164
+ const egressToday = countScalar(
18165
+ this.db,
17984
18166
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
17985
18167
  FROM audit_events
17986
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
17987
- ).get(startMs, endMs).n;
18168
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18169
+ [startMs, endMs]
18170
+ );
17988
18171
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
17989
18172
  }
17990
18173
  listSessions(query) {
@@ -18006,7 +18189,7 @@ var SqliteActivityRepository = class {
18006
18189
  conditions.push("started_at <= ?");
18007
18190
  params.push(toMs);
18008
18191
  if (query.q) {
18009
- const pattern = `%${escapeLikePattern(query.q)}%`;
18192
+ const pattern = containsPattern(query.q);
18010
18193
  conditions.push(
18011
18194
  `(content LIKE ? ESCAPE '\\'
18012
18195
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18025,8 +18208,9 @@ var SqliteActivityRepository = class {
18025
18208
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18026
18209
  }
18027
18210
  const limit = query.limit;
18028
- const rows = this.db.prepare(
18029
- `SELECT id,
18211
+ const rows = allRows(
18212
+ this.db.prepare(
18213
+ `SELECT id,
18030
18214
  json_extract(attributes, '$.harness') AS harness,
18031
18215
  content AS title,
18032
18216
  json_extract(attributes, '$.project') AS project,
@@ -18039,7 +18223,9 @@ var SqliteActivityRepository = class {
18039
18223
  WHERE ${conditions.join(" AND ")}
18040
18224
  ORDER BY started_at DESC, id DESC
18041
18225
  LIMIT ?`
18042
- ).all(...params, limit + 1);
18226
+ ),
18227
+ [...params, limit + 1]
18228
+ );
18043
18229
  const hasMore = rows.length > limit;
18044
18230
  const page = hasMore ? rows.slice(0, limit) : rows;
18045
18231
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18056,8 +18242,9 @@ var SqliteActivityRepository = class {
18056
18242
  return Promise.resolve({ items, nextCursor });
18057
18243
  }
18058
18244
  getSession(sessionId) {
18059
- const rootRow = this.db.prepare(
18060
- `SELECT id,
18245
+ const rootRow = getRow(
18246
+ this.db.prepare(
18247
+ `SELECT id,
18061
18248
  json_extract(attributes, '$.harness') AS harness,
18062
18249
  content AS title,
18063
18250
  json_extract(attributes, '$.project') AS project,
@@ -18074,47 +18261,66 @@ var SqliteActivityRepository = class {
18074
18261
  FROM audit_events
18075
18262
  WHERE id = ? AND event_type = 'session'
18076
18263
  LIMIT 1`
18077
- ).get(sessionId);
18264
+ ),
18265
+ [sessionId]
18266
+ );
18078
18267
  if (!rootRow) return Promise.resolve(null);
18079
- const timelineRows = this.db.prepare(
18080
- `SELECT ${TIMELINE_COLUMNS}
18268
+ const timelineRows = allRows(
18269
+ this.db.prepare(
18270
+ `SELECT ${TIMELINE_COLUMNS}
18081
18271
  FROM audit_events
18082
18272
  WHERE id = ? OR root_session_id = ?
18083
18273
  ORDER BY started_at ASC, id ASC`
18084
- ).all(sessionId, sessionId);
18274
+ ),
18275
+ [sessionId, sessionId]
18276
+ );
18085
18277
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18086
- const tokenRow = this.db.prepare(
18087
- `SELECT
18278
+ const tokenRow = getRow(
18279
+ this.db.prepare(
18280
+ `SELECT
18088
18281
  coalesce(sum(input_tokens), 0) AS input,
18089
18282
  coalesce(sum(output_tokens), 0) AS output,
18090
18283
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18091
18284
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18092
18285
  FROM audit_events
18093
18286
  WHERE root_session_id = ? AND event_type = 'llm_call'`
18094
- ).get(sessionId);
18095
- const primaryModel = this.db.prepare(
18096
- `SELECT model, provider FROM audit_events
18287
+ ),
18288
+ [sessionId]
18289
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
18290
+ const primaryModel = getRow(
18291
+ this.db.prepare(
18292
+ `SELECT model, provider FROM audit_events
18097
18293
  WHERE root_session_id = ? AND event_type = 'llm_call'
18098
18294
  ORDER BY started_at ASC, id ASC
18099
18295
  LIMIT 1`
18100
- ).get(sessionId);
18101
- const toolRows = this.db.prepare(
18102
- `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18296
+ ),
18297
+ [sessionId]
18298
+ );
18299
+ const toolRows = allRows(
18300
+ this.db.prepare(
18301
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18103
18302
  count(*) AS n
18104
18303
  FROM audit_events
18105
18304
  WHERE root_session_id = ? AND event_type = 'tool_call'
18106
18305
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
18107
- ).all(sessionId);
18108
- const modelRows = this.db.prepare(
18109
- `SELECT DISTINCT model FROM audit_events
18306
+ ),
18307
+ [sessionId]
18308
+ );
18309
+ const modelRows = allRows(
18310
+ this.db.prepare(
18311
+ `SELECT DISTINCT model FROM audit_events
18110
18312
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18111
18313
  ORDER BY model`
18112
- ).all(sessionId);
18314
+ ),
18315
+ [sessionId]
18316
+ );
18113
18317
  const derivedModels = modelRows.map((r) => r.model);
18114
- const commits = this.db.prepare(
18318
+ const commits = countScalar(
18319
+ this.db,
18115
18320
  `SELECT count(*) AS n FROM audit_events
18116
- WHERE root_session_id = ? AND event_type = 'commit'`
18117
- ).get(sessionId).n;
18321
+ WHERE root_session_id = ? AND event_type = 'commit'`,
18322
+ [sessionId]
18323
+ );
18118
18324
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18119
18325
  turns: 0,
18120
18326
  findings: 0,
@@ -18181,7 +18387,10 @@ var SqliteActivityRepository = class {
18181
18387
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18182
18388
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18183
18389
  );
18184
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
18390
+ const rows = allRows(
18391
+ stmt,
18392
+ fromMs === void 0 ? void 0 : [fromMs]
18393
+ );
18185
18394
  const seen = /* @__PURE__ */ new Set();
18186
18395
  for (const row of rows) seen.add(toHarness(row.harness));
18187
18396
  return Promise.resolve([...seen]);
@@ -18204,23 +18413,23 @@ var SqliteActivityRepository = class {
18204
18413
  conditions.push("started_at >= ?");
18205
18414
  params.push(opts.fromMs);
18206
18415
  }
18207
- const rows = this.db.prepare(
18208
- `SELECT root_session_id AS sessionId, attributes
18416
+ const rows = allRows(
18417
+ this.db.prepare(
18418
+ `SELECT root_session_id AS sessionId, attributes
18209
18419
  FROM audit_events
18210
18420
  WHERE ${conditions.join(" AND ")}`
18211
- ).all(...params);
18212
- const leaves = [];
18213
- for (const row of rows) {
18214
- if (row.sessionId === null) continue;
18215
- try {
18216
- leaves.push({
18217
- sessionId: row.sessionId,
18218
- attributes: JSON.parse(row.attributes)
18219
- });
18220
- } catch {
18221
- }
18222
- }
18223
- return leaves;
18421
+ ),
18422
+ params
18423
+ );
18424
+ return mapRowsTolerant(
18425
+ rows.filter(
18426
+ (row) => row.sessionId !== null
18427
+ ),
18428
+ (row) => ({
18429
+ sessionId: row.sessionId,
18430
+ attributes: JSON.parse(row.attributes)
18431
+ })
18432
+ );
18224
18433
  }
18225
18434
  /**
18226
18435
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -18234,57 +18443,72 @@ var SqliteActivityRepository = class {
18234
18443
  );
18235
18444
  if (sessionIds.length === 0) return result;
18236
18445
  const inClause = placeholders(sessionIds.length);
18237
- const lastActivityRows = this.db.prepare(
18238
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18446
+ const lastActivityRows = allRows(
18447
+ this.db.prepare(
18448
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18239
18449
  WHERE root_session_id IN (${inClause})
18240
18450
  GROUP BY root_session_id`
18241
- ).all(...sessionIds);
18451
+ ),
18452
+ sessionIds
18453
+ );
18242
18454
  for (const row of lastActivityRows) {
18243
18455
  if (row.id === null) continue;
18244
18456
  const entry = result.get(row.id);
18245
18457
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
18246
18458
  }
18247
- const turnsRows = this.db.prepare(
18248
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18459
+ const turnsRows = allRows(
18460
+ this.db.prepare(
18461
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18249
18462
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
18250
18463
  GROUP BY root_session_id`
18251
- ).all(...sessionIds);
18464
+ ),
18465
+ sessionIds
18466
+ );
18252
18467
  for (const row of turnsRows) {
18253
18468
  if (row.id === null) continue;
18254
18469
  const entry = result.get(row.id);
18255
18470
  if (entry) entry.turns = row.n;
18256
18471
  }
18257
- const runKeyRows = this.db.prepare(
18258
- `SELECT root_session_id AS id,
18472
+ const runKeyRows = allRows(
18473
+ this.db.prepare(
18474
+ `SELECT root_session_id AS id,
18259
18475
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
18260
18476
  FROM audit_events
18261
18477
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
18262
18478
  AND json_extract(attributes, '$.run_key') IS NOT NULL
18263
18479
  GROUP BY root_session_id`
18264
- ).all(...sessionIds);
18480
+ ),
18481
+ sessionIds
18482
+ );
18265
18483
  for (const row of runKeyRows) {
18266
18484
  if (row.id === null) continue;
18267
18485
  const entry = result.get(row.id);
18268
18486
  if (entry) entry.turns = Math.max(entry.turns, row.n);
18269
18487
  }
18270
- const findingsRows = this.db.prepare(
18271
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18488
+ const findingsRows = allRows(
18489
+ this.db.prepare(
18490
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18272
18491
  JOIN audit_events e ON e.id = f.audit_event_id
18273
18492
  WHERE e.root_session_id IN (${inClause})
18274
18493
  GROUP BY e.root_session_id`
18275
- ).all(...sessionIds);
18494
+ ),
18495
+ sessionIds
18496
+ );
18276
18497
  for (const row of findingsRows) {
18277
18498
  if (row.id === null) continue;
18278
18499
  const entry = result.get(row.id);
18279
18500
  if (entry) entry.findings = row.n;
18280
18501
  }
18281
- const sharesRows = this.db.prepare(
18282
- `SELECT root_session_id AS id,
18502
+ const sharesRows = allRows(
18503
+ this.db.prepare(
18504
+ `SELECT root_session_id AS id,
18283
18505
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
18284
18506
  FROM audit_events
18285
18507
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
18286
18508
  GROUP BY root_session_id`
18287
- ).all(...sessionIds);
18509
+ ),
18510
+ sessionIds
18511
+ );
18288
18512
  for (const row of sharesRows) {
18289
18513
  if (row.id === null) continue;
18290
18514
  const entry = result.get(row.id);
@@ -18334,33 +18558,28 @@ var SqliteAuditEventsRepository = class {
18334
18558
  // the caller fails open and drops the whole pass — recovered idempotently on the
18335
18559
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
18336
18560
  runInTransaction(fn) {
18337
- this.db.exec("BEGIN");
18338
- try {
18339
- fn();
18340
- this.db.exec("COMMIT");
18341
- } catch (err) {
18342
- this.db.exec("ROLLBACK");
18343
- throw err;
18344
- }
18561
+ withTransaction(this.db, fn);
18345
18562
  }
18346
18563
  insertAuditEvent(input) {
18347
18564
  const row = toAuditEventRow(input);
18348
- this.insertStmt.run({
18349
- id: row.id,
18350
- parentId: row.parentId ?? null,
18351
- rootSessionId: row.rootSessionId ?? null,
18352
- eventType: row.eventType,
18353
- hostId: row.hostId ?? null,
18354
- harnessId: row.harnessId ?? null,
18355
- sourceProjectId: row.sourceProjectId ?? null,
18356
- startedAt: row.startedAt,
18357
- endedAt: row.endedAt ?? null,
18358
- severity: row.severity ?? null,
18359
- priority: row.priority ?? null,
18360
- content: row.content ?? null,
18361
- contentHash: row.contentHash ?? null,
18362
- attributes: row.attributes ?? null
18363
- });
18565
+ this.insertStmt.run(
18566
+ bindParams({
18567
+ id: row.id,
18568
+ parentId: row.parentId,
18569
+ rootSessionId: row.rootSessionId,
18570
+ eventType: row.eventType,
18571
+ hostId: row.hostId,
18572
+ harnessId: row.harnessId,
18573
+ sourceProjectId: row.sourceProjectId,
18574
+ startedAt: row.startedAt,
18575
+ endedAt: row.endedAt,
18576
+ severity: row.severity,
18577
+ priority: row.priority,
18578
+ content: row.content,
18579
+ contentHash: row.contentHash,
18580
+ attributes: row.attributes
18581
+ })
18582
+ );
18364
18583
  }
18365
18584
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18366
18585
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -18374,22 +18593,24 @@ var SqliteAuditEventsRepository = class {
18374
18593
  const startedAt = isoToEpochMillis(input.startedAt);
18375
18594
  if (!Number.isFinite(startedAt)) return;
18376
18595
  const id = llmCallId(input.sessionId, input.messageId);
18377
- this.upsertLlmCallStmt.run({
18378
- id,
18379
- parentId: input.parentId,
18380
- rootSessionId: input.rootSessionId,
18381
- eventType: "llm_call",
18382
- hostId: null,
18383
- harnessId: null,
18384
- sourceProjectId: null,
18385
- startedAt,
18386
- endedAt: null,
18387
- severity: null,
18388
- priority: null,
18389
- content: null,
18390
- contentHash: null,
18391
- attributes: JSON.stringify(input.attributes)
18392
- });
18596
+ this.upsertLlmCallStmt.run(
18597
+ bindParams({
18598
+ id,
18599
+ parentId: input.parentId,
18600
+ rootSessionId: input.rootSessionId,
18601
+ eventType: "llm_call",
18602
+ hostId: null,
18603
+ harnessId: null,
18604
+ sourceProjectId: null,
18605
+ startedAt,
18606
+ endedAt: null,
18607
+ severity: null,
18608
+ priority: null,
18609
+ content: null,
18610
+ contentHash: null,
18611
+ attributes: JSON.stringify(input.attributes)
18612
+ })
18613
+ );
18393
18614
  }
18394
18615
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
18395
18616
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -18413,25 +18634,29 @@ var SqliteAuditEventsRepository = class {
18413
18634
  const startedAt = isoToEpochMillis(input.startedAt);
18414
18635
  if (!Number.isFinite(startedAt)) return;
18415
18636
  const id = toolCallId(input.sessionId, input.toolUseId);
18416
- this.insertStmt.run({
18417
- id,
18418
- parentId: input.parentId,
18419
- rootSessionId: input.rootSessionId,
18420
- eventType: "tool_call",
18421
- hostId: null,
18422
- harnessId: null,
18423
- sourceProjectId: null,
18424
- startedAt,
18425
- endedAt: null,
18426
- severity: null,
18427
- priority: null,
18428
- content: null,
18429
- contentHash: null,
18430
- attributes: JSON.stringify(input.attributes)
18431
- });
18637
+ this.insertStmt.run(
18638
+ bindParams({
18639
+ id,
18640
+ parentId: input.parentId,
18641
+ rootSessionId: input.rootSessionId,
18642
+ eventType: "tool_call",
18643
+ hostId: null,
18644
+ harnessId: null,
18645
+ sourceProjectId: null,
18646
+ startedAt,
18647
+ endedAt: null,
18648
+ severity: null,
18649
+ priority: null,
18650
+ content: null,
18651
+ contentHash: null,
18652
+ attributes: JSON.stringify(input.attributes)
18653
+ })
18654
+ );
18432
18655
  }
18433
18656
  findById(id) {
18434
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
18657
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
18658
+ id
18659
+ });
18435
18660
  }
18436
18661
  // Read the `provider` snapshotted onto a session root's attributes.
18437
18662
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -18441,14 +18666,8 @@ var SqliteAuditEventsRepository = class {
18441
18666
  sessionProvider(sessionId) {
18442
18667
  const row = this.findById(sessionId);
18443
18668
  if (!row?.attributes) return void 0;
18444
- try {
18445
- const parsed = JSON.parse(row.attributes);
18446
- if (typeof parsed === "object" && parsed !== null) {
18447
- const provider = parsed.provider;
18448
- if (typeof provider === "string") return provider;
18449
- }
18450
- } catch {
18451
- }
18669
+ const provider = parseJsonObject(row.attributes)?.provider;
18670
+ if (typeof provider === "string") return provider;
18452
18671
  return void 0;
18453
18672
  }
18454
18673
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -18457,11 +18676,13 @@ var SqliteAuditEventsRepository = class {
18457
18676
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
18458
18677
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
18459
18678
  llmCallLeaves() {
18460
- return this.db.prepare(
18461
- `SELECT root_session_id AS sessionId, attributes
18679
+ return allRows(
18680
+ this.db.prepare(
18681
+ `SELECT root_session_id AS sessionId, attributes
18462
18682
  FROM audit_events
18463
18683
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
18464
- ).all();
18684
+ )
18685
+ );
18465
18686
  }
18466
18687
  };
18467
18688
 
@@ -18480,16 +18701,29 @@ var SqliteClassifiedDataRepository = class {
18480
18701
  upsert(input) {
18481
18702
  const id = classifiedDataId(input.class);
18482
18703
  const row = toClassifiedDataRow(input, id);
18483
- this.insertStmt.run({
18484
- id: row.id,
18485
- class: row.class,
18486
- label: row.label ?? null,
18487
- attributes: row.attributes ?? null
18488
- });
18704
+ this.insertStmt.run(
18705
+ bindParams({
18706
+ id: row.id,
18707
+ class: row.class,
18708
+ label: row.label,
18709
+ attributes: row.attributes
18710
+ })
18711
+ );
18489
18712
  return id;
18490
18713
  }
18491
18714
  };
18492
18715
 
18716
+ // ../../packages/persistence/src/repositories/config-scan.ts
18717
+ function latestConfigScan(db) {
18718
+ return getRow(
18719
+ db.prepare(
18720
+ `SELECT id, started_at, attributes FROM audit_events
18721
+ WHERE event_type = 'config_scan'
18722
+ ORDER BY started_at DESC, id DESC LIMIT 1`
18723
+ )
18724
+ );
18725
+ }
18726
+
18493
18727
  // ../../packages/persistence/src/repositories/config-inventory.ts
18494
18728
  var SqliteConfigInventoryRepository = class {
18495
18729
  constructor(db) {
@@ -18497,7 +18731,7 @@ var SqliteConfigInventoryRepository = class {
18497
18731
  }
18498
18732
  db;
18499
18733
  report() {
18500
- const scan2 = this.latestScan();
18734
+ const scan2 = latestConfigScan(this.db);
18501
18735
  if (!scan2) {
18502
18736
  return {
18503
18737
  scannedAt: null,
@@ -18508,17 +18742,23 @@ var SqliteConfigInventoryRepository = class {
18508
18742
  topics: []
18509
18743
  };
18510
18744
  }
18511
- const rows = this.db.prepare(
18512
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18745
+ const rows = allRows(
18746
+ this.db.prepare(
18747
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18513
18748
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
18514
18749
  ORDER BY object_type, title`
18515
- ).all({ startedAt: scan2.started_at });
18516
- const findings = this.db.prepare(
18517
- `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18750
+ ),
18751
+ { startedAt: scan2.started_at }
18752
+ );
18753
+ const findings = allRows(
18754
+ this.db.prepare(
18755
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18518
18756
  FROM inspection_findings f
18519
18757
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
18520
18758
  WHERE f.audit_event_id = :scanId`
18521
- ).all({ scanId: scan2.id });
18759
+ ),
18760
+ { scanId: scan2.id }
18761
+ );
18522
18762
  const skills = [];
18523
18763
  const hooks = [];
18524
18764
  const mcpServers = [];
@@ -18547,7 +18787,9 @@ var SqliteConfigInventoryRepository = class {
18547
18787
  // schema note); an override whose asset is gone simply never matches. A row
18548
18788
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
18549
18789
  trustOverrides() {
18550
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
18790
+ const rows = allRows(
18791
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
18792
+ );
18551
18793
  const map2 = /* @__PURE__ */ new Map();
18552
18794
  for (const row of rows) {
18553
18795
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -18556,13 +18798,6 @@ var SqliteConfigInventoryRepository = class {
18556
18798
  }
18557
18799
  return map2;
18558
18800
  }
18559
- latestScan() {
18560
- return this.db.prepare(
18561
- `SELECT id, started_at, attributes FROM audit_events
18562
- WHERE event_type = 'config_scan'
18563
- ORDER BY started_at DESC, id DESC LIMIT 1`
18564
- ).get();
18565
- }
18566
18801
  };
18567
18802
  function toSkillItem(row, bag) {
18568
18803
  const item = {
@@ -18673,22 +18908,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
18673
18908
  return topics;
18674
18909
  }
18675
18910
  function countScanErrors(attributes) {
18676
- if (!attributes) return 0;
18677
- try {
18678
- const parsed = JSON.parse(attributes);
18679
- const errors = parsed?.errors;
18680
- return typeof errors === "number" ? errors : 0;
18681
- } catch {
18682
- return 0;
18683
- }
18911
+ const errors = parseJsonObject(attributes)?.errors;
18912
+ return typeof errors === "number" ? errors : 0;
18684
18913
  }
18685
18914
  function parseBag(raw) {
18686
- try {
18687
- const parsed = JSON.parse(raw);
18688
- if (typeof parsed === "object" && parsed !== null) return parsed;
18689
- } catch {
18690
- }
18691
- return void 0;
18915
+ return parseJsonObject(raw);
18692
18916
  }
18693
18917
  function str(value) {
18694
18918
  return typeof value === "string" ? value : void 0;
@@ -18697,12 +18921,7 @@ function str(value) {
18697
18921
  // ../../packages/persistence/src/repositories/detections.ts
18698
18922
  var DAY_MS2 = 864e5;
18699
18923
  function parseRules(rulesJson) {
18700
- let raw;
18701
- try {
18702
- raw = JSON.parse(rulesJson);
18703
- } catch {
18704
- return [];
18705
- }
18924
+ const raw = safeJson(rulesJson, []);
18706
18925
  if (!Array.isArray(raw)) return [];
18707
18926
  const rules = [];
18708
18927
  for (const entry of raw) {
@@ -18721,11 +18940,13 @@ var SqliteDetectionsRepository = class {
18721
18940
  db;
18722
18941
  now;
18723
18942
  listDetections(query) {
18724
- const rows = this.db.prepare(
18725
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18943
+ const rows = allRows(
18944
+ this.db.prepare(
18945
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18726
18946
  rules_json AS rulesJson
18727
18947
  FROM installed_packs`
18728
- ).all();
18948
+ )
18949
+ );
18729
18950
  const available = this.availableByPack();
18730
18951
  const summaries = rows.map((r) => {
18731
18952
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -18734,7 +18955,7 @@ var SqliteDetectionsRepository = class {
18734
18955
  packId: r.packId,
18735
18956
  version: r.version,
18736
18957
  name: r.name,
18737
- enabled: r.enabled === 1,
18958
+ enabled: intToBool(r.enabled),
18738
18959
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
18739
18960
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
18740
18961
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -18751,19 +18972,23 @@ var SqliteDetectionsRepository = class {
18751
18972
  // available_packs keyed by the "namespace/packId" slug (one read per list /
18752
18973
  // detail call; the table is a handful of rows).
18753
18974
  availableByPack() {
18754
- const rows = this.db.prepare(
18755
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18975
+ const rows = allRows(
18976
+ this.db.prepare(
18977
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18756
18978
  FROM available_packs`
18757
- ).all();
18979
+ )
18980
+ );
18758
18981
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
18759
18982
  }
18760
18983
  getDetectionStats() {
18761
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
18984
+ const rows = allRows(
18985
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
18986
+ );
18762
18987
  let rules = 0;
18763
18988
  let active = 0;
18764
18989
  const ruleIds = /* @__PURE__ */ new Set();
18765
18990
  for (const r of rows) {
18766
- if (r.enabled === 1) active += 1;
18991
+ if (intToBool(r.enabled)) active += 1;
18767
18992
  const parsed = parseRules(r.rulesJson);
18768
18993
  rules += parsed.length;
18769
18994
  for (const rule of parsed) {
@@ -18781,12 +19006,15 @@ var SqliteDetectionsRepository = class {
18781
19006
  const parts = splitDetectionId(id);
18782
19007
  if (!parts) return Promise.resolve(null);
18783
19008
  const { namespace, packId } = parts;
18784
- const row = this.db.prepare(
18785
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19009
+ const row = getRow(
19010
+ this.db.prepare(
19011
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18786
19012
  rules_json AS rulesJson, updated_at AS updatedAt
18787
19013
  FROM installed_packs
18788
19014
  WHERE namespace = ? AND pack_id = ?`
18789
- ).get(namespace, packId);
19015
+ ),
19016
+ [namespace, packId]
19017
+ );
18790
19018
  if (!row) return Promise.resolve(null);
18791
19019
  const rules = parseRules(row.rulesJson);
18792
19020
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -18804,7 +19032,7 @@ var SqliteDetectionsRepository = class {
18804
19032
  packId: row.packId,
18805
19033
  version: row.version,
18806
19034
  name: row.name,
18807
- enabled: row.enabled === 1,
19035
+ enabled: intToBool(row.enabled),
18808
19036
  rules,
18809
19037
  updatedAt: new Date(row.updatedAt),
18810
19038
  policyId: row.policyId
@@ -18819,13 +19047,14 @@ var SqliteDetectionsRepository = class {
18819
19047
  countFindingsLast30d(ruleIds) {
18820
19048
  if (ruleIds.length === 0) return 0;
18821
19049
  const since = this.now() - 30 * DAY_MS2;
18822
- const placeholders2 = ruleIds.map(() => "?").join(", ");
18823
- const row = this.db.prepare(
18824
- `SELECT count(*) AS c
19050
+ const inClause = placeholders(ruleIds.length);
19051
+ return countScalar(
19052
+ this.db,
19053
+ `SELECT count(*) AS n
18825
19054
  FROM findings f JOIN events e ON e.id = f.event_id
18826
- WHERE e.occurred_at >= ? AND f.rule_id IN (${placeholders2})`
18827
- ).get(since, ...ruleIds);
18828
- return row.c;
19055
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19056
+ [since, ...ruleIds]
19057
+ );
18829
19058
  }
18830
19059
  };
18831
19060
 
@@ -18842,16 +19071,17 @@ var SqliteEventsRepository = class {
18842
19071
  insertStmt;
18843
19072
  insertEvent(event) {
18844
19073
  const row = toEventRow(event);
18845
- this.insertStmt.run({
18846
- id: row.id,
18847
- sourceTool: row.sourceTool,
18848
- kind: row.kind,
18849
- occurredAt: row.occurredAt,
18850
- contentHash: row.contentHash,
18851
- content: row.content,
18852
- // exactOptionalPropertyTypes: the column is nullable, never undefined.
18853
- metadata: row.metadata ?? null
18854
- });
19074
+ this.insertStmt.run(
19075
+ bindParams({
19076
+ id: row.id,
19077
+ sourceTool: row.sourceTool,
19078
+ kind: row.kind,
19079
+ occurredAt: row.occurredAt,
19080
+ contentHash: row.contentHash,
19081
+ content: row.content,
19082
+ metadata: row.metadata
19083
+ })
19084
+ );
18855
19085
  }
18856
19086
  // Every recorded event's content hash — the historical backfill loads this once
18857
19087
  // to skip transcript messages it has already stored, so re-running the scan
@@ -18859,13 +19089,23 @@ var SqliteEventsRepository = class {
18859
19089
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
18860
19090
  // async EventsReadPort contract.
18861
19091
  contentHashes() {
18862
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19092
+ const rows = allRows(
19093
+ this.db.prepare("SELECT content_hash FROM events")
19094
+ );
18863
19095
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
18864
19096
  }
18865
19097
  };
18866
19098
 
18867
19099
  // ../../packages/persistence/src/repositories/exceptions.ts
18868
19100
  import { randomUUID } from "crypto";
19101
+
19102
+ // ../../packages/persistence/src/internal/sqlite-errors.ts
19103
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19104
+ function isUniqueConstraintError(err) {
19105
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19106
+ }
19107
+
19108
+ // ../../packages/persistence/src/repositories/exceptions.ts
18869
19109
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
18870
19110
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
18871
19111
  var DuplicateActiveExceptionError = class extends Error {
@@ -18886,10 +19126,6 @@ var AmbiguousExceptionIdError = class extends Error {
18886
19126
  this.name = "AmbiguousExceptionIdError";
18887
19127
  }
18888
19128
  };
18889
- var SQLITE_CONSTRAINT_UNIQUE = 2067;
18890
- function isUniqueConstraintError(err) {
18891
- return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
18892
- }
18893
19129
  var ACTIVE_PREDICATE = `revoked_at IS NULL
18894
19130
  AND (expires_at IS NULL OR expires_at > :now)
18895
19131
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -18945,10 +19181,11 @@ var SqliteExceptionsRepository = class {
18945
19181
  this.insertExceptionRow(id, input, now);
18946
19182
  } catch (err) {
18947
19183
  if (!isUniqueConstraintError(err)) throw err;
18948
- this.db.exec("BEGIN IMMEDIATE");
18949
- try {
18950
- const superseded = this.db.prepare(
18951
- `UPDATE exceptions
19184
+ withTransaction(
19185
+ this.db,
19186
+ () => {
19187
+ const superseded = this.db.prepare(
19188
+ `UPDATE exceptions
18952
19189
  SET revoked_at = :now, revoked_by = :revokedBy,
18953
19190
  revoke_reason = 'superseded by a new grant for the same value',
18954
19191
  updated_at = :now
@@ -18956,24 +19193,27 @@ var SqliteExceptionsRepository = class {
18956
19193
  AND key_version = :keyVersion AND revoked_at IS NULL
18957
19194
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
18958
19195
  OR (max_uses IS NOT NULL AND use_count >= max_uses))`
18959
- ).run({
18960
- now,
18961
- revokedBy: input.createdBy,
18962
- ruleId: input.ruleId,
18963
- valueFingerprint: input.valueFingerprint,
18964
- keyVersion: input.keyVersion
18965
- });
18966
- if (Number(superseded.changes) !== 1) {
18967
- throw new DuplicateActiveExceptionError(input.ruleId);
18968
- }
18969
- this.insertExceptionRow(id, input, now);
18970
- this.db.exec("COMMIT");
18971
- } catch (retryErr) {
18972
- this.db.exec("ROLLBACK");
18973
- throw retryErr;
18974
- }
19196
+ ).run({
19197
+ now,
19198
+ revokedBy: input.createdBy,
19199
+ ruleId: input.ruleId,
19200
+ valueFingerprint: input.valueFingerprint,
19201
+ keyVersion: input.keyVersion
19202
+ });
19203
+ if (Number(superseded.changes) !== 1) {
19204
+ throw new DuplicateActiveExceptionError(input.ruleId);
19205
+ }
19206
+ this.insertExceptionRow(id, input, now);
19207
+ },
19208
+ "IMMEDIATE"
19209
+ );
19210
+ }
19211
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
19212
+ id
19213
+ });
19214
+ if (row === void 0) {
19215
+ throw new Error("exception row not found immediately after insert");
18975
19216
  }
18976
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
18977
19217
  return parseExceptionRow(row);
18978
19218
  }
18979
19219
  insertExceptionRow(id, input, now) {
@@ -19011,14 +19251,11 @@ var SqliteExceptionsRepository = class {
19011
19251
  */
19012
19252
  list(opts) {
19013
19253
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19014
- const rows = this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`).all(opts?.includeTerminal ? {} : { now: Date.now() });
19015
- const exceptions = [];
19016
- for (const row of rows) {
19017
- try {
19018
- exceptions.push(parseExceptionRow(row));
19019
- } catch {
19020
- }
19021
- }
19254
+ const rows = allRows(
19255
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19256
+ opts?.includeTerminal ? {} : { now: Date.now() }
19257
+ );
19258
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19022
19259
  return Promise.resolve(exceptions);
19023
19260
  }
19024
19261
  /**
@@ -19028,7 +19265,12 @@ var SqliteExceptionsRepository = class {
19028
19265
  */
19029
19266
  getByIdPrefix(prefix) {
19030
19267
  if (prefix.length === 0) return Promise.resolve(void 0);
19031
- const rows = this.db.prepare(String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`).all({ pattern: `${escapeLikePattern(prefix)}%` });
19268
+ const rows = allRows(
19269
+ this.db.prepare(
19270
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
19271
+ ),
19272
+ { pattern: `${escapeLikePattern(prefix)}%` }
19273
+ );
19032
19274
  if (rows.length > 1) {
19033
19275
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19034
19276
  }
@@ -19070,30 +19312,27 @@ var SqliteExceptionsRepository = class {
19070
19312
  * a different (rotated-away) key never match, so they are excluded at read.
19071
19313
  */
19072
19314
  activeBundleEntries(keyVersion, now = Date.now()) {
19073
- const rows = this.db.prepare(
19074
- `SELECT * FROM exceptions
19315
+ const rows = allRows(
19316
+ this.db.prepare(
19317
+ `SELECT * FROM exceptions
19075
19318
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19076
19319
  ORDER BY created_at DESC, rowid DESC`
19077
- ).all({ keyVersion, now });
19078
- const entries = [];
19079
- for (const row of rows) {
19080
- try {
19081
- const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19082
- entries.push(
19083
- ExceptionBundleEntry.parse({
19084
- id: row.id,
19085
- ruleId: row.rule_id,
19086
- valueFingerprint: row.value_fingerprint,
19087
- keyVersion: row.key_version,
19088
- expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19089
- maxUses: row.max_uses,
19090
- useCount: row.use_count,
19091
- conditions
19092
- })
19093
- );
19094
- } catch {
19095
- }
19096
- }
19320
+ ),
19321
+ { keyVersion, now }
19322
+ );
19323
+ const entries = mapRowsTolerant(rows, (row) => {
19324
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19325
+ return ExceptionBundleEntry.parse({
19326
+ id: row.id,
19327
+ ruleId: row.rule_id,
19328
+ valueFingerprint: row.value_fingerprint,
19329
+ keyVersion: row.key_version,
19330
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19331
+ maxUses: row.max_uses,
19332
+ useCount: row.use_count,
19333
+ conditions
19334
+ });
19335
+ });
19097
19336
  return Promise.resolve(entries);
19098
19337
  }
19099
19338
  /**
@@ -19120,11 +19359,14 @@ var SqliteExceptionsRepository = class {
19120
19359
  }
19121
19360
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19122
19361
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19123
- const rows = this.db.prepare(
19124
- `SELECT * FROM blocked_detections
19362
+ const rows = allRows(
19363
+ this.db.prepare(
19364
+ `SELECT * FROM blocked_detections
19125
19365
  WHERE blocked_at > :cutoff
19126
19366
  ORDER BY blocked_at DESC, rowid DESC`
19127
- ).all({ cutoff: Date.now() - windowMs });
19367
+ ),
19368
+ { cutoff: Date.now() - windowMs }
19369
+ );
19128
19370
  return Promise.resolve(
19129
19371
  rows.map((row) => ({
19130
19372
  reference: row.reference,
@@ -19204,7 +19446,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
19204
19446
  )`;
19205
19447
 
19206
19448
  // ../../packages/persistence/src/repositories/findings.ts
19207
- var GROUP_ROW_CAP = 2e3;
19449
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
19450
+ var CONCAT_SEP = ",";
19451
+ var TUPLE_SEP = "|";
19452
+ function splitConcat(value) {
19453
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
19454
+ }
19208
19455
  function deriveInstanceStatus(row) {
19209
19456
  return deriveFindingStatus({
19210
19457
  kind: row.kind,
@@ -19272,13 +19519,16 @@ var SqliteFindingsRepository = class {
19272
19519
  }
19273
19520
  recentFindings(opts) {
19274
19521
  const limit = opts?.limit ?? 50;
19275
- const rows = this.db.prepare(
19276
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19522
+ const rows = allRows(
19523
+ this.db.prepare(
19524
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19277
19525
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19278
19526
  FROM findings f JOIN events e ON e.id = f.event_id
19279
19527
  ORDER BY e.occurred_at DESC, f.rowid DESC
19280
19528
  LIMIT :limit`
19281
- ).all({ limit });
19529
+ ),
19530
+ { limit }
19531
+ );
19282
19532
  return Promise.resolve(
19283
19533
  rows.map((r) => ({
19284
19534
  id: r.id,
@@ -19301,20 +19551,47 @@ var SqliteFindingsRepository = class {
19301
19551
  * applies the requested filters, and sorts by severity then recency. Filtering
19302
19552
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19303
19553
  * reflect the full filtered set; `items` is the requested
19304
- * page (default 100); no cursor (nextCursor is always null).
19554
+ * page (default 50); no cursor (nextCursor is always null).
19555
+ *
19556
+ * Two reads, neither of which materializes a row per finding:
19557
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
19558
+ * the group and the filters need (count, providers, actions, statuses,
19559
+ * latest, search text);
19560
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
19561
+ * populate `instances` for the table's expanded rows.
19562
+ * The aggregates carry raw DB values and are translated by the same
19563
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
19564
+ * rule is ever restated in SQL.
19305
19565
  */
19306
19566
  listGroupedFindings(query) {
19307
- const rows = this.db.prepare(
19308
- `SELECT f.id, f.rule_id, f.category, f.severity, f.masked_match,
19309
- f.action_taken, f.confidence, e.occurred_at, e.source_tool,
19310
- json_extract(e.metadata, '$.repo') AS repo,
19311
- json_extract(e.metadata, '$.filePath') AS file,
19312
- e.kind AS kind, f.finding_key AS finding_key,
19313
- ${latestResolutionStatusSql("f")} AS latest_status
19314
- FROM findings f JOIN events e ON e.id = f.event_id
19315
- ORDER BY e.occurred_at DESC, f.id DESC
19316
- LIMIT :cap`
19317
- ).all({ cap: GROUP_ROW_CAP });
19567
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
19568
+ const rows = allRows(
19569
+ this.db.prepare(
19570
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19571
+ occurred_at, source_tool, repo, file, kind, finding_key, latest_status
19572
+ FROM (
19573
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19574
+ f.severity AS severity, f.masked_match AS masked_match,
19575
+ f.action_taken AS action_taken, f.confidence AS confidence,
19576
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19577
+ json_extract(e.metadata, '$.repo') AS repo,
19578
+ json_extract(e.metadata, '$.filePath') AS file,
19579
+ e.kind AS kind, f.finding_key AS finding_key,
19580
+ latest.status AS latest_status,
19581
+ ROW_NUMBER() OVER (
19582
+ PARTITION BY f.rule_id
19583
+ ORDER BY e.occurred_at DESC, f.id DESC
19584
+ ) AS rn
19585
+ FROM findings f
19586
+ JOIN events e ON e.id = f.event_id
19587
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19588
+ ON latest.finding_key = f.finding_key
19589
+ )
19590
+ WHERE rn <= :cap
19591
+ ORDER BY occurred_at DESC, id DESC`
19592
+ ),
19593
+ { cap: PREVIEW_INSTANCES_PER_GROUP }
19594
+ );
19318
19595
  const groupable = rows.map((r) => ({
19319
19596
  id: r.id,
19320
19597
  ruleId: r.rule_id,
@@ -19329,7 +19606,7 @@ var SqliteFindingsRepository = class {
19329
19606
  file: r.file ?? "",
19330
19607
  status: deriveInstanceStatus(r)
19331
19608
  }));
19332
- const allGroups = buildFindingGroups(groupable);
19609
+ const allGroups = buildFindingGroups(groupable, { aggregates });
19333
19610
  const filterOpts = {
19334
19611
  severity: query.severity,
19335
19612
  providers: query.provider,
@@ -19347,42 +19624,122 @@ var SqliteFindingsRepository = class {
19347
19624
  const items = sorted.slice(0, limit);
19348
19625
  return Promise.resolve({ totals, facets, items, nextCursor: null });
19349
19626
  }
19627
+ /**
19628
+ * One row per rule_id, folding EVERY instance of the group into the values
19629
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
19630
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
19631
+ *
19632
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
19633
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19634
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19635
+ * status keeps the classifier itself in @akasecurity/schema, where
19636
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
19637
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
19638
+ * a group's row stays small however many findings it holds.
19639
+ *
19640
+ * `withSearchText` is the exception, and the one column here that does NOT
19641
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
19642
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19643
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
19644
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
19645
+ * match an instance outside the preview, which searching the preview alone
19646
+ * would silently lose, so it is fetched only when the request actually
19647
+ * carries a `q`.
19648
+ */
19649
+ groupAggregates(withSearchText) {
19650
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19651
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
19652
+ const rows = this.db.prepare(
19653
+ `SELECT f.rule_id AS rule_id,
19654
+ count(*) AS instance_count,
19655
+ max(e.occurred_at) AS latest_at,
19656
+ group_concat(DISTINCT e.source_tool) AS source_tools,
19657
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
19658
+ group_concat(DISTINCT (
19659
+ e.kind || '${TUPLE_SEP}' ||
19660
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19661
+ coalesce(latest.status, '')
19662
+ )) AS status_inputs
19663
+ ${searchTextColumns}
19664
+ FROM findings f
19665
+ JOIN events e ON e.id = f.event_id
19666
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19667
+ ON latest.finding_key = f.finding_key
19668
+ GROUP BY f.rule_id`
19669
+ ).all();
19670
+ return new Map(
19671
+ rows.map((r) => [
19672
+ r.rule_id,
19673
+ {
19674
+ instanceCount: r.instance_count,
19675
+ sourceTools: splitConcat(r.source_tools),
19676
+ actionsTaken: splitConcat(r.actions_taken),
19677
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19678
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
19679
+ return {
19680
+ // deriveFindingStatus only distinguishes null from non-null here,
19681
+ // so the marker stands in for the key itself (never rendered).
19682
+ kind,
19683
+ findingKey: keyMarker === "" ? null : keyMarker,
19684
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
19685
+ };
19686
+ }),
19687
+ latestDetectedAt: epochMillisToIso(r.latest_at),
19688
+ // Free text only — joined and substring-matched, so group_concat's
19689
+ // commas need no unpicking (a repo/path containing one still matches).
19690
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
19691
+ // tell "no q this request" from "a group with no repo/file at all"
19692
+ // and skip priming a haystack nothing will read.
19693
+ ...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
19694
+ }
19695
+ ])
19696
+ );
19697
+ }
19350
19698
  healthSummary() {
19351
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
19699
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
19352
19700
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
19353
- const grouped = this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken").all();
19701
+ const grouped = allRows(
19702
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
19703
+ );
19354
19704
  for (const row of grouped) {
19355
19705
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
19356
19706
  }
19357
19707
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
19358
- const sevRows = this.db.prepare(
19359
- `SELECT f.severity AS severity, count(*) AS c
19708
+ const sevRows = allRows(
19709
+ this.db.prepare(
19710
+ `SELECT f.severity AS severity, count(*) AS c
19360
19711
  FROM findings f
19361
19712
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19362
19713
  ON latest.finding_key = f.finding_key
19363
19714
  WHERE latest.status IS NULL OR latest.status != 'resolved'
19364
19715
  GROUP BY f.severity`
19365
- ).all();
19716
+ )
19717
+ );
19366
19718
  for (const row of sevRows) {
19367
19719
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
19368
19720
  }
19369
19721
  const categories = ENFORCEABLE_CATEGORIES;
19370
- const enabledRows = this.db.prepare(
19371
- `SELECT DISTINCT json_extract(target, '$.category') AS category
19722
+ const enabledRows = allRows(
19723
+ this.db.prepare(
19724
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
19372
19725
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
19373
- ).all();
19726
+ )
19727
+ );
19374
19728
  const enabled = new Set(enabledRows.map((r) => r.category));
19375
19729
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
19376
19730
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
19377
19731
  }
19378
19732
  activityByDay(days = 7) {
19379
19733
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
19380
- const rows = this.db.prepare(
19381
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19734
+ const rows = allRows(
19735
+ this.db.prepare(
19736
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19382
19737
  FROM findings f JOIN events e ON e.id = f.event_id
19383
19738
  WHERE e.occurred_at >= :since
19384
19739
  GROUP BY day, f.action_taken`
19385
- ).all({ since });
19740
+ ),
19741
+ { since }
19742
+ );
19386
19743
  const buckets = /* @__PURE__ */ new Map();
19387
19744
  for (let i = 0; i < days; i++) {
19388
19745
  const day = isoDay(since + i * DAY_MS3);
@@ -19454,17 +19811,19 @@ var SqliteInspectionFindingsRepository = class {
19454
19811
  insertStmt;
19455
19812
  insertFinding(input) {
19456
19813
  const row = toInspectionFindingRow(input);
19457
- this.insertStmt.run({
19458
- id: row.id,
19459
- auditEventId: row.auditEventId,
19460
- inspectionDefinitionId: row.inspectionDefinitionId,
19461
- classifiedDataId: row.classifiedDataId ?? null,
19462
- spanStart: row.spanStart,
19463
- spanEnd: row.spanEnd,
19464
- maskedMatch: row.maskedMatch,
19465
- actionTaken: row.actionTaken,
19466
- confidence: row.confidence
19467
- });
19814
+ this.insertStmt.run(
19815
+ bindParams({
19816
+ id: row.id,
19817
+ auditEventId: row.auditEventId,
19818
+ inspectionDefinitionId: row.inspectionDefinitionId,
19819
+ classifiedDataId: row.classifiedDataId,
19820
+ spanStart: row.spanStart,
19821
+ spanEnd: row.spanEnd,
19822
+ maskedMatch: row.maskedMatch,
19823
+ actionTaken: row.actionTaken,
19824
+ confidence: row.confidence
19825
+ })
19826
+ );
19468
19827
  }
19469
19828
  };
19470
19829
 
@@ -19538,14 +19897,9 @@ function isMirrorDowngrade(incoming, stored) {
19538
19897
  for (const id of stored.ruleIds) if (!incoming.ruleIds.has(id)) return true;
19539
19898
  return false;
19540
19899
  }
19541
- function ruleIdsOf(rulesJson) {
19542
- const ids = /* @__PURE__ */ new Set();
19543
- let raw;
19544
- try {
19545
- raw = JSON.parse(rulesJson);
19546
- } catch {
19547
- return ids;
19548
- }
19900
+ function ruleIdsOf(rulesJson) {
19901
+ const ids = /* @__PURE__ */ new Set();
19902
+ const raw = safeJson(rulesJson, []);
19549
19903
  if (!Array.isArray(raw)) return ids;
19550
19904
  for (const entry of raw) {
19551
19905
  if (entry && typeof entry === "object") {
@@ -19618,47 +19972,48 @@ var SqliteInstalledPacksRepository = class {
19618
19972
  }));
19619
19973
  if (this.storedSignature() === inventorySignature(rows)) return;
19620
19974
  const now = Date.now();
19621
- this.db.exec("BEGIN IMMEDIATE");
19622
- try {
19623
- const mirror = this.mirrorState();
19624
- let behind = false;
19625
- for (const row of rows) {
19626
- const params = {
19627
- id: randomUUID2(),
19628
- namespace: row.namespace,
19629
- packId: row.packId,
19630
- version: row.version,
19631
- name: row.name,
19632
- rulesJson: row.rulesJson,
19633
- now
19634
- };
19635
- const stored = mirror.get(`${row.namespace}/${row.packId}`);
19636
- if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
19637
- this.upsertAvailableStmt.run({
19638
- ...params,
19975
+ withTransaction(
19976
+ this.db,
19977
+ () => {
19978
+ const mirror = this.mirrorState();
19979
+ let behind = false;
19980
+ for (const row of rows) {
19981
+ const params = {
19639
19982
  id: randomUUID2(),
19640
- recordedBy: meta3?.recordedBy ?? null
19641
- });
19642
- } else {
19643
- behind = true;
19983
+ namespace: row.namespace,
19984
+ packId: row.packId,
19985
+ version: row.version,
19986
+ name: row.name,
19987
+ rulesJson: row.rulesJson,
19988
+ now
19989
+ };
19990
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
19991
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
19992
+ this.upsertAvailableStmt.run({
19993
+ ...params,
19994
+ id: randomUUID2(),
19995
+ recordedBy: meta3?.recordedBy ?? null
19996
+ });
19997
+ } else {
19998
+ behind = true;
19999
+ }
20000
+ this.insertMissingStmt.run(params);
19644
20001
  }
19645
- this.insertMissingStmt.run(params);
19646
- }
19647
- if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
19648
- this.db.exec("COMMIT");
19649
- } catch (err) {
19650
- this.db.exec("ROLLBACK");
19651
- throw err;
19652
- }
20002
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20003
+ },
20004
+ "IMMEDIATE"
20005
+ );
19653
20006
  } catch {
19654
20007
  }
19655
20008
  }
19656
20009
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
19657
20010
  // input to the downgrade guard. Read INSIDE the write transaction.
19658
20011
  mirrorState() {
19659
- const rows = this.db.prepare(
19660
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
19661
- ).all();
20012
+ const rows = allRows(
20013
+ this.db.prepare(
20014
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20015
+ )
20016
+ );
19662
20017
  return new Map(
19663
20018
  rows.map((r) => [
19664
20019
  `${r.namespace}/${r.packId}`,
@@ -19670,7 +20025,9 @@ var SqliteInstalledPacksRepository = class {
19670
20025
  // (keys joined with '/', matching the detection id slug encoding — packId may
19671
20026
  // itself contain '/', but namespace may not, so the join is unambiguous).
19672
20027
  pruneAvailable(keep) {
19673
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20028
+ const rows = allRows(
20029
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20030
+ );
19674
20031
  const keepSet = new Set(keep);
19675
20032
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
19676
20033
  for (const r of rows) {
@@ -19697,11 +20054,13 @@ var SqliteInstalledPacksRepository = class {
19697
20054
  if (this.db.isTransaction) {
19698
20055
  throw new Error("applyUpdate must not be called inside an open transaction");
19699
20056
  }
19700
- this.db.exec("BEGIN IMMEDIATE");
19701
- try {
19702
- this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
19703
- const res = this.db.prepare(
19704
- `UPDATE installed_packs SET
20057
+ let changed = false;
20058
+ withTransaction(
20059
+ this.db,
20060
+ () => {
20061
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20062
+ const res = this.db.prepare(
20063
+ `UPDATE installed_packs SET
19705
20064
  version = (SELECT a.version FROM available_packs a
19706
20065
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
19707
20066
  name = (SELECT a.name FROM available_packs a
@@ -19712,17 +20071,13 @@ var SqliteInstalledPacksRepository = class {
19712
20071
  WHERE namespace = :namespace AND pack_id = :packId
19713
20072
  AND EXISTS (SELECT 1 FROM available_packs a
19714
20073
  WHERE a.namespace = :namespace AND a.pack_id = :packId)`
19715
- ).run({ namespace, packId, now: Date.now() });
19716
- this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
19717
- this.db.exec("COMMIT");
19718
- return Number(res.changes) > 0;
19719
- } catch (err) {
19720
- try {
19721
- this.db.exec("ROLLBACK");
19722
- } catch {
19723
- }
19724
- throw err;
19725
- }
20074
+ ).run({ namespace, packId, now: Date.now() });
20075
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20076
+ changed = Number(res.changes) > 0;
20077
+ },
20078
+ "IMMEDIATE"
20079
+ );
20080
+ return changed;
19726
20081
  }
19727
20082
  /**
19728
20083
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -19736,9 +20091,11 @@ var SqliteInstalledPacksRepository = class {
19736
20091
  * JSON-level failure therefore counts as invalid.
19737
20092
  */
19738
20093
  installedRuleset() {
19739
- const rows = this.db.prepare(
19740
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
19741
- ).all();
20094
+ const rows = allRows(
20095
+ this.db.prepare(
20096
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20097
+ )
20098
+ );
19742
20099
  const out = {
19743
20100
  installedPacks: rows.length,
19744
20101
  enabledPacks: 0,
@@ -19747,7 +20104,7 @@ var SqliteInstalledPacksRepository = class {
19747
20104
  ruleActions: /* @__PURE__ */ new Map()
19748
20105
  };
19749
20106
  for (const row of rows) {
19750
- if (row.enabled !== 1) continue;
20107
+ if (!intToBool(row.enabled)) continue;
19751
20108
  out.enabledPacks += 1;
19752
20109
  const action = policyIdToAction(row.policyId);
19753
20110
  let raw;
@@ -19782,9 +20139,11 @@ var SqliteInstalledPacksRepository = class {
19782
20139
  * running max would mask a genuinely-newer parseable stamp.
19783
20140
  */
19784
20141
  newestRecordedBinary() {
19785
- const rows = this.db.prepare(
19786
- `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
19787
- ).all();
20142
+ const rows = allRows(
20143
+ this.db.prepare(
20144
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20145
+ )
20146
+ );
19788
20147
  let newest = null;
19789
20148
  for (const row of rows) {
19790
20149
  const at = row.recordedBy.lastIndexOf("@");
@@ -19799,13 +20158,15 @@ var SqliteInstalledPacksRepository = class {
19799
20158
  return newest;
19800
20159
  }
19801
20160
  counts() {
19802
- const row = this.db.prepare(
19803
- `SELECT count(*) AS packs,
20161
+ const row = getRow(
20162
+ this.db.prepare(
20163
+ `SELECT count(*) AS packs,
19804
20164
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
19805
20165
  coalesce(sum(enabled), 0) AS enabled
19806
20166
  FROM installed_packs`
19807
- ).get();
19808
- return Promise.resolve(row);
20167
+ )
20168
+ );
20169
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
19809
20170
  }
19810
20171
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
19811
20172
  // Back the Policies page's built-in catalog: how many
@@ -19818,26 +20179,29 @@ var SqliteInstalledPacksRepository = class {
19818
20179
  * attributed to Monitor, matching the Detections views.
19819
20180
  */
19820
20181
  countsByPolicyId() {
19821
- const rows = this.db.prepare(
19822
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20182
+ return countBy(
20183
+ this.db,
20184
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
19823
20185
  FROM installed_packs
19824
- GROUP BY pid`
19825
- ).all();
19826
- return new Map(rows.map((r) => [r.pid, r.n]));
20186
+ GROUP BY k`
20187
+ );
19827
20188
  }
19828
20189
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
19829
20190
  listByPolicyId(policyId) {
19830
- const rows = this.db.prepare(
19831
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20191
+ const rows = allRows(
20192
+ this.db.prepare(
20193
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
19832
20194
  FROM installed_packs
19833
20195
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
19834
20196
  ORDER BY name ASC`
19835
- ).all(policyId);
20197
+ ),
20198
+ [policyId]
20199
+ );
19836
20200
  return rows.map((r) => ({
19837
20201
  id: `${r.namespace}/${r.packId}`,
19838
20202
  name: r.name,
19839
20203
  ruleCount: parseRules(r.rulesJson).length,
19840
- enabled: r.enabled === 1
20204
+ enabled: intToBool(r.enabled)
19841
20205
  }));
19842
20206
  }
19843
20207
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -19866,14 +20230,14 @@ var SqliteInstalledPacksRepository = class {
19866
20230
  const res = this.db.prepare(
19867
20231
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
19868
20232
  WHERE namespace = :namespace AND pack_id = :packId`
19869
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
20233
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
19870
20234
  return Number(res.changes) > 0;
19871
20235
  }
19872
20236
  // Fingerprint of the recorded available mirror — compared against the
19873
20237
  // incoming inventory's signature to skip the write entirely when the running
19874
20238
  // binary's inventory hasn't changed since the last record.
19875
20239
  storedSignature() {
19876
- const rows = this.signatureStmt.all();
20240
+ const rows = allRows(this.signatureStmt);
19877
20241
  return inventorySignature(rows);
19878
20242
  }
19879
20243
  };
@@ -19901,42 +20265,48 @@ var SqliteInventoryRepository = class {
19901
20265
  upsert(input, now = Date.now()) {
19902
20266
  const id = inventoryId(input.objectType, input.identityKey);
19903
20267
  const row = toInventoryRow(input, id, now);
19904
- this.upsertStmt.run({
19905
- id: row.id,
19906
- objectType: row.objectType,
19907
- location: row.location ?? null,
19908
- title: row.title ?? null,
19909
- hostId: row.hostId ?? null,
19910
- attributes: row.attributes,
19911
- firstSeen: row.firstSeen,
19912
- lastSeen: row.lastSeen
19913
- });
20268
+ this.upsertStmt.run(
20269
+ bindParams({
20270
+ id: row.id,
20271
+ objectType: row.objectType,
20272
+ location: row.location,
20273
+ title: row.title,
20274
+ hostId: row.hostId,
20275
+ attributes: row.attributes,
20276
+ firstSeen: row.firstSeen,
20277
+ lastSeen: row.lastSeen
20278
+ })
20279
+ );
19914
20280
  return id;
19915
20281
  }
19916
20282
  // The full row, for round-trip assertions.
19917
20283
  findById(id) {
19918
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
19919
- return row;
20284
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
19920
20285
  }
19921
20286
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
19922
20287
  // served from the object_type index, never from audit_events.
19923
20288
  distinctTitles(objectType) {
19924
- const rows = this.db.prepare(
19925
- `SELECT DISTINCT title FROM inventory
20289
+ const rows = allRows(
20290
+ this.db.prepare(
20291
+ `SELECT DISTINCT title FROM inventory
19926
20292
  WHERE object_type = :objectType AND title IS NOT NULL
19927
20293
  ORDER BY title`
19928
- ).all({ objectType });
20294
+ ),
20295
+ { objectType }
20296
+ );
19929
20297
  return rows.map((r) => r.title);
19930
20298
  }
19931
20299
  // Distinct host os_version values — a facet served from an inventory index
19932
20300
  // over the generated column, never from the audit fact (confirm via EXPLAIN
19933
20301
  // QUERY PLAN).
19934
20302
  osVersions() {
19935
- const rows = this.db.prepare(
19936
- `SELECT DISTINCT os_version AS value FROM inventory
20303
+ const rows = allRows(
20304
+ this.db.prepare(
20305
+ `SELECT DISTINCT os_version AS value FROM inventory
19937
20306
  WHERE object_type = 'host' AND os_version IS NOT NULL
19938
20307
  ORDER BY value`
19939
- ).all();
20308
+ )
20309
+ );
19940
20310
  return rows.map((r) => r.value);
19941
20311
  }
19942
20312
  };
@@ -19956,14 +20326,6 @@ var EMPTY_PROJECT_AGG = {
19956
20326
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
19957
20327
  findingsCount: 0
19958
20328
  };
19959
- function safeJson(s, fallback) {
19960
- if (s == null) return fallback;
19961
- try {
19962
- return JSON.parse(s);
19963
- } catch {
19964
- return fallback;
19965
- }
19966
- }
19967
20329
  function resolveHarnessId(attrs, row) {
19968
20330
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
19969
20331
  return attrs.provider;
@@ -20159,30 +20521,49 @@ var SqliteInventoryAssetsRepository = class {
20159
20521
  configRowsCache;
20160
20522
  // ─── stats ─────────────────────────────────────────────────────────────────
20161
20523
  getInventoryStats() {
20162
- const byType = { project: 0, skill: 0, mcp: 0, hook: 0, config: 0 };
20163
- for (const r of this.db.prepare("SELECT asset_type AS t, count(*) AS n FROM inventory_asset GROUP BY asset_type").all()) {
20164
- if (r.t in byType) byType[r.t] = r.n;
20165
- }
20166
- byType.project = this.db.prepare(`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`).get().n;
20167
- const mcpTrust = { "known-good": 0, risky: 0, unapproved: 0 };
20168
- for (const r of this.db.prepare(
20169
- `SELECT coalesce(o.trust, a.trust) AS trust, count(*) AS n
20524
+ const typeCounts = countBy(
20525
+ this.db,
20526
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
20527
+ );
20528
+ const byType = {
20529
+ project: 0,
20530
+ skill: typeCounts.get("skill") ?? 0,
20531
+ mcp: typeCounts.get("mcp") ?? 0,
20532
+ hook: typeCounts.get("hook") ?? 0,
20533
+ config: typeCounts.get("config") ?? 0
20534
+ };
20535
+ byType.project = countScalar(
20536
+ this.db,
20537
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
20538
+ );
20539
+ const mcpTrustCounts = countBy(
20540
+ this.db,
20541
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20170
20542
  FROM inventory_asset a
20171
20543
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20172
20544
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20173
20545
  GROUP BY coalesce(o.trust, a.trust)`
20174
- ).all()) {
20175
- if (r.trust in mcpTrust) mcpTrust[r.trust] = r.n;
20176
- }
20177
- const harnesses = this.db.prepare(
20546
+ );
20547
+ const mcpTrust = {
20548
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
20549
+ risky: mcpTrustCounts.get("risky") ?? 0,
20550
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
20551
+ };
20552
+ const harnesses = countScalar(
20553
+ this.db,
20178
20554
  `SELECT count(*) AS n FROM inventory
20179
20555
  WHERE object_type = 'harness'
20180
- AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20181
- ).get({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }).n;
20182
- const flaggedAssets = this.db.prepare("SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'").get().n;
20183
- const flaggedProjects = this.db.prepare(
20556
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
20557
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20558
+ );
20559
+ const flaggedAssets = countScalar(
20560
+ this.db,
20561
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
20562
+ );
20563
+ const flaggedProjects = countScalar(
20564
+ this.db,
20184
20565
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20185
- ).get().n;
20566
+ );
20186
20567
  const configRows = this.configAssetRows();
20187
20568
  for (const r of configRows) {
20188
20569
  byType[r.assetType] += 1;
@@ -20439,12 +20820,15 @@ var SqliteInventoryAssetsRepository = class {
20439
20820
  }
20440
20821
  // ─── raw fetchers ────────────────────────────────────────────────────────────
20441
20822
  fetchHarnessRows() {
20442
- return this.db.prepare(
20443
- `SELECT id, title, attributes, harness_version AS harnessVersion
20823
+ return allRows(
20824
+ this.db.prepare(
20825
+ `SELECT id, title, attributes, harness_version AS harnessVersion
20444
20826
  FROM inventory
20445
20827
  WHERE object_type = 'harness'
20446
20828
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20447
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
20829
+ ),
20830
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20831
+ );
20448
20832
  }
20449
20833
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
20450
20834
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -20454,12 +20838,13 @@ var SqliteInventoryAssetsRepository = class {
20454
20838
  const params = [...harnessInvIds];
20455
20839
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
20456
20840
  if (q) {
20457
- const pat = `%${escapeLikePattern(q)}%`;
20458
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
20841
+ const pat = containsPattern(q);
20842
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
20459
20843
  params.push(pat, pat);
20460
20844
  }
20461
- const rows = this.db.prepare(
20462
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20845
+ const rows = allRows(
20846
+ this.db.prepare(
20847
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20463
20848
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20464
20849
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20465
20850
  FROM harness_asset ha
@@ -20467,7 +20852,9 @@ var SqliteInventoryAssetsRepository = class {
20467
20852
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20468
20853
  WHERE ${where}
20469
20854
  ORDER BY a.name ASC`
20470
- ).all(...params);
20855
+ ),
20856
+ params
20857
+ );
20471
20858
  for (const raw of rows) {
20472
20859
  const harnessInvId = raw.harnessInvId;
20473
20860
  const [asset] = this.mapAssetRows([raw]);
@@ -20486,21 +20873,24 @@ var SqliteInventoryAssetsRepository = class {
20486
20873
  params.push(...types);
20487
20874
  }
20488
20875
  if (q) {
20489
- const pat = `%${escapeLikePattern(q)}%`;
20490
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
20876
+ const pat = containsPattern(q);
20877
+ conditions.push(likeAny(["a.name", "a.sub"]));
20491
20878
  params.push(pat, pat);
20492
20879
  }
20493
20880
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
20494
20881
  const sampleRows = this.mapAssetRows(
20495
- this.db.prepare(
20496
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20882
+ allRows(
20883
+ this.db.prepare(
20884
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20497
20885
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20498
20886
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20499
20887
  FROM inventory_asset a
20500
20888
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20501
20889
  ${where}
20502
20890
  ORDER BY a.name ASC`
20503
- ).all(...params)
20891
+ ),
20892
+ params
20893
+ )
20504
20894
  );
20505
20895
  const configRows = this.configAssetRows(q).filter(
20506
20896
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -20509,14 +20899,17 @@ var SqliteInventoryAssetsRepository = class {
20509
20899
  }
20510
20900
  fetchAssetById(assetId) {
20511
20901
  const rows = this.mapAssetRows(
20512
- this.db.prepare(
20513
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20902
+ allRows(
20903
+ this.db.prepare(
20904
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20514
20905
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20515
20906
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20516
20907
  FROM inventory_asset a
20517
20908
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20518
20909
  WHERE a.id = ?`
20519
- ).all(assetId)
20910
+ ),
20911
+ [assetId]
20912
+ )
20520
20913
  );
20521
20914
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
20522
20915
  }
@@ -20572,37 +20965,39 @@ var SqliteInventoryAssetsRepository = class {
20572
20965
  return rows;
20573
20966
  }
20574
20967
  latestConfigScanId() {
20575
- const row = this.db.prepare(
20576
- `SELECT id FROM audit_events WHERE event_type = 'config_scan'
20577
- ORDER BY started_at DESC, id DESC LIMIT 1`
20578
- ).get();
20579
- return row?.id ?? null;
20968
+ return latestConfigScan(this.db)?.id ?? null;
20580
20969
  }
20581
20970
  fetchProjects(q) {
20582
20971
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
20583
20972
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
20584
20973
  const params = [];
20585
20974
  if (q) {
20586
- const pat = `%${escapeLikePattern(q)}%`;
20587
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
20975
+ const pat = containsPattern(q);
20976
+ sql += ` AND ${likeAny(["name", "url"])}`;
20588
20977
  params.push(pat, pat);
20589
20978
  }
20590
20979
  sql += " ORDER BY name ASC";
20591
- return this.db.prepare(sql).all(...params);
20980
+ return allRows(this.db.prepare(sql), params);
20592
20981
  }
20593
20982
  fetchProjectById(projectId) {
20594
- return this.db.prepare(
20595
- "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
20596
- ).get(projectId) ?? null;
20983
+ return getRow(
20984
+ this.db.prepare(
20985
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
20986
+ ),
20987
+ [projectId]
20988
+ ) ?? null;
20597
20989
  }
20598
20990
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
20599
20991
  fetchProjectsByIds(projectIds) {
20600
20992
  const map2 = /* @__PURE__ */ new Map();
20601
20993
  if (projectIds.length === 0) return map2;
20602
- const rows = this.db.prepare(
20603
- `SELECT id, url, name, attributes, last_seen AS lastSeen
20994
+ const rows = allRows(
20995
+ this.db.prepare(
20996
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
20604
20997
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
20605
- ).all(...projectIds);
20998
+ ),
20999
+ projectIds
21000
+ );
20606
21001
  for (const r of rows) map2.set(r.id, r);
20607
21002
  return map2;
20608
21003
  }
@@ -20613,8 +21008,9 @@ var SqliteInventoryAssetsRepository = class {
20613
21008
  projectAggregates(projectIds) {
20614
21009
  const map2 = /* @__PURE__ */ new Map();
20615
21010
  if (projectIds.length === 0) return map2;
20616
- const rows = this.db.prepare(
20617
- `SELECT f.project_id AS projectId,
21011
+ const rows = allRows(
21012
+ this.db.prepare(
21013
+ `SELECT f.project_id AS projectId,
20618
21014
  coalesce(o.access, f.default_access) AS eff,
20619
21015
  count(*) AS n,
20620
21016
  coalesce(sum(f.findings_count), 0) AS findings
@@ -20622,7 +21018,9 @@ var SqliteInventoryAssetsRepository = class {
20622
21018
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
20623
21019
  WHERE f.project_id IN (${placeholders(projectIds.length)})
20624
21020
  GROUP BY f.project_id, eff`
20625
- ).all(...projectIds);
21021
+ ),
21022
+ projectIds
21023
+ );
20626
21024
  for (const r of rows) {
20627
21025
  let agg = map2.get(r.projectId);
20628
21026
  if (!agg) {
@@ -20660,37 +21058,52 @@ var SqliteInventoryAssetsRepository = class {
20660
21058
  fetchProjectFilesUnder(projectId, prefix) {
20661
21059
  if (prefix === "") {
20662
21060
  return this.mapFileRows(
20663
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21061
+ allRows(
21062
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21063
+ [projectId]
21064
+ )
20664
21065
  );
20665
21066
  }
20666
21067
  return this.mapFileRows(
20667
- this.db.prepare(
20668
- this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
20669
- ).all(projectId, `${escapeLikePattern(prefix)}/%`)
21068
+ allRows(
21069
+ this.db.prepare(
21070
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21071
+ ),
21072
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21073
+ )
20670
21074
  );
20671
21075
  }
20672
21076
  fetchProjectFilesSearch(projectId, q) {
20673
- const pat = `%${escapeLikePattern(q)}%`;
21077
+ const pat = containsPattern(q);
20674
21078
  return this.mapFileRows(
20675
- this.db.prepare(
20676
- this.fileSelect(
20677
- "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
20678
- )
20679
- ).all(projectId, pat, pat)
21079
+ allRows(
21080
+ this.db.prepare(
21081
+ this.fileSelect(
21082
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21083
+ )
21084
+ ),
21085
+ [projectId, pat, pat]
21086
+ )
20680
21087
  );
20681
21088
  }
20682
21089
  fetchProjectFilesBlocked(projectId) {
20683
21090
  return this.mapFileRows(
20684
- this.db.prepare(
20685
- this.fileSelect(
20686
- "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
20687
- )
20688
- ).all(projectId)
21091
+ allRows(
21092
+ this.db.prepare(
21093
+ this.fileSelect(
21094
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21095
+ )
21096
+ ),
21097
+ [projectId]
21098
+ )
20689
21099
  );
20690
21100
  }
20691
21101
  fetchProjectFile(projectId, path) {
20692
21102
  const rows = this.mapFileRows(
20693
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21103
+ allRows(
21104
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21105
+ [projectId, path]
21106
+ )
20694
21107
  );
20695
21108
  return rows[0] ?? null;
20696
21109
  }
@@ -20704,39 +21117,32 @@ var SqlitePoliciesRepository = class {
20704
21117
  }
20705
21118
  db;
20706
21119
  readPolicies() {
20707
- const rows = this.db.prepare("SELECT * FROM policies").all();
20708
- const policies = [];
20709
- for (const row of rows) {
20710
- try {
20711
- const target = JSON.parse(row.target);
20712
- const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
20713
- policies.push(
20714
- Policy.parse({
20715
- id: row.id,
20716
- scope: row.scope,
20717
- target,
20718
- action: row.action,
20719
- enabled: row.enabled === 1,
20720
- customKeywords
20721
- })
20722
- );
20723
- } catch {
20724
- }
20725
- }
21120
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21121
+ const policies = mapRowsTolerant(rows, (row) => {
21122
+ const target = JSON.parse(row.target);
21123
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21124
+ return Policy.parse({
21125
+ id: row.id,
21126
+ scope: row.scope,
21127
+ target,
21128
+ action: row.action,
21129
+ enabled: intToBool(row.enabled),
21130
+ customKeywords
21131
+ });
21132
+ });
20726
21133
  return Promise.resolve(policies);
20727
21134
  }
20728
21135
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
20729
21136
  // detection-type config exists from first run. Only when the table is empty,
20730
21137
  // so a user's edits are never clobbered.
20731
21138
  seedDefaults() {
20732
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21139
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
20733
21140
  if (count > 0) return;
20734
21141
  const stmt = this.db.prepare(
20735
21142
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
20736
21143
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
20737
21144
  );
20738
- this.db.exec("BEGIN");
20739
- try {
21145
+ failOpenTransaction(this.db, () => {
20740
21146
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
20741
21147
  stmt.run({
20742
21148
  id: randomUUID4(),
@@ -20745,10 +21151,41 @@ var SqlitePoliciesRepository = class {
20745
21151
  now: Date.now()
20746
21152
  });
20747
21153
  }
20748
- this.db.exec("COMMIT");
20749
- } catch {
20750
- this.db.exec("ROLLBACK");
20751
- }
21154
+ });
21155
+ }
21156
+ // Insert-or-update the single global per-category policy row, keyed on the
21157
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21158
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21159
+ // values), so the runtime's resolveAction reads rows written by either path
21160
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21161
+ // `id` and `created_at` are left exactly as they were.
21162
+ upsertCategoryAction(category, action) {
21163
+ const now = Date.now();
21164
+ this.db.prepare(
21165
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21166
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21167
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21168
+ ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
21169
+ }
21170
+ // Caps every global per-category policy currently set to block/redact down
21171
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21172
+ // Returns the number of rows changed.
21173
+ capCategoryActions() {
21174
+ const info = this.db.prepare(
21175
+ `UPDATE policies SET action='warn', updated_at=:now
21176
+ WHERE scope='global' AND action IN ('block','redact')
21177
+ AND json_extract(target,'$.category') IS NOT NULL`
21178
+ ).run({ now: Date.now() });
21179
+ return Number(info.changes);
21180
+ }
21181
+ // Read the current action for a single global per-category policy row, mirroring
21182
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21183
+ // row exists yet, so callers can distinguish an unset category from a set one.
21184
+ getCategoryAction(category) {
21185
+ const row = this.db.prepare(
21186
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21187
+ ).get({ category });
21188
+ return row?.action;
20752
21189
  }
20753
21190
  };
20754
21191
 
@@ -20824,7 +21261,7 @@ var SqliteProjectFilesRepository = class {
20824
21261
  maxStampStmt;
20825
21262
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
20826
21263
  replaceForProject(projectId, scan2, now) {
20827
- const { maxStamp } = this.maxStampStmt.get({ projectId });
21264
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
20828
21265
  const stamp = Math.max(now, maxStamp + 1);
20829
21266
  for (const file2 of scan2.files) {
20830
21267
  this.upsertStmt.run({
@@ -20907,7 +21344,7 @@ var SqliteResolutionsRepository = class {
20907
21344
  }
20908
21345
  /** The newest disposition recorded for a finding key, or undefined if none. */
20909
21346
  latestByKey(key) {
20910
- const row = this.latestStmt.get({ findingKey: key });
21347
+ const row = getRow(this.latestStmt, { findingKey: key });
20911
21348
  if (!row) return void 0;
20912
21349
  return {
20913
21350
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -20924,7 +21361,7 @@ var SqliteResolutionsRepository = class {
20924
21361
  * the CLI) surfaces for that file.
20925
21362
  */
20926
21363
  openAtRestKeysForPath(path) {
20927
- const rows = this.openAtRestStmt.all({ path });
21364
+ const rows = allRows(this.openAtRestStmt, { path });
20928
21365
  return rows.map((r) => r.finding_key);
20929
21366
  }
20930
21367
  /**
@@ -20935,7 +21372,7 @@ var SqliteResolutionsRepository = class {
20935
21372
  * resolution row (see scan.ts).
20936
21373
  */
20937
21374
  resolvedAtRestKeysForPath(path) {
20938
- const rows = this.resolvedAtRestStmt.all({ path });
21375
+ const rows = allRows(this.resolvedAtRestStmt, { path });
20939
21376
  return rows.map((r) => r.finding_key);
20940
21377
  }
20941
21378
  };
@@ -20964,31 +21401,25 @@ var SqliteScanLedgerRepository = class {
20964
21401
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
20965
21402
  // older ruleset are simply absent, which reads as "never scanned".
20966
21403
  entriesForRuleset(rulesetHash) {
20967
- const rows = this.readStmt.all({ rulesetHash });
21404
+ const rows = allRows(this.readStmt, {
21405
+ rulesetHash
21406
+ });
20968
21407
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
20969
21408
  }
20970
21409
  upsertEntries(entries) {
20971
21410
  if (entries.length === 0) return;
20972
21411
  const scannedAt = Date.now();
20973
- try {
20974
- this.db.exec("BEGIN");
20975
- try {
20976
- for (const entry of entries) {
20977
- this.upsertStmt.run({
20978
- path: entry.path,
20979
- mtime: entry.mtime,
20980
- contentHash: entry.contentHash,
20981
- rulesetHash: entry.rulesetHash,
20982
- scannedAt
20983
- });
20984
- }
20985
- this.db.exec("COMMIT");
20986
- } catch (err) {
20987
- this.db.exec("ROLLBACK");
20988
- throw err;
21412
+ failOpenTransaction(this.db, () => {
21413
+ for (const entry of entries) {
21414
+ this.upsertStmt.run({
21415
+ path: entry.path,
21416
+ mtime: entry.mtime,
21417
+ contentHash: entry.contentHash,
21418
+ rulesetHash: entry.rulesetHash,
21419
+ scannedAt
21420
+ });
20989
21421
  }
20990
- } catch {
20991
- }
21422
+ });
20992
21423
  }
20993
21424
  };
20994
21425
 
@@ -21065,8 +21496,9 @@ var SqliteSecurityRepository = class {
21065
21496
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21066
21497
  // double-counting a key that accumulated several append-only rows.
21067
21498
  severitySummary() {
21068
- const rows = this.db.prepare(
21069
- `SELECT f.severity AS severity,
21499
+ const rows = allRows(
21500
+ this.db.prepare(
21501
+ `SELECT f.severity AS severity,
21070
21502
  COUNT(*) AS count,
21071
21503
  SUM(CASE
21072
21504
  WHEN e.kind != 'code_change' THEN 1
@@ -21085,7 +21517,8 @@ var SqliteSecurityRepository = class {
21085
21517
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21086
21518
  ON latest.finding_key = f.finding_key
21087
21519
  GROUP BY f.severity`
21088
- ).all();
21520
+ )
21521
+ );
21089
21522
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21090
21523
  const bySeverity = SEVERITIES.map((severity) => ({
21091
21524
  severity,
@@ -21169,14 +21602,15 @@ var SqliteSecurityRepository = class {
21169
21602
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21170
21603
  const now = this.now();
21171
21604
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
21172
- const rows = this.db.prepare(
21173
- // first_detected_at is the PRESERVED first-detection time (set once on a
21174
- // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21175
- // measures from first sighting not the latest re-scan's event, whose
21176
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21177
- // the parent event's occurred_at defends against any legacy/edge row the
21178
- // backfill left null.
21179
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21605
+ const rows = allRows(
21606
+ this.db.prepare(
21607
+ // first_detected_at is the PRESERVED first-detection time (set once on a
21608
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21609
+ // measures from first sighting not the latest re-scan's event, whose
21610
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21611
+ // the parent event's occurred_at defends against any legacy/edge row the
21612
+ // backfill left null.
21613
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21180
21614
  (
21181
21615
  SELECT fr.status FROM finding_resolution fr
21182
21616
  WHERE fr.finding_key = f.finding_key
@@ -21202,14 +21636,16 @@ var SqliteSecurityRepository = class {
21202
21636
  WHERE fr.finding_key = f.finding_key
21203
21637
  AND fr.resolved_at >= :windowStart
21204
21638
  )`
21205
- // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21206
- // any resolution activity at/after the window start — a row this method
21207
- // ultimately counts has its LATEST resolution inside the window, which
21208
- // implies such a row exists, so nothing wanted is dropped. The exact
21209
- // latest-wins + status/method + window gate stays in JS below,
21210
- // dialect-agnostic. Without this, a
21211
- // 7d request evaluated the store's entire trackable-findings history.
21212
- ).all({ windowStart });
21639
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21640
+ // any resolution activity at/after the window start — a row this method
21641
+ // ultimately counts has its LATEST resolution inside the window, which
21642
+ // implies such a row exists, so nothing wanted is dropped. The exact
21643
+ // latest-wins + status/method + window gate stays in JS below,
21644
+ // dialect-agnostic. Without this, a
21645
+ // 7d request evaluated the store's entire trackable-findings history.
21646
+ ),
21647
+ { windowStart }
21648
+ );
21213
21649
  const sums = /* @__PURE__ */ new Map();
21214
21650
  const counts = /* @__PURE__ */ new Map();
21215
21651
  for (const r of rows) {
@@ -21239,8 +21675,9 @@ var SqliteSecurityRepository = class {
21239
21675
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
21240
21676
  const now = this.now();
21241
21677
  const from = now - RANGE_DAYS[range] * DAY_MS4;
21242
- const rows = this.db.prepare(
21243
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21678
+ const rows = allRows(
21679
+ this.db.prepare(
21680
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21244
21681
  FROM findings f JOIN events e ON e.id = f.event_id
21245
21682
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21246
21683
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -21248,7 +21685,9 @@ var SqliteSecurityRepository = class {
21248
21685
  GROUP BY repo
21249
21686
  ORDER BY c DESC, repo
21250
21687
  LIMIT :limit`
21251
- ).all({ from, to: now, limit });
21688
+ ),
21689
+ { from, to: now, limit }
21690
+ );
21252
21691
  const items = rows.map((r) => ({
21253
21692
  id: `repo_${r.repo}`,
21254
21693
  name: r.repo,
@@ -21270,8 +21709,9 @@ var SqliteSecurityRepository = class {
21270
21709
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21271
21710
  // capped at `limit`.
21272
21711
  recentlyResolved(limit = 20) {
21273
- const rows = this.db.prepare(
21274
- `SELECT f.finding_key AS finding_key,
21712
+ const rows = allRows(
21713
+ this.db.prepare(
21714
+ `SELECT f.finding_key AS finding_key,
21275
21715
  f.rule_id AS rule_id,
21276
21716
  f.severity AS severity,
21277
21717
  json_extract(e.metadata, '$.filePath') AS path,
@@ -21305,7 +21745,9 @@ var SqliteSecurityRepository = class {
21305
21745
  ) IS NOT NULL
21306
21746
  ORDER BY latest_resolved_at DESC
21307
21747
  LIMIT :limit`
21308
- ).all({ limit });
21748
+ ),
21749
+ { limit }
21750
+ );
21309
21751
  const items = rows.map((r) => ({
21310
21752
  findingKey: r.finding_key,
21311
21753
  ruleId: r.rule_id,
@@ -21322,12 +21764,15 @@ var SqliteSecurityRepository = class {
21322
21764
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
21323
21765
  // numeric and the JS aggregations bucket/split on ms directly.
21324
21766
  findingsInRange(fromMs, toMs) {
21325
- const rows = this.db.prepare(
21326
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21767
+ const rows = allRows(
21768
+ this.db.prepare(
21769
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21327
21770
  FROM findings f JOIN events e ON e.id = f.event_id
21328
21771
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21329
21772
  ORDER BY e.occurred_at`
21330
- ).all({ from: fromMs, to: toMs });
21773
+ ),
21774
+ { from: fromMs, to: toMs }
21775
+ );
21331
21776
  return rows.map((r) => ({
21332
21777
  occurredAt: r.occurred_at,
21333
21778
  severity: r.severity,
@@ -21341,12 +21786,7 @@ import { randomUUID as randomUUID7 } from "crypto";
21341
21786
  var KIND_ORDER = ["provider", "internal", "ip"];
21342
21787
  var CALL_SITE_EMBED_CAP = 200;
21343
21788
  function parseNetwork(networkJson) {
21344
- if (!networkJson) return null;
21345
- try {
21346
- return JSON.parse(networkJson);
21347
- } catch {
21348
- return null;
21349
- }
21789
+ return safeJson(networkJson, null);
21350
21790
  }
21351
21791
  function toEndpointSummary(row) {
21352
21792
  return {
@@ -21433,27 +21873,39 @@ var SqliteSharesRepository = class {
21433
21873
  }
21434
21874
  db;
21435
21875
  stats() {
21436
- const scalar = (sql) => this.db.prepare(sql).get()?.n ?? 0;
21437
- const destinations = scalar("SELECT count(*) AS n FROM share_destination");
21438
- const endpoints = scalar("SELECT count(*) AS n FROM share_endpoint");
21439
- const callSites = scalar("SELECT count(*) AS n FROM share_call_site");
21440
- const insecure = scalar(
21876
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
21877
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
21878
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
21879
+ const insecure = countScalar(
21880
+ this.db,
21441
21881
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
21442
21882
  );
21443
- const needsReview = scalar(
21883
+ const needsReview = countScalar(
21884
+ this.db,
21444
21885
  `SELECT count(DISTINCT d.id) AS n
21445
21886
  FROM share_destination d
21446
21887
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
21447
21888
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
21448
21889
  );
21449
- const byKind = { provider: 0, internal: 0, ip: 0 };
21450
- for (const row of this.db.prepare("SELECT kind, count(*) AS n FROM share_destination GROUP BY kind").all()) {
21451
- byKind[row.kind] = row.n;
21452
- }
21453
- const byTrust = { recognized: 0, internal: 0, unverified: 0, ip: 0 };
21454
- for (const row of this.db.prepare("SELECT trust, count(*) AS n FROM share_destination GROUP BY trust").all()) {
21455
- byTrust[row.trust] = row.n;
21456
- }
21890
+ const kindCounts = countBy(
21891
+ this.db,
21892
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
21893
+ );
21894
+ const byKind = {
21895
+ provider: kindCounts.get("provider") ?? 0,
21896
+ internal: kindCounts.get("internal") ?? 0,
21897
+ ip: kindCounts.get("ip") ?? 0
21898
+ };
21899
+ const trustCounts = countBy(
21900
+ this.db,
21901
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
21902
+ );
21903
+ const byTrust = {
21904
+ recognized: trustCounts.get("recognized") ?? 0,
21905
+ internal: trustCounts.get("internal") ?? 0,
21906
+ unverified: trustCounts.get("unverified") ?? 0,
21907
+ ip: trustCounts.get("ip") ?? 0
21908
+ };
21457
21909
  return Promise.resolve({
21458
21910
  destinations,
21459
21911
  endpoints,
@@ -21569,7 +22021,7 @@ var SqliteSharesRepository = class {
21569
22021
  }
21570
22022
  let sql;
21571
22023
  if (q) {
21572
- const pattern = `%${escapeLikePattern(q)}%`;
22024
+ const pattern = containsPattern(q);
21573
22025
  conditions.push(
21574
22026
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
21575
22027
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -21589,24 +22041,31 @@ var SqliteSharesRepository = class {
21589
22041
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
21590
22042
  ORDER BY d.created_at ASC, d.id ASC`;
21591
22043
  }
21592
- const rows = this.db.prepare(sql).all(...params);
22044
+ const rows = allRows(
22045
+ this.db.prepare(sql),
22046
+ params
22047
+ );
21593
22048
  return rows.map((r) => this.mapDestRow(r));
21594
22049
  }
21595
22050
  fetchDestinationById(destinationId) {
21596
- const row = this.db.prepare(
21597
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22051
+ const row = getRow(
22052
+ this.db.prepare(
22053
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
21598
22054
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
21599
22055
  o.decision AS overrideDecision
21600
22056
  FROM share_destination d
21601
22057
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
21602
22058
  WHERE d.id = ?`
21603
- ).get(destinationId);
22059
+ ),
22060
+ [destinationId]
22061
+ );
21604
22062
  return row ? this.mapDestRow(row) : null;
21605
22063
  }
21606
22064
  fetchEndpoints(destinationIds) {
21607
22065
  if (destinationIds.length === 0) return [];
21608
- const rows = this.db.prepare(
21609
- `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
22066
+ const rows = allRows(
22067
+ this.db.prepare(
22068
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
21610
22069
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
21611
22070
  count(c.id) AS callSiteCount
21612
22071
  FROM share_endpoint e
@@ -21614,7 +22073,9 @@ var SqliteSharesRepository = class {
21614
22073
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
21615
22074
  GROUP BY e.id
21616
22075
  ORDER BY e.created_at ASC, e.id ASC`
21617
- ).all(...destinationIds);
22076
+ ),
22077
+ destinationIds
22078
+ );
21618
22079
  return rows.map((r) => ({
21619
22080
  id: r.id,
21620
22081
  destinationId: r.destinationId,
@@ -21639,13 +22100,16 @@ var SqliteSharesRepository = class {
21639
22100
  }
21640
22101
  fetchCallSites(endpointIds) {
21641
22102
  if (endpointIds.length === 0) return [];
21642
- const rows = this.db.prepare(
21643
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22103
+ const rows = allRows(
22104
+ this.db.prepare(
22105
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
21644
22106
  project_id AS projectId
21645
22107
  FROM share_call_site
21646
22108
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
21647
22109
  ORDER BY created_at ASC, id ASC`
21648
- ).all(...endpointIds);
22110
+ ),
22111
+ endpointIds
22112
+ );
21649
22113
  return rows.map((r) => ({
21650
22114
  id: r.id,
21651
22115
  endpointId: r.endpointId,
@@ -21681,27 +22145,36 @@ var SqliteSourceProjectRepository = class {
21681
22145
  upsert(input, now = Date.now()) {
21682
22146
  const id = sourceProjectId(input.url);
21683
22147
  const row = toSourceProjectRow(input, id, now);
21684
- this.upsertStmt.run({
21685
- id: row.id,
21686
- url: row.url ?? null,
21687
- name: row.name ?? null,
21688
- attributes: row.attributes,
21689
- firstSeen: row.firstSeen,
21690
- lastSeen: row.lastSeen
21691
- });
22148
+ this.upsertStmt.run(
22149
+ bindParams({
22150
+ id: row.id,
22151
+ url: row.url,
22152
+ name: row.name,
22153
+ attributes: row.attributes,
22154
+ firstSeen: row.firstSeen,
22155
+ lastSeen: row.lastSeen
22156
+ })
22157
+ );
21692
22158
  return id;
21693
22159
  }
21694
22160
  findById(id) {
21695
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22161
+ return getRow(
22162
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22163
+ {
22164
+ id
22165
+ }
22166
+ );
21696
22167
  }
21697
22168
  // Distinct project names — a filter facet, served from the source_project
21698
22169
  // table, never from the audit fact table.
21699
22170
  distinctNames() {
21700
- const rows = this.db.prepare(
21701
- `SELECT DISTINCT name FROM source_project
22171
+ const rows = allRows(
22172
+ this.db.prepare(
22173
+ `SELECT DISTINCT name FROM source_project
21702
22174
  WHERE name IS NOT NULL
21703
22175
  ORDER BY name`
21704
- ).all();
22176
+ )
22177
+ );
21705
22178
  return rows.map((r) => r.name);
21706
22179
  }
21707
22180
  };
@@ -21720,8 +22193,7 @@ function hasLegacySampleRows(db) {
21720
22193
  function purgeSampleData(db) {
21721
22194
  try {
21722
22195
  if (!hasLegacySampleRows(db)) return;
21723
- db.exec("BEGIN");
21724
- try {
22196
+ withTransaction(db, () => {
21725
22197
  db.exec(
21726
22198
  `DELETE FROM share_call_site WHERE endpoint_id IN (
21727
22199
  SELECT e.id FROM share_endpoint e
@@ -21766,11 +22238,7 @@ function purgeSampleData(db) {
21766
22238
  value TEXT NOT NULL
21767
22239
  )`);
21768
22240
  db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
21769
- db.exec("COMMIT");
21770
- } catch (err) {
21771
- db.exec("ROLLBACK");
21772
- throw err;
21773
- }
22241
+ });
21774
22242
  } catch {
21775
22243
  }
21776
22244
  }
@@ -21789,7 +22257,7 @@ function openWithPragmas(file2) {
21789
22257
  function backupLegacyStore(file2) {
21790
22258
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
21791
22259
  renameSync(file2, backup);
21792
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
22260
+ for (const sidecar of walSidecars(file2)) {
21793
22261
  if (existsSync(sidecar)) rmSync(sidecar);
21794
22262
  }
21795
22263
  return backup;
@@ -21802,9 +22270,8 @@ function openLocalDatabase(dir) {
21802
22270
  db.close();
21803
22271
  const backup = backupLegacyStore(file2);
21804
22272
  db = openWithPragmas(file2);
21805
- process.stderr.write(
21806
- `[aka] Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.
21807
- `
22273
+ akaWarn(
22274
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
21808
22275
  );
21809
22276
  }
21810
22277
  applyMigrations(db);
@@ -21832,96 +22299,77 @@ function openLocalDatabase(dir) {
21832
22299
  const configInventory = new SqliteConfigInventoryRepository(db);
21833
22300
  policies.seedDefaults();
21834
22301
  function recordCapture(event, detected) {
21835
- try {
21836
- db.exec("BEGIN");
21837
- try {
21838
- events.insertEvent(event);
21839
- const sessionId = event.metadata?.sessionId;
21840
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
21841
- db.exec("COMMIT");
21842
- } catch (err) {
21843
- db.exec("ROLLBACK");
21844
- throw err;
21845
- }
21846
- } catch {
21847
- }
22302
+ failOpenTransaction(db, () => {
22303
+ events.insertEvent(event);
22304
+ const sessionId = event.metadata?.sessionId;
22305
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
22306
+ });
21848
22307
  }
21849
22308
  function ensureInventory(ctx) {
21850
22309
  const resolved = {};
21851
- try {
21852
- db.exec("BEGIN");
21853
- try {
21854
- const now = Date.now();
21855
- if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
21856
- if (ctx.harness) {
21857
- resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
21858
- }
21859
- resolved.accountId = inventory.upsert(
21860
- linkHost(
21861
- {
21862
- objectType: "user",
21863
- identityKey: "local",
21864
- attributes: { source: "local" }
21865
- },
21866
- resolved.hostId
21867
- ),
21868
- now
21869
- );
21870
- if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
21871
- db.exec("COMMIT");
21872
- } catch (err) {
21873
- db.exec("ROLLBACK");
21874
- throw err;
21875
- }
21876
- } catch {
21877
- return {};
21878
- }
21879
- return resolved;
22310
+ const committed = failOpenTransaction(db, () => {
22311
+ const now = Date.now();
22312
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22313
+ if (ctx.harness) {
22314
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
22315
+ }
22316
+ resolved.accountId = inventory.upsert(
22317
+ linkHost(
22318
+ {
22319
+ objectType: "user",
22320
+ identityKey: "local",
22321
+ attributes: { source: "local" }
22322
+ },
22323
+ resolved.hostId
22324
+ ),
22325
+ now
22326
+ );
22327
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22328
+ });
22329
+ return committed ? resolved : {};
21880
22330
  }
21881
22331
  function recordConfigScan(record2) {
21882
- try {
21883
- db.exec("BEGIN");
21884
- try {
21885
- const now = isoToEpochMillis(record2.scanEvent.startedAt);
21886
- for (const item of record2.items) inventory.upsert(item, now);
21887
- auditEvents.insertAuditEvent(record2.scanEvent);
21888
- const definitionIds = /* @__PURE__ */ new Map();
21889
- for (const def of record2.definitions ?? []) {
21890
- definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
21891
- }
21892
- for (const finding of record2.findings ?? []) {
21893
- const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
21894
- if (!definitionId) continue;
21895
- inspectionFindings.insertFinding({
21896
- id: randomUUID8(),
21897
- auditEventId: record2.scanEvent.id,
21898
- inspectionDefinitionId: definitionId,
21899
- span: finding.span,
21900
- maskedMatch: finding.maskedMatch,
21901
- actionTaken: finding.actionTaken,
21902
- confidence: finding.confidence
21903
- });
21904
- }
21905
- db.exec("COMMIT");
21906
- } catch (err) {
21907
- db.exec("ROLLBACK");
21908
- throw err;
22332
+ failOpenTransaction(db, () => {
22333
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
22334
+ for (const item of record2.items) inventory.upsert(item, now);
22335
+ auditEvents.insertAuditEvent(record2.scanEvent);
22336
+ const definitionIds = /* @__PURE__ */ new Map();
22337
+ for (const def of record2.definitions ?? []) {
22338
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22339
+ }
22340
+ for (const finding of record2.findings ?? []) {
22341
+ const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
22342
+ if (!definitionId) continue;
22343
+ inspectionFindings.insertFinding({
22344
+ id: randomUUID8(),
22345
+ auditEventId: record2.scanEvent.id,
22346
+ inspectionDefinitionId: definitionId,
22347
+ span: finding.span,
22348
+ maskedMatch: finding.maskedMatch,
22349
+ actionTaken: finding.actionTaken,
22350
+ confidence: finding.confidence
22351
+ });
21909
22352
  }
21910
- } catch {
21911
- }
22353
+ });
21912
22354
  }
21913
22355
  function recordProjectFiles(projectId, scan2) {
21914
22356
  if (scan2.files.length === 0) return;
22357
+ failOpenTransaction(db, () => {
22358
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
22359
+ });
22360
+ }
22361
+ async function transaction(fn) {
22362
+ db.exec("BEGIN");
21915
22363
  try {
21916
- db.exec("BEGIN");
22364
+ const result = await fn();
22365
+ db.exec("COMMIT");
22366
+ return result;
22367
+ } catch (err) {
21917
22368
  try {
21918
- projectFiles.replaceForProject(projectId, scan2, Date.now());
21919
- db.exec("COMMIT");
21920
- } catch (err) {
21921
22369
  db.exec("ROLLBACK");
21922
- throw err;
22370
+ } catch {
21923
22371
  }
21924
- } catch {
22372
+ throw err;
21925
22373
  }
21926
22374
  }
21927
22375
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -21940,8 +22388,7 @@ function openLocalDatabase(dir) {
21940
22388
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
21941
22389
  });
21942
22390
  if (stale.length === 0) return;
21943
- db.exec("BEGIN");
21944
- try {
22391
+ withTransaction(db, () => {
21945
22392
  for (const { id } of stale) {
21946
22393
  db.prepare(
21947
22394
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -21953,11 +22400,7 @@ function openLocalDatabase(dir) {
21953
22400
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
21954
22401
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
21955
22402
  }
21956
- db.exec("COMMIT");
21957
- } catch (err) {
21958
- db.exec("ROLLBACK");
21959
- throw err;
21960
- }
22403
+ });
21961
22404
  } catch {
21962
22405
  }
21963
22406
  }
@@ -21999,6 +22442,7 @@ function openLocalDatabase(dir) {
21999
22442
  purgeSampleData: () => {
22000
22443
  purgeSampleData(db);
22001
22444
  },
22445
+ transaction,
22002
22446
  close: () => {
22003
22447
  db.close();
22004
22448
  }
@@ -22091,12 +22535,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
22091
22535
  }
22092
22536
  }
22093
22537
  function readJson(file2) {
22538
+ let text;
22094
22539
  try {
22095
- const parsed = JSON.parse(readFileSync2(file2, "utf8"));
22096
- return typeof parsed === "object" && parsed !== null ? parsed : null;
22540
+ text = readFileSync2(file2, "utf8");
22097
22541
  } catch {
22098
22542
  return null;
22099
22543
  }
22544
+ return parseJsonObject(text) ?? null;
22545
+ }
22546
+
22547
+ // ../../packages/persistence/src/warn-era-cap.ts
22548
+ import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
22549
+ import { join as join5 } from "path";
22550
+ var MARKER = "warn-era-capped";
22551
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22552
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
22553
+ const marker = join5(dataDir2, MARKER);
22554
+ if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22555
+ const capped = db.policies.capCategoryActions();
22556
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
22557
+ `, { mode: DATA_FILE_MODE });
22558
+ return { capped };
22100
22559
  }
22101
22560
 
22102
22561
  // ../../packages/plugin-sdk/src/provider-env.ts
@@ -22171,7 +22630,7 @@ function resolveProviderSafe() {
22171
22630
  // ../../packages/plugin-sdk/src/config-inventory.ts
22172
22631
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22173
22632
  import { homedir as homedir2 } from "os";
22174
- import { basename as basename2, join as join6 } from "path";
22633
+ import { basename as basename2, join as join7 } from "path";
22175
22634
 
22176
22635
  // ../../packages/detections/src/matchers/keyword.ts
22177
22636
  var KeywordMatcher2 = class {
@@ -24305,8 +24764,8 @@ function bundledDetections() {
24305
24764
  }
24306
24765
 
24307
24766
  // ../../packages/plugin-sdk/src/repo.ts
24308
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
24309
- import { basename, dirname, isAbsolute, join as join5, sep as sep2 } from "path";
24767
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
24768
+ import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
24310
24769
 
24311
24770
  // ../../packages/plugin-sdk/src/events.ts
24312
24771
  import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
@@ -24318,20 +24777,23 @@ import { createHash as createHash4 } from "crypto";
24318
24777
  import { arch, hostname as hostname3, platform, release } from "os";
24319
24778
 
24320
24779
  // ../../packages/plugin-sdk/src/nudge.ts
24321
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
24322
- import { join as join7 } from "path";
24780
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
24781
+ import { join as join8 } from "path";
24323
24782
 
24324
24783
  // ../../packages/plugin-sdk/src/project-files.ts
24325
24784
  var import_ignore = __toESM(require_ignore(), 1);
24326
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
24327
- import { basename as basename3, join as join8, relative, sep as sep3 } from "path";
24785
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
24786
+ import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
24328
24787
 
24329
24788
  // ../../packages/plugin-sdk/src/runtime.ts
24330
24789
  import { randomUUID as randomUUID10 } from "crypto";
24331
24790
 
24791
+ // ../../packages/plugin-sdk/src/suppressions.ts
24792
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
24793
+
24332
24794
  // ../../packages/plugin-sdk/src/throttle.ts
24333
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
24334
- import { join as join9 } from "path";
24795
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
24796
+ import { join as join10 } from "path";
24335
24797
 
24336
24798
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
24337
24799
  import { randomUUID as randomUUID11 } from "crypto";
@@ -24524,6 +24986,14 @@ var StandaloneDataGateway = class {
24524
24986
  sweepTerminalExceptions(retentionMs) {
24525
24987
  return this.db.exceptions.sweepTerminal(retentionMs);
24526
24988
  }
24989
+ // The warn-era enforcement cap, standalone-only store maintenance invoked
24990
+ // from SessionStart, not part of the DataGateway port. Returns the number
24991
+ // of block/redact rows capped to warn (0 for a redact-policy store or an
24992
+ // already-capped one).
24993
+ capWarnEraEnforcement(policyMode) {
24994
+ const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
24995
+ return { capped };
24996
+ }
24527
24997
  // One project-file scan → the local project_file tree (one transaction inside
24528
24998
  // the LocalDatabase, fail-open there). Like the sweep above, this is
24529
24999
  // NOT part of the DataGateway port: the file tree is a local-store read model.
@@ -24673,6 +25143,7 @@ var SEVERITY_GLYPH = {
24673
25143
  medium: SHADE.medium,
24674
25144
  low: SHADE.light
24675
25145
  };
25146
+ var CATEGORY_ORDER2 = DetectionCategory.options;
24676
25147
  function healthScore(summary) {
24677
25148
  const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
24678
25149
  const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;