@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.
@@ -491,6 +491,9 @@ var require_ignore = __commonJS({
491
491
  }
492
492
  });
493
493
 
494
+ // src/backfill.ts
495
+ import { fileURLToPath } from "url";
496
+
494
497
  // ../../packages/persistence/src/database.ts
495
498
  import { randomUUID as randomUUID8 } from "crypto";
496
499
  import { existsSync, renameSync, rmSync } from "fs";
@@ -15430,6 +15433,11 @@ var AuditEventType = external_exports.enum([
15430
15433
  "prompt",
15431
15434
  "response",
15432
15435
  "code_change",
15436
+ // The events.kind of a scanned tool call, widened in to keep this a
15437
+ // superset. Narrower than 'tool_call' above and not a duplicate of it:
15438
+ // 'tool_call' is the reconciler's structural row for every call, while
15439
+ // 'tool_use' exists only where a hook enforced against the arguments.
15440
+ "tool_use",
15433
15441
  // One row per config-inventory scan, hung off the session root. It is the
15434
15442
  // fact the posture inspection findings reference (findings require an
15435
15443
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -15821,7 +15829,7 @@ var ActivityOverviewResponse = external_exports.object({
15821
15829
  }).meta({ id: "ActivityOverviewResponse" });
15822
15830
 
15823
15831
  // ../../packages/schema/src/zod/event.ts
15824
- var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
15832
+ var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15825
15833
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15826
15834
  var EventMetadata = external_exports.object({
15827
15835
  sessionId: external_exports.string().optional(),
@@ -16134,7 +16142,7 @@ var DetectionException = external_exports.object({
16134
16142
  justification: external_exports.string().min(1),
16135
16143
  conditions: ExceptionConditions.nullable(),
16136
16144
  createdBy: external_exports.string(),
16137
- createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
16145
+ createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
16138
16146
  createdAt: external_exports.iso.datetime(),
16139
16147
  updatedAt: external_exports.iso.datetime(),
16140
16148
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16303,20 +16311,26 @@ var PolicyBundle = external_exports.object({
16303
16311
  customKeywords: external_exports.array(external_exports.string()),
16304
16312
  fetchedAt: external_exports.iso.datetime()
16305
16313
  }).meta({ id: "PolicyBundle" });
16306
- var DEFAULT_ACTIONS = {
16307
- secret: "block",
16308
- pii: "redact",
16309
- financial: "redact",
16310
- phi: "redact",
16311
- code_context: "warn",
16312
- code_flaw: "warn",
16313
- custom: "warn",
16314
- // Config-posture findings only observe today (they land in
16315
- // inspection_findings, outside the live-capture enforcement path).
16316
- config: "warn"
16317
- };
16318
16314
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16319
16315
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
16316
+ var CATEGORY_PEAK_SEVERITY = {
16317
+ secret: "critical",
16318
+ financial: "critical",
16319
+ // core-financial/credit-card
16320
+ code_flaw: "critical",
16321
+ pii: "high",
16322
+ phi: "high",
16323
+ custom: "high",
16324
+ // user-defined; conservative
16325
+ code_context: "low",
16326
+ config: "low"
16327
+ // observe-only; floors to monitor regardless
16328
+ };
16329
+ function severityFloorPolicy(category) {
16330
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
16331
+ const peak = CATEGORY_PEAK_SEVERITY[category];
16332
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
16333
+ }
16320
16334
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
16321
16335
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
16322
16336
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -16343,6 +16357,12 @@ var BUILTIN_POLICY_SPECS = {
16343
16357
  description: "Refuse the request entirely whenever any rule in this detection matches."
16344
16358
  }
16345
16359
  };
16360
+ function builtinPolicyToAction(id) {
16361
+ return BUILTIN_POLICY_SPECS[id].action;
16362
+ }
16363
+ var DEFAULT_ACTIONS = Object.fromEntries(
16364
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
16365
+ );
16346
16366
  var BUILTIN_POLICIES = Object.fromEntries(
16347
16367
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
16348
16368
  );
@@ -16749,10 +16769,8 @@ function toApiProvider(sourceTool) {
16749
16769
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
16750
16770
  }
16751
16771
  var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
16752
- function deriveGroupStatus(instances) {
16753
- const statuses = new Set(
16754
- instances.map((i) => i.status).filter((s) => s !== void 0)
16755
- );
16772
+ function foldGroupStatus(instanceStatuses) {
16773
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
16756
16774
  if (statuses.size === 0) return void 0;
16757
16775
  for (const candidate of STATUS_PRECEDENCE) {
16758
16776
  if (statuses.has(candidate)) return candidate;
@@ -16770,6 +16788,7 @@ function deriveFindingStatus(row) {
16770
16788
  function buildFindingGroups(rows, opts = {}) {
16771
16789
  const overrides = opts.overrides;
16772
16790
  const packNames = opts.packNames;
16791
+ const aggregates = opts.aggregates;
16773
16792
  const byRuleId = /* @__PURE__ */ new Map();
16774
16793
  for (const row of rows) {
16775
16794
  const existing = byRuleId.get(row.ruleId);
@@ -16791,17 +16810,20 @@ function buildFindingGroups(rows, opts = {}) {
16791
16810
  status: r.status
16792
16811
  };
16793
16812
  });
16794
- const latestDetectedAt = ruleRows.reduce(
16813
+ const agg = aggregates?.get(ruleId);
16814
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
16795
16815
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
16796
16816
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
16797
16817
  );
16798
16818
  const seenProviders = /* @__PURE__ */ new Set();
16799
- const providers = instances.map((i) => i.provider).filter((p) => {
16819
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
16800
16820
  if (seenProviders.has(p)) return false;
16801
16821
  seenProviders.add(p);
16802
16822
  return true;
16803
16823
  });
16804
- const actionSet = new Set(instances.map((i) => i.action));
16824
+ const actionSet = new Set(
16825
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
16826
+ );
16805
16827
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
16806
16828
  const severity = ruleRows[0]?.severity ?? "low";
16807
16829
  const detection = {
@@ -16815,8 +16837,10 @@ function buildFindingGroups(rows, opts = {}) {
16815
16837
  contextPrefix: ""
16816
16838
  // empty (pending privacy review)
16817
16839
  };
16818
- const status = deriveGroupStatus(instances);
16819
- groups.push({
16840
+ const status = foldGroupStatus(
16841
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
16842
+ );
16843
+ const group = {
16820
16844
  id: ruleId,
16821
16845
  category: apiCategory,
16822
16846
  subtype: ruleId,
@@ -16825,21 +16849,26 @@ function buildFindingGroups(rows, opts = {}) {
16825
16849
  match,
16826
16850
  detection,
16827
16851
  policy,
16828
- instanceCount: instances.length,
16852
+ instanceCount: agg?.instanceCount ?? instances.length,
16829
16853
  providers,
16830
16854
  aggregateAction,
16831
16855
  latestDetectedAt,
16832
16856
  instances,
16833
16857
  status
16834
- });
16858
+ };
16859
+ if (agg) {
16860
+ actionsCache.set(group, [...actionSet]);
16861
+ if (agg.searchText !== void 0) {
16862
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
16863
+ }
16864
+ }
16865
+ groups.push(group);
16835
16866
  }
16836
16867
  return groups;
16837
16868
  }
16838
16869
  var haystackCache = /* @__PURE__ */ new WeakMap();
16839
- function groupHaystack(g) {
16840
- const cached2 = haystackCache.get(g);
16841
- if (cached2 !== void 0) return cached2;
16842
- const haystack = [
16870
+ function buildHaystack(g, extra) {
16871
+ return [
16843
16872
  g.subtype,
16844
16873
  g.category,
16845
16874
  g.match.maskedValue,
@@ -16847,11 +16876,25 @@ function groupHaystack(g) {
16847
16876
  g.id,
16848
16877
  ...g.instances.map((i) => i.repo),
16849
16878
  ...g.instances.map((i) => i.file),
16850
- ...g.instances.map((i) => i.id)
16879
+ ...g.instances.map((i) => i.id),
16880
+ ...extra === void 0 ? [] : [extra]
16851
16881
  ].join(" ").toLowerCase();
16882
+ }
16883
+ function groupHaystack(g) {
16884
+ const cached2 = haystackCache.get(g);
16885
+ if (cached2 !== void 0) return cached2;
16886
+ const haystack = buildHaystack(g);
16852
16887
  haystackCache.set(g, haystack);
16853
16888
  return haystack;
16854
16889
  }
16890
+ var actionsCache = /* @__PURE__ */ new WeakMap();
16891
+ function groupActions(g) {
16892
+ const cached2 = actionsCache.get(g);
16893
+ if (cached2 !== void 0) return cached2;
16894
+ const actions = [...new Set(g.instances.map((i) => i.action))];
16895
+ actionsCache.set(g, actions);
16896
+ return actions;
16897
+ }
16855
16898
  function applyFindingFilters(groups, opts) {
16856
16899
  let filtered = groups;
16857
16900
  if (opts.severity && opts.severity.length > 0) {
@@ -16864,7 +16907,7 @@ function applyFindingFilters(groups, opts) {
16864
16907
  }
16865
16908
  if (opts.actions && opts.actions.length > 0) {
16866
16909
  const actionSet = new Set(opts.actions);
16867
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
16910
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
16868
16911
  }
16869
16912
  if (opts.subtype && opts.subtype.length > 0) {
16870
16913
  const subtypeSet = new Set(opts.subtype);
@@ -16916,8 +16959,7 @@ function computeFindingFacets(allGroups, opts) {
16916
16959
  });
16917
16960
  const actionMap = /* @__PURE__ */ new Map();
16918
16961
  for (const g of forAction) {
16919
- const actionSet = new Set(g.instances.map((i) => i.action));
16920
- for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
16962
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
16921
16963
  }
16922
16964
  const forSubtype = applyFindingFilters(allGroups, {
16923
16965
  providers: opts.providers,
@@ -17479,6 +17521,132 @@ function reviewSeverityRank(reasons) {
17479
17521
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17480
17522
  }
17481
17523
 
17524
+ // ../../packages/schema/src/zod/triage.ts
17525
+ var TriageHit = external_exports.object({
17526
+ ruleId: external_exports.string(),
17527
+ category: DetectionCategory,
17528
+ severity: Severity,
17529
+ maskedMatch: external_exports.string(),
17530
+ rawMatch: external_exports.string(),
17531
+ context: external_exports.string(),
17532
+ filePath: external_exports.string().optional(),
17533
+ confidence: external_exports.number().min(0).max(1),
17534
+ id: external_exports.string().optional(),
17535
+ valueFingerprint: external_exports.string().optional(),
17536
+ keyVersion: external_exports.number().int().nonnegative().optional()
17537
+ });
17538
+ var TriagePolicy = BuiltinPolicyId;
17539
+ var TriageCategoryRec = external_exports.object({
17540
+ category: DetectionCategory,
17541
+ action: TriagePolicy,
17542
+ reasoning: external_exports.string(),
17543
+ genuineCount: external_exports.number().int().nonnegative(),
17544
+ fpCount: external_exports.number().int().nonnegative(),
17545
+ // TriageHit ids judged false-positive in this category. fpCount must equal
17546
+ // this array's length — enforced by the consumer, not this schema.
17547
+ fpIds: external_exports.array(external_exports.string())
17548
+ });
17549
+ var TriageRecommendation = external_exports.object({
17550
+ perCategory: external_exports.array(TriageCategoryRec),
17551
+ notes: external_exports.string()
17552
+ });
17553
+
17554
+ // ../../packages/persistence/src/internal/sql-text.ts
17555
+ function escapeLikePattern(s) {
17556
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17557
+ }
17558
+ function placeholders(n) {
17559
+ return Array.from({ length: n }, () => "?").join(", ");
17560
+ }
17561
+ function containsPattern(q) {
17562
+ return `%${escapeLikePattern(q)}%`;
17563
+ }
17564
+ function likeAny(exprs) {
17565
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
17566
+ }
17567
+
17568
+ // ../../packages/persistence/src/internal/transactions.ts
17569
+ var savepointSeq = 0;
17570
+ function withTransaction(db, fn, mode = "DEFERRED") {
17571
+ if (db.isTransaction) {
17572
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
17573
+ db.exec(`SAVEPOINT ${savepoint}`);
17574
+ try {
17575
+ fn();
17576
+ db.exec(`RELEASE ${savepoint}`);
17577
+ } catch (error51) {
17578
+ try {
17579
+ db.exec(`ROLLBACK TO ${savepoint}`);
17580
+ db.exec(`RELEASE ${savepoint}`);
17581
+ } catch {
17582
+ }
17583
+ throw error51;
17584
+ }
17585
+ return;
17586
+ }
17587
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
17588
+ try {
17589
+ fn();
17590
+ db.exec("COMMIT");
17591
+ } catch (error51) {
17592
+ try {
17593
+ db.exec("ROLLBACK");
17594
+ } catch {
17595
+ }
17596
+ throw error51;
17597
+ }
17598
+ }
17599
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
17600
+ const nested = db.isTransaction;
17601
+ try {
17602
+ withTransaction(db, fn, mode);
17603
+ return true;
17604
+ } catch (error51) {
17605
+ if (!db.isTransaction && nested) throw error51;
17606
+ return false;
17607
+ }
17608
+ }
17609
+
17610
+ // ../../packages/persistence/src/internal/warn.ts
17611
+ function akaWarn(message) {
17612
+ process.stderr.write(`[aka] ${message}
17613
+ `);
17614
+ }
17615
+
17616
+ // ../../packages/persistence/src/db/migrations/introspection.ts
17617
+ function evidenceObjects(sql) {
17618
+ const objects = [];
17619
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17620
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17621
+ objects.push({ kind: "table", name: m[1] });
17622
+ }
17623
+ }
17624
+ for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17625
+ if (m[1] !== void 0 && m[2] !== void 0) {
17626
+ objects.push({ kind: "column", table: m[1], name: m[2] });
17627
+ }
17628
+ }
17629
+ return objects;
17630
+ }
17631
+ function schemaObjectExists(db, kind, name) {
17632
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
17633
+ return row !== void 0;
17634
+ }
17635
+ function indexExists(db, name) {
17636
+ return schemaObjectExists(db, "index", name);
17637
+ }
17638
+ function columnNames(db, table, opts) {
17639
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
17640
+ const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
17641
+ return columns.map((c) => c.name);
17642
+ }
17643
+ function evidenceExists(db, object2) {
17644
+ if (object2.kind === "column") {
17645
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
17646
+ }
17647
+ return schemaObjectExists(db, "table", object2.name);
17648
+ }
17649
+
17482
17650
  // ../../packages/persistence/src/ids.ts
17483
17651
  import { createHash } from "crypto";
17484
17652
  function sha256Hex(input) {
@@ -17515,28 +17683,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17515
17683
  }
17516
17684
 
17517
17685
  // ../../packages/persistence/src/migrations.ts
17518
- function evidenceObjects(sql) {
17519
- const objects = [];
17520
- for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17521
- if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17522
- objects.push({ kind: "table", name: m[1] });
17523
- }
17524
- }
17525
- for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17526
- if (m[1] !== void 0 && m[2] !== void 0) {
17527
- objects.push({ kind: "column", table: m[1], name: m[2] });
17528
- }
17529
- }
17530
- return objects;
17531
- }
17532
- function evidenceExists(db, object2) {
17533
- if (object2.kind === "column") {
17534
- const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
17535
- return columns.some((c) => c.name === object2.name);
17536
- }
17537
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
17538
- return row !== void 0;
17539
- }
17540
17686
  function describeObject(object2) {
17541
17687
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
17542
17688
  }
@@ -17547,10 +17693,6 @@ function createdIndexName(statement) {
17547
17693
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17548
17694
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17549
17695
  }
17550
- function indexExists(db, name) {
17551
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
17552
- return row !== void 0;
17553
- }
17554
17696
  function applyMigrations(db) {
17555
17697
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17556
17698
  db.exec(
@@ -17569,44 +17711,39 @@ function applyMigrations(db) {
17569
17711
  const present = evidence.filter((o) => evidenceExists(db, o));
17570
17712
  if (present.length > 0 && present.length < evidence.length) {
17571
17713
  const missing = evidence.filter((o) => !present.includes(o));
17572
- 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.`;
17573
- process.stderr.write(`${message}
17574
- `);
17575
- throw new Error(message);
17714
+ 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.`;
17715
+ akaWarn(message);
17716
+ throw new Error(`[aka] ${message}`);
17576
17717
  }
17577
17718
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
17578
17719
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
17579
17720
  const statements = splitStatements(migration.sql);
17580
17721
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
17581
17722
  try {
17582
- db.exec("BEGIN IMMEDIATE");
17583
- try {
17584
- for (const statement of statements) {
17585
- const indexName = createdIndexName(statement);
17586
- if (indexName === void 0) {
17587
- if (alreadyApplied) continue;
17588
- } else if (indexExists(db, indexName)) {
17589
- continue;
17723
+ withTransaction(
17724
+ db,
17725
+ () => {
17726
+ for (const statement of statements) {
17727
+ const indexName = createdIndexName(statement);
17728
+ if (indexName === void 0) {
17729
+ if (alreadyApplied) continue;
17730
+ } else if (indexExists(db, indexName)) {
17731
+ continue;
17732
+ }
17733
+ db.exec(statement);
17590
17734
  }
17591
- db.exec(statement);
17592
- }
17593
- if (wantsFkOff && !alreadyApplied) {
17594
- const violations = db.prepare("PRAGMA foreign_key_check").all();
17595
- if (violations.length > 0) {
17596
- throw new Error(
17597
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17598
- );
17735
+ if (wantsFkOff && !alreadyApplied) {
17736
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
17737
+ if (violations.length > 0) {
17738
+ throw new Error(
17739
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17740
+ );
17741
+ }
17599
17742
  }
17600
- }
17601
- record2.run(migration.tag, Date.now());
17602
- db.exec("COMMIT");
17603
- } catch (error51) {
17604
- try {
17605
- db.exec("ROLLBACK");
17606
- } catch {
17607
- }
17608
- throw error51;
17609
- }
17743
+ record2.run(migration.tag, Date.now());
17744
+ },
17745
+ "IMMEDIATE"
17746
+ );
17610
17747
  } finally {
17611
17748
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
17612
17749
  }
@@ -17649,8 +17786,7 @@ var TOKEN_USAGE_COLUMNS = [
17649
17786
  }
17650
17787
  ];
17651
17788
  function ensureTokenUsageColumns(db) {
17652
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
17653
- const existing = new Set(columns.map((c) => c.name));
17789
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17654
17790
  for (const column of TOKEN_USAGE_COLUMNS) {
17655
17791
  if (!existing.has(column.name)) {
17656
17792
  db.exec(column.ddl);
@@ -17688,47 +17824,39 @@ function reconcileSourceProjectIds(db) {
17688
17824
  repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
17689
17825
  }));
17690
17826
  const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
17691
- db.exec("BEGIN IMMEDIATE");
17692
- try {
17693
- for (const { row, canonicalId } of legacy) {
17694
- foldProject.run(
17695
- canonicalId,
17696
- row.url,
17697
- row.name,
17698
- row.attributes,
17699
- row.firstSeen,
17700
- row.lastSeen
17701
- );
17702
- repointAudit.run(canonicalId, row.id);
17703
- for (const { dropCollisions, repoint } of pathTables) {
17704
- dropCollisions.run(row.id, canonicalId);
17705
- repoint.run(canonicalId, row.id);
17827
+ withTransaction(
17828
+ db,
17829
+ () => {
17830
+ for (const { row, canonicalId } of legacy) {
17831
+ foldProject.run(
17832
+ canonicalId,
17833
+ row.url,
17834
+ row.name,
17835
+ row.attributes,
17836
+ row.firstSeen,
17837
+ row.lastSeen
17838
+ );
17839
+ repointAudit.run(canonicalId, row.id);
17840
+ for (const { dropCollisions, repoint } of pathTables) {
17841
+ dropCollisions.run(row.id, canonicalId);
17842
+ repoint.run(canonicalId, row.id);
17843
+ }
17844
+ repointCallSite.run(canonicalId, row.id);
17845
+ deleteLegacy.run(row.id);
17706
17846
  }
17707
- repointCallSite.run(canonicalId, row.id);
17708
- deleteLegacy.run(row.id);
17709
- }
17710
- db.exec("COMMIT");
17711
- } catch (error51) {
17712
- try {
17713
- db.exec("ROLLBACK");
17714
- } catch {
17715
- }
17716
- throw error51;
17717
- }
17847
+ },
17848
+ "IMMEDIATE"
17849
+ );
17718
17850
  } catch (error51) {
17719
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
17720
- `);
17851
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
17721
17852
  }
17722
17853
  }
17723
17854
  function isForeignSqliteLineage(db) {
17724
- const tenantsTable = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tenants' LIMIT 1").get();
17725
- if (tenantsTable !== void 0) return true;
17726
- const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
17727
- return eventsColumns.some((c) => c.name === "tenant_id");
17855
+ if (schemaObjectExists(db, "table", "tenants")) return true;
17856
+ return columnNames(db, "events").includes("tenant_id");
17728
17857
  }
17729
17858
  function ensureSyncedAtColumn(db, table) {
17730
- const columns = db.prepare(`PRAGMA table_info(${table})`).all();
17731
- if (!columns.some((c) => c.name === "synced_at")) {
17859
+ if (!columnNames(db, table).includes("synced_at")) {
17732
17860
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
17733
17861
  }
17734
17862
  }
@@ -17779,8 +17907,11 @@ function ensureDataDirSync(dir) {
17779
17907
  } catch {
17780
17908
  }
17781
17909
  }
17910
+ function walSidecars(file2) {
17911
+ return [`${file2}-wal`, `${file2}-shm`];
17912
+ }
17782
17913
  function tightenPerms(file2) {
17783
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
17914
+ for (const path of [file2, ...walSidecars(file2)]) {
17784
17915
  try {
17785
17916
  chmodSync(path, DATA_FILE_MODE);
17786
17917
  } catch {
@@ -17788,12 +17919,68 @@ function tightenPerms(file2) {
17788
17919
  }
17789
17920
  }
17790
17921
 
17791
- // ../../packages/persistence/src/repositories/sql-utils.ts
17792
- function escapeLikePattern(s) {
17793
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17922
+ // ../../packages/persistence/src/internal/json.ts
17923
+ function safeJson(s, fallback) {
17924
+ if (s == null) return fallback;
17925
+ try {
17926
+ return JSON.parse(s);
17927
+ } catch {
17928
+ return fallback;
17929
+ }
17794
17930
  }
17795
- function placeholders(n) {
17796
- return Array.from({ length: n }, () => "?").join(", ");
17931
+ function parseJsonObject(s) {
17932
+ if (s == null) return void 0;
17933
+ try {
17934
+ const parsed = JSON.parse(s);
17935
+ if (typeof parsed === "object" && parsed !== null) return parsed;
17936
+ } catch {
17937
+ }
17938
+ return void 0;
17939
+ }
17940
+
17941
+ // ../../packages/persistence/src/internal/rows.ts
17942
+ function allRows(stmt, params) {
17943
+ if (params === void 0) return stmt.all();
17944
+ if (Array.isArray(params)) return stmt.all(...params);
17945
+ return stmt.all(params);
17946
+ }
17947
+ function getRow(stmt, params) {
17948
+ if (params === void 0) return stmt.get();
17949
+ if (Array.isArray(params)) return stmt.get(...params);
17950
+ return stmt.get(params);
17951
+ }
17952
+ function intToBool(raw) {
17953
+ return raw === 1 || raw === true;
17954
+ }
17955
+ function boolToInt(b) {
17956
+ return b ? 1 : 0;
17957
+ }
17958
+ function bindParams(row) {
17959
+ const out = {};
17960
+ for (const [key, value] of Object.entries(row)) {
17961
+ out[key] = value === void 0 ? null : value;
17962
+ }
17963
+ return out;
17964
+ }
17965
+ function countScalar(db, sql, params) {
17966
+ return getRow(db.prepare(sql), params)?.n ?? 0;
17967
+ }
17968
+ function countBy(db, sql, params) {
17969
+ const map2 = /* @__PURE__ */ new Map();
17970
+ for (const row of allRows(db.prepare(sql), params)) {
17971
+ map2.set(row.k, row.n);
17972
+ }
17973
+ return map2;
17974
+ }
17975
+ function mapRowsTolerant(rows, map2) {
17976
+ const out = [];
17977
+ for (const row of rows) {
17978
+ try {
17979
+ out.push(map2(row));
17980
+ } catch {
17981
+ }
17982
+ }
17983
+ return out;
17797
17984
  }
17798
17985
 
17799
17986
  // ../../packages/persistence/src/repositories/activity.ts
@@ -17845,15 +18032,11 @@ function encodeCursor(payload) {
17845
18032
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
17846
18033
  }
17847
18034
  function decodeCursor(cursor) {
17848
- try {
17849
- const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
17850
- if (parsed !== null && typeof parsed === "object" && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
17851
- return parsed;
17852
- }
17853
- return null;
17854
- } catch {
17855
- return null;
18035
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18036
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18037
+ return parsed;
17856
18038
  }
18039
+ return null;
17857
18040
  }
17858
18041
  var DB_EVENT_TYPE_TO_KIND = {
17859
18042
  session: "session",
@@ -17870,15 +18053,8 @@ var DB_EVENT_TYPE_TO_KIND = {
17870
18053
  };
17871
18054
  function safeParseStringArray(raw) {
17872
18055
  if (!raw) return [];
17873
- try {
17874
- const parsed = JSON.parse(raw);
17875
- return Array.isArray(parsed) ? parsed : [];
17876
- } catch {
17877
- return [];
17878
- }
17879
- }
17880
- function toBool(raw) {
17881
- return raw === 1 || raw === true;
18056
+ const parsed = safeJson(raw, null);
18057
+ return Array.isArray(parsed) ? parsed : [];
17882
18058
  }
17883
18059
  function toHarness(raw) {
17884
18060
  const parsed = Harness.safeParse(raw);
@@ -17931,8 +18107,8 @@ function buildAuditEvent(row) {
17931
18107
  severity: severityParsed?.success ? severityParsed.data : null,
17932
18108
  link: linkParsed?.success ? linkParsed.data : null,
17933
18109
  targetId: row.target_id,
17934
- internal: toBool(row.internal),
17935
- flagged: toBool(row.flagged)
18110
+ internal: intToBool(row.internal),
18111
+ flagged: intToBool(row.flagged)
17936
18112
  };
17937
18113
  }
17938
18114
  var TIMELINE_COLUMNS = `
@@ -17958,12 +18134,15 @@ var SqliteActivityRepository = class {
17958
18134
  stats(tz) {
17959
18135
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
17960
18136
  const { startMs, endMs } = window;
17961
- const sessionsToday = this.db.prepare(
18137
+ const sessionsToday = countScalar(
18138
+ this.db,
17962
18139
  `SELECT count(*) AS n FROM audit_events
17963
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
17964
- ).get(startMs, endMs).n;
18140
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
18141
+ [startMs, endMs]
18142
+ );
17965
18143
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
17966
- const liveNow = this.db.prepare(
18144
+ const liveNow = countScalar(
18145
+ this.db,
17967
18146
  `SELECT count(*) AS n FROM audit_events s
17968
18147
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
17969
18148
  AND max(
@@ -17972,22 +18151,29 @@ var SqliteActivityRepository = class {
17972
18151
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
17973
18152
  s.started_at
17974
18153
  )
17975
- ) >= ?`
17976
- ).get(liveThreshold).n;
17977
- const toolCallsToday = this.db.prepare(
18154
+ ) >= ?`,
18155
+ [liveThreshold]
18156
+ );
18157
+ const toolCallsToday = countScalar(
18158
+ this.db,
17978
18159
  `SELECT count(*) AS n FROM audit_events
17979
- WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`
17980
- ).get(startMs, endMs).n;
17981
- const findingsToday = this.db.prepare(
18160
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18161
+ [startMs, endMs]
18162
+ );
18163
+ const findingsToday = countScalar(
18164
+ this.db,
17982
18165
  `SELECT count(*) AS n FROM inspection_findings f
17983
18166
  JOIN audit_events e ON e.id = f.audit_event_id
17984
- WHERE e.started_at >= ? AND e.started_at < ?`
17985
- ).get(startMs, endMs).n;
17986
- const egressToday = this.db.prepare(
18167
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18168
+ [startMs, endMs]
18169
+ );
18170
+ const egressToday = countScalar(
18171
+ this.db,
17987
18172
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
17988
18173
  FROM audit_events
17989
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
17990
- ).get(startMs, endMs).n;
18174
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18175
+ [startMs, endMs]
18176
+ );
17991
18177
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
17992
18178
  }
17993
18179
  listSessions(query) {
@@ -18009,7 +18195,7 @@ var SqliteActivityRepository = class {
18009
18195
  conditions.push("started_at <= ?");
18010
18196
  params.push(toMs);
18011
18197
  if (query.q) {
18012
- const pattern = `%${escapeLikePattern(query.q)}%`;
18198
+ const pattern = containsPattern(query.q);
18013
18199
  conditions.push(
18014
18200
  `(content LIKE ? ESCAPE '\\'
18015
18201
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18028,8 +18214,9 @@ var SqliteActivityRepository = class {
18028
18214
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18029
18215
  }
18030
18216
  const limit = query.limit;
18031
- const rows = this.db.prepare(
18032
- `SELECT id,
18217
+ const rows = allRows(
18218
+ this.db.prepare(
18219
+ `SELECT id,
18033
18220
  json_extract(attributes, '$.harness') AS harness,
18034
18221
  content AS title,
18035
18222
  json_extract(attributes, '$.project') AS project,
@@ -18042,7 +18229,9 @@ var SqliteActivityRepository = class {
18042
18229
  WHERE ${conditions.join(" AND ")}
18043
18230
  ORDER BY started_at DESC, id DESC
18044
18231
  LIMIT ?`
18045
- ).all(...params, limit + 1);
18232
+ ),
18233
+ [...params, limit + 1]
18234
+ );
18046
18235
  const hasMore = rows.length > limit;
18047
18236
  const page = hasMore ? rows.slice(0, limit) : rows;
18048
18237
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18059,8 +18248,9 @@ var SqliteActivityRepository = class {
18059
18248
  return Promise.resolve({ items, nextCursor });
18060
18249
  }
18061
18250
  getSession(sessionId) {
18062
- const rootRow = this.db.prepare(
18063
- `SELECT id,
18251
+ const rootRow = getRow(
18252
+ this.db.prepare(
18253
+ `SELECT id,
18064
18254
  json_extract(attributes, '$.harness') AS harness,
18065
18255
  content AS title,
18066
18256
  json_extract(attributes, '$.project') AS project,
@@ -18077,47 +18267,66 @@ var SqliteActivityRepository = class {
18077
18267
  FROM audit_events
18078
18268
  WHERE id = ? AND event_type = 'session'
18079
18269
  LIMIT 1`
18080
- ).get(sessionId);
18270
+ ),
18271
+ [sessionId]
18272
+ );
18081
18273
  if (!rootRow) return Promise.resolve(null);
18082
- const timelineRows = this.db.prepare(
18083
- `SELECT ${TIMELINE_COLUMNS}
18274
+ const timelineRows = allRows(
18275
+ this.db.prepare(
18276
+ `SELECT ${TIMELINE_COLUMNS}
18084
18277
  FROM audit_events
18085
18278
  WHERE id = ? OR root_session_id = ?
18086
18279
  ORDER BY started_at ASC, id ASC`
18087
- ).all(sessionId, sessionId);
18280
+ ),
18281
+ [sessionId, sessionId]
18282
+ );
18088
18283
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18089
- const tokenRow = this.db.prepare(
18090
- `SELECT
18284
+ const tokenRow = getRow(
18285
+ this.db.prepare(
18286
+ `SELECT
18091
18287
  coalesce(sum(input_tokens), 0) AS input,
18092
18288
  coalesce(sum(output_tokens), 0) AS output,
18093
18289
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18094
18290
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18095
18291
  FROM audit_events
18096
18292
  WHERE root_session_id = ? AND event_type = 'llm_call'`
18097
- ).get(sessionId);
18098
- const primaryModel = this.db.prepare(
18099
- `SELECT model, provider FROM audit_events
18293
+ ),
18294
+ [sessionId]
18295
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
18296
+ const primaryModel = getRow(
18297
+ this.db.prepare(
18298
+ `SELECT model, provider FROM audit_events
18100
18299
  WHERE root_session_id = ? AND event_type = 'llm_call'
18101
18300
  ORDER BY started_at ASC, id ASC
18102
18301
  LIMIT 1`
18103
- ).get(sessionId);
18104
- const toolRows = this.db.prepare(
18105
- `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18302
+ ),
18303
+ [sessionId]
18304
+ );
18305
+ const toolRows = allRows(
18306
+ this.db.prepare(
18307
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18106
18308
  count(*) AS n
18107
18309
  FROM audit_events
18108
18310
  WHERE root_session_id = ? AND event_type = 'tool_call'
18109
18311
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
18110
- ).all(sessionId);
18111
- const modelRows = this.db.prepare(
18112
- `SELECT DISTINCT model FROM audit_events
18312
+ ),
18313
+ [sessionId]
18314
+ );
18315
+ const modelRows = allRows(
18316
+ this.db.prepare(
18317
+ `SELECT DISTINCT model FROM audit_events
18113
18318
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18114
18319
  ORDER BY model`
18115
- ).all(sessionId);
18320
+ ),
18321
+ [sessionId]
18322
+ );
18116
18323
  const derivedModels = modelRows.map((r) => r.model);
18117
- const commits = this.db.prepare(
18324
+ const commits = countScalar(
18325
+ this.db,
18118
18326
  `SELECT count(*) AS n FROM audit_events
18119
- WHERE root_session_id = ? AND event_type = 'commit'`
18120
- ).get(sessionId).n;
18327
+ WHERE root_session_id = ? AND event_type = 'commit'`,
18328
+ [sessionId]
18329
+ );
18121
18330
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18122
18331
  turns: 0,
18123
18332
  findings: 0,
@@ -18184,7 +18393,10 @@ var SqliteActivityRepository = class {
18184
18393
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18185
18394
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18186
18395
  );
18187
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
18396
+ const rows = allRows(
18397
+ stmt,
18398
+ fromMs === void 0 ? void 0 : [fromMs]
18399
+ );
18188
18400
  const seen = /* @__PURE__ */ new Set();
18189
18401
  for (const row of rows) seen.add(toHarness(row.harness));
18190
18402
  return Promise.resolve([...seen]);
@@ -18207,23 +18419,23 @@ var SqliteActivityRepository = class {
18207
18419
  conditions.push("started_at >= ?");
18208
18420
  params.push(opts.fromMs);
18209
18421
  }
18210
- const rows = this.db.prepare(
18211
- `SELECT root_session_id AS sessionId, attributes
18422
+ const rows = allRows(
18423
+ this.db.prepare(
18424
+ `SELECT root_session_id AS sessionId, attributes
18212
18425
  FROM audit_events
18213
18426
  WHERE ${conditions.join(" AND ")}`
18214
- ).all(...params);
18215
- const leaves = [];
18216
- for (const row of rows) {
18217
- if (row.sessionId === null) continue;
18218
- try {
18219
- leaves.push({
18220
- sessionId: row.sessionId,
18221
- attributes: JSON.parse(row.attributes)
18222
- });
18223
- } catch {
18224
- }
18225
- }
18226
- return leaves;
18427
+ ),
18428
+ params
18429
+ );
18430
+ return mapRowsTolerant(
18431
+ rows.filter(
18432
+ (row) => row.sessionId !== null
18433
+ ),
18434
+ (row) => ({
18435
+ sessionId: row.sessionId,
18436
+ attributes: JSON.parse(row.attributes)
18437
+ })
18438
+ );
18227
18439
  }
18228
18440
  /**
18229
18441
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -18237,57 +18449,72 @@ var SqliteActivityRepository = class {
18237
18449
  );
18238
18450
  if (sessionIds.length === 0) return result;
18239
18451
  const inClause = placeholders(sessionIds.length);
18240
- const lastActivityRows = this.db.prepare(
18241
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18452
+ const lastActivityRows = allRows(
18453
+ this.db.prepare(
18454
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18242
18455
  WHERE root_session_id IN (${inClause})
18243
18456
  GROUP BY root_session_id`
18244
- ).all(...sessionIds);
18457
+ ),
18458
+ sessionIds
18459
+ );
18245
18460
  for (const row of lastActivityRows) {
18246
18461
  if (row.id === null) continue;
18247
18462
  const entry = result.get(row.id);
18248
18463
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
18249
18464
  }
18250
- const turnsRows = this.db.prepare(
18251
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18465
+ const turnsRows = allRows(
18466
+ this.db.prepare(
18467
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18252
18468
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
18253
18469
  GROUP BY root_session_id`
18254
- ).all(...sessionIds);
18470
+ ),
18471
+ sessionIds
18472
+ );
18255
18473
  for (const row of turnsRows) {
18256
18474
  if (row.id === null) continue;
18257
18475
  const entry = result.get(row.id);
18258
18476
  if (entry) entry.turns = row.n;
18259
18477
  }
18260
- const runKeyRows = this.db.prepare(
18261
- `SELECT root_session_id AS id,
18478
+ const runKeyRows = allRows(
18479
+ this.db.prepare(
18480
+ `SELECT root_session_id AS id,
18262
18481
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
18263
18482
  FROM audit_events
18264
18483
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
18265
18484
  AND json_extract(attributes, '$.run_key') IS NOT NULL
18266
18485
  GROUP BY root_session_id`
18267
- ).all(...sessionIds);
18486
+ ),
18487
+ sessionIds
18488
+ );
18268
18489
  for (const row of runKeyRows) {
18269
18490
  if (row.id === null) continue;
18270
18491
  const entry = result.get(row.id);
18271
18492
  if (entry) entry.turns = Math.max(entry.turns, row.n);
18272
18493
  }
18273
- const findingsRows = this.db.prepare(
18274
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18494
+ const findingsRows = allRows(
18495
+ this.db.prepare(
18496
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18275
18497
  JOIN audit_events e ON e.id = f.audit_event_id
18276
18498
  WHERE e.root_session_id IN (${inClause})
18277
18499
  GROUP BY e.root_session_id`
18278
- ).all(...sessionIds);
18500
+ ),
18501
+ sessionIds
18502
+ );
18279
18503
  for (const row of findingsRows) {
18280
18504
  if (row.id === null) continue;
18281
18505
  const entry = result.get(row.id);
18282
18506
  if (entry) entry.findings = row.n;
18283
18507
  }
18284
- const sharesRows = this.db.prepare(
18285
- `SELECT root_session_id AS id,
18508
+ const sharesRows = allRows(
18509
+ this.db.prepare(
18510
+ `SELECT root_session_id AS id,
18286
18511
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
18287
18512
  FROM audit_events
18288
18513
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
18289
18514
  GROUP BY root_session_id`
18290
- ).all(...sessionIds);
18515
+ ),
18516
+ sessionIds
18517
+ );
18291
18518
  for (const row of sharesRows) {
18292
18519
  if (row.id === null) continue;
18293
18520
  const entry = result.get(row.id);
@@ -18337,33 +18564,28 @@ var SqliteAuditEventsRepository = class {
18337
18564
  // the caller fails open and drops the whole pass — recovered idempotently on the
18338
18565
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
18339
18566
  runInTransaction(fn) {
18340
- this.db.exec("BEGIN");
18341
- try {
18342
- fn();
18343
- this.db.exec("COMMIT");
18344
- } catch (err) {
18345
- this.db.exec("ROLLBACK");
18346
- throw err;
18347
- }
18567
+ withTransaction(this.db, fn);
18348
18568
  }
18349
18569
  insertAuditEvent(input) {
18350
18570
  const row = toAuditEventRow(input);
18351
- this.insertStmt.run({
18352
- id: row.id,
18353
- parentId: row.parentId ?? null,
18354
- rootSessionId: row.rootSessionId ?? null,
18355
- eventType: row.eventType,
18356
- hostId: row.hostId ?? null,
18357
- harnessId: row.harnessId ?? null,
18358
- sourceProjectId: row.sourceProjectId ?? null,
18359
- startedAt: row.startedAt,
18360
- endedAt: row.endedAt ?? null,
18361
- severity: row.severity ?? null,
18362
- priority: row.priority ?? null,
18363
- content: row.content ?? null,
18364
- contentHash: row.contentHash ?? null,
18365
- attributes: row.attributes ?? null
18366
- });
18571
+ this.insertStmt.run(
18572
+ bindParams({
18573
+ id: row.id,
18574
+ parentId: row.parentId,
18575
+ rootSessionId: row.rootSessionId,
18576
+ eventType: row.eventType,
18577
+ hostId: row.hostId,
18578
+ harnessId: row.harnessId,
18579
+ sourceProjectId: row.sourceProjectId,
18580
+ startedAt: row.startedAt,
18581
+ endedAt: row.endedAt,
18582
+ severity: row.severity,
18583
+ priority: row.priority,
18584
+ content: row.content,
18585
+ contentHash: row.contentHash,
18586
+ attributes: row.attributes
18587
+ })
18588
+ );
18367
18589
  }
18368
18590
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18369
18591
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -18377,22 +18599,24 @@ var SqliteAuditEventsRepository = class {
18377
18599
  const startedAt = isoToEpochMillis(input.startedAt);
18378
18600
  if (!Number.isFinite(startedAt)) return;
18379
18601
  const id = llmCallId(input.sessionId, input.messageId);
18380
- this.upsertLlmCallStmt.run({
18381
- id,
18382
- parentId: input.parentId,
18383
- rootSessionId: input.rootSessionId,
18384
- eventType: "llm_call",
18385
- hostId: null,
18386
- harnessId: null,
18387
- sourceProjectId: null,
18388
- startedAt,
18389
- endedAt: null,
18390
- severity: null,
18391
- priority: null,
18392
- content: null,
18393
- contentHash: null,
18394
- attributes: JSON.stringify(input.attributes)
18395
- });
18602
+ this.upsertLlmCallStmt.run(
18603
+ bindParams({
18604
+ id,
18605
+ parentId: input.parentId,
18606
+ rootSessionId: input.rootSessionId,
18607
+ eventType: "llm_call",
18608
+ hostId: null,
18609
+ harnessId: null,
18610
+ sourceProjectId: null,
18611
+ startedAt,
18612
+ endedAt: null,
18613
+ severity: null,
18614
+ priority: null,
18615
+ content: null,
18616
+ contentHash: null,
18617
+ attributes: JSON.stringify(input.attributes)
18618
+ })
18619
+ );
18396
18620
  }
18397
18621
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
18398
18622
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -18416,25 +18640,29 @@ var SqliteAuditEventsRepository = class {
18416
18640
  const startedAt = isoToEpochMillis(input.startedAt);
18417
18641
  if (!Number.isFinite(startedAt)) return;
18418
18642
  const id = toolCallId(input.sessionId, input.toolUseId);
18419
- this.insertStmt.run({
18420
- id,
18421
- parentId: input.parentId,
18422
- rootSessionId: input.rootSessionId,
18423
- eventType: "tool_call",
18424
- hostId: null,
18425
- harnessId: null,
18426
- sourceProjectId: null,
18427
- startedAt,
18428
- endedAt: null,
18429
- severity: null,
18430
- priority: null,
18431
- content: null,
18432
- contentHash: null,
18433
- attributes: JSON.stringify(input.attributes)
18434
- });
18643
+ this.insertStmt.run(
18644
+ bindParams({
18645
+ id,
18646
+ parentId: input.parentId,
18647
+ rootSessionId: input.rootSessionId,
18648
+ eventType: "tool_call",
18649
+ hostId: null,
18650
+ harnessId: null,
18651
+ sourceProjectId: null,
18652
+ startedAt,
18653
+ endedAt: null,
18654
+ severity: null,
18655
+ priority: null,
18656
+ content: null,
18657
+ contentHash: null,
18658
+ attributes: JSON.stringify(input.attributes)
18659
+ })
18660
+ );
18435
18661
  }
18436
18662
  findById(id) {
18437
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
18663
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
18664
+ id
18665
+ });
18438
18666
  }
18439
18667
  // Read the `provider` snapshotted onto a session root's attributes.
18440
18668
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -18444,14 +18672,8 @@ var SqliteAuditEventsRepository = class {
18444
18672
  sessionProvider(sessionId) {
18445
18673
  const row = this.findById(sessionId);
18446
18674
  if (!row?.attributes) return void 0;
18447
- try {
18448
- const parsed = JSON.parse(row.attributes);
18449
- if (typeof parsed === "object" && parsed !== null) {
18450
- const provider = parsed.provider;
18451
- if (typeof provider === "string") return provider;
18452
- }
18453
- } catch {
18454
- }
18675
+ const provider = parseJsonObject(row.attributes)?.provider;
18676
+ if (typeof provider === "string") return provider;
18455
18677
  return void 0;
18456
18678
  }
18457
18679
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -18460,11 +18682,13 @@ var SqliteAuditEventsRepository = class {
18460
18682
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
18461
18683
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
18462
18684
  llmCallLeaves() {
18463
- return this.db.prepare(
18464
- `SELECT root_session_id AS sessionId, attributes
18685
+ return allRows(
18686
+ this.db.prepare(
18687
+ `SELECT root_session_id AS sessionId, attributes
18465
18688
  FROM audit_events
18466
18689
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
18467
- ).all();
18690
+ )
18691
+ );
18468
18692
  }
18469
18693
  };
18470
18694
 
@@ -18483,16 +18707,29 @@ var SqliteClassifiedDataRepository = class {
18483
18707
  upsert(input) {
18484
18708
  const id = classifiedDataId(input.class);
18485
18709
  const row = toClassifiedDataRow(input, id);
18486
- this.insertStmt.run({
18487
- id: row.id,
18488
- class: row.class,
18489
- label: row.label ?? null,
18490
- attributes: row.attributes ?? null
18491
- });
18710
+ this.insertStmt.run(
18711
+ bindParams({
18712
+ id: row.id,
18713
+ class: row.class,
18714
+ label: row.label,
18715
+ attributes: row.attributes
18716
+ })
18717
+ );
18492
18718
  return id;
18493
18719
  }
18494
18720
  };
18495
18721
 
18722
+ // ../../packages/persistence/src/repositories/config-scan.ts
18723
+ function latestConfigScan(db) {
18724
+ return getRow(
18725
+ db.prepare(
18726
+ `SELECT id, started_at, attributes FROM audit_events
18727
+ WHERE event_type = 'config_scan'
18728
+ ORDER BY started_at DESC, id DESC LIMIT 1`
18729
+ )
18730
+ );
18731
+ }
18732
+
18496
18733
  // ../../packages/persistence/src/repositories/config-inventory.ts
18497
18734
  var SqliteConfigInventoryRepository = class {
18498
18735
  constructor(db) {
@@ -18500,7 +18737,7 @@ var SqliteConfigInventoryRepository = class {
18500
18737
  }
18501
18738
  db;
18502
18739
  report() {
18503
- const scan2 = this.latestScan();
18740
+ const scan2 = latestConfigScan(this.db);
18504
18741
  if (!scan2) {
18505
18742
  return {
18506
18743
  scannedAt: null,
@@ -18511,17 +18748,23 @@ var SqliteConfigInventoryRepository = class {
18511
18748
  topics: []
18512
18749
  };
18513
18750
  }
18514
- const rows = this.db.prepare(
18515
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18751
+ const rows = allRows(
18752
+ this.db.prepare(
18753
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18516
18754
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
18517
18755
  ORDER BY object_type, title`
18518
- ).all({ startedAt: scan2.started_at });
18519
- const findings = this.db.prepare(
18520
- `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18756
+ ),
18757
+ { startedAt: scan2.started_at }
18758
+ );
18759
+ const findings = allRows(
18760
+ this.db.prepare(
18761
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18521
18762
  FROM inspection_findings f
18522
18763
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
18523
18764
  WHERE f.audit_event_id = :scanId`
18524
- ).all({ scanId: scan2.id });
18765
+ ),
18766
+ { scanId: scan2.id }
18767
+ );
18525
18768
  const skills = [];
18526
18769
  const hooks = [];
18527
18770
  const mcpServers = [];
@@ -18550,7 +18793,9 @@ var SqliteConfigInventoryRepository = class {
18550
18793
  // schema note); an override whose asset is gone simply never matches. A row
18551
18794
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
18552
18795
  trustOverrides() {
18553
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
18796
+ const rows = allRows(
18797
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
18798
+ );
18554
18799
  const map2 = /* @__PURE__ */ new Map();
18555
18800
  for (const row of rows) {
18556
18801
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -18559,13 +18804,6 @@ var SqliteConfigInventoryRepository = class {
18559
18804
  }
18560
18805
  return map2;
18561
18806
  }
18562
- latestScan() {
18563
- return this.db.prepare(
18564
- `SELECT id, started_at, attributes FROM audit_events
18565
- WHERE event_type = 'config_scan'
18566
- ORDER BY started_at DESC, id DESC LIMIT 1`
18567
- ).get();
18568
- }
18569
18807
  };
18570
18808
  function toSkillItem(row, bag) {
18571
18809
  const item = {
@@ -18676,22 +18914,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
18676
18914
  return topics;
18677
18915
  }
18678
18916
  function countScanErrors(attributes) {
18679
- if (!attributes) return 0;
18680
- try {
18681
- const parsed = JSON.parse(attributes);
18682
- const errors = parsed?.errors;
18683
- return typeof errors === "number" ? errors : 0;
18684
- } catch {
18685
- return 0;
18686
- }
18917
+ const errors = parseJsonObject(attributes)?.errors;
18918
+ return typeof errors === "number" ? errors : 0;
18687
18919
  }
18688
18920
  function parseBag(raw) {
18689
- try {
18690
- const parsed = JSON.parse(raw);
18691
- if (typeof parsed === "object" && parsed !== null) return parsed;
18692
- } catch {
18693
- }
18694
- return void 0;
18921
+ return parseJsonObject(raw);
18695
18922
  }
18696
18923
  function str(value) {
18697
18924
  return typeof value === "string" ? value : void 0;
@@ -18700,12 +18927,7 @@ function str(value) {
18700
18927
  // ../../packages/persistence/src/repositories/detections.ts
18701
18928
  var DAY_MS2 = 864e5;
18702
18929
  function parseRules(rulesJson) {
18703
- let raw;
18704
- try {
18705
- raw = JSON.parse(rulesJson);
18706
- } catch {
18707
- return [];
18708
- }
18930
+ const raw = safeJson(rulesJson, []);
18709
18931
  if (!Array.isArray(raw)) return [];
18710
18932
  const rules = [];
18711
18933
  for (const entry of raw) {
@@ -18724,11 +18946,13 @@ var SqliteDetectionsRepository = class {
18724
18946
  db;
18725
18947
  now;
18726
18948
  listDetections(query) {
18727
- const rows = this.db.prepare(
18728
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18949
+ const rows = allRows(
18950
+ this.db.prepare(
18951
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18729
18952
  rules_json AS rulesJson
18730
18953
  FROM installed_packs`
18731
- ).all();
18954
+ )
18955
+ );
18732
18956
  const available = this.availableByPack();
18733
18957
  const summaries = rows.map((r) => {
18734
18958
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -18737,7 +18961,7 @@ var SqliteDetectionsRepository = class {
18737
18961
  packId: r.packId,
18738
18962
  version: r.version,
18739
18963
  name: r.name,
18740
- enabled: r.enabled === 1,
18964
+ enabled: intToBool(r.enabled),
18741
18965
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
18742
18966
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
18743
18967
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -18754,19 +18978,23 @@ var SqliteDetectionsRepository = class {
18754
18978
  // available_packs keyed by the "namespace/packId" slug (one read per list /
18755
18979
  // detail call; the table is a handful of rows).
18756
18980
  availableByPack() {
18757
- const rows = this.db.prepare(
18758
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18981
+ const rows = allRows(
18982
+ this.db.prepare(
18983
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18759
18984
  FROM available_packs`
18760
- ).all();
18985
+ )
18986
+ );
18761
18987
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
18762
18988
  }
18763
18989
  getDetectionStats() {
18764
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
18990
+ const rows = allRows(
18991
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
18992
+ );
18765
18993
  let rules = 0;
18766
18994
  let active = 0;
18767
18995
  const ruleIds = /* @__PURE__ */ new Set();
18768
18996
  for (const r of rows) {
18769
- if (r.enabled === 1) active += 1;
18997
+ if (intToBool(r.enabled)) active += 1;
18770
18998
  const parsed = parseRules(r.rulesJson);
18771
18999
  rules += parsed.length;
18772
19000
  for (const rule of parsed) {
@@ -18784,12 +19012,15 @@ var SqliteDetectionsRepository = class {
18784
19012
  const parts = splitDetectionId(id);
18785
19013
  if (!parts) return Promise.resolve(null);
18786
19014
  const { namespace, packId } = parts;
18787
- const row = this.db.prepare(
18788
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19015
+ const row = getRow(
19016
+ this.db.prepare(
19017
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18789
19018
  rules_json AS rulesJson, updated_at AS updatedAt
18790
19019
  FROM installed_packs
18791
19020
  WHERE namespace = ? AND pack_id = ?`
18792
- ).get(namespace, packId);
19021
+ ),
19022
+ [namespace, packId]
19023
+ );
18793
19024
  if (!row) return Promise.resolve(null);
18794
19025
  const rules = parseRules(row.rulesJson);
18795
19026
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -18807,7 +19038,7 @@ var SqliteDetectionsRepository = class {
18807
19038
  packId: row.packId,
18808
19039
  version: row.version,
18809
19040
  name: row.name,
18810
- enabled: row.enabled === 1,
19041
+ enabled: intToBool(row.enabled),
18811
19042
  rules,
18812
19043
  updatedAt: new Date(row.updatedAt),
18813
19044
  policyId: row.policyId
@@ -18822,13 +19053,14 @@ var SqliteDetectionsRepository = class {
18822
19053
  countFindingsLast30d(ruleIds) {
18823
19054
  if (ruleIds.length === 0) return 0;
18824
19055
  const since = this.now() - 30 * DAY_MS2;
18825
- const placeholders2 = ruleIds.map(() => "?").join(", ");
18826
- const row = this.db.prepare(
18827
- `SELECT count(*) AS c
19056
+ const inClause = placeholders(ruleIds.length);
19057
+ return countScalar(
19058
+ this.db,
19059
+ `SELECT count(*) AS n
18828
19060
  FROM findings f JOIN events e ON e.id = f.event_id
18829
- WHERE e.occurred_at >= ? AND f.rule_id IN (${placeholders2})`
18830
- ).get(since, ...ruleIds);
18831
- return row.c;
19061
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19062
+ [since, ...ruleIds]
19063
+ );
18832
19064
  }
18833
19065
  };
18834
19066
 
@@ -18845,16 +19077,17 @@ var SqliteEventsRepository = class {
18845
19077
  insertStmt;
18846
19078
  insertEvent(event) {
18847
19079
  const row = toEventRow(event);
18848
- this.insertStmt.run({
18849
- id: row.id,
18850
- sourceTool: row.sourceTool,
18851
- kind: row.kind,
18852
- occurredAt: row.occurredAt,
18853
- contentHash: row.contentHash,
18854
- content: row.content,
18855
- // exactOptionalPropertyTypes: the column is nullable, never undefined.
18856
- metadata: row.metadata ?? null
18857
- });
19080
+ this.insertStmt.run(
19081
+ bindParams({
19082
+ id: row.id,
19083
+ sourceTool: row.sourceTool,
19084
+ kind: row.kind,
19085
+ occurredAt: row.occurredAt,
19086
+ contentHash: row.contentHash,
19087
+ content: row.content,
19088
+ metadata: row.metadata
19089
+ })
19090
+ );
18858
19091
  }
18859
19092
  // Every recorded event's content hash — the historical backfill loads this once
18860
19093
  // to skip transcript messages it has already stored, so re-running the scan
@@ -18862,13 +19095,23 @@ var SqliteEventsRepository = class {
18862
19095
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
18863
19096
  // async EventsReadPort contract.
18864
19097
  contentHashes() {
18865
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19098
+ const rows = allRows(
19099
+ this.db.prepare("SELECT content_hash FROM events")
19100
+ );
18866
19101
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
18867
19102
  }
18868
19103
  };
18869
19104
 
18870
19105
  // ../../packages/persistence/src/repositories/exceptions.ts
18871
19106
  import { randomUUID } from "crypto";
19107
+
19108
+ // ../../packages/persistence/src/internal/sqlite-errors.ts
19109
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19110
+ function isUniqueConstraintError(err) {
19111
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19112
+ }
19113
+
19114
+ // ../../packages/persistence/src/repositories/exceptions.ts
18872
19115
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
18873
19116
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
18874
19117
  var DuplicateActiveExceptionError = class extends Error {
@@ -18889,10 +19132,6 @@ var AmbiguousExceptionIdError = class extends Error {
18889
19132
  this.name = "AmbiguousExceptionIdError";
18890
19133
  }
18891
19134
  };
18892
- var SQLITE_CONSTRAINT_UNIQUE = 2067;
18893
- function isUniqueConstraintError(err) {
18894
- return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
18895
- }
18896
19135
  var ACTIVE_PREDICATE = `revoked_at IS NULL
18897
19136
  AND (expires_at IS NULL OR expires_at > :now)
18898
19137
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -18948,10 +19187,11 @@ var SqliteExceptionsRepository = class {
18948
19187
  this.insertExceptionRow(id, input, now);
18949
19188
  } catch (err) {
18950
19189
  if (!isUniqueConstraintError(err)) throw err;
18951
- this.db.exec("BEGIN IMMEDIATE");
18952
- try {
18953
- const superseded = this.db.prepare(
18954
- `UPDATE exceptions
19190
+ withTransaction(
19191
+ this.db,
19192
+ () => {
19193
+ const superseded = this.db.prepare(
19194
+ `UPDATE exceptions
18955
19195
  SET revoked_at = :now, revoked_by = :revokedBy,
18956
19196
  revoke_reason = 'superseded by a new grant for the same value',
18957
19197
  updated_at = :now
@@ -18959,24 +19199,27 @@ var SqliteExceptionsRepository = class {
18959
19199
  AND key_version = :keyVersion AND revoked_at IS NULL
18960
19200
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
18961
19201
  OR (max_uses IS NOT NULL AND use_count >= max_uses))`
18962
- ).run({
18963
- now,
18964
- revokedBy: input.createdBy,
18965
- ruleId: input.ruleId,
18966
- valueFingerprint: input.valueFingerprint,
18967
- keyVersion: input.keyVersion
18968
- });
18969
- if (Number(superseded.changes) !== 1) {
18970
- throw new DuplicateActiveExceptionError(input.ruleId);
18971
- }
18972
- this.insertExceptionRow(id, input, now);
18973
- this.db.exec("COMMIT");
18974
- } catch (retryErr) {
18975
- this.db.exec("ROLLBACK");
18976
- throw retryErr;
18977
- }
19202
+ ).run({
19203
+ now,
19204
+ revokedBy: input.createdBy,
19205
+ ruleId: input.ruleId,
19206
+ valueFingerprint: input.valueFingerprint,
19207
+ keyVersion: input.keyVersion
19208
+ });
19209
+ if (Number(superseded.changes) !== 1) {
19210
+ throw new DuplicateActiveExceptionError(input.ruleId);
19211
+ }
19212
+ this.insertExceptionRow(id, input, now);
19213
+ },
19214
+ "IMMEDIATE"
19215
+ );
19216
+ }
19217
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
19218
+ id
19219
+ });
19220
+ if (row === void 0) {
19221
+ throw new Error("exception row not found immediately after insert");
18978
19222
  }
18979
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
18980
19223
  return parseExceptionRow(row);
18981
19224
  }
18982
19225
  insertExceptionRow(id, input, now) {
@@ -19014,14 +19257,11 @@ var SqliteExceptionsRepository = class {
19014
19257
  */
19015
19258
  list(opts) {
19016
19259
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19017
- const rows = this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`).all(opts?.includeTerminal ? {} : { now: Date.now() });
19018
- const exceptions = [];
19019
- for (const row of rows) {
19020
- try {
19021
- exceptions.push(parseExceptionRow(row));
19022
- } catch {
19023
- }
19024
- }
19260
+ const rows = allRows(
19261
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19262
+ opts?.includeTerminal ? {} : { now: Date.now() }
19263
+ );
19264
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19025
19265
  return Promise.resolve(exceptions);
19026
19266
  }
19027
19267
  /**
@@ -19031,7 +19271,12 @@ var SqliteExceptionsRepository = class {
19031
19271
  */
19032
19272
  getByIdPrefix(prefix) {
19033
19273
  if (prefix.length === 0) return Promise.resolve(void 0);
19034
- const rows = this.db.prepare(String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`).all({ pattern: `${escapeLikePattern(prefix)}%` });
19274
+ const rows = allRows(
19275
+ this.db.prepare(
19276
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
19277
+ ),
19278
+ { pattern: `${escapeLikePattern(prefix)}%` }
19279
+ );
19035
19280
  if (rows.length > 1) {
19036
19281
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19037
19282
  }
@@ -19073,30 +19318,27 @@ var SqliteExceptionsRepository = class {
19073
19318
  * a different (rotated-away) key never match, so they are excluded at read.
19074
19319
  */
19075
19320
  activeBundleEntries(keyVersion, now = Date.now()) {
19076
- const rows = this.db.prepare(
19077
- `SELECT * FROM exceptions
19321
+ const rows = allRows(
19322
+ this.db.prepare(
19323
+ `SELECT * FROM exceptions
19078
19324
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19079
19325
  ORDER BY created_at DESC, rowid DESC`
19080
- ).all({ keyVersion, now });
19081
- const entries = [];
19082
- for (const row of rows) {
19083
- try {
19084
- const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19085
- entries.push(
19086
- ExceptionBundleEntry.parse({
19087
- id: row.id,
19088
- ruleId: row.rule_id,
19089
- valueFingerprint: row.value_fingerprint,
19090
- keyVersion: row.key_version,
19091
- expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19092
- maxUses: row.max_uses,
19093
- useCount: row.use_count,
19094
- conditions
19095
- })
19096
- );
19097
- } catch {
19098
- }
19099
- }
19326
+ ),
19327
+ { keyVersion, now }
19328
+ );
19329
+ const entries = mapRowsTolerant(rows, (row) => {
19330
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19331
+ return ExceptionBundleEntry.parse({
19332
+ id: row.id,
19333
+ ruleId: row.rule_id,
19334
+ valueFingerprint: row.value_fingerprint,
19335
+ keyVersion: row.key_version,
19336
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19337
+ maxUses: row.max_uses,
19338
+ useCount: row.use_count,
19339
+ conditions
19340
+ });
19341
+ });
19100
19342
  return Promise.resolve(entries);
19101
19343
  }
19102
19344
  /**
@@ -19123,11 +19365,14 @@ var SqliteExceptionsRepository = class {
19123
19365
  }
19124
19366
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19125
19367
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19126
- const rows = this.db.prepare(
19127
- `SELECT * FROM blocked_detections
19368
+ const rows = allRows(
19369
+ this.db.prepare(
19370
+ `SELECT * FROM blocked_detections
19128
19371
  WHERE blocked_at > :cutoff
19129
19372
  ORDER BY blocked_at DESC, rowid DESC`
19130
- ).all({ cutoff: Date.now() - windowMs });
19373
+ ),
19374
+ { cutoff: Date.now() - windowMs }
19375
+ );
19131
19376
  return Promise.resolve(
19132
19377
  rows.map((row) => ({
19133
19378
  reference: row.reference,
@@ -19207,7 +19452,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
19207
19452
  )`;
19208
19453
 
19209
19454
  // ../../packages/persistence/src/repositories/findings.ts
19210
- var GROUP_ROW_CAP = 2e3;
19455
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
19456
+ var CONCAT_SEP = ",";
19457
+ var TUPLE_SEP = "|";
19458
+ function splitConcat(value) {
19459
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
19460
+ }
19211
19461
  function deriveInstanceStatus(row) {
19212
19462
  return deriveFindingStatus({
19213
19463
  kind: row.kind,
@@ -19275,13 +19525,16 @@ var SqliteFindingsRepository = class {
19275
19525
  }
19276
19526
  recentFindings(opts) {
19277
19527
  const limit = opts?.limit ?? 50;
19278
- const rows = this.db.prepare(
19279
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19528
+ const rows = allRows(
19529
+ this.db.prepare(
19530
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19280
19531
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19281
19532
  FROM findings f JOIN events e ON e.id = f.event_id
19282
19533
  ORDER BY e.occurred_at DESC, f.rowid DESC
19283
19534
  LIMIT :limit`
19284
- ).all({ limit });
19535
+ ),
19536
+ { limit }
19537
+ );
19285
19538
  return Promise.resolve(
19286
19539
  rows.map((r) => ({
19287
19540
  id: r.id,
@@ -19304,20 +19557,47 @@ var SqliteFindingsRepository = class {
19304
19557
  * applies the requested filters, and sorts by severity then recency. Filtering
19305
19558
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19306
19559
  * reflect the full filtered set; `items` is the requested
19307
- * page (default 100); no cursor (nextCursor is always null).
19560
+ * page (default 50); no cursor (nextCursor is always null).
19561
+ *
19562
+ * Two reads, neither of which materializes a row per finding:
19563
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
19564
+ * the group and the filters need (count, providers, actions, statuses,
19565
+ * latest, search text);
19566
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
19567
+ * populate `instances` for the table's expanded rows.
19568
+ * The aggregates carry raw DB values and are translated by the same
19569
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
19570
+ * rule is ever restated in SQL.
19308
19571
  */
19309
19572
  listGroupedFindings(query) {
19310
- const rows = this.db.prepare(
19311
- `SELECT f.id, f.rule_id, f.category, f.severity, f.masked_match,
19312
- f.action_taken, f.confidence, e.occurred_at, e.source_tool,
19313
- json_extract(e.metadata, '$.repo') AS repo,
19314
- json_extract(e.metadata, '$.filePath') AS file,
19315
- e.kind AS kind, f.finding_key AS finding_key,
19316
- ${latestResolutionStatusSql("f")} AS latest_status
19317
- FROM findings f JOIN events e ON e.id = f.event_id
19318
- ORDER BY e.occurred_at DESC, f.id DESC
19319
- LIMIT :cap`
19320
- ).all({ cap: GROUP_ROW_CAP });
19573
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
19574
+ const rows = allRows(
19575
+ this.db.prepare(
19576
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19577
+ occurred_at, source_tool, repo, file, kind, finding_key, latest_status
19578
+ FROM (
19579
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19580
+ f.severity AS severity, f.masked_match AS masked_match,
19581
+ f.action_taken AS action_taken, f.confidence AS confidence,
19582
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19583
+ json_extract(e.metadata, '$.repo') AS repo,
19584
+ json_extract(e.metadata, '$.filePath') AS file,
19585
+ e.kind AS kind, f.finding_key AS finding_key,
19586
+ latest.status AS latest_status,
19587
+ ROW_NUMBER() OVER (
19588
+ PARTITION BY f.rule_id
19589
+ ORDER BY e.occurred_at DESC, f.id DESC
19590
+ ) AS rn
19591
+ FROM findings f
19592
+ JOIN events e ON e.id = f.event_id
19593
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19594
+ ON latest.finding_key = f.finding_key
19595
+ )
19596
+ WHERE rn <= :cap
19597
+ ORDER BY occurred_at DESC, id DESC`
19598
+ ),
19599
+ { cap: PREVIEW_INSTANCES_PER_GROUP }
19600
+ );
19321
19601
  const groupable = rows.map((r) => ({
19322
19602
  id: r.id,
19323
19603
  ruleId: r.rule_id,
@@ -19332,7 +19612,7 @@ var SqliteFindingsRepository = class {
19332
19612
  file: r.file ?? "",
19333
19613
  status: deriveInstanceStatus(r)
19334
19614
  }));
19335
- const allGroups = buildFindingGroups(groupable);
19615
+ const allGroups = buildFindingGroups(groupable, { aggregates });
19336
19616
  const filterOpts = {
19337
19617
  severity: query.severity,
19338
19618
  providers: query.provider,
@@ -19350,42 +19630,122 @@ var SqliteFindingsRepository = class {
19350
19630
  const items = sorted.slice(0, limit);
19351
19631
  return Promise.resolve({ totals, facets, items, nextCursor: null });
19352
19632
  }
19633
+ /**
19634
+ * One row per rule_id, folding EVERY instance of the group into the values
19635
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
19636
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
19637
+ *
19638
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
19639
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19640
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19641
+ * status keeps the classifier itself in @akasecurity/schema, where
19642
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
19643
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
19644
+ * a group's row stays small however many findings it holds.
19645
+ *
19646
+ * `withSearchText` is the exception, and the one column here that does NOT
19647
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
19648
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19649
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
19650
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
19651
+ * match an instance outside the preview, which searching the preview alone
19652
+ * would silently lose, so it is fetched only when the request actually
19653
+ * carries a `q`.
19654
+ */
19655
+ groupAggregates(withSearchText) {
19656
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19657
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
19658
+ const rows = this.db.prepare(
19659
+ `SELECT f.rule_id AS rule_id,
19660
+ count(*) AS instance_count,
19661
+ max(e.occurred_at) AS latest_at,
19662
+ group_concat(DISTINCT e.source_tool) AS source_tools,
19663
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
19664
+ group_concat(DISTINCT (
19665
+ e.kind || '${TUPLE_SEP}' ||
19666
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19667
+ coalesce(latest.status, '')
19668
+ )) AS status_inputs
19669
+ ${searchTextColumns}
19670
+ FROM findings f
19671
+ JOIN events e ON e.id = f.event_id
19672
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19673
+ ON latest.finding_key = f.finding_key
19674
+ GROUP BY f.rule_id`
19675
+ ).all();
19676
+ return new Map(
19677
+ rows.map((r) => [
19678
+ r.rule_id,
19679
+ {
19680
+ instanceCount: r.instance_count,
19681
+ sourceTools: splitConcat(r.source_tools),
19682
+ actionsTaken: splitConcat(r.actions_taken),
19683
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19684
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
19685
+ return {
19686
+ // deriveFindingStatus only distinguishes null from non-null here,
19687
+ // so the marker stands in for the key itself (never rendered).
19688
+ kind,
19689
+ findingKey: keyMarker === "" ? null : keyMarker,
19690
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
19691
+ };
19692
+ }),
19693
+ latestDetectedAt: epochMillisToIso(r.latest_at),
19694
+ // Free text only — joined and substring-matched, so group_concat's
19695
+ // commas need no unpicking (a repo/path containing one still matches).
19696
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
19697
+ // tell "no q this request" from "a group with no repo/file at all"
19698
+ // and skip priming a haystack nothing will read.
19699
+ ...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
19700
+ }
19701
+ ])
19702
+ );
19703
+ }
19353
19704
  healthSummary() {
19354
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
19705
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
19355
19706
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
19356
- const grouped = this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken").all();
19707
+ const grouped = allRows(
19708
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
19709
+ );
19357
19710
  for (const row of grouped) {
19358
19711
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
19359
19712
  }
19360
19713
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
19361
- const sevRows = this.db.prepare(
19362
- `SELECT f.severity AS severity, count(*) AS c
19714
+ const sevRows = allRows(
19715
+ this.db.prepare(
19716
+ `SELECT f.severity AS severity, count(*) AS c
19363
19717
  FROM findings f
19364
19718
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19365
19719
  ON latest.finding_key = f.finding_key
19366
19720
  WHERE latest.status IS NULL OR latest.status != 'resolved'
19367
19721
  GROUP BY f.severity`
19368
- ).all();
19722
+ )
19723
+ );
19369
19724
  for (const row of sevRows) {
19370
19725
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
19371
19726
  }
19372
19727
  const categories = ENFORCEABLE_CATEGORIES;
19373
- const enabledRows = this.db.prepare(
19374
- `SELECT DISTINCT json_extract(target, '$.category') AS category
19728
+ const enabledRows = allRows(
19729
+ this.db.prepare(
19730
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
19375
19731
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
19376
- ).all();
19732
+ )
19733
+ );
19377
19734
  const enabled = new Set(enabledRows.map((r) => r.category));
19378
19735
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
19379
19736
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
19380
19737
  }
19381
19738
  activityByDay(days = 7) {
19382
19739
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
19383
- const rows = this.db.prepare(
19384
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19740
+ const rows = allRows(
19741
+ this.db.prepare(
19742
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19385
19743
  FROM findings f JOIN events e ON e.id = f.event_id
19386
19744
  WHERE e.occurred_at >= :since
19387
19745
  GROUP BY day, f.action_taken`
19388
- ).all({ since });
19746
+ ),
19747
+ { since }
19748
+ );
19389
19749
  const buckets = /* @__PURE__ */ new Map();
19390
19750
  for (let i = 0; i < days; i++) {
19391
19751
  const day = isoDay(since + i * DAY_MS3);
@@ -19457,17 +19817,19 @@ var SqliteInspectionFindingsRepository = class {
19457
19817
  insertStmt;
19458
19818
  insertFinding(input) {
19459
19819
  const row = toInspectionFindingRow(input);
19460
- this.insertStmt.run({
19461
- id: row.id,
19462
- auditEventId: row.auditEventId,
19463
- inspectionDefinitionId: row.inspectionDefinitionId,
19464
- classifiedDataId: row.classifiedDataId ?? null,
19465
- spanStart: row.spanStart,
19466
- spanEnd: row.spanEnd,
19467
- maskedMatch: row.maskedMatch,
19468
- actionTaken: row.actionTaken,
19469
- confidence: row.confidence
19470
- });
19820
+ this.insertStmt.run(
19821
+ bindParams({
19822
+ id: row.id,
19823
+ auditEventId: row.auditEventId,
19824
+ inspectionDefinitionId: row.inspectionDefinitionId,
19825
+ classifiedDataId: row.classifiedDataId,
19826
+ spanStart: row.spanStart,
19827
+ spanEnd: row.spanEnd,
19828
+ maskedMatch: row.maskedMatch,
19829
+ actionTaken: row.actionTaken,
19830
+ confidence: row.confidence
19831
+ })
19832
+ );
19471
19833
  }
19472
19834
  };
19473
19835
 
@@ -19543,12 +19905,7 @@ function isMirrorDowngrade(incoming, stored) {
19543
19905
  }
19544
19906
  function ruleIdsOf(rulesJson) {
19545
19907
  const ids = /* @__PURE__ */ new Set();
19546
- let raw;
19547
- try {
19548
- raw = JSON.parse(rulesJson);
19549
- } catch {
19550
- return ids;
19551
- }
19908
+ const raw = safeJson(rulesJson, []);
19552
19909
  if (!Array.isArray(raw)) return ids;
19553
19910
  for (const entry of raw) {
19554
19911
  if (entry && typeof entry === "object") {
@@ -19621,47 +19978,48 @@ var SqliteInstalledPacksRepository = class {
19621
19978
  }));
19622
19979
  if (this.storedSignature() === inventorySignature(rows)) return;
19623
19980
  const now = Date.now();
19624
- this.db.exec("BEGIN IMMEDIATE");
19625
- try {
19626
- const mirror = this.mirrorState();
19627
- let behind = false;
19628
- for (const row of rows) {
19629
- const params = {
19630
- id: randomUUID2(),
19631
- namespace: row.namespace,
19632
- packId: row.packId,
19633
- version: row.version,
19634
- name: row.name,
19635
- rulesJson: row.rulesJson,
19636
- now
19637
- };
19638
- const stored = mirror.get(`${row.namespace}/${row.packId}`);
19639
- if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
19640
- this.upsertAvailableStmt.run({
19641
- ...params,
19981
+ withTransaction(
19982
+ this.db,
19983
+ () => {
19984
+ const mirror = this.mirrorState();
19985
+ let behind = false;
19986
+ for (const row of rows) {
19987
+ const params = {
19642
19988
  id: randomUUID2(),
19643
- recordedBy: meta3?.recordedBy ?? null
19644
- });
19645
- } else {
19646
- behind = true;
19989
+ namespace: row.namespace,
19990
+ packId: row.packId,
19991
+ version: row.version,
19992
+ name: row.name,
19993
+ rulesJson: row.rulesJson,
19994
+ now
19995
+ };
19996
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
19997
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
19998
+ this.upsertAvailableStmt.run({
19999
+ ...params,
20000
+ id: randomUUID2(),
20001
+ recordedBy: meta3?.recordedBy ?? null
20002
+ });
20003
+ } else {
20004
+ behind = true;
20005
+ }
20006
+ this.insertMissingStmt.run(params);
19647
20007
  }
19648
- this.insertMissingStmt.run(params);
19649
- }
19650
- if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
19651
- this.db.exec("COMMIT");
19652
- } catch (err) {
19653
- this.db.exec("ROLLBACK");
19654
- throw err;
19655
- }
20008
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20009
+ },
20010
+ "IMMEDIATE"
20011
+ );
19656
20012
  } catch {
19657
20013
  }
19658
20014
  }
19659
20015
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
19660
20016
  // input to the downgrade guard. Read INSIDE the write transaction.
19661
20017
  mirrorState() {
19662
- const rows = this.db.prepare(
19663
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
19664
- ).all();
20018
+ const rows = allRows(
20019
+ this.db.prepare(
20020
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20021
+ )
20022
+ );
19665
20023
  return new Map(
19666
20024
  rows.map((r) => [
19667
20025
  `${r.namespace}/${r.packId}`,
@@ -19673,7 +20031,9 @@ var SqliteInstalledPacksRepository = class {
19673
20031
  // (keys joined with '/', matching the detection id slug encoding — packId may
19674
20032
  // itself contain '/', but namespace may not, so the join is unambiguous).
19675
20033
  pruneAvailable(keep) {
19676
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20034
+ const rows = allRows(
20035
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20036
+ );
19677
20037
  const keepSet = new Set(keep);
19678
20038
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
19679
20039
  for (const r of rows) {
@@ -19700,11 +20060,13 @@ var SqliteInstalledPacksRepository = class {
19700
20060
  if (this.db.isTransaction) {
19701
20061
  throw new Error("applyUpdate must not be called inside an open transaction");
19702
20062
  }
19703
- this.db.exec("BEGIN IMMEDIATE");
19704
- try {
19705
- this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
19706
- const res = this.db.prepare(
19707
- `UPDATE installed_packs SET
20063
+ let changed = false;
20064
+ withTransaction(
20065
+ this.db,
20066
+ () => {
20067
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20068
+ const res = this.db.prepare(
20069
+ `UPDATE installed_packs SET
19708
20070
  version = (SELECT a.version FROM available_packs a
19709
20071
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
19710
20072
  name = (SELECT a.name FROM available_packs a
@@ -19715,17 +20077,13 @@ var SqliteInstalledPacksRepository = class {
19715
20077
  WHERE namespace = :namespace AND pack_id = :packId
19716
20078
  AND EXISTS (SELECT 1 FROM available_packs a
19717
20079
  WHERE a.namespace = :namespace AND a.pack_id = :packId)`
19718
- ).run({ namespace, packId, now: Date.now() });
19719
- this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
19720
- this.db.exec("COMMIT");
19721
- return Number(res.changes) > 0;
19722
- } catch (err) {
19723
- try {
19724
- this.db.exec("ROLLBACK");
19725
- } catch {
19726
- }
19727
- throw err;
19728
- }
20080
+ ).run({ namespace, packId, now: Date.now() });
20081
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20082
+ changed = Number(res.changes) > 0;
20083
+ },
20084
+ "IMMEDIATE"
20085
+ );
20086
+ return changed;
19729
20087
  }
19730
20088
  /**
19731
20089
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -19739,9 +20097,11 @@ var SqliteInstalledPacksRepository = class {
19739
20097
  * JSON-level failure therefore counts as invalid.
19740
20098
  */
19741
20099
  installedRuleset() {
19742
- const rows = this.db.prepare(
19743
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
19744
- ).all();
20100
+ const rows = allRows(
20101
+ this.db.prepare(
20102
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20103
+ )
20104
+ );
19745
20105
  const out = {
19746
20106
  installedPacks: rows.length,
19747
20107
  enabledPacks: 0,
@@ -19750,7 +20110,7 @@ var SqliteInstalledPacksRepository = class {
19750
20110
  ruleActions: /* @__PURE__ */ new Map()
19751
20111
  };
19752
20112
  for (const row of rows) {
19753
- if (row.enabled !== 1) continue;
20113
+ if (!intToBool(row.enabled)) continue;
19754
20114
  out.enabledPacks += 1;
19755
20115
  const action = policyIdToAction(row.policyId);
19756
20116
  let raw;
@@ -19785,9 +20145,11 @@ var SqliteInstalledPacksRepository = class {
19785
20145
  * running max would mask a genuinely-newer parseable stamp.
19786
20146
  */
19787
20147
  newestRecordedBinary() {
19788
- const rows = this.db.prepare(
19789
- `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
19790
- ).all();
20148
+ const rows = allRows(
20149
+ this.db.prepare(
20150
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20151
+ )
20152
+ );
19791
20153
  let newest = null;
19792
20154
  for (const row of rows) {
19793
20155
  const at = row.recordedBy.lastIndexOf("@");
@@ -19802,13 +20164,15 @@ var SqliteInstalledPacksRepository = class {
19802
20164
  return newest;
19803
20165
  }
19804
20166
  counts() {
19805
- const row = this.db.prepare(
19806
- `SELECT count(*) AS packs,
20167
+ const row = getRow(
20168
+ this.db.prepare(
20169
+ `SELECT count(*) AS packs,
19807
20170
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
19808
20171
  coalesce(sum(enabled), 0) AS enabled
19809
20172
  FROM installed_packs`
19810
- ).get();
19811
- return Promise.resolve(row);
20173
+ )
20174
+ );
20175
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
19812
20176
  }
19813
20177
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
19814
20178
  // Back the Policies page's built-in catalog: how many
@@ -19821,26 +20185,29 @@ var SqliteInstalledPacksRepository = class {
19821
20185
  * attributed to Monitor, matching the Detections views.
19822
20186
  */
19823
20187
  countsByPolicyId() {
19824
- const rows = this.db.prepare(
19825
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20188
+ return countBy(
20189
+ this.db,
20190
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
19826
20191
  FROM installed_packs
19827
- GROUP BY pid`
19828
- ).all();
19829
- return new Map(rows.map((r) => [r.pid, r.n]));
20192
+ GROUP BY k`
20193
+ );
19830
20194
  }
19831
20195
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
19832
20196
  listByPolicyId(policyId) {
19833
- const rows = this.db.prepare(
19834
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20197
+ const rows = allRows(
20198
+ this.db.prepare(
20199
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
19835
20200
  FROM installed_packs
19836
20201
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
19837
20202
  ORDER BY name ASC`
19838
- ).all(policyId);
20203
+ ),
20204
+ [policyId]
20205
+ );
19839
20206
  return rows.map((r) => ({
19840
20207
  id: `${r.namespace}/${r.packId}`,
19841
20208
  name: r.name,
19842
20209
  ruleCount: parseRules(r.rulesJson).length,
19843
- enabled: r.enabled === 1
20210
+ enabled: intToBool(r.enabled)
19844
20211
  }));
19845
20212
  }
19846
20213
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -19869,14 +20236,14 @@ var SqliteInstalledPacksRepository = class {
19869
20236
  const res = this.db.prepare(
19870
20237
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
19871
20238
  WHERE namespace = :namespace AND pack_id = :packId`
19872
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
20239
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
19873
20240
  return Number(res.changes) > 0;
19874
20241
  }
19875
20242
  // Fingerprint of the recorded available mirror — compared against the
19876
20243
  // incoming inventory's signature to skip the write entirely when the running
19877
20244
  // binary's inventory hasn't changed since the last record.
19878
20245
  storedSignature() {
19879
- const rows = this.signatureStmt.all();
20246
+ const rows = allRows(this.signatureStmt);
19880
20247
  return inventorySignature(rows);
19881
20248
  }
19882
20249
  };
@@ -19904,42 +20271,48 @@ var SqliteInventoryRepository = class {
19904
20271
  upsert(input, now = Date.now()) {
19905
20272
  const id = inventoryId(input.objectType, input.identityKey);
19906
20273
  const row = toInventoryRow(input, id, now);
19907
- this.upsertStmt.run({
19908
- id: row.id,
19909
- objectType: row.objectType,
19910
- location: row.location ?? null,
19911
- title: row.title ?? null,
19912
- hostId: row.hostId ?? null,
19913
- attributes: row.attributes,
19914
- firstSeen: row.firstSeen,
19915
- lastSeen: row.lastSeen
19916
- });
20274
+ this.upsertStmt.run(
20275
+ bindParams({
20276
+ id: row.id,
20277
+ objectType: row.objectType,
20278
+ location: row.location,
20279
+ title: row.title,
20280
+ hostId: row.hostId,
20281
+ attributes: row.attributes,
20282
+ firstSeen: row.firstSeen,
20283
+ lastSeen: row.lastSeen
20284
+ })
20285
+ );
19917
20286
  return id;
19918
20287
  }
19919
20288
  // The full row, for round-trip assertions.
19920
20289
  findById(id) {
19921
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
19922
- return row;
20290
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
19923
20291
  }
19924
20292
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
19925
20293
  // served from the object_type index, never from audit_events.
19926
20294
  distinctTitles(objectType) {
19927
- const rows = this.db.prepare(
19928
- `SELECT DISTINCT title FROM inventory
20295
+ const rows = allRows(
20296
+ this.db.prepare(
20297
+ `SELECT DISTINCT title FROM inventory
19929
20298
  WHERE object_type = :objectType AND title IS NOT NULL
19930
20299
  ORDER BY title`
19931
- ).all({ objectType });
20300
+ ),
20301
+ { objectType }
20302
+ );
19932
20303
  return rows.map((r) => r.title);
19933
20304
  }
19934
20305
  // Distinct host os_version values — a facet served from an inventory index
19935
20306
  // over the generated column, never from the audit fact (confirm via EXPLAIN
19936
20307
  // QUERY PLAN).
19937
20308
  osVersions() {
19938
- const rows = this.db.prepare(
19939
- `SELECT DISTINCT os_version AS value FROM inventory
20309
+ const rows = allRows(
20310
+ this.db.prepare(
20311
+ `SELECT DISTINCT os_version AS value FROM inventory
19940
20312
  WHERE object_type = 'host' AND os_version IS NOT NULL
19941
20313
  ORDER BY value`
19942
- ).all();
20314
+ )
20315
+ );
19943
20316
  return rows.map((r) => r.value);
19944
20317
  }
19945
20318
  };
@@ -19959,14 +20332,6 @@ var EMPTY_PROJECT_AGG = {
19959
20332
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
19960
20333
  findingsCount: 0
19961
20334
  };
19962
- function safeJson(s, fallback) {
19963
- if (s == null) return fallback;
19964
- try {
19965
- return JSON.parse(s);
19966
- } catch {
19967
- return fallback;
19968
- }
19969
- }
19970
20335
  function resolveHarnessId(attrs, row) {
19971
20336
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
19972
20337
  return attrs.provider;
@@ -20162,30 +20527,49 @@ var SqliteInventoryAssetsRepository = class {
20162
20527
  configRowsCache;
20163
20528
  // ─── stats ─────────────────────────────────────────────────────────────────
20164
20529
  getInventoryStats() {
20165
- const byType = { project: 0, skill: 0, mcp: 0, hook: 0, config: 0 };
20166
- for (const r of this.db.prepare("SELECT asset_type AS t, count(*) AS n FROM inventory_asset GROUP BY asset_type").all()) {
20167
- if (r.t in byType) byType[r.t] = r.n;
20168
- }
20169
- byType.project = this.db.prepare(`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`).get().n;
20170
- const mcpTrust = { "known-good": 0, risky: 0, unapproved: 0 };
20171
- for (const r of this.db.prepare(
20172
- `SELECT coalesce(o.trust, a.trust) AS trust, count(*) AS n
20530
+ const typeCounts = countBy(
20531
+ this.db,
20532
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
20533
+ );
20534
+ const byType = {
20535
+ project: 0,
20536
+ skill: typeCounts.get("skill") ?? 0,
20537
+ mcp: typeCounts.get("mcp") ?? 0,
20538
+ hook: typeCounts.get("hook") ?? 0,
20539
+ config: typeCounts.get("config") ?? 0
20540
+ };
20541
+ byType.project = countScalar(
20542
+ this.db,
20543
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
20544
+ );
20545
+ const mcpTrustCounts = countBy(
20546
+ this.db,
20547
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20173
20548
  FROM inventory_asset a
20174
20549
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20175
20550
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20176
20551
  GROUP BY coalesce(o.trust, a.trust)`
20177
- ).all()) {
20178
- if (r.trust in mcpTrust) mcpTrust[r.trust] = r.n;
20179
- }
20180
- const harnesses = this.db.prepare(
20552
+ );
20553
+ const mcpTrust = {
20554
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
20555
+ risky: mcpTrustCounts.get("risky") ?? 0,
20556
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
20557
+ };
20558
+ const harnesses = countScalar(
20559
+ this.db,
20181
20560
  `SELECT count(*) AS n FROM inventory
20182
20561
  WHERE object_type = 'harness'
20183
- AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20184
- ).get({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }).n;
20185
- const flaggedAssets = this.db.prepare("SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'").get().n;
20186
- const flaggedProjects = this.db.prepare(
20562
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
20563
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20564
+ );
20565
+ const flaggedAssets = countScalar(
20566
+ this.db,
20567
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
20568
+ );
20569
+ const flaggedProjects = countScalar(
20570
+ this.db,
20187
20571
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20188
- ).get().n;
20572
+ );
20189
20573
  const configRows = this.configAssetRows();
20190
20574
  for (const r of configRows) {
20191
20575
  byType[r.assetType] += 1;
@@ -20442,12 +20826,15 @@ var SqliteInventoryAssetsRepository = class {
20442
20826
  }
20443
20827
  // ─── raw fetchers ────────────────────────────────────────────────────────────
20444
20828
  fetchHarnessRows() {
20445
- return this.db.prepare(
20446
- `SELECT id, title, attributes, harness_version AS harnessVersion
20829
+ return allRows(
20830
+ this.db.prepare(
20831
+ `SELECT id, title, attributes, harness_version AS harnessVersion
20447
20832
  FROM inventory
20448
20833
  WHERE object_type = 'harness'
20449
20834
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20450
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
20835
+ ),
20836
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20837
+ );
20451
20838
  }
20452
20839
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
20453
20840
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -20457,12 +20844,13 @@ var SqliteInventoryAssetsRepository = class {
20457
20844
  const params = [...harnessInvIds];
20458
20845
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
20459
20846
  if (q) {
20460
- const pat = `%${escapeLikePattern(q)}%`;
20461
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
20847
+ const pat = containsPattern(q);
20848
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
20462
20849
  params.push(pat, pat);
20463
20850
  }
20464
- const rows = this.db.prepare(
20465
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20851
+ const rows = allRows(
20852
+ this.db.prepare(
20853
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20466
20854
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20467
20855
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20468
20856
  FROM harness_asset ha
@@ -20470,7 +20858,9 @@ var SqliteInventoryAssetsRepository = class {
20470
20858
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20471
20859
  WHERE ${where}
20472
20860
  ORDER BY a.name ASC`
20473
- ).all(...params);
20861
+ ),
20862
+ params
20863
+ );
20474
20864
  for (const raw of rows) {
20475
20865
  const harnessInvId = raw.harnessInvId;
20476
20866
  const [asset] = this.mapAssetRows([raw]);
@@ -20489,21 +20879,24 @@ var SqliteInventoryAssetsRepository = class {
20489
20879
  params.push(...types);
20490
20880
  }
20491
20881
  if (q) {
20492
- const pat = `%${escapeLikePattern(q)}%`;
20493
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
20882
+ const pat = containsPattern(q);
20883
+ conditions.push(likeAny(["a.name", "a.sub"]));
20494
20884
  params.push(pat, pat);
20495
20885
  }
20496
20886
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
20497
20887
  const sampleRows = this.mapAssetRows(
20498
- this.db.prepare(
20499
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20888
+ allRows(
20889
+ this.db.prepare(
20890
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20500
20891
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20501
20892
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20502
20893
  FROM inventory_asset a
20503
20894
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20504
20895
  ${where}
20505
20896
  ORDER BY a.name ASC`
20506
- ).all(...params)
20897
+ ),
20898
+ params
20899
+ )
20507
20900
  );
20508
20901
  const configRows = this.configAssetRows(q).filter(
20509
20902
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -20512,14 +20905,17 @@ var SqliteInventoryAssetsRepository = class {
20512
20905
  }
20513
20906
  fetchAssetById(assetId) {
20514
20907
  const rows = this.mapAssetRows(
20515
- this.db.prepare(
20516
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20908
+ allRows(
20909
+ this.db.prepare(
20910
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20517
20911
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20518
20912
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20519
20913
  FROM inventory_asset a
20520
20914
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20521
20915
  WHERE a.id = ?`
20522
- ).all(assetId)
20916
+ ),
20917
+ [assetId]
20918
+ )
20523
20919
  );
20524
20920
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
20525
20921
  }
@@ -20575,37 +20971,39 @@ var SqliteInventoryAssetsRepository = class {
20575
20971
  return rows;
20576
20972
  }
20577
20973
  latestConfigScanId() {
20578
- const row = this.db.prepare(
20579
- `SELECT id FROM audit_events WHERE event_type = 'config_scan'
20580
- ORDER BY started_at DESC, id DESC LIMIT 1`
20581
- ).get();
20582
- return row?.id ?? null;
20974
+ return latestConfigScan(this.db)?.id ?? null;
20583
20975
  }
20584
20976
  fetchProjects(q) {
20585
20977
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
20586
20978
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
20587
20979
  const params = [];
20588
20980
  if (q) {
20589
- const pat = `%${escapeLikePattern(q)}%`;
20590
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
20981
+ const pat = containsPattern(q);
20982
+ sql += ` AND ${likeAny(["name", "url"])}`;
20591
20983
  params.push(pat, pat);
20592
20984
  }
20593
20985
  sql += " ORDER BY name ASC";
20594
- return this.db.prepare(sql).all(...params);
20986
+ return allRows(this.db.prepare(sql), params);
20595
20987
  }
20596
20988
  fetchProjectById(projectId) {
20597
- return this.db.prepare(
20598
- "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
20599
- ).get(projectId) ?? null;
20989
+ return getRow(
20990
+ this.db.prepare(
20991
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
20992
+ ),
20993
+ [projectId]
20994
+ ) ?? null;
20600
20995
  }
20601
20996
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
20602
20997
  fetchProjectsByIds(projectIds) {
20603
20998
  const map2 = /* @__PURE__ */ new Map();
20604
20999
  if (projectIds.length === 0) return map2;
20605
- const rows = this.db.prepare(
20606
- `SELECT id, url, name, attributes, last_seen AS lastSeen
21000
+ const rows = allRows(
21001
+ this.db.prepare(
21002
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
20607
21003
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
20608
- ).all(...projectIds);
21004
+ ),
21005
+ projectIds
21006
+ );
20609
21007
  for (const r of rows) map2.set(r.id, r);
20610
21008
  return map2;
20611
21009
  }
@@ -20616,8 +21014,9 @@ var SqliteInventoryAssetsRepository = class {
20616
21014
  projectAggregates(projectIds) {
20617
21015
  const map2 = /* @__PURE__ */ new Map();
20618
21016
  if (projectIds.length === 0) return map2;
20619
- const rows = this.db.prepare(
20620
- `SELECT f.project_id AS projectId,
21017
+ const rows = allRows(
21018
+ this.db.prepare(
21019
+ `SELECT f.project_id AS projectId,
20621
21020
  coalesce(o.access, f.default_access) AS eff,
20622
21021
  count(*) AS n,
20623
21022
  coalesce(sum(f.findings_count), 0) AS findings
@@ -20625,7 +21024,9 @@ var SqliteInventoryAssetsRepository = class {
20625
21024
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
20626
21025
  WHERE f.project_id IN (${placeholders(projectIds.length)})
20627
21026
  GROUP BY f.project_id, eff`
20628
- ).all(...projectIds);
21027
+ ),
21028
+ projectIds
21029
+ );
20629
21030
  for (const r of rows) {
20630
21031
  let agg = map2.get(r.projectId);
20631
21032
  if (!agg) {
@@ -20663,37 +21064,52 @@ var SqliteInventoryAssetsRepository = class {
20663
21064
  fetchProjectFilesUnder(projectId, prefix) {
20664
21065
  if (prefix === "") {
20665
21066
  return this.mapFileRows(
20666
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21067
+ allRows(
21068
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21069
+ [projectId]
21070
+ )
20667
21071
  );
20668
21072
  }
20669
21073
  return this.mapFileRows(
20670
- this.db.prepare(
20671
- this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
20672
- ).all(projectId, `${escapeLikePattern(prefix)}/%`)
21074
+ allRows(
21075
+ this.db.prepare(
21076
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21077
+ ),
21078
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21079
+ )
20673
21080
  );
20674
21081
  }
20675
21082
  fetchProjectFilesSearch(projectId, q) {
20676
- const pat = `%${escapeLikePattern(q)}%`;
21083
+ const pat = containsPattern(q);
20677
21084
  return this.mapFileRows(
20678
- this.db.prepare(
20679
- this.fileSelect(
20680
- "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
20681
- )
20682
- ).all(projectId, pat, pat)
21085
+ allRows(
21086
+ this.db.prepare(
21087
+ this.fileSelect(
21088
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21089
+ )
21090
+ ),
21091
+ [projectId, pat, pat]
21092
+ )
20683
21093
  );
20684
21094
  }
20685
21095
  fetchProjectFilesBlocked(projectId) {
20686
21096
  return this.mapFileRows(
20687
- this.db.prepare(
20688
- this.fileSelect(
20689
- "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
20690
- )
20691
- ).all(projectId)
21097
+ allRows(
21098
+ this.db.prepare(
21099
+ this.fileSelect(
21100
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21101
+ )
21102
+ ),
21103
+ [projectId]
21104
+ )
20692
21105
  );
20693
21106
  }
20694
21107
  fetchProjectFile(projectId, path) {
20695
21108
  const rows = this.mapFileRows(
20696
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21109
+ allRows(
21110
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21111
+ [projectId, path]
21112
+ )
20697
21113
  );
20698
21114
  return rows[0] ?? null;
20699
21115
  }
@@ -20707,39 +21123,32 @@ var SqlitePoliciesRepository = class {
20707
21123
  }
20708
21124
  db;
20709
21125
  readPolicies() {
20710
- const rows = this.db.prepare("SELECT * FROM policies").all();
20711
- const policies = [];
20712
- for (const row of rows) {
20713
- try {
20714
- const target = JSON.parse(row.target);
20715
- const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
20716
- policies.push(
20717
- Policy.parse({
20718
- id: row.id,
20719
- scope: row.scope,
20720
- target,
20721
- action: row.action,
20722
- enabled: row.enabled === 1,
20723
- customKeywords
20724
- })
20725
- );
20726
- } catch {
20727
- }
20728
- }
21126
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21127
+ const policies = mapRowsTolerant(rows, (row) => {
21128
+ const target = JSON.parse(row.target);
21129
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21130
+ return Policy.parse({
21131
+ id: row.id,
21132
+ scope: row.scope,
21133
+ target,
21134
+ action: row.action,
21135
+ enabled: intToBool(row.enabled),
21136
+ customKeywords
21137
+ });
21138
+ });
20729
21139
  return Promise.resolve(policies);
20730
21140
  }
20731
21141
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
20732
21142
  // detection-type config exists from first run. Only when the table is empty,
20733
21143
  // so a user's edits are never clobbered.
20734
21144
  seedDefaults() {
20735
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21145
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
20736
21146
  if (count > 0) return;
20737
21147
  const stmt = this.db.prepare(
20738
21148
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
20739
21149
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
20740
21150
  );
20741
- this.db.exec("BEGIN");
20742
- try {
21151
+ failOpenTransaction(this.db, () => {
20743
21152
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
20744
21153
  stmt.run({
20745
21154
  id: randomUUID4(),
@@ -20748,10 +21157,41 @@ var SqlitePoliciesRepository = class {
20748
21157
  now: Date.now()
20749
21158
  });
20750
21159
  }
20751
- this.db.exec("COMMIT");
20752
- } catch {
20753
- this.db.exec("ROLLBACK");
20754
- }
21160
+ });
21161
+ }
21162
+ // Insert-or-update the single global per-category policy row, keyed on the
21163
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21164
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21165
+ // values), so the runtime's resolveAction reads rows written by either path
21166
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21167
+ // `id` and `created_at` are left exactly as they were.
21168
+ upsertCategoryAction(category, action) {
21169
+ const now = Date.now();
21170
+ this.db.prepare(
21171
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21172
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21173
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21174
+ ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
21175
+ }
21176
+ // Caps every global per-category policy currently set to block/redact down
21177
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21178
+ // Returns the number of rows changed.
21179
+ capCategoryActions() {
21180
+ const info = this.db.prepare(
21181
+ `UPDATE policies SET action='warn', updated_at=:now
21182
+ WHERE scope='global' AND action IN ('block','redact')
21183
+ AND json_extract(target,'$.category') IS NOT NULL`
21184
+ ).run({ now: Date.now() });
21185
+ return Number(info.changes);
21186
+ }
21187
+ // Read the current action for a single global per-category policy row, mirroring
21188
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21189
+ // row exists yet, so callers can distinguish an unset category from a set one.
21190
+ getCategoryAction(category) {
21191
+ const row = this.db.prepare(
21192
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21193
+ ).get({ category });
21194
+ return row?.action;
20755
21195
  }
20756
21196
  };
20757
21197
 
@@ -20827,7 +21267,7 @@ var SqliteProjectFilesRepository = class {
20827
21267
  maxStampStmt;
20828
21268
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
20829
21269
  replaceForProject(projectId, scan2, now) {
20830
- const { maxStamp } = this.maxStampStmt.get({ projectId });
21270
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
20831
21271
  const stamp = Math.max(now, maxStamp + 1);
20832
21272
  for (const file2 of scan2.files) {
20833
21273
  this.upsertStmt.run({
@@ -20910,7 +21350,7 @@ var SqliteResolutionsRepository = class {
20910
21350
  }
20911
21351
  /** The newest disposition recorded for a finding key, or undefined if none. */
20912
21352
  latestByKey(key) {
20913
- const row = this.latestStmt.get({ findingKey: key });
21353
+ const row = getRow(this.latestStmt, { findingKey: key });
20914
21354
  if (!row) return void 0;
20915
21355
  return {
20916
21356
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -20927,7 +21367,7 @@ var SqliteResolutionsRepository = class {
20927
21367
  * the CLI) surfaces for that file.
20928
21368
  */
20929
21369
  openAtRestKeysForPath(path) {
20930
- const rows = this.openAtRestStmt.all({ path });
21370
+ const rows = allRows(this.openAtRestStmt, { path });
20931
21371
  return rows.map((r) => r.finding_key);
20932
21372
  }
20933
21373
  /**
@@ -20938,7 +21378,7 @@ var SqliteResolutionsRepository = class {
20938
21378
  * resolution row (see scan.ts).
20939
21379
  */
20940
21380
  resolvedAtRestKeysForPath(path) {
20941
- const rows = this.resolvedAtRestStmt.all({ path });
21381
+ const rows = allRows(this.resolvedAtRestStmt, { path });
20942
21382
  return rows.map((r) => r.finding_key);
20943
21383
  }
20944
21384
  };
@@ -20967,31 +21407,25 @@ var SqliteScanLedgerRepository = class {
20967
21407
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
20968
21408
  // older ruleset are simply absent, which reads as "never scanned".
20969
21409
  entriesForRuleset(rulesetHash) {
20970
- const rows = this.readStmt.all({ rulesetHash });
21410
+ const rows = allRows(this.readStmt, {
21411
+ rulesetHash
21412
+ });
20971
21413
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
20972
21414
  }
20973
21415
  upsertEntries(entries) {
20974
21416
  if (entries.length === 0) return;
20975
21417
  const scannedAt = Date.now();
20976
- try {
20977
- this.db.exec("BEGIN");
20978
- try {
20979
- for (const entry of entries) {
20980
- this.upsertStmt.run({
20981
- path: entry.path,
20982
- mtime: entry.mtime,
20983
- contentHash: entry.contentHash,
20984
- rulesetHash: entry.rulesetHash,
20985
- scannedAt
20986
- });
20987
- }
20988
- this.db.exec("COMMIT");
20989
- } catch (err) {
20990
- this.db.exec("ROLLBACK");
20991
- throw err;
21418
+ failOpenTransaction(this.db, () => {
21419
+ for (const entry of entries) {
21420
+ this.upsertStmt.run({
21421
+ path: entry.path,
21422
+ mtime: entry.mtime,
21423
+ contentHash: entry.contentHash,
21424
+ rulesetHash: entry.rulesetHash,
21425
+ scannedAt
21426
+ });
20992
21427
  }
20993
- } catch {
20994
- }
21428
+ });
20995
21429
  }
20996
21430
  };
20997
21431
 
@@ -21068,8 +21502,9 @@ var SqliteSecurityRepository = class {
21068
21502
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21069
21503
  // double-counting a key that accumulated several append-only rows.
21070
21504
  severitySummary() {
21071
- const rows = this.db.prepare(
21072
- `SELECT f.severity AS severity,
21505
+ const rows = allRows(
21506
+ this.db.prepare(
21507
+ `SELECT f.severity AS severity,
21073
21508
  COUNT(*) AS count,
21074
21509
  SUM(CASE
21075
21510
  WHEN e.kind != 'code_change' THEN 1
@@ -21088,7 +21523,8 @@ var SqliteSecurityRepository = class {
21088
21523
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21089
21524
  ON latest.finding_key = f.finding_key
21090
21525
  GROUP BY f.severity`
21091
- ).all();
21526
+ )
21527
+ );
21092
21528
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21093
21529
  const bySeverity = SEVERITIES.map((severity) => ({
21094
21530
  severity,
@@ -21172,14 +21608,15 @@ var SqliteSecurityRepository = class {
21172
21608
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21173
21609
  const now = this.now();
21174
21610
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
21175
- const rows = this.db.prepare(
21176
- // first_detected_at is the PRESERVED first-detection time (set once on a
21177
- // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21178
- // measures from first sighting not the latest re-scan's event, whose
21179
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21180
- // the parent event's occurred_at defends against any legacy/edge row the
21181
- // backfill left null.
21182
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21611
+ const rows = allRows(
21612
+ this.db.prepare(
21613
+ // first_detected_at is the PRESERVED first-detection time (set once on a
21614
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21615
+ // measures from first sighting not the latest re-scan's event, whose
21616
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21617
+ // the parent event's occurred_at defends against any legacy/edge row the
21618
+ // backfill left null.
21619
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21183
21620
  (
21184
21621
  SELECT fr.status FROM finding_resolution fr
21185
21622
  WHERE fr.finding_key = f.finding_key
@@ -21205,14 +21642,16 @@ var SqliteSecurityRepository = class {
21205
21642
  WHERE fr.finding_key = f.finding_key
21206
21643
  AND fr.resolved_at >= :windowStart
21207
21644
  )`
21208
- // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21209
- // any resolution activity at/after the window start — a row this method
21210
- // ultimately counts has its LATEST resolution inside the window, which
21211
- // implies such a row exists, so nothing wanted is dropped. The exact
21212
- // latest-wins + status/method + window gate stays in JS below,
21213
- // dialect-agnostic. Without this, a
21214
- // 7d request evaluated the store's entire trackable-findings history.
21215
- ).all({ windowStart });
21645
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21646
+ // any resolution activity at/after the window start — a row this method
21647
+ // ultimately counts has its LATEST resolution inside the window, which
21648
+ // implies such a row exists, so nothing wanted is dropped. The exact
21649
+ // latest-wins + status/method + window gate stays in JS below,
21650
+ // dialect-agnostic. Without this, a
21651
+ // 7d request evaluated the store's entire trackable-findings history.
21652
+ ),
21653
+ { windowStart }
21654
+ );
21216
21655
  const sums = /* @__PURE__ */ new Map();
21217
21656
  const counts = /* @__PURE__ */ new Map();
21218
21657
  for (const r of rows) {
@@ -21242,8 +21681,9 @@ var SqliteSecurityRepository = class {
21242
21681
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
21243
21682
  const now = this.now();
21244
21683
  const from = now - RANGE_DAYS[range] * DAY_MS4;
21245
- const rows = this.db.prepare(
21246
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21684
+ const rows = allRows(
21685
+ this.db.prepare(
21686
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21247
21687
  FROM findings f JOIN events e ON e.id = f.event_id
21248
21688
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21249
21689
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -21251,7 +21691,9 @@ var SqliteSecurityRepository = class {
21251
21691
  GROUP BY repo
21252
21692
  ORDER BY c DESC, repo
21253
21693
  LIMIT :limit`
21254
- ).all({ from, to: now, limit });
21694
+ ),
21695
+ { from, to: now, limit }
21696
+ );
21255
21697
  const items = rows.map((r) => ({
21256
21698
  id: `repo_${r.repo}`,
21257
21699
  name: r.repo,
@@ -21273,8 +21715,9 @@ var SqliteSecurityRepository = class {
21273
21715
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21274
21716
  // capped at `limit`.
21275
21717
  recentlyResolved(limit = 20) {
21276
- const rows = this.db.prepare(
21277
- `SELECT f.finding_key AS finding_key,
21718
+ const rows = allRows(
21719
+ this.db.prepare(
21720
+ `SELECT f.finding_key AS finding_key,
21278
21721
  f.rule_id AS rule_id,
21279
21722
  f.severity AS severity,
21280
21723
  json_extract(e.metadata, '$.filePath') AS path,
@@ -21308,7 +21751,9 @@ var SqliteSecurityRepository = class {
21308
21751
  ) IS NOT NULL
21309
21752
  ORDER BY latest_resolved_at DESC
21310
21753
  LIMIT :limit`
21311
- ).all({ limit });
21754
+ ),
21755
+ { limit }
21756
+ );
21312
21757
  const items = rows.map((r) => ({
21313
21758
  findingKey: r.finding_key,
21314
21759
  ruleId: r.rule_id,
@@ -21325,12 +21770,15 @@ var SqliteSecurityRepository = class {
21325
21770
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
21326
21771
  // numeric and the JS aggregations bucket/split on ms directly.
21327
21772
  findingsInRange(fromMs, toMs) {
21328
- const rows = this.db.prepare(
21329
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21773
+ const rows = allRows(
21774
+ this.db.prepare(
21775
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21330
21776
  FROM findings f JOIN events e ON e.id = f.event_id
21331
21777
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21332
21778
  ORDER BY e.occurred_at`
21333
- ).all({ from: fromMs, to: toMs });
21779
+ ),
21780
+ { from: fromMs, to: toMs }
21781
+ );
21334
21782
  return rows.map((r) => ({
21335
21783
  occurredAt: r.occurred_at,
21336
21784
  severity: r.severity,
@@ -21344,12 +21792,7 @@ import { randomUUID as randomUUID7 } from "crypto";
21344
21792
  var KIND_ORDER = ["provider", "internal", "ip"];
21345
21793
  var CALL_SITE_EMBED_CAP = 200;
21346
21794
  function parseNetwork(networkJson) {
21347
- if (!networkJson) return null;
21348
- try {
21349
- return JSON.parse(networkJson);
21350
- } catch {
21351
- return null;
21352
- }
21795
+ return safeJson(networkJson, null);
21353
21796
  }
21354
21797
  function toEndpointSummary(row) {
21355
21798
  return {
@@ -21436,27 +21879,39 @@ var SqliteSharesRepository = class {
21436
21879
  }
21437
21880
  db;
21438
21881
  stats() {
21439
- const scalar = (sql) => this.db.prepare(sql).get()?.n ?? 0;
21440
- const destinations = scalar("SELECT count(*) AS n FROM share_destination");
21441
- const endpoints = scalar("SELECT count(*) AS n FROM share_endpoint");
21442
- const callSites = scalar("SELECT count(*) AS n FROM share_call_site");
21443
- const insecure = scalar(
21882
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
21883
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
21884
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
21885
+ const insecure = countScalar(
21886
+ this.db,
21444
21887
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
21445
21888
  );
21446
- const needsReview = scalar(
21889
+ const needsReview = countScalar(
21890
+ this.db,
21447
21891
  `SELECT count(DISTINCT d.id) AS n
21448
21892
  FROM share_destination d
21449
21893
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
21450
21894
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
21451
21895
  );
21452
- const byKind = { provider: 0, internal: 0, ip: 0 };
21453
- for (const row of this.db.prepare("SELECT kind, count(*) AS n FROM share_destination GROUP BY kind").all()) {
21454
- byKind[row.kind] = row.n;
21455
- }
21456
- const byTrust = { recognized: 0, internal: 0, unverified: 0, ip: 0 };
21457
- for (const row of this.db.prepare("SELECT trust, count(*) AS n FROM share_destination GROUP BY trust").all()) {
21458
- byTrust[row.trust] = row.n;
21459
- }
21896
+ const kindCounts = countBy(
21897
+ this.db,
21898
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
21899
+ );
21900
+ const byKind = {
21901
+ provider: kindCounts.get("provider") ?? 0,
21902
+ internal: kindCounts.get("internal") ?? 0,
21903
+ ip: kindCounts.get("ip") ?? 0
21904
+ };
21905
+ const trustCounts = countBy(
21906
+ this.db,
21907
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
21908
+ );
21909
+ const byTrust = {
21910
+ recognized: trustCounts.get("recognized") ?? 0,
21911
+ internal: trustCounts.get("internal") ?? 0,
21912
+ unverified: trustCounts.get("unverified") ?? 0,
21913
+ ip: trustCounts.get("ip") ?? 0
21914
+ };
21460
21915
  return Promise.resolve({
21461
21916
  destinations,
21462
21917
  endpoints,
@@ -21572,7 +22027,7 @@ var SqliteSharesRepository = class {
21572
22027
  }
21573
22028
  let sql;
21574
22029
  if (q) {
21575
- const pattern = `%${escapeLikePattern(q)}%`;
22030
+ const pattern = containsPattern(q);
21576
22031
  conditions.push(
21577
22032
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
21578
22033
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -21592,24 +22047,31 @@ var SqliteSharesRepository = class {
21592
22047
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
21593
22048
  ORDER BY d.created_at ASC, d.id ASC`;
21594
22049
  }
21595
- const rows = this.db.prepare(sql).all(...params);
22050
+ const rows = allRows(
22051
+ this.db.prepare(sql),
22052
+ params
22053
+ );
21596
22054
  return rows.map((r) => this.mapDestRow(r));
21597
22055
  }
21598
22056
  fetchDestinationById(destinationId) {
21599
- const row = this.db.prepare(
21600
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22057
+ const row = getRow(
22058
+ this.db.prepare(
22059
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
21601
22060
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
21602
22061
  o.decision AS overrideDecision
21603
22062
  FROM share_destination d
21604
22063
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
21605
22064
  WHERE d.id = ?`
21606
- ).get(destinationId);
22065
+ ),
22066
+ [destinationId]
22067
+ );
21607
22068
  return row ? this.mapDestRow(row) : null;
21608
22069
  }
21609
22070
  fetchEndpoints(destinationIds) {
21610
22071
  if (destinationIds.length === 0) return [];
21611
- const rows = this.db.prepare(
21612
- `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
22072
+ const rows = allRows(
22073
+ this.db.prepare(
22074
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
21613
22075
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
21614
22076
  count(c.id) AS callSiteCount
21615
22077
  FROM share_endpoint e
@@ -21617,7 +22079,9 @@ var SqliteSharesRepository = class {
21617
22079
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
21618
22080
  GROUP BY e.id
21619
22081
  ORDER BY e.created_at ASC, e.id ASC`
21620
- ).all(...destinationIds);
22082
+ ),
22083
+ destinationIds
22084
+ );
21621
22085
  return rows.map((r) => ({
21622
22086
  id: r.id,
21623
22087
  destinationId: r.destinationId,
@@ -21642,13 +22106,16 @@ var SqliteSharesRepository = class {
21642
22106
  }
21643
22107
  fetchCallSites(endpointIds) {
21644
22108
  if (endpointIds.length === 0) return [];
21645
- const rows = this.db.prepare(
21646
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22109
+ const rows = allRows(
22110
+ this.db.prepare(
22111
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
21647
22112
  project_id AS projectId
21648
22113
  FROM share_call_site
21649
22114
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
21650
22115
  ORDER BY created_at ASC, id ASC`
21651
- ).all(...endpointIds);
22116
+ ),
22117
+ endpointIds
22118
+ );
21652
22119
  return rows.map((r) => ({
21653
22120
  id: r.id,
21654
22121
  endpointId: r.endpointId,
@@ -21684,27 +22151,36 @@ var SqliteSourceProjectRepository = class {
21684
22151
  upsert(input, now = Date.now()) {
21685
22152
  const id = sourceProjectId(input.url);
21686
22153
  const row = toSourceProjectRow(input, id, now);
21687
- this.upsertStmt.run({
21688
- id: row.id,
21689
- url: row.url ?? null,
21690
- name: row.name ?? null,
21691
- attributes: row.attributes,
21692
- firstSeen: row.firstSeen,
21693
- lastSeen: row.lastSeen
21694
- });
22154
+ this.upsertStmt.run(
22155
+ bindParams({
22156
+ id: row.id,
22157
+ url: row.url,
22158
+ name: row.name,
22159
+ attributes: row.attributes,
22160
+ firstSeen: row.firstSeen,
22161
+ lastSeen: row.lastSeen
22162
+ })
22163
+ );
21695
22164
  return id;
21696
22165
  }
21697
22166
  findById(id) {
21698
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22167
+ return getRow(
22168
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22169
+ {
22170
+ id
22171
+ }
22172
+ );
21699
22173
  }
21700
22174
  // Distinct project names — a filter facet, served from the source_project
21701
22175
  // table, never from the audit fact table.
21702
22176
  distinctNames() {
21703
- const rows = this.db.prepare(
21704
- `SELECT DISTINCT name FROM source_project
22177
+ const rows = allRows(
22178
+ this.db.prepare(
22179
+ `SELECT DISTINCT name FROM source_project
21705
22180
  WHERE name IS NOT NULL
21706
22181
  ORDER BY name`
21707
- ).all();
22182
+ )
22183
+ );
21708
22184
  return rows.map((r) => r.name);
21709
22185
  }
21710
22186
  };
@@ -21723,8 +22199,7 @@ function hasLegacySampleRows(db) {
21723
22199
  function purgeSampleData(db) {
21724
22200
  try {
21725
22201
  if (!hasLegacySampleRows(db)) return;
21726
- db.exec("BEGIN");
21727
- try {
22202
+ withTransaction(db, () => {
21728
22203
  db.exec(
21729
22204
  `DELETE FROM share_call_site WHERE endpoint_id IN (
21730
22205
  SELECT e.id FROM share_endpoint e
@@ -21769,11 +22244,7 @@ function purgeSampleData(db) {
21769
22244
  value TEXT NOT NULL
21770
22245
  )`);
21771
22246
  db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
21772
- db.exec("COMMIT");
21773
- } catch (err) {
21774
- db.exec("ROLLBACK");
21775
- throw err;
21776
- }
22247
+ });
21777
22248
  } catch {
21778
22249
  }
21779
22250
  }
@@ -21792,7 +22263,7 @@ function openWithPragmas(file2) {
21792
22263
  function backupLegacyStore(file2) {
21793
22264
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
21794
22265
  renameSync(file2, backup);
21795
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
22266
+ for (const sidecar of walSidecars(file2)) {
21796
22267
  if (existsSync(sidecar)) rmSync(sidecar);
21797
22268
  }
21798
22269
  return backup;
@@ -21805,9 +22276,8 @@ function openLocalDatabase(dir) {
21805
22276
  db.close();
21806
22277
  const backup = backupLegacyStore(file2);
21807
22278
  db = openWithPragmas(file2);
21808
- process.stderr.write(
21809
- `[aka] Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.
21810
- `
22279
+ akaWarn(
22280
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
21811
22281
  );
21812
22282
  }
21813
22283
  applyMigrations(db);
@@ -21835,96 +22305,77 @@ function openLocalDatabase(dir) {
21835
22305
  const configInventory = new SqliteConfigInventoryRepository(db);
21836
22306
  policies.seedDefaults();
21837
22307
  function recordCapture(event, detected) {
21838
- try {
21839
- db.exec("BEGIN");
21840
- try {
21841
- events.insertEvent(event);
21842
- const sessionId = event.metadata?.sessionId;
21843
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
21844
- db.exec("COMMIT");
21845
- } catch (err) {
21846
- db.exec("ROLLBACK");
21847
- throw err;
21848
- }
21849
- } catch {
21850
- }
22308
+ failOpenTransaction(db, () => {
22309
+ events.insertEvent(event);
22310
+ const sessionId = event.metadata?.sessionId;
22311
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
22312
+ });
21851
22313
  }
21852
22314
  function ensureInventory(ctx) {
21853
22315
  const resolved = {};
21854
- try {
21855
- db.exec("BEGIN");
21856
- try {
21857
- const now = Date.now();
21858
- if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
21859
- if (ctx.harness) {
21860
- resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
21861
- }
21862
- resolved.accountId = inventory.upsert(
21863
- linkHost(
21864
- {
21865
- objectType: "user",
21866
- identityKey: "local",
21867
- attributes: { source: "local" }
21868
- },
21869
- resolved.hostId
21870
- ),
21871
- now
21872
- );
21873
- if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
21874
- db.exec("COMMIT");
21875
- } catch (err) {
21876
- db.exec("ROLLBACK");
21877
- throw err;
21878
- }
21879
- } catch {
21880
- return {};
21881
- }
21882
- return resolved;
22316
+ const committed = failOpenTransaction(db, () => {
22317
+ const now = Date.now();
22318
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22319
+ if (ctx.harness) {
22320
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
22321
+ }
22322
+ resolved.accountId = inventory.upsert(
22323
+ linkHost(
22324
+ {
22325
+ objectType: "user",
22326
+ identityKey: "local",
22327
+ attributes: { source: "local" }
22328
+ },
22329
+ resolved.hostId
22330
+ ),
22331
+ now
22332
+ );
22333
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22334
+ });
22335
+ return committed ? resolved : {};
21883
22336
  }
21884
22337
  function recordConfigScan(record2) {
21885
- try {
21886
- db.exec("BEGIN");
21887
- try {
21888
- const now = isoToEpochMillis(record2.scanEvent.startedAt);
21889
- for (const item of record2.items) inventory.upsert(item, now);
21890
- auditEvents.insertAuditEvent(record2.scanEvent);
21891
- const definitionIds = /* @__PURE__ */ new Map();
21892
- for (const def of record2.definitions ?? []) {
21893
- definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
21894
- }
21895
- for (const finding of record2.findings ?? []) {
21896
- const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
21897
- if (!definitionId) continue;
21898
- inspectionFindings.insertFinding({
21899
- id: randomUUID8(),
21900
- auditEventId: record2.scanEvent.id,
21901
- inspectionDefinitionId: definitionId,
21902
- span: finding.span,
21903
- maskedMatch: finding.maskedMatch,
21904
- actionTaken: finding.actionTaken,
21905
- confidence: finding.confidence
21906
- });
21907
- }
21908
- db.exec("COMMIT");
21909
- } catch (err) {
21910
- db.exec("ROLLBACK");
21911
- throw err;
22338
+ failOpenTransaction(db, () => {
22339
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
22340
+ for (const item of record2.items) inventory.upsert(item, now);
22341
+ auditEvents.insertAuditEvent(record2.scanEvent);
22342
+ const definitionIds = /* @__PURE__ */ new Map();
22343
+ for (const def of record2.definitions ?? []) {
22344
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22345
+ }
22346
+ for (const finding of record2.findings ?? []) {
22347
+ const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
22348
+ if (!definitionId) continue;
22349
+ inspectionFindings.insertFinding({
22350
+ id: randomUUID8(),
22351
+ auditEventId: record2.scanEvent.id,
22352
+ inspectionDefinitionId: definitionId,
22353
+ span: finding.span,
22354
+ maskedMatch: finding.maskedMatch,
22355
+ actionTaken: finding.actionTaken,
22356
+ confidence: finding.confidence
22357
+ });
21912
22358
  }
21913
- } catch {
21914
- }
22359
+ });
21915
22360
  }
21916
22361
  function recordProjectFiles(projectId, scan2) {
21917
22362
  if (scan2.files.length === 0) return;
22363
+ failOpenTransaction(db, () => {
22364
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
22365
+ });
22366
+ }
22367
+ async function transaction(fn) {
22368
+ db.exec("BEGIN");
21918
22369
  try {
21919
- db.exec("BEGIN");
22370
+ const result = await fn();
22371
+ db.exec("COMMIT");
22372
+ return result;
22373
+ } catch (err) {
21920
22374
  try {
21921
- projectFiles.replaceForProject(projectId, scan2, Date.now());
21922
- db.exec("COMMIT");
21923
- } catch (err) {
21924
22375
  db.exec("ROLLBACK");
21925
- throw err;
22376
+ } catch {
21926
22377
  }
21927
- } catch {
22378
+ throw err;
21928
22379
  }
21929
22380
  }
21930
22381
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -21943,8 +22394,7 @@ function openLocalDatabase(dir) {
21943
22394
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
21944
22395
  });
21945
22396
  if (stale.length === 0) return;
21946
- db.exec("BEGIN");
21947
- try {
22397
+ withTransaction(db, () => {
21948
22398
  for (const { id } of stale) {
21949
22399
  db.prepare(
21950
22400
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -21956,11 +22406,7 @@ function openLocalDatabase(dir) {
21956
22406
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
21957
22407
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
21958
22408
  }
21959
- db.exec("COMMIT");
21960
- } catch (err) {
21961
- db.exec("ROLLBACK");
21962
- throw err;
21963
- }
22409
+ });
21964
22410
  } catch {
21965
22411
  }
21966
22412
  }
@@ -22002,6 +22448,7 @@ function openLocalDatabase(dir) {
22002
22448
  purgeSampleData: () => {
22003
22449
  purgeSampleData(db);
22004
22450
  },
22451
+ transaction,
22005
22452
  close: () => {
22006
22453
  db.close();
22007
22454
  }
@@ -22122,12 +22569,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
22122
22569
  }
22123
22570
  }
22124
22571
  function readJson(file2) {
22572
+ let text;
22125
22573
  try {
22126
- const parsed = JSON.parse(readFileSync2(file2, "utf8"));
22127
- return typeof parsed === "object" && parsed !== null ? parsed : null;
22574
+ text = readFileSync2(file2, "utf8");
22128
22575
  } catch {
22129
22576
  return null;
22130
22577
  }
22578
+ return parseJsonObject(text) ?? null;
22579
+ }
22580
+
22581
+ // ../../packages/persistence/src/warn-era-cap.ts
22582
+ import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
22583
+ import { join as join5 } from "path";
22584
+ var MARKER = "warn-era-capped";
22585
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22586
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
22587
+ const marker = join5(dataDir2, MARKER);
22588
+ if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22589
+ const capped = db.policies.capCategoryActions();
22590
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
22591
+ `, { mode: DATA_FILE_MODE });
22592
+ return { capped };
22131
22593
  }
22132
22594
 
22133
22595
  // ../../packages/plugin-sdk/src/provider-env.ts
@@ -22212,7 +22674,7 @@ function resolveProviderSafe() {
22212
22674
  // ../../packages/plugin-sdk/src/config-inventory.ts
22213
22675
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22214
22676
  import { homedir as homedir2 } from "os";
22215
- import { basename as basename2, join as join6 } from "path";
22677
+ import { basename as basename2, join as join7 } from "path";
22216
22678
 
22217
22679
  // ../../packages/detections/src/matchers/keyword.ts
22218
22680
  var KeywordMatcher2 = class {
@@ -24591,8 +25053,8 @@ function scanText(text) {
24591
25053
  }
24592
25054
 
24593
25055
  // ../../packages/plugin-sdk/src/repo.ts
24594
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
24595
- import { basename, dirname, isAbsolute, join as join5, sep as sep2 } from "path";
25056
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25057
+ import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
24596
25058
  function resolveRepoIdentity(cwd) {
24597
25059
  try {
24598
25060
  const root = findGitRoot(cwd);
@@ -24625,32 +25087,32 @@ function resolveRepoNwo(cwd) {
24625
25087
  function findGitRoot(start) {
24626
25088
  let dir = start;
24627
25089
  for (; ; ) {
24628
- if (existsSync2(join5(dir, ".git"))) return dir;
25090
+ if (existsSync3(join6(dir, ".git"))) return dir;
24629
25091
  const parent = dirname(dir);
24630
25092
  if (parent === dir) return void 0;
24631
25093
  dir = parent;
24632
25094
  }
24633
25095
  }
24634
25096
  function resolveGitContext(root) {
24635
- const dotGit = join5(root, ".git");
25097
+ const dotGit = join6(root, ".git");
24636
25098
  try {
24637
25099
  if (statSync(dotGit).isDirectory()) {
24638
- return { configPath: join5(dotGit, "config"), headRoot: root };
25100
+ return { configPath: join6(dotGit, "config"), headRoot: root };
24639
25101
  }
24640
25102
  } catch {
24641
25103
  return void 0;
24642
25104
  }
24643
25105
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
24644
25106
  if (!target) return void 0;
24645
- const gitdir = isAbsolute(target) ? target : join5(root, target);
24646
- if (existsSync2(join5(gitdir, "config"))) {
24647
- return { configPath: join5(gitdir, "config"), headRoot: root };
25107
+ const gitdir = isAbsolute(target) ? target : join6(root, target);
25108
+ if (existsSync3(join6(gitdir, "config"))) {
25109
+ return { configPath: join6(gitdir, "config"), headRoot: root };
24648
25110
  }
24649
- const commonRaw = safeRead(join5(gitdir, "commondir"))?.trim();
25111
+ const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
24650
25112
  if (!commonRaw) return void 0;
24651
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join5(gitdir, commonRaw);
25113
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
24652
25114
  const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
24653
- return { configPath: join5(commonGitDir, "config"), headRoot };
25115
+ return { configPath: join6(commonGitDir, "config"), headRoot };
24654
25116
  }
24655
25117
  function safeRead(path) {
24656
25118
  try {
@@ -24767,16 +25229,57 @@ function resolveInventoryContext(input) {
24767
25229
  }
24768
25230
 
24769
25231
  // ../../packages/plugin-sdk/src/nudge.ts
24770
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
24771
- import { join as join7 } from "path";
25232
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25233
+ import { join as join8 } from "path";
24772
25234
 
24773
25235
  // ../../packages/plugin-sdk/src/project-files.ts
24774
25236
  var import_ignore = __toESM(require_ignore(), 1);
24775
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
24776
- import { basename as basename3, join as join8, relative, sep as sep3 } from "path";
25237
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25238
+ import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
25239
+
25240
+ // ../../packages/plugin-sdk/src/raw-egress.ts
25241
+ var RawEgressError = class extends Error {
25242
+ constructor(message) {
25243
+ super(message);
25244
+ this.name = "RawEgressError";
25245
+ }
25246
+ };
25247
+ var MIN_RAW_LEN = 4;
25248
+ function maskContextSlice(slice, sliceStart, hits) {
25249
+ const findings = [];
25250
+ for (const h of hits) {
25251
+ const start = Math.max(0, h.span.start - sliceStart);
25252
+ const end = Math.min(slice.length, h.span.end - sliceStart);
25253
+ if (end > start) {
25254
+ findings.push({
25255
+ ruleId: "raw-egress",
25256
+ category: "secret",
25257
+ severity: "critical",
25258
+ span: { start, end },
25259
+ rawMatch: "",
25260
+ confidence: 1
25261
+ });
25262
+ }
25263
+ }
25264
+ const masked = findings.length > 0 ? redact(slice, findings) : slice;
25265
+ for (const h of hits) {
25266
+ if (h.rawMatch.length >= MIN_RAW_LEN && masked.includes(h.rawMatch)) {
25267
+ throw new RawEgressError("raw match survived context masking");
25268
+ }
25269
+ }
25270
+ return masked;
25271
+ }
25272
+ function safeMaskedMatch(rawMatch) {
25273
+ const masked = maskMatch(rawMatch);
25274
+ if (masked === rawMatch || rawMatch.length >= MIN_RAW_LEN && masked.includes(rawMatch)) {
25275
+ return "***";
25276
+ }
25277
+ return masked;
25278
+ }
24777
25279
 
24778
25280
  // ../../packages/plugin-sdk/src/runtime.ts
24779
25281
  import { randomUUID as randomUUID10 } from "crypto";
25282
+ var ENFORCEMENT_CEILING_ENABLED = false;
24780
25283
  var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
24781
25284
  function entryIsActive(entry, now) {
24782
25285
  if (entry.expiresAt !== null && Date.parse(entry.expiresAt) <= now) return false;
@@ -24857,7 +25360,7 @@ function createPluginRuntime(gateway, settings, opts) {
24857
25360
  const action = actionFor(finding);
24858
25361
  if (ACTION_PRIORITY.indexOf(action) < ACTION_PRIORITY.indexOf(worst)) worst = action;
24859
25362
  }
24860
- if (policyMode === "warn" && (worst === "block" || worst === "redact")) {
25363
+ if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (worst === "block" || worst === "redact")) {
24861
25364
  return { action: "warn", text, findings };
24862
25365
  }
24863
25366
  if (worst === "block") return { action: "block", text: null, findings };
@@ -24879,7 +25382,7 @@ function createPluginRuntime(gateway, settings, opts) {
24879
25382
  const excepted = /* @__PURE__ */ new Set();
24880
25383
  const exceptionIds = [];
24881
25384
  try {
24882
- if (policyMode === "warn") return { excepted, exceptionIds };
25385
+ if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn") return { excepted, exceptionIds };
24883
25386
  const enforced = findings.filter((f) => {
24884
25387
  const action = resolveAction(f.ruleId, f.category);
24885
25388
  return action === "block" || action === "redact";
@@ -25050,9 +25553,12 @@ function createPluginRuntime(gateway, settings, opts) {
25050
25553
  return { processText, capture, rulesetFingerprint, close };
25051
25554
  }
25052
25555
 
25556
+ // ../../packages/plugin-sdk/src/suppressions.ts
25557
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
25558
+
25053
25559
  // ../../packages/plugin-sdk/src/throttle.ts
25054
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
25055
- import { join as join9 } from "path";
25560
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
25561
+ import { join as join10 } from "path";
25056
25562
 
25057
25563
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
25058
25564
  import { randomUUID as randomUUID11 } from "crypto";
@@ -25245,6 +25751,14 @@ var StandaloneDataGateway = class {
25245
25751
  sweepTerminalExceptions(retentionMs) {
25246
25752
  return this.db.exceptions.sweepTerminal(retentionMs);
25247
25753
  }
25754
+ // The warn-era enforcement cap, standalone-only store maintenance invoked
25755
+ // from SessionStart, not part of the DataGateway port. Returns the number
25756
+ // of block/redact rows capped to warn (0 for a redact-policy store or an
25757
+ // already-capped one).
25758
+ capWarnEraEnforcement(policyMode) {
25759
+ const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
25760
+ return { capped };
25761
+ }
25248
25762
  // One project-file scan → the local project_file tree (one transaction inside
25249
25763
  // the LocalDatabase, fail-open there). Like the sweep above, this is
25250
25764
  // NOT part of the DataGateway port: the file tree is a local-store read model.
@@ -25344,9 +25858,9 @@ var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
25344
25858
  // src/history/transcripts.ts
25345
25859
  import { readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
25346
25860
  import { homedir as homedir3 } from "os";
25347
- import { join as join10 } from "path";
25861
+ import { join as join11 } from "path";
25348
25862
  function transcriptsDir() {
25349
- return join10(homedir3(), ".claude", "projects");
25863
+ return join11(homedir3(), ".claude", "projects");
25350
25864
  }
25351
25865
  function isRecord(value) {
25352
25866
  return typeof value === "object" && value !== null;
@@ -25362,7 +25876,7 @@ function extractText(content) {
25362
25876
  }
25363
25877
  return parts.join("\n");
25364
25878
  }
25365
- function parseTranscript(jsonl, sinceMs = 0) {
25879
+ function parseTranscript(jsonl, sinceMs = 0, beforeMs = Infinity) {
25366
25880
  const out = [];
25367
25881
  for (const line of jsonl.split("\n")) {
25368
25882
  const trimmed = line.trim();
@@ -25380,6 +25894,7 @@ function parseTranscript(jsonl, sinceMs = 0) {
25380
25894
  const occurredMs = Date.parse(occurredAt);
25381
25895
  if (Number.isNaN(occurredMs)) continue;
25382
25896
  if (sinceMs > 0 && occurredMs < sinceMs) continue;
25897
+ if (occurredMs >= beforeMs) continue;
25383
25898
  const message = rec.message;
25384
25899
  if (!isRecord(message)) continue;
25385
25900
  const text = extractText(message.content);
@@ -25585,7 +26100,7 @@ function parseTranscriptToolCalls(jsonl, sinceMs = 0) {
25585
26100
  return out;
25586
26101
  }
25587
26102
  var DAY_MS5 = 24 * 60 * 60 * 1e3;
25588
- function* iterateFileContents(dir) {
26103
+ function* iterateFileContents(dir, excludeSessionId) {
25589
26104
  let projects;
25590
26105
  try {
25591
26106
  projects = readdirSync3(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -25593,7 +26108,7 @@ function* iterateFileContents(dir) {
25593
26108
  return;
25594
26109
  }
25595
26110
  for (const project of projects) {
25596
- const projectDir = join10(dir, project);
26111
+ const projectDir = join11(dir, project);
25597
26112
  let files;
25598
26113
  try {
25599
26114
  files = readdirSync3(projectDir).filter((name) => name.endsWith(".jsonl"));
@@ -25601,9 +26116,11 @@ function* iterateFileContents(dir) {
25601
26116
  continue;
25602
26117
  }
25603
26118
  for (const file2 of files) {
26119
+ if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
26120
+ continue;
25604
26121
  let content;
25605
26122
  try {
25606
- content = readFileSync7(join10(projectDir, file2), "utf8");
26123
+ content = readFileSync7(join11(projectDir, file2), "utf8");
25607
26124
  } catch {
25608
26125
  continue;
25609
26126
  }
@@ -25617,8 +26134,9 @@ function windowStartMs(opts) {
25617
26134
  }
25618
26135
  function* iterateHistory(opts = {}) {
25619
26136
  const sinceMs = windowStartMs(opts);
25620
- for (const content of iterateFileContents(opts.dir ?? transcriptsDir()))
25621
- yield* parseTranscript(content, sinceMs);
26137
+ const beforeMs = opts.beforeMs ?? Infinity;
26138
+ for (const content of iterateFileContents(opts.dir ?? transcriptsDir(), opts.excludeSessionId))
26139
+ yield* parseTranscript(content, sinceMs, beforeMs);
25622
26140
  }
25623
26141
  function* iterateUsageAndToolCalls(opts = {}) {
25624
26142
  const sinceMs = windowStartMs(opts);
@@ -25631,7 +26149,35 @@ function* iterateUsageAndToolCalls(opts = {}) {
25631
26149
  }
25632
26150
 
25633
26151
  // src/history/scan.ts
25634
- async function scanHistory(config2, opts = {}) {
26152
+ var CONTEXT_RADIUS = 120;
26153
+ function redactOverlapping(rawContext, contextStart, others) {
26154
+ if (others.length === 0) return rawContext;
26155
+ try {
26156
+ return maskContextSlice(rawContext, contextStart, others);
26157
+ } catch {
26158
+ let safe = rawContext;
26159
+ for (const other of others) {
26160
+ if (other.rawMatch.length > 0) safe = safe.split(other.rawMatch).join("[REDACTED]");
26161
+ }
26162
+ return safe;
26163
+ }
26164
+ }
26165
+ function buildTriageHit(text, f, otherFindings = []) {
26166
+ const start = Math.max(0, f.span.start - CONTEXT_RADIUS);
26167
+ const end = Math.min(text.length, f.span.end + CONTEXT_RADIUS);
26168
+ const rawContext = text.slice(start, end);
26169
+ const overlapping = otherFindings.filter((o) => o.span.start < end && o.span.end > start);
26170
+ return {
26171
+ ruleId: f.ruleId,
26172
+ category: f.category,
26173
+ severity: f.severity,
26174
+ maskedMatch: safeMaskedMatch(f.rawMatch),
26175
+ rawMatch: f.rawMatch,
26176
+ context: redactOverlapping(rawContext, start, overlapping),
26177
+ confidence: f.confidence
26178
+ };
26179
+ }
26180
+ async function scanHistory(config2, opts = {}, onHit) {
25635
26181
  const windowDays = opts.windowDays ?? 30;
25636
26182
  if (config2.settings.historicalAccess !== "full") {
25637
26183
  return { consented: false, scanned: 0, skipped: 0, findings: 0, bySeverity: {}, windowDays };
@@ -25667,6 +26213,14 @@ async function scanHistory(config2, opts = {}) {
25667
26213
  for (const finding of result.findings) {
25668
26214
  findings++;
25669
26215
  bySeverity[finding.severity] = (bySeverity[finding.severity] ?? 0) + 1;
26216
+ if (onHit) {
26217
+ const otherFindings = result.findings.filter((other) => other !== finding);
26218
+ const hit = buildTriageHit(message.text, finding, otherFindings);
26219
+ try {
26220
+ onHit(hit);
26221
+ } catch {
26222
+ }
26223
+ }
25670
26224
  }
25671
26225
  }
25672
26226
  } finally {
@@ -25684,9 +26238,9 @@ import {
25684
26238
  openSync,
25685
26239
  readFileSync as readFileSync8,
25686
26240
  readSync,
25687
- writeFileSync as writeFileSync5
26241
+ writeFileSync as writeFileSync6
25688
26242
  } from "fs";
25689
- import { join as join11 } from "path";
26243
+ import { join as join12 } from "path";
25690
26244
 
25691
26245
  // src/history/usage.ts
25692
26246
  var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
@@ -25929,25 +26483,113 @@ function fenced(body) {
25929
26483
  }
25930
26484
 
25931
26485
  // src/backfill.ts
25932
- try {
25933
- const cfg = loadConfig();
25934
- if (cfg.settings.historicalAccess !== "full") {
25935
- process.stdout.write("Historical scan skipped \u2014 full review was not granted.\n");
25936
- process.exit(0);
25937
- }
25938
- const summary = await scanHistory(cfg);
26486
+ function triageSentinel(count, status) {
26487
+ return JSON.stringify({ done: true, count, status }) + "\n";
26488
+ }
26489
+ async function runBackfill(deps) {
26490
+ const { triage, io } = deps;
25939
26491
  try {
25940
- await reconcileHistory(cfg);
25941
- } catch {
25942
- }
25943
- const heading = "\u2713 Historical scan complete";
25944
- const scope = `Scanned ${String(summary.scanned)} messages from the last ${String(summary.windowDays)} days of Claude Code history.`;
25945
- const result = summary.findings > 0 ? `Found ${String(summary.findings)} pre-install finding${summary.findings === 1 ? "" : "s"} \u2014 review them with /findings.` : "No new pre-install secrets found in your history.";
25946
- process.stdout.write(`${fenced([heading, "", indent(scope), "", indent(result)].join("\n"))}
26492
+ const cfg = deps.loadConfig();
26493
+ if (cfg.settings.historicalAccess !== "full") {
26494
+ if (triage) {
26495
+ io.stdout(triageSentinel(0, "skipped:no-consent"));
26496
+ } else {
26497
+ io.stdout("Historical scan skipped \u2014 full review was not granted.\n");
26498
+ }
26499
+ return;
26500
+ }
26501
+ let fpKey = null;
26502
+ if (triage) {
26503
+ try {
26504
+ fpKey = loadOrCreateFingerprintKey(cfg.dataDir);
26505
+ } catch {
26506
+ fpKey = null;
26507
+ }
26508
+ }
26509
+ let count = 0;
26510
+ let onHitError;
26511
+ const summary = await deps.scanHistory(
26512
+ cfg,
26513
+ deps.guard ?? {},
26514
+ triage ? (hit) => {
26515
+ if (onHitError !== void 0) return;
26516
+ try {
26517
+ const enriched = {
26518
+ ...hit,
26519
+ id: String(count),
26520
+ valueFingerprint: fpKey ? fingerprintValue(fpKey, hit.rawMatch) : void 0,
26521
+ keyVersion: fpKey?.version
26522
+ };
26523
+ const validated = TriageHit.safeParse(enriched);
26524
+ if (!validated.success) {
26525
+ throw new Error("enriched triage hit failed TriageHit validation");
26526
+ }
26527
+ io.stdout(JSON.stringify(validated.data) + "\n");
26528
+ count += 1;
26529
+ } catch (err) {
26530
+ onHitError = err;
26531
+ }
26532
+ } : void 0
26533
+ );
26534
+ if (onHitError !== void 0) {
26535
+ throw onHitError instanceof Error ? onHitError : new Error(typeof onHitError === "string" ? onHitError : "triage stream write failed");
26536
+ }
26537
+ try {
26538
+ await deps.reconcileHistory(cfg);
26539
+ } catch {
26540
+ }
26541
+ if (triage) {
26542
+ io.stdout(triageSentinel(count, "complete"));
26543
+ } else {
26544
+ const heading = "\u2713 Historical scan complete";
26545
+ const scope = `Scanned ${String(summary.scanned)} messages from the last ${String(summary.windowDays)} days of Claude Code history.`;
26546
+ const result = summary.findings > 0 ? `Found ${String(summary.findings)} pre-install finding${summary.findings === 1 ? "" : "s"} \u2014 review them with /findings.` : "No new pre-install secrets found in your history.";
26547
+ io.stdout(`${fenced([heading, "", indent(scope), "", indent(result)].join("\n"))}
25947
26548
  `);
25948
- } catch {
25949
- process.stdout.write(
25950
- "AKA could not scan your history right now. It will still protect everything from here on.\n"
25951
- );
26549
+ }
26550
+ } catch (err) {
26551
+ if (triage) {
26552
+ io.stderr(`aka backfill --triage: history scan failed: ${String(err)}
26553
+ `);
26554
+ io.fail();
26555
+ } else {
26556
+ io.stdout(
26557
+ "AKA could not scan your history right now. It will still protect everything from here on.\n"
26558
+ );
26559
+ }
26560
+ }
25952
26561
  }
25953
- process.exit(0);
26562
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
26563
+ const triage = process.argv.includes("--triage");
26564
+ const startedAt = Date.now();
26565
+ const sessionId = process.env.CLAUDE_CODE_BRIDGE_SESSION_ID;
26566
+ await runBackfill({
26567
+ triage,
26568
+ io: {
26569
+ stdout: (chunk) => process.stdout.write(chunk),
26570
+ stderr: (chunk) => process.stderr.write(chunk),
26571
+ fail: () => {
26572
+ process.exitCode = 1;
26573
+ }
26574
+ },
26575
+ loadConfig,
26576
+ scanHistory,
26577
+ reconcileHistory,
26578
+ guard: {
26579
+ beforeMs: startedAt,
26580
+ ...sessionId ? { excludeSessionId: sessionId } : {}
26581
+ }
26582
+ });
26583
+ if (process.stdout.writableLength > 0) {
26584
+ await new Promise((resolve) => {
26585
+ process.stdout.write("", () => {
26586
+ resolve();
26587
+ });
26588
+ });
26589
+ }
26590
+ process.exit(process.exitCode ?? 0);
26591
+ }
26592
+ export {
26593
+ runBackfill,
26594
+ triageSentinel
26595
+ };