@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.
@@ -15430,6 +15430,11 @@ var AuditEventType = external_exports.enum([
15430
15430
  "prompt",
15431
15431
  "response",
15432
15432
  "code_change",
15433
+ // The events.kind of a scanned tool call, widened in to keep this a
15434
+ // superset. Narrower than 'tool_call' above and not a duplicate of it:
15435
+ // 'tool_call' is the reconciler's structural row for every call, while
15436
+ // 'tool_use' exists only where a hook enforced against the arguments.
15437
+ "tool_use",
15433
15438
  // One row per config-inventory scan, hung off the session root. It is the
15434
15439
  // fact the posture inspection findings reference (findings require an
15435
15440
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -15821,7 +15826,7 @@ var ActivityOverviewResponse = external_exports.object({
15821
15826
  }).meta({ id: "ActivityOverviewResponse" });
15822
15827
 
15823
15828
  // ../../packages/schema/src/zod/event.ts
15824
- var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
15829
+ var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15825
15830
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15826
15831
  var EventMetadata = external_exports.object({
15827
15832
  sessionId: external_exports.string().optional(),
@@ -16134,7 +16139,7 @@ var DetectionException = external_exports.object({
16134
16139
  justification: external_exports.string().min(1),
16135
16140
  conditions: ExceptionConditions.nullable(),
16136
16141
  createdBy: external_exports.string(),
16137
- createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
16142
+ createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
16138
16143
  createdAt: external_exports.iso.datetime(),
16139
16144
  updatedAt: external_exports.iso.datetime(),
16140
16145
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16303,20 +16308,26 @@ var PolicyBundle = external_exports.object({
16303
16308
  customKeywords: external_exports.array(external_exports.string()),
16304
16309
  fetchedAt: external_exports.iso.datetime()
16305
16310
  }).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
16311
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16319
16312
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
16313
+ var CATEGORY_PEAK_SEVERITY = {
16314
+ secret: "critical",
16315
+ financial: "critical",
16316
+ // core-financial/credit-card
16317
+ code_flaw: "critical",
16318
+ pii: "high",
16319
+ phi: "high",
16320
+ custom: "high",
16321
+ // user-defined; conservative
16322
+ code_context: "low",
16323
+ config: "low"
16324
+ // observe-only; floors to monitor regardless
16325
+ };
16326
+ function severityFloorPolicy(category) {
16327
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
16328
+ const peak = CATEGORY_PEAK_SEVERITY[category];
16329
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
16330
+ }
16320
16331
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
16321
16332
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
16322
16333
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -16343,6 +16354,12 @@ var BUILTIN_POLICY_SPECS = {
16343
16354
  description: "Refuse the request entirely whenever any rule in this detection matches."
16344
16355
  }
16345
16356
  };
16357
+ function builtinPolicyToAction(id) {
16358
+ return BUILTIN_POLICY_SPECS[id].action;
16359
+ }
16360
+ var DEFAULT_ACTIONS = Object.fromEntries(
16361
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
16362
+ );
16346
16363
  var BUILTIN_POLICIES = Object.fromEntries(
16347
16364
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
16348
16365
  );
@@ -16749,10 +16766,8 @@ function toApiProvider(sourceTool) {
16749
16766
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
16750
16767
  }
16751
16768
  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
- );
16769
+ function foldGroupStatus(instanceStatuses) {
16770
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
16756
16771
  if (statuses.size === 0) return void 0;
16757
16772
  for (const candidate of STATUS_PRECEDENCE) {
16758
16773
  if (statuses.has(candidate)) return candidate;
@@ -16770,6 +16785,7 @@ function deriveFindingStatus(row) {
16770
16785
  function buildFindingGroups(rows, opts = {}) {
16771
16786
  const overrides = opts.overrides;
16772
16787
  const packNames = opts.packNames;
16788
+ const aggregates = opts.aggregates;
16773
16789
  const byRuleId = /* @__PURE__ */ new Map();
16774
16790
  for (const row of rows) {
16775
16791
  const existing = byRuleId.get(row.ruleId);
@@ -16791,17 +16807,20 @@ function buildFindingGroups(rows, opts = {}) {
16791
16807
  status: r.status
16792
16808
  };
16793
16809
  });
16794
- const latestDetectedAt = ruleRows.reduce(
16810
+ const agg = aggregates?.get(ruleId);
16811
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
16795
16812
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
16796
16813
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
16797
16814
  );
16798
16815
  const seenProviders = /* @__PURE__ */ new Set();
16799
- const providers = instances.map((i) => i.provider).filter((p) => {
16816
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
16800
16817
  if (seenProviders.has(p)) return false;
16801
16818
  seenProviders.add(p);
16802
16819
  return true;
16803
16820
  });
16804
- const actionSet = new Set(instances.map((i) => i.action));
16821
+ const actionSet = new Set(
16822
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
16823
+ );
16805
16824
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
16806
16825
  const severity = ruleRows[0]?.severity ?? "low";
16807
16826
  const detection = {
@@ -16815,8 +16834,10 @@ function buildFindingGroups(rows, opts = {}) {
16815
16834
  contextPrefix: ""
16816
16835
  // empty (pending privacy review)
16817
16836
  };
16818
- const status = deriveGroupStatus(instances);
16819
- groups.push({
16837
+ const status = foldGroupStatus(
16838
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
16839
+ );
16840
+ const group = {
16820
16841
  id: ruleId,
16821
16842
  category: apiCategory,
16822
16843
  subtype: ruleId,
@@ -16825,21 +16846,26 @@ function buildFindingGroups(rows, opts = {}) {
16825
16846
  match,
16826
16847
  detection,
16827
16848
  policy,
16828
- instanceCount: instances.length,
16849
+ instanceCount: agg?.instanceCount ?? instances.length,
16829
16850
  providers,
16830
16851
  aggregateAction,
16831
16852
  latestDetectedAt,
16832
16853
  instances,
16833
16854
  status
16834
- });
16855
+ };
16856
+ if (agg) {
16857
+ actionsCache.set(group, [...actionSet]);
16858
+ if (agg.searchText !== void 0) {
16859
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
16860
+ }
16861
+ }
16862
+ groups.push(group);
16835
16863
  }
16836
16864
  return groups;
16837
16865
  }
16838
16866
  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 = [
16867
+ function buildHaystack(g, extra) {
16868
+ return [
16843
16869
  g.subtype,
16844
16870
  g.category,
16845
16871
  g.match.maskedValue,
@@ -16847,11 +16873,25 @@ function groupHaystack(g) {
16847
16873
  g.id,
16848
16874
  ...g.instances.map((i) => i.repo),
16849
16875
  ...g.instances.map((i) => i.file),
16850
- ...g.instances.map((i) => i.id)
16876
+ ...g.instances.map((i) => i.id),
16877
+ ...extra === void 0 ? [] : [extra]
16851
16878
  ].join(" ").toLowerCase();
16879
+ }
16880
+ function groupHaystack(g) {
16881
+ const cached2 = haystackCache.get(g);
16882
+ if (cached2 !== void 0) return cached2;
16883
+ const haystack = buildHaystack(g);
16852
16884
  haystackCache.set(g, haystack);
16853
16885
  return haystack;
16854
16886
  }
16887
+ var actionsCache = /* @__PURE__ */ new WeakMap();
16888
+ function groupActions(g) {
16889
+ const cached2 = actionsCache.get(g);
16890
+ if (cached2 !== void 0) return cached2;
16891
+ const actions = [...new Set(g.instances.map((i) => i.action))];
16892
+ actionsCache.set(g, actions);
16893
+ return actions;
16894
+ }
16855
16895
  function applyFindingFilters(groups, opts) {
16856
16896
  let filtered = groups;
16857
16897
  if (opts.severity && opts.severity.length > 0) {
@@ -16864,7 +16904,7 @@ function applyFindingFilters(groups, opts) {
16864
16904
  }
16865
16905
  if (opts.actions && opts.actions.length > 0) {
16866
16906
  const actionSet = new Set(opts.actions);
16867
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
16907
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
16868
16908
  }
16869
16909
  if (opts.subtype && opts.subtype.length > 0) {
16870
16910
  const subtypeSet = new Set(opts.subtype);
@@ -16916,8 +16956,7 @@ function computeFindingFacets(allGroups, opts) {
16916
16956
  });
16917
16957
  const actionMap = /* @__PURE__ */ new Map();
16918
16958
  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);
16959
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
16921
16960
  }
16922
16961
  const forSubtype = applyFindingFilters(allGroups, {
16923
16962
  providers: opts.providers,
@@ -17479,6 +17518,132 @@ function reviewSeverityRank(reasons) {
17479
17518
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17480
17519
  }
17481
17520
 
17521
+ // ../../packages/schema/src/zod/triage.ts
17522
+ var TriageHit = external_exports.object({
17523
+ ruleId: external_exports.string(),
17524
+ category: DetectionCategory,
17525
+ severity: Severity,
17526
+ maskedMatch: external_exports.string(),
17527
+ rawMatch: external_exports.string(),
17528
+ context: external_exports.string(),
17529
+ filePath: external_exports.string().optional(),
17530
+ confidence: external_exports.number().min(0).max(1),
17531
+ id: external_exports.string().optional(),
17532
+ valueFingerprint: external_exports.string().optional(),
17533
+ keyVersion: external_exports.number().int().nonnegative().optional()
17534
+ });
17535
+ var TriagePolicy = BuiltinPolicyId;
17536
+ var TriageCategoryRec = external_exports.object({
17537
+ category: DetectionCategory,
17538
+ action: TriagePolicy,
17539
+ reasoning: external_exports.string(),
17540
+ genuineCount: external_exports.number().int().nonnegative(),
17541
+ fpCount: external_exports.number().int().nonnegative(),
17542
+ // TriageHit ids judged false-positive in this category. fpCount must equal
17543
+ // this array's length — enforced by the consumer, not this schema.
17544
+ fpIds: external_exports.array(external_exports.string())
17545
+ });
17546
+ var TriageRecommendation = external_exports.object({
17547
+ perCategory: external_exports.array(TriageCategoryRec),
17548
+ notes: external_exports.string()
17549
+ });
17550
+
17551
+ // ../../packages/persistence/src/internal/sql-text.ts
17552
+ function escapeLikePattern(s) {
17553
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17554
+ }
17555
+ function placeholders(n) {
17556
+ return Array.from({ length: n }, () => "?").join(", ");
17557
+ }
17558
+ function containsPattern(q) {
17559
+ return `%${escapeLikePattern(q)}%`;
17560
+ }
17561
+ function likeAny(exprs) {
17562
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
17563
+ }
17564
+
17565
+ // ../../packages/persistence/src/internal/transactions.ts
17566
+ var savepointSeq = 0;
17567
+ function withTransaction(db, fn, mode = "DEFERRED") {
17568
+ if (db.isTransaction) {
17569
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
17570
+ db.exec(`SAVEPOINT ${savepoint}`);
17571
+ try {
17572
+ fn();
17573
+ db.exec(`RELEASE ${savepoint}`);
17574
+ } catch (error51) {
17575
+ try {
17576
+ db.exec(`ROLLBACK TO ${savepoint}`);
17577
+ db.exec(`RELEASE ${savepoint}`);
17578
+ } catch {
17579
+ }
17580
+ throw error51;
17581
+ }
17582
+ return;
17583
+ }
17584
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
17585
+ try {
17586
+ fn();
17587
+ db.exec("COMMIT");
17588
+ } catch (error51) {
17589
+ try {
17590
+ db.exec("ROLLBACK");
17591
+ } catch {
17592
+ }
17593
+ throw error51;
17594
+ }
17595
+ }
17596
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
17597
+ const nested = db.isTransaction;
17598
+ try {
17599
+ withTransaction(db, fn, mode);
17600
+ return true;
17601
+ } catch (error51) {
17602
+ if (!db.isTransaction && nested) throw error51;
17603
+ return false;
17604
+ }
17605
+ }
17606
+
17607
+ // ../../packages/persistence/src/internal/warn.ts
17608
+ function akaWarn(message) {
17609
+ process.stderr.write(`[aka] ${message}
17610
+ `);
17611
+ }
17612
+
17613
+ // ../../packages/persistence/src/db/migrations/introspection.ts
17614
+ function evidenceObjects(sql) {
17615
+ const objects = [];
17616
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17617
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17618
+ objects.push({ kind: "table", name: m[1] });
17619
+ }
17620
+ }
17621
+ for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17622
+ if (m[1] !== void 0 && m[2] !== void 0) {
17623
+ objects.push({ kind: "column", table: m[1], name: m[2] });
17624
+ }
17625
+ }
17626
+ return objects;
17627
+ }
17628
+ function schemaObjectExists(db, kind, name) {
17629
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
17630
+ return row !== void 0;
17631
+ }
17632
+ function indexExists(db, name) {
17633
+ return schemaObjectExists(db, "index", name);
17634
+ }
17635
+ function columnNames(db, table, opts) {
17636
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
17637
+ const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
17638
+ return columns.map((c) => c.name);
17639
+ }
17640
+ function evidenceExists(db, object2) {
17641
+ if (object2.kind === "column") {
17642
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
17643
+ }
17644
+ return schemaObjectExists(db, "table", object2.name);
17645
+ }
17646
+
17482
17647
  // ../../packages/persistence/src/ids.ts
17483
17648
  import { createHash } from "crypto";
17484
17649
  function sha256Hex(input) {
@@ -17515,28 +17680,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17515
17680
  }
17516
17681
 
17517
17682
  // ../../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
17683
  function describeObject(object2) {
17541
17684
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
17542
17685
  }
@@ -17547,10 +17690,6 @@ function createdIndexName(statement) {
17547
17690
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17548
17691
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17549
17692
  }
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
17693
  function applyMigrations(db) {
17555
17694
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17556
17695
  db.exec(
@@ -17569,44 +17708,39 @@ function applyMigrations(db) {
17569
17708
  const present = evidence.filter((o) => evidenceExists(db, o));
17570
17709
  if (present.length > 0 && present.length < evidence.length) {
17571
17710
  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);
17711
+ 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.`;
17712
+ akaWarn(message);
17713
+ throw new Error(`[aka] ${message}`);
17576
17714
  }
17577
17715
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
17578
17716
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
17579
17717
  const statements = splitStatements(migration.sql);
17580
17718
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
17581
17719
  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;
17720
+ withTransaction(
17721
+ db,
17722
+ () => {
17723
+ for (const statement of statements) {
17724
+ const indexName = createdIndexName(statement);
17725
+ if (indexName === void 0) {
17726
+ if (alreadyApplied) continue;
17727
+ } else if (indexExists(db, indexName)) {
17728
+ continue;
17729
+ }
17730
+ db.exec(statement);
17590
17731
  }
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
- );
17732
+ if (wantsFkOff && !alreadyApplied) {
17733
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
17734
+ if (violations.length > 0) {
17735
+ throw new Error(
17736
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17737
+ );
17738
+ }
17599
17739
  }
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
- }
17740
+ record2.run(migration.tag, Date.now());
17741
+ },
17742
+ "IMMEDIATE"
17743
+ );
17610
17744
  } finally {
17611
17745
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
17612
17746
  }
@@ -17649,8 +17783,7 @@ var TOKEN_USAGE_COLUMNS = [
17649
17783
  }
17650
17784
  ];
17651
17785
  function ensureTokenUsageColumns(db) {
17652
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
17653
- const existing = new Set(columns.map((c) => c.name));
17786
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17654
17787
  for (const column of TOKEN_USAGE_COLUMNS) {
17655
17788
  if (!existing.has(column.name)) {
17656
17789
  db.exec(column.ddl);
@@ -17688,47 +17821,39 @@ function reconcileSourceProjectIds(db) {
17688
17821
  repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
17689
17822
  }));
17690
17823
  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);
17824
+ withTransaction(
17825
+ db,
17826
+ () => {
17827
+ for (const { row, canonicalId } of legacy) {
17828
+ foldProject.run(
17829
+ canonicalId,
17830
+ row.url,
17831
+ row.name,
17832
+ row.attributes,
17833
+ row.firstSeen,
17834
+ row.lastSeen
17835
+ );
17836
+ repointAudit.run(canonicalId, row.id);
17837
+ for (const { dropCollisions, repoint } of pathTables) {
17838
+ dropCollisions.run(row.id, canonicalId);
17839
+ repoint.run(canonicalId, row.id);
17840
+ }
17841
+ repointCallSite.run(canonicalId, row.id);
17842
+ deleteLegacy.run(row.id);
17706
17843
  }
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
- }
17844
+ },
17845
+ "IMMEDIATE"
17846
+ );
17718
17847
  } catch (error51) {
17719
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
17720
- `);
17848
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
17721
17849
  }
17722
17850
  }
17723
17851
  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");
17852
+ if (schemaObjectExists(db, "table", "tenants")) return true;
17853
+ return columnNames(db, "events").includes("tenant_id");
17728
17854
  }
17729
17855
  function ensureSyncedAtColumn(db, table) {
17730
- const columns = db.prepare(`PRAGMA table_info(${table})`).all();
17731
- if (!columns.some((c) => c.name === "synced_at")) {
17856
+ if (!columnNames(db, table).includes("synced_at")) {
17732
17857
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
17733
17858
  }
17734
17859
  }
@@ -17779,8 +17904,11 @@ function ensureDataDirSync(dir) {
17779
17904
  } catch {
17780
17905
  }
17781
17906
  }
17907
+ function walSidecars(file2) {
17908
+ return [`${file2}-wal`, `${file2}-shm`];
17909
+ }
17782
17910
  function tightenPerms(file2) {
17783
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
17911
+ for (const path of [file2, ...walSidecars(file2)]) {
17784
17912
  try {
17785
17913
  chmodSync(path, DATA_FILE_MODE);
17786
17914
  } catch {
@@ -17788,12 +17916,68 @@ function tightenPerms(file2) {
17788
17916
  }
17789
17917
  }
17790
17918
 
17791
- // ../../packages/persistence/src/repositories/sql-utils.ts
17792
- function escapeLikePattern(s) {
17793
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17919
+ // ../../packages/persistence/src/internal/json.ts
17920
+ function safeJson(s, fallback) {
17921
+ if (s == null) return fallback;
17922
+ try {
17923
+ return JSON.parse(s);
17924
+ } catch {
17925
+ return fallback;
17926
+ }
17794
17927
  }
17795
- function placeholders(n) {
17796
- return Array.from({ length: n }, () => "?").join(", ");
17928
+ function parseJsonObject(s) {
17929
+ if (s == null) return void 0;
17930
+ try {
17931
+ const parsed = JSON.parse(s);
17932
+ if (typeof parsed === "object" && parsed !== null) return parsed;
17933
+ } catch {
17934
+ }
17935
+ return void 0;
17936
+ }
17937
+
17938
+ // ../../packages/persistence/src/internal/rows.ts
17939
+ function allRows(stmt, params) {
17940
+ if (params === void 0) return stmt.all();
17941
+ if (Array.isArray(params)) return stmt.all(...params);
17942
+ return stmt.all(params);
17943
+ }
17944
+ function getRow(stmt, params) {
17945
+ if (params === void 0) return stmt.get();
17946
+ if (Array.isArray(params)) return stmt.get(...params);
17947
+ return stmt.get(params);
17948
+ }
17949
+ function intToBool(raw) {
17950
+ return raw === 1 || raw === true;
17951
+ }
17952
+ function boolToInt(b) {
17953
+ return b ? 1 : 0;
17954
+ }
17955
+ function bindParams(row) {
17956
+ const out = {};
17957
+ for (const [key, value] of Object.entries(row)) {
17958
+ out[key] = value === void 0 ? null : value;
17959
+ }
17960
+ return out;
17961
+ }
17962
+ function countScalar(db, sql, params) {
17963
+ return getRow(db.prepare(sql), params)?.n ?? 0;
17964
+ }
17965
+ function countBy(db, sql, params) {
17966
+ const map2 = /* @__PURE__ */ new Map();
17967
+ for (const row of allRows(db.prepare(sql), params)) {
17968
+ map2.set(row.k, row.n);
17969
+ }
17970
+ return map2;
17971
+ }
17972
+ function mapRowsTolerant(rows, map2) {
17973
+ const out = [];
17974
+ for (const row of rows) {
17975
+ try {
17976
+ out.push(map2(row));
17977
+ } catch {
17978
+ }
17979
+ }
17980
+ return out;
17797
17981
  }
17798
17982
 
17799
17983
  // ../../packages/persistence/src/repositories/activity.ts
@@ -17845,15 +18029,11 @@ function encodeCursor(payload) {
17845
18029
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
17846
18030
  }
17847
18031
  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;
18032
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18033
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18034
+ return parsed;
17856
18035
  }
18036
+ return null;
17857
18037
  }
17858
18038
  var DB_EVENT_TYPE_TO_KIND = {
17859
18039
  session: "session",
@@ -17870,15 +18050,8 @@ var DB_EVENT_TYPE_TO_KIND = {
17870
18050
  };
17871
18051
  function safeParseStringArray(raw) {
17872
18052
  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;
18053
+ const parsed = safeJson(raw, null);
18054
+ return Array.isArray(parsed) ? parsed : [];
17882
18055
  }
17883
18056
  function toHarness(raw) {
17884
18057
  const parsed = Harness.safeParse(raw);
@@ -17931,8 +18104,8 @@ function buildAuditEvent(row) {
17931
18104
  severity: severityParsed?.success ? severityParsed.data : null,
17932
18105
  link: linkParsed?.success ? linkParsed.data : null,
17933
18106
  targetId: row.target_id,
17934
- internal: toBool(row.internal),
17935
- flagged: toBool(row.flagged)
18107
+ internal: intToBool(row.internal),
18108
+ flagged: intToBool(row.flagged)
17936
18109
  };
17937
18110
  }
17938
18111
  var TIMELINE_COLUMNS = `
@@ -17958,12 +18131,15 @@ var SqliteActivityRepository = class {
17958
18131
  stats(tz) {
17959
18132
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
17960
18133
  const { startMs, endMs } = window;
17961
- const sessionsToday = this.db.prepare(
18134
+ const sessionsToday = countScalar(
18135
+ this.db,
17962
18136
  `SELECT count(*) AS n FROM audit_events
17963
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
17964
- ).get(startMs, endMs).n;
18137
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
18138
+ [startMs, endMs]
18139
+ );
17965
18140
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
17966
- const liveNow = this.db.prepare(
18141
+ const liveNow = countScalar(
18142
+ this.db,
17967
18143
  `SELECT count(*) AS n FROM audit_events s
17968
18144
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
17969
18145
  AND max(
@@ -17972,22 +18148,29 @@ var SqliteActivityRepository = class {
17972
18148
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
17973
18149
  s.started_at
17974
18150
  )
17975
- ) >= ?`
17976
- ).get(liveThreshold).n;
17977
- const toolCallsToday = this.db.prepare(
18151
+ ) >= ?`,
18152
+ [liveThreshold]
18153
+ );
18154
+ const toolCallsToday = countScalar(
18155
+ this.db,
17978
18156
  `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(
18157
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18158
+ [startMs, endMs]
18159
+ );
18160
+ const findingsToday = countScalar(
18161
+ this.db,
17982
18162
  `SELECT count(*) AS n FROM inspection_findings f
17983
18163
  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(
18164
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18165
+ [startMs, endMs]
18166
+ );
18167
+ const egressToday = countScalar(
18168
+ this.db,
17987
18169
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
17988
18170
  FROM audit_events
17989
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
17990
- ).get(startMs, endMs).n;
18171
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18172
+ [startMs, endMs]
18173
+ );
17991
18174
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
17992
18175
  }
17993
18176
  listSessions(query) {
@@ -18009,7 +18192,7 @@ var SqliteActivityRepository = class {
18009
18192
  conditions.push("started_at <= ?");
18010
18193
  params.push(toMs);
18011
18194
  if (query.q) {
18012
- const pattern = `%${escapeLikePattern(query.q)}%`;
18195
+ const pattern = containsPattern(query.q);
18013
18196
  conditions.push(
18014
18197
  `(content LIKE ? ESCAPE '\\'
18015
18198
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18028,8 +18211,9 @@ var SqliteActivityRepository = class {
18028
18211
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18029
18212
  }
18030
18213
  const limit = query.limit;
18031
- const rows = this.db.prepare(
18032
- `SELECT id,
18214
+ const rows = allRows(
18215
+ this.db.prepare(
18216
+ `SELECT id,
18033
18217
  json_extract(attributes, '$.harness') AS harness,
18034
18218
  content AS title,
18035
18219
  json_extract(attributes, '$.project') AS project,
@@ -18042,7 +18226,9 @@ var SqliteActivityRepository = class {
18042
18226
  WHERE ${conditions.join(" AND ")}
18043
18227
  ORDER BY started_at DESC, id DESC
18044
18228
  LIMIT ?`
18045
- ).all(...params, limit + 1);
18229
+ ),
18230
+ [...params, limit + 1]
18231
+ );
18046
18232
  const hasMore = rows.length > limit;
18047
18233
  const page = hasMore ? rows.slice(0, limit) : rows;
18048
18234
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18059,8 +18245,9 @@ var SqliteActivityRepository = class {
18059
18245
  return Promise.resolve({ items, nextCursor });
18060
18246
  }
18061
18247
  getSession(sessionId) {
18062
- const rootRow = this.db.prepare(
18063
- `SELECT id,
18248
+ const rootRow = getRow(
18249
+ this.db.prepare(
18250
+ `SELECT id,
18064
18251
  json_extract(attributes, '$.harness') AS harness,
18065
18252
  content AS title,
18066
18253
  json_extract(attributes, '$.project') AS project,
@@ -18077,47 +18264,66 @@ var SqliteActivityRepository = class {
18077
18264
  FROM audit_events
18078
18265
  WHERE id = ? AND event_type = 'session'
18079
18266
  LIMIT 1`
18080
- ).get(sessionId);
18267
+ ),
18268
+ [sessionId]
18269
+ );
18081
18270
  if (!rootRow) return Promise.resolve(null);
18082
- const timelineRows = this.db.prepare(
18083
- `SELECT ${TIMELINE_COLUMNS}
18271
+ const timelineRows = allRows(
18272
+ this.db.prepare(
18273
+ `SELECT ${TIMELINE_COLUMNS}
18084
18274
  FROM audit_events
18085
18275
  WHERE id = ? OR root_session_id = ?
18086
18276
  ORDER BY started_at ASC, id ASC`
18087
- ).all(sessionId, sessionId);
18277
+ ),
18278
+ [sessionId, sessionId]
18279
+ );
18088
18280
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18089
- const tokenRow = this.db.prepare(
18090
- `SELECT
18281
+ const tokenRow = getRow(
18282
+ this.db.prepare(
18283
+ `SELECT
18091
18284
  coalesce(sum(input_tokens), 0) AS input,
18092
18285
  coalesce(sum(output_tokens), 0) AS output,
18093
18286
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18094
18287
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18095
18288
  FROM audit_events
18096
18289
  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
18290
+ ),
18291
+ [sessionId]
18292
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
18293
+ const primaryModel = getRow(
18294
+ this.db.prepare(
18295
+ `SELECT model, provider FROM audit_events
18100
18296
  WHERE root_session_id = ? AND event_type = 'llm_call'
18101
18297
  ORDER BY started_at ASC, id ASC
18102
18298
  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,
18299
+ ),
18300
+ [sessionId]
18301
+ );
18302
+ const toolRows = allRows(
18303
+ this.db.prepare(
18304
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18106
18305
  count(*) AS n
18107
18306
  FROM audit_events
18108
18307
  WHERE root_session_id = ? AND event_type = 'tool_call'
18109
18308
  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
18309
+ ),
18310
+ [sessionId]
18311
+ );
18312
+ const modelRows = allRows(
18313
+ this.db.prepare(
18314
+ `SELECT DISTINCT model FROM audit_events
18113
18315
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18114
18316
  ORDER BY model`
18115
- ).all(sessionId);
18317
+ ),
18318
+ [sessionId]
18319
+ );
18116
18320
  const derivedModels = modelRows.map((r) => r.model);
18117
- const commits = this.db.prepare(
18321
+ const commits = countScalar(
18322
+ this.db,
18118
18323
  `SELECT count(*) AS n FROM audit_events
18119
- WHERE root_session_id = ? AND event_type = 'commit'`
18120
- ).get(sessionId).n;
18324
+ WHERE root_session_id = ? AND event_type = 'commit'`,
18325
+ [sessionId]
18326
+ );
18121
18327
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18122
18328
  turns: 0,
18123
18329
  findings: 0,
@@ -18184,7 +18390,10 @@ var SqliteActivityRepository = class {
18184
18390
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18185
18391
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18186
18392
  );
18187
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
18393
+ const rows = allRows(
18394
+ stmt,
18395
+ fromMs === void 0 ? void 0 : [fromMs]
18396
+ );
18188
18397
  const seen = /* @__PURE__ */ new Set();
18189
18398
  for (const row of rows) seen.add(toHarness(row.harness));
18190
18399
  return Promise.resolve([...seen]);
@@ -18207,23 +18416,23 @@ var SqliteActivityRepository = class {
18207
18416
  conditions.push("started_at >= ?");
18208
18417
  params.push(opts.fromMs);
18209
18418
  }
18210
- const rows = this.db.prepare(
18211
- `SELECT root_session_id AS sessionId, attributes
18419
+ const rows = allRows(
18420
+ this.db.prepare(
18421
+ `SELECT root_session_id AS sessionId, attributes
18212
18422
  FROM audit_events
18213
18423
  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;
18424
+ ),
18425
+ params
18426
+ );
18427
+ return mapRowsTolerant(
18428
+ rows.filter(
18429
+ (row) => row.sessionId !== null
18430
+ ),
18431
+ (row) => ({
18432
+ sessionId: row.sessionId,
18433
+ attributes: JSON.parse(row.attributes)
18434
+ })
18435
+ );
18227
18436
  }
18228
18437
  /**
18229
18438
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -18237,57 +18446,72 @@ var SqliteActivityRepository = class {
18237
18446
  );
18238
18447
  if (sessionIds.length === 0) return result;
18239
18448
  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
18449
+ const lastActivityRows = allRows(
18450
+ this.db.prepare(
18451
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18242
18452
  WHERE root_session_id IN (${inClause})
18243
18453
  GROUP BY root_session_id`
18244
- ).all(...sessionIds);
18454
+ ),
18455
+ sessionIds
18456
+ );
18245
18457
  for (const row of lastActivityRows) {
18246
18458
  if (row.id === null) continue;
18247
18459
  const entry = result.get(row.id);
18248
18460
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
18249
18461
  }
18250
- const turnsRows = this.db.prepare(
18251
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18462
+ const turnsRows = allRows(
18463
+ this.db.prepare(
18464
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18252
18465
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
18253
18466
  GROUP BY root_session_id`
18254
- ).all(...sessionIds);
18467
+ ),
18468
+ sessionIds
18469
+ );
18255
18470
  for (const row of turnsRows) {
18256
18471
  if (row.id === null) continue;
18257
18472
  const entry = result.get(row.id);
18258
18473
  if (entry) entry.turns = row.n;
18259
18474
  }
18260
- const runKeyRows = this.db.prepare(
18261
- `SELECT root_session_id AS id,
18475
+ const runKeyRows = allRows(
18476
+ this.db.prepare(
18477
+ `SELECT root_session_id AS id,
18262
18478
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
18263
18479
  FROM audit_events
18264
18480
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
18265
18481
  AND json_extract(attributes, '$.run_key') IS NOT NULL
18266
18482
  GROUP BY root_session_id`
18267
- ).all(...sessionIds);
18483
+ ),
18484
+ sessionIds
18485
+ );
18268
18486
  for (const row of runKeyRows) {
18269
18487
  if (row.id === null) continue;
18270
18488
  const entry = result.get(row.id);
18271
18489
  if (entry) entry.turns = Math.max(entry.turns, row.n);
18272
18490
  }
18273
- const findingsRows = this.db.prepare(
18274
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18491
+ const findingsRows = allRows(
18492
+ this.db.prepare(
18493
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18275
18494
  JOIN audit_events e ON e.id = f.audit_event_id
18276
18495
  WHERE e.root_session_id IN (${inClause})
18277
18496
  GROUP BY e.root_session_id`
18278
- ).all(...sessionIds);
18497
+ ),
18498
+ sessionIds
18499
+ );
18279
18500
  for (const row of findingsRows) {
18280
18501
  if (row.id === null) continue;
18281
18502
  const entry = result.get(row.id);
18282
18503
  if (entry) entry.findings = row.n;
18283
18504
  }
18284
- const sharesRows = this.db.prepare(
18285
- `SELECT root_session_id AS id,
18505
+ const sharesRows = allRows(
18506
+ this.db.prepare(
18507
+ `SELECT root_session_id AS id,
18286
18508
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
18287
18509
  FROM audit_events
18288
18510
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
18289
18511
  GROUP BY root_session_id`
18290
- ).all(...sessionIds);
18512
+ ),
18513
+ sessionIds
18514
+ );
18291
18515
  for (const row of sharesRows) {
18292
18516
  if (row.id === null) continue;
18293
18517
  const entry = result.get(row.id);
@@ -18337,33 +18561,28 @@ var SqliteAuditEventsRepository = class {
18337
18561
  // the caller fails open and drops the whole pass — recovered idempotently on the
18338
18562
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
18339
18563
  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
- }
18564
+ withTransaction(this.db, fn);
18348
18565
  }
18349
18566
  insertAuditEvent(input) {
18350
18567
  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
- });
18568
+ this.insertStmt.run(
18569
+ bindParams({
18570
+ id: row.id,
18571
+ parentId: row.parentId,
18572
+ rootSessionId: row.rootSessionId,
18573
+ eventType: row.eventType,
18574
+ hostId: row.hostId,
18575
+ harnessId: row.harnessId,
18576
+ sourceProjectId: row.sourceProjectId,
18577
+ startedAt: row.startedAt,
18578
+ endedAt: row.endedAt,
18579
+ severity: row.severity,
18580
+ priority: row.priority,
18581
+ content: row.content,
18582
+ contentHash: row.contentHash,
18583
+ attributes: row.attributes
18584
+ })
18585
+ );
18367
18586
  }
18368
18587
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18369
18588
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -18377,22 +18596,24 @@ var SqliteAuditEventsRepository = class {
18377
18596
  const startedAt = isoToEpochMillis(input.startedAt);
18378
18597
  if (!Number.isFinite(startedAt)) return;
18379
18598
  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
- });
18599
+ this.upsertLlmCallStmt.run(
18600
+ bindParams({
18601
+ id,
18602
+ parentId: input.parentId,
18603
+ rootSessionId: input.rootSessionId,
18604
+ eventType: "llm_call",
18605
+ hostId: null,
18606
+ harnessId: null,
18607
+ sourceProjectId: null,
18608
+ startedAt,
18609
+ endedAt: null,
18610
+ severity: null,
18611
+ priority: null,
18612
+ content: null,
18613
+ contentHash: null,
18614
+ attributes: JSON.stringify(input.attributes)
18615
+ })
18616
+ );
18396
18617
  }
18397
18618
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
18398
18619
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -18416,25 +18637,29 @@ var SqliteAuditEventsRepository = class {
18416
18637
  const startedAt = isoToEpochMillis(input.startedAt);
18417
18638
  if (!Number.isFinite(startedAt)) return;
18418
18639
  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
- });
18640
+ this.insertStmt.run(
18641
+ bindParams({
18642
+ id,
18643
+ parentId: input.parentId,
18644
+ rootSessionId: input.rootSessionId,
18645
+ eventType: "tool_call",
18646
+ hostId: null,
18647
+ harnessId: null,
18648
+ sourceProjectId: null,
18649
+ startedAt,
18650
+ endedAt: null,
18651
+ severity: null,
18652
+ priority: null,
18653
+ content: null,
18654
+ contentHash: null,
18655
+ attributes: JSON.stringify(input.attributes)
18656
+ })
18657
+ );
18435
18658
  }
18436
18659
  findById(id) {
18437
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
18660
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
18661
+ id
18662
+ });
18438
18663
  }
18439
18664
  // Read the `provider` snapshotted onto a session root's attributes.
18440
18665
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -18444,14 +18669,8 @@ var SqliteAuditEventsRepository = class {
18444
18669
  sessionProvider(sessionId) {
18445
18670
  const row = this.findById(sessionId);
18446
18671
  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
- }
18672
+ const provider = parseJsonObject(row.attributes)?.provider;
18673
+ if (typeof provider === "string") return provider;
18455
18674
  return void 0;
18456
18675
  }
18457
18676
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -18460,11 +18679,13 @@ var SqliteAuditEventsRepository = class {
18460
18679
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
18461
18680
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
18462
18681
  llmCallLeaves() {
18463
- return this.db.prepare(
18464
- `SELECT root_session_id AS sessionId, attributes
18682
+ return allRows(
18683
+ this.db.prepare(
18684
+ `SELECT root_session_id AS sessionId, attributes
18465
18685
  FROM audit_events
18466
18686
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
18467
- ).all();
18687
+ )
18688
+ );
18468
18689
  }
18469
18690
  };
18470
18691
 
@@ -18483,16 +18704,29 @@ var SqliteClassifiedDataRepository = class {
18483
18704
  upsert(input) {
18484
18705
  const id = classifiedDataId(input.class);
18485
18706
  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
- });
18707
+ this.insertStmt.run(
18708
+ bindParams({
18709
+ id: row.id,
18710
+ class: row.class,
18711
+ label: row.label,
18712
+ attributes: row.attributes
18713
+ })
18714
+ );
18492
18715
  return id;
18493
18716
  }
18494
18717
  };
18495
18718
 
18719
+ // ../../packages/persistence/src/repositories/config-scan.ts
18720
+ function latestConfigScan(db) {
18721
+ return getRow(
18722
+ db.prepare(
18723
+ `SELECT id, started_at, attributes FROM audit_events
18724
+ WHERE event_type = 'config_scan'
18725
+ ORDER BY started_at DESC, id DESC LIMIT 1`
18726
+ )
18727
+ );
18728
+ }
18729
+
18496
18730
  // ../../packages/persistence/src/repositories/config-inventory.ts
18497
18731
  var SqliteConfigInventoryRepository = class {
18498
18732
  constructor(db) {
@@ -18500,7 +18734,7 @@ var SqliteConfigInventoryRepository = class {
18500
18734
  }
18501
18735
  db;
18502
18736
  report() {
18503
- const scan2 = this.latestScan();
18737
+ const scan2 = latestConfigScan(this.db);
18504
18738
  if (!scan2) {
18505
18739
  return {
18506
18740
  scannedAt: null,
@@ -18511,17 +18745,23 @@ var SqliteConfigInventoryRepository = class {
18511
18745
  topics: []
18512
18746
  };
18513
18747
  }
18514
- const rows = this.db.prepare(
18515
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18748
+ const rows = allRows(
18749
+ this.db.prepare(
18750
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18516
18751
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
18517
18752
  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
18753
+ ),
18754
+ { startedAt: scan2.started_at }
18755
+ );
18756
+ const findings = allRows(
18757
+ this.db.prepare(
18758
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18521
18759
  FROM inspection_findings f
18522
18760
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
18523
18761
  WHERE f.audit_event_id = :scanId`
18524
- ).all({ scanId: scan2.id });
18762
+ ),
18763
+ { scanId: scan2.id }
18764
+ );
18525
18765
  const skills = [];
18526
18766
  const hooks = [];
18527
18767
  const mcpServers = [];
@@ -18550,7 +18790,9 @@ var SqliteConfigInventoryRepository = class {
18550
18790
  // schema note); an override whose asset is gone simply never matches. A row
18551
18791
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
18552
18792
  trustOverrides() {
18553
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
18793
+ const rows = allRows(
18794
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
18795
+ );
18554
18796
  const map2 = /* @__PURE__ */ new Map();
18555
18797
  for (const row of rows) {
18556
18798
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -18559,13 +18801,6 @@ var SqliteConfigInventoryRepository = class {
18559
18801
  }
18560
18802
  return map2;
18561
18803
  }
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
18804
  };
18570
18805
  function toSkillItem(row, bag) {
18571
18806
  const item = {
@@ -18676,22 +18911,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
18676
18911
  return topics;
18677
18912
  }
18678
18913
  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
- }
18914
+ const errors = parseJsonObject(attributes)?.errors;
18915
+ return typeof errors === "number" ? errors : 0;
18687
18916
  }
18688
18917
  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;
18918
+ return parseJsonObject(raw);
18695
18919
  }
18696
18920
  function str(value) {
18697
18921
  return typeof value === "string" ? value : void 0;
@@ -18700,12 +18924,7 @@ function str(value) {
18700
18924
  // ../../packages/persistence/src/repositories/detections.ts
18701
18925
  var DAY_MS2 = 864e5;
18702
18926
  function parseRules(rulesJson) {
18703
- let raw;
18704
- try {
18705
- raw = JSON.parse(rulesJson);
18706
- } catch {
18707
- return [];
18708
- }
18927
+ const raw = safeJson(rulesJson, []);
18709
18928
  if (!Array.isArray(raw)) return [];
18710
18929
  const rules = [];
18711
18930
  for (const entry of raw) {
@@ -18724,11 +18943,13 @@ var SqliteDetectionsRepository = class {
18724
18943
  db;
18725
18944
  now;
18726
18945
  listDetections(query) {
18727
- const rows = this.db.prepare(
18728
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18946
+ const rows = allRows(
18947
+ this.db.prepare(
18948
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18729
18949
  rules_json AS rulesJson
18730
18950
  FROM installed_packs`
18731
- ).all();
18951
+ )
18952
+ );
18732
18953
  const available = this.availableByPack();
18733
18954
  const summaries = rows.map((r) => {
18734
18955
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -18737,7 +18958,7 @@ var SqliteDetectionsRepository = class {
18737
18958
  packId: r.packId,
18738
18959
  version: r.version,
18739
18960
  name: r.name,
18740
- enabled: r.enabled === 1,
18961
+ enabled: intToBool(r.enabled),
18741
18962
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
18742
18963
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
18743
18964
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -18754,19 +18975,23 @@ var SqliteDetectionsRepository = class {
18754
18975
  // available_packs keyed by the "namespace/packId" slug (one read per list /
18755
18976
  // detail call; the table is a handful of rows).
18756
18977
  availableByPack() {
18757
- const rows = this.db.prepare(
18758
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18978
+ const rows = allRows(
18979
+ this.db.prepare(
18980
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18759
18981
  FROM available_packs`
18760
- ).all();
18982
+ )
18983
+ );
18761
18984
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
18762
18985
  }
18763
18986
  getDetectionStats() {
18764
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
18987
+ const rows = allRows(
18988
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
18989
+ );
18765
18990
  let rules = 0;
18766
18991
  let active = 0;
18767
18992
  const ruleIds = /* @__PURE__ */ new Set();
18768
18993
  for (const r of rows) {
18769
- if (r.enabled === 1) active += 1;
18994
+ if (intToBool(r.enabled)) active += 1;
18770
18995
  const parsed = parseRules(r.rulesJson);
18771
18996
  rules += parsed.length;
18772
18997
  for (const rule of parsed) {
@@ -18784,12 +19009,15 @@ var SqliteDetectionsRepository = class {
18784
19009
  const parts = splitDetectionId(id);
18785
19010
  if (!parts) return Promise.resolve(null);
18786
19011
  const { namespace, packId } = parts;
18787
- const row = this.db.prepare(
18788
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19012
+ const row = getRow(
19013
+ this.db.prepare(
19014
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18789
19015
  rules_json AS rulesJson, updated_at AS updatedAt
18790
19016
  FROM installed_packs
18791
19017
  WHERE namespace = ? AND pack_id = ?`
18792
- ).get(namespace, packId);
19018
+ ),
19019
+ [namespace, packId]
19020
+ );
18793
19021
  if (!row) return Promise.resolve(null);
18794
19022
  const rules = parseRules(row.rulesJson);
18795
19023
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -18807,7 +19035,7 @@ var SqliteDetectionsRepository = class {
18807
19035
  packId: row.packId,
18808
19036
  version: row.version,
18809
19037
  name: row.name,
18810
- enabled: row.enabled === 1,
19038
+ enabled: intToBool(row.enabled),
18811
19039
  rules,
18812
19040
  updatedAt: new Date(row.updatedAt),
18813
19041
  policyId: row.policyId
@@ -18822,13 +19050,14 @@ var SqliteDetectionsRepository = class {
18822
19050
  countFindingsLast30d(ruleIds) {
18823
19051
  if (ruleIds.length === 0) return 0;
18824
19052
  const since = this.now() - 30 * DAY_MS2;
18825
- const placeholders2 = ruleIds.map(() => "?").join(", ");
18826
- const row = this.db.prepare(
18827
- `SELECT count(*) AS c
19053
+ const inClause = placeholders(ruleIds.length);
19054
+ return countScalar(
19055
+ this.db,
19056
+ `SELECT count(*) AS n
18828
19057
  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;
19058
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19059
+ [since, ...ruleIds]
19060
+ );
18832
19061
  }
18833
19062
  };
18834
19063
 
@@ -18845,16 +19074,17 @@ var SqliteEventsRepository = class {
18845
19074
  insertStmt;
18846
19075
  insertEvent(event) {
18847
19076
  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
- });
19077
+ this.insertStmt.run(
19078
+ bindParams({
19079
+ id: row.id,
19080
+ sourceTool: row.sourceTool,
19081
+ kind: row.kind,
19082
+ occurredAt: row.occurredAt,
19083
+ contentHash: row.contentHash,
19084
+ content: row.content,
19085
+ metadata: row.metadata
19086
+ })
19087
+ );
18858
19088
  }
18859
19089
  // Every recorded event's content hash — the historical backfill loads this once
18860
19090
  // to skip transcript messages it has already stored, so re-running the scan
@@ -18862,13 +19092,23 @@ var SqliteEventsRepository = class {
18862
19092
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
18863
19093
  // async EventsReadPort contract.
18864
19094
  contentHashes() {
18865
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19095
+ const rows = allRows(
19096
+ this.db.prepare("SELECT content_hash FROM events")
19097
+ );
18866
19098
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
18867
19099
  }
18868
19100
  };
18869
19101
 
18870
19102
  // ../../packages/persistence/src/repositories/exceptions.ts
18871
19103
  import { randomUUID } from "crypto";
19104
+
19105
+ // ../../packages/persistence/src/internal/sqlite-errors.ts
19106
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19107
+ function isUniqueConstraintError(err) {
19108
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19109
+ }
19110
+
19111
+ // ../../packages/persistence/src/repositories/exceptions.ts
18872
19112
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
18873
19113
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
18874
19114
  var DuplicateActiveExceptionError = class extends Error {
@@ -18889,10 +19129,6 @@ var AmbiguousExceptionIdError = class extends Error {
18889
19129
  this.name = "AmbiguousExceptionIdError";
18890
19130
  }
18891
19131
  };
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
19132
  var ACTIVE_PREDICATE = `revoked_at IS NULL
18897
19133
  AND (expires_at IS NULL OR expires_at > :now)
18898
19134
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -18948,10 +19184,11 @@ var SqliteExceptionsRepository = class {
18948
19184
  this.insertExceptionRow(id, input, now);
18949
19185
  } catch (err) {
18950
19186
  if (!isUniqueConstraintError(err)) throw err;
18951
- this.db.exec("BEGIN IMMEDIATE");
18952
- try {
18953
- const superseded = this.db.prepare(
18954
- `UPDATE exceptions
19187
+ withTransaction(
19188
+ this.db,
19189
+ () => {
19190
+ const superseded = this.db.prepare(
19191
+ `UPDATE exceptions
18955
19192
  SET revoked_at = :now, revoked_by = :revokedBy,
18956
19193
  revoke_reason = 'superseded by a new grant for the same value',
18957
19194
  updated_at = :now
@@ -18959,24 +19196,27 @@ var SqliteExceptionsRepository = class {
18959
19196
  AND key_version = :keyVersion AND revoked_at IS NULL
18960
19197
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
18961
19198
  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
- }
19199
+ ).run({
19200
+ now,
19201
+ revokedBy: input.createdBy,
19202
+ ruleId: input.ruleId,
19203
+ valueFingerprint: input.valueFingerprint,
19204
+ keyVersion: input.keyVersion
19205
+ });
19206
+ if (Number(superseded.changes) !== 1) {
19207
+ throw new DuplicateActiveExceptionError(input.ruleId);
19208
+ }
19209
+ this.insertExceptionRow(id, input, now);
19210
+ },
19211
+ "IMMEDIATE"
19212
+ );
19213
+ }
19214
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
19215
+ id
19216
+ });
19217
+ if (row === void 0) {
19218
+ throw new Error("exception row not found immediately after insert");
18978
19219
  }
18979
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
18980
19220
  return parseExceptionRow(row);
18981
19221
  }
18982
19222
  insertExceptionRow(id, input, now) {
@@ -19014,14 +19254,11 @@ var SqliteExceptionsRepository = class {
19014
19254
  */
19015
19255
  list(opts) {
19016
19256
  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
- }
19257
+ const rows = allRows(
19258
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19259
+ opts?.includeTerminal ? {} : { now: Date.now() }
19260
+ );
19261
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19025
19262
  return Promise.resolve(exceptions);
19026
19263
  }
19027
19264
  /**
@@ -19031,7 +19268,12 @@ var SqliteExceptionsRepository = class {
19031
19268
  */
19032
19269
  getByIdPrefix(prefix) {
19033
19270
  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)}%` });
19271
+ const rows = allRows(
19272
+ this.db.prepare(
19273
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
19274
+ ),
19275
+ { pattern: `${escapeLikePattern(prefix)}%` }
19276
+ );
19035
19277
  if (rows.length > 1) {
19036
19278
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19037
19279
  }
@@ -19073,30 +19315,27 @@ var SqliteExceptionsRepository = class {
19073
19315
  * a different (rotated-away) key never match, so they are excluded at read.
19074
19316
  */
19075
19317
  activeBundleEntries(keyVersion, now = Date.now()) {
19076
- const rows = this.db.prepare(
19077
- `SELECT * FROM exceptions
19318
+ const rows = allRows(
19319
+ this.db.prepare(
19320
+ `SELECT * FROM exceptions
19078
19321
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19079
19322
  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
- }
19323
+ ),
19324
+ { keyVersion, now }
19325
+ );
19326
+ const entries = mapRowsTolerant(rows, (row) => {
19327
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19328
+ return ExceptionBundleEntry.parse({
19329
+ id: row.id,
19330
+ ruleId: row.rule_id,
19331
+ valueFingerprint: row.value_fingerprint,
19332
+ keyVersion: row.key_version,
19333
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19334
+ maxUses: row.max_uses,
19335
+ useCount: row.use_count,
19336
+ conditions
19337
+ });
19338
+ });
19100
19339
  return Promise.resolve(entries);
19101
19340
  }
19102
19341
  /**
@@ -19123,11 +19362,14 @@ var SqliteExceptionsRepository = class {
19123
19362
  }
19124
19363
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19125
19364
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19126
- const rows = this.db.prepare(
19127
- `SELECT * FROM blocked_detections
19365
+ const rows = allRows(
19366
+ this.db.prepare(
19367
+ `SELECT * FROM blocked_detections
19128
19368
  WHERE blocked_at > :cutoff
19129
19369
  ORDER BY blocked_at DESC, rowid DESC`
19130
- ).all({ cutoff: Date.now() - windowMs });
19370
+ ),
19371
+ { cutoff: Date.now() - windowMs }
19372
+ );
19131
19373
  return Promise.resolve(
19132
19374
  rows.map((row) => ({
19133
19375
  reference: row.reference,
@@ -19207,7 +19449,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
19207
19449
  )`;
19208
19450
 
19209
19451
  // ../../packages/persistence/src/repositories/findings.ts
19210
- var GROUP_ROW_CAP = 2e3;
19452
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
19453
+ var CONCAT_SEP = ",";
19454
+ var TUPLE_SEP = "|";
19455
+ function splitConcat(value) {
19456
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
19457
+ }
19211
19458
  function deriveInstanceStatus(row) {
19212
19459
  return deriveFindingStatus({
19213
19460
  kind: row.kind,
@@ -19275,13 +19522,16 @@ var SqliteFindingsRepository = class {
19275
19522
  }
19276
19523
  recentFindings(opts) {
19277
19524
  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,
19525
+ const rows = allRows(
19526
+ this.db.prepare(
19527
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19280
19528
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19281
19529
  FROM findings f JOIN events e ON e.id = f.event_id
19282
19530
  ORDER BY e.occurred_at DESC, f.rowid DESC
19283
19531
  LIMIT :limit`
19284
- ).all({ limit });
19532
+ ),
19533
+ { limit }
19534
+ );
19285
19535
  return Promise.resolve(
19286
19536
  rows.map((r) => ({
19287
19537
  id: r.id,
@@ -19304,20 +19554,47 @@ var SqliteFindingsRepository = class {
19304
19554
  * applies the requested filters, and sorts by severity then recency. Filtering
19305
19555
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19306
19556
  * reflect the full filtered set; `items` is the requested
19307
- * page (default 100); no cursor (nextCursor is always null).
19557
+ * page (default 50); no cursor (nextCursor is always null).
19558
+ *
19559
+ * Two reads, neither of which materializes a row per finding:
19560
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
19561
+ * the group and the filters need (count, providers, actions, statuses,
19562
+ * latest, search text);
19563
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
19564
+ * populate `instances` for the table's expanded rows.
19565
+ * The aggregates carry raw DB values and are translated by the same
19566
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
19567
+ * rule is ever restated in SQL.
19308
19568
  */
19309
19569
  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 });
19570
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
19571
+ const rows = allRows(
19572
+ this.db.prepare(
19573
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19574
+ occurred_at, source_tool, repo, file, kind, finding_key, latest_status
19575
+ FROM (
19576
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19577
+ f.severity AS severity, f.masked_match AS masked_match,
19578
+ f.action_taken AS action_taken, f.confidence AS confidence,
19579
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19580
+ json_extract(e.metadata, '$.repo') AS repo,
19581
+ json_extract(e.metadata, '$.filePath') AS file,
19582
+ e.kind AS kind, f.finding_key AS finding_key,
19583
+ latest.status AS latest_status,
19584
+ ROW_NUMBER() OVER (
19585
+ PARTITION BY f.rule_id
19586
+ ORDER BY e.occurred_at DESC, f.id DESC
19587
+ ) AS rn
19588
+ FROM findings f
19589
+ JOIN events e ON e.id = f.event_id
19590
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19591
+ ON latest.finding_key = f.finding_key
19592
+ )
19593
+ WHERE rn <= :cap
19594
+ ORDER BY occurred_at DESC, id DESC`
19595
+ ),
19596
+ { cap: PREVIEW_INSTANCES_PER_GROUP }
19597
+ );
19321
19598
  const groupable = rows.map((r) => ({
19322
19599
  id: r.id,
19323
19600
  ruleId: r.rule_id,
@@ -19332,7 +19609,7 @@ var SqliteFindingsRepository = class {
19332
19609
  file: r.file ?? "",
19333
19610
  status: deriveInstanceStatus(r)
19334
19611
  }));
19335
- const allGroups = buildFindingGroups(groupable);
19612
+ const allGroups = buildFindingGroups(groupable, { aggregates });
19336
19613
  const filterOpts = {
19337
19614
  severity: query.severity,
19338
19615
  providers: query.provider,
@@ -19350,42 +19627,122 @@ var SqliteFindingsRepository = class {
19350
19627
  const items = sorted.slice(0, limit);
19351
19628
  return Promise.resolve({ totals, facets, items, nextCursor: null });
19352
19629
  }
19630
+ /**
19631
+ * One row per rule_id, folding EVERY instance of the group into the values
19632
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
19633
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
19634
+ *
19635
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
19636
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19637
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19638
+ * status keeps the classifier itself in @akasecurity/schema, where
19639
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
19640
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
19641
+ * a group's row stays small however many findings it holds.
19642
+ *
19643
+ * `withSearchText` is the exception, and the one column here that does NOT
19644
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
19645
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19646
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
19647
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
19648
+ * match an instance outside the preview, which searching the preview alone
19649
+ * would silently lose, so it is fetched only when the request actually
19650
+ * carries a `q`.
19651
+ */
19652
+ groupAggregates(withSearchText) {
19653
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19654
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
19655
+ const rows = this.db.prepare(
19656
+ `SELECT f.rule_id AS rule_id,
19657
+ count(*) AS instance_count,
19658
+ max(e.occurred_at) AS latest_at,
19659
+ group_concat(DISTINCT e.source_tool) AS source_tools,
19660
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
19661
+ group_concat(DISTINCT (
19662
+ e.kind || '${TUPLE_SEP}' ||
19663
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19664
+ coalesce(latest.status, '')
19665
+ )) AS status_inputs
19666
+ ${searchTextColumns}
19667
+ FROM findings f
19668
+ JOIN events e ON e.id = f.event_id
19669
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19670
+ ON latest.finding_key = f.finding_key
19671
+ GROUP BY f.rule_id`
19672
+ ).all();
19673
+ return new Map(
19674
+ rows.map((r) => [
19675
+ r.rule_id,
19676
+ {
19677
+ instanceCount: r.instance_count,
19678
+ sourceTools: splitConcat(r.source_tools),
19679
+ actionsTaken: splitConcat(r.actions_taken),
19680
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19681
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
19682
+ return {
19683
+ // deriveFindingStatus only distinguishes null from non-null here,
19684
+ // so the marker stands in for the key itself (never rendered).
19685
+ kind,
19686
+ findingKey: keyMarker === "" ? null : keyMarker,
19687
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
19688
+ };
19689
+ }),
19690
+ latestDetectedAt: epochMillisToIso(r.latest_at),
19691
+ // Free text only — joined and substring-matched, so group_concat's
19692
+ // commas need no unpicking (a repo/path containing one still matches).
19693
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
19694
+ // tell "no q this request" from "a group with no repo/file at all"
19695
+ // and skip priming a haystack nothing will read.
19696
+ ...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
19697
+ }
19698
+ ])
19699
+ );
19700
+ }
19353
19701
  healthSummary() {
19354
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
19702
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
19355
19703
  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();
19704
+ const grouped = allRows(
19705
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
19706
+ );
19357
19707
  for (const row of grouped) {
19358
19708
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
19359
19709
  }
19360
19710
  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
19711
+ const sevRows = allRows(
19712
+ this.db.prepare(
19713
+ `SELECT f.severity AS severity, count(*) AS c
19363
19714
  FROM findings f
19364
19715
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19365
19716
  ON latest.finding_key = f.finding_key
19366
19717
  WHERE latest.status IS NULL OR latest.status != 'resolved'
19367
19718
  GROUP BY f.severity`
19368
- ).all();
19719
+ )
19720
+ );
19369
19721
  for (const row of sevRows) {
19370
19722
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
19371
19723
  }
19372
19724
  const categories = ENFORCEABLE_CATEGORIES;
19373
- const enabledRows = this.db.prepare(
19374
- `SELECT DISTINCT json_extract(target, '$.category') AS category
19725
+ const enabledRows = allRows(
19726
+ this.db.prepare(
19727
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
19375
19728
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
19376
- ).all();
19729
+ )
19730
+ );
19377
19731
  const enabled = new Set(enabledRows.map((r) => r.category));
19378
19732
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
19379
19733
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
19380
19734
  }
19381
19735
  activityByDay(days = 7) {
19382
19736
  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
19737
+ const rows = allRows(
19738
+ this.db.prepare(
19739
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19385
19740
  FROM findings f JOIN events e ON e.id = f.event_id
19386
19741
  WHERE e.occurred_at >= :since
19387
19742
  GROUP BY day, f.action_taken`
19388
- ).all({ since });
19743
+ ),
19744
+ { since }
19745
+ );
19389
19746
  const buckets = /* @__PURE__ */ new Map();
19390
19747
  for (let i = 0; i < days; i++) {
19391
19748
  const day = isoDay(since + i * DAY_MS3);
@@ -19457,17 +19814,19 @@ var SqliteInspectionFindingsRepository = class {
19457
19814
  insertStmt;
19458
19815
  insertFinding(input) {
19459
19816
  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
- });
19817
+ this.insertStmt.run(
19818
+ bindParams({
19819
+ id: row.id,
19820
+ auditEventId: row.auditEventId,
19821
+ inspectionDefinitionId: row.inspectionDefinitionId,
19822
+ classifiedDataId: row.classifiedDataId,
19823
+ spanStart: row.spanStart,
19824
+ spanEnd: row.spanEnd,
19825
+ maskedMatch: row.maskedMatch,
19826
+ actionTaken: row.actionTaken,
19827
+ confidence: row.confidence
19828
+ })
19829
+ );
19471
19830
  }
19472
19831
  };
19473
19832
 
@@ -19543,12 +19902,7 @@ function isMirrorDowngrade(incoming, stored) {
19543
19902
  }
19544
19903
  function ruleIdsOf(rulesJson) {
19545
19904
  const ids = /* @__PURE__ */ new Set();
19546
- let raw;
19547
- try {
19548
- raw = JSON.parse(rulesJson);
19549
- } catch {
19550
- return ids;
19551
- }
19905
+ const raw = safeJson(rulesJson, []);
19552
19906
  if (!Array.isArray(raw)) return ids;
19553
19907
  for (const entry of raw) {
19554
19908
  if (entry && typeof entry === "object") {
@@ -19621,47 +19975,48 @@ var SqliteInstalledPacksRepository = class {
19621
19975
  }));
19622
19976
  if (this.storedSignature() === inventorySignature(rows)) return;
19623
19977
  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,
19978
+ withTransaction(
19979
+ this.db,
19980
+ () => {
19981
+ const mirror = this.mirrorState();
19982
+ let behind = false;
19983
+ for (const row of rows) {
19984
+ const params = {
19642
19985
  id: randomUUID2(),
19643
- recordedBy: meta3?.recordedBy ?? null
19644
- });
19645
- } else {
19646
- behind = true;
19986
+ namespace: row.namespace,
19987
+ packId: row.packId,
19988
+ version: row.version,
19989
+ name: row.name,
19990
+ rulesJson: row.rulesJson,
19991
+ now
19992
+ };
19993
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
19994
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
19995
+ this.upsertAvailableStmt.run({
19996
+ ...params,
19997
+ id: randomUUID2(),
19998
+ recordedBy: meta3?.recordedBy ?? null
19999
+ });
20000
+ } else {
20001
+ behind = true;
20002
+ }
20003
+ this.insertMissingStmt.run(params);
19647
20004
  }
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
- }
20005
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20006
+ },
20007
+ "IMMEDIATE"
20008
+ );
19656
20009
  } catch {
19657
20010
  }
19658
20011
  }
19659
20012
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
19660
20013
  // input to the downgrade guard. Read INSIDE the write transaction.
19661
20014
  mirrorState() {
19662
- const rows = this.db.prepare(
19663
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
19664
- ).all();
20015
+ const rows = allRows(
20016
+ this.db.prepare(
20017
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20018
+ )
20019
+ );
19665
20020
  return new Map(
19666
20021
  rows.map((r) => [
19667
20022
  `${r.namespace}/${r.packId}`,
@@ -19673,7 +20028,9 @@ var SqliteInstalledPacksRepository = class {
19673
20028
  // (keys joined with '/', matching the detection id slug encoding — packId may
19674
20029
  // itself contain '/', but namespace may not, so the join is unambiguous).
19675
20030
  pruneAvailable(keep) {
19676
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20031
+ const rows = allRows(
20032
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20033
+ );
19677
20034
  const keepSet = new Set(keep);
19678
20035
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
19679
20036
  for (const r of rows) {
@@ -19700,11 +20057,13 @@ var SqliteInstalledPacksRepository = class {
19700
20057
  if (this.db.isTransaction) {
19701
20058
  throw new Error("applyUpdate must not be called inside an open transaction");
19702
20059
  }
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
20060
+ let changed = false;
20061
+ withTransaction(
20062
+ this.db,
20063
+ () => {
20064
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20065
+ const res = this.db.prepare(
20066
+ `UPDATE installed_packs SET
19708
20067
  version = (SELECT a.version FROM available_packs a
19709
20068
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
19710
20069
  name = (SELECT a.name FROM available_packs a
@@ -19715,17 +20074,13 @@ var SqliteInstalledPacksRepository = class {
19715
20074
  WHERE namespace = :namespace AND pack_id = :packId
19716
20075
  AND EXISTS (SELECT 1 FROM available_packs a
19717
20076
  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
- }
20077
+ ).run({ namespace, packId, now: Date.now() });
20078
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20079
+ changed = Number(res.changes) > 0;
20080
+ },
20081
+ "IMMEDIATE"
20082
+ );
20083
+ return changed;
19729
20084
  }
19730
20085
  /**
19731
20086
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -19739,9 +20094,11 @@ var SqliteInstalledPacksRepository = class {
19739
20094
  * JSON-level failure therefore counts as invalid.
19740
20095
  */
19741
20096
  installedRuleset() {
19742
- const rows = this.db.prepare(
19743
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
19744
- ).all();
20097
+ const rows = allRows(
20098
+ this.db.prepare(
20099
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20100
+ )
20101
+ );
19745
20102
  const out = {
19746
20103
  installedPacks: rows.length,
19747
20104
  enabledPacks: 0,
@@ -19750,7 +20107,7 @@ var SqliteInstalledPacksRepository = class {
19750
20107
  ruleActions: /* @__PURE__ */ new Map()
19751
20108
  };
19752
20109
  for (const row of rows) {
19753
- if (row.enabled !== 1) continue;
20110
+ if (!intToBool(row.enabled)) continue;
19754
20111
  out.enabledPacks += 1;
19755
20112
  const action = policyIdToAction(row.policyId);
19756
20113
  let raw;
@@ -19785,9 +20142,11 @@ var SqliteInstalledPacksRepository = class {
19785
20142
  * running max would mask a genuinely-newer parseable stamp.
19786
20143
  */
19787
20144
  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();
20145
+ const rows = allRows(
20146
+ this.db.prepare(
20147
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20148
+ )
20149
+ );
19791
20150
  let newest = null;
19792
20151
  for (const row of rows) {
19793
20152
  const at = row.recordedBy.lastIndexOf("@");
@@ -19802,13 +20161,15 @@ var SqliteInstalledPacksRepository = class {
19802
20161
  return newest;
19803
20162
  }
19804
20163
  counts() {
19805
- const row = this.db.prepare(
19806
- `SELECT count(*) AS packs,
20164
+ const row = getRow(
20165
+ this.db.prepare(
20166
+ `SELECT count(*) AS packs,
19807
20167
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
19808
20168
  coalesce(sum(enabled), 0) AS enabled
19809
20169
  FROM installed_packs`
19810
- ).get();
19811
- return Promise.resolve(row);
20170
+ )
20171
+ );
20172
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
19812
20173
  }
19813
20174
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
19814
20175
  // Back the Policies page's built-in catalog: how many
@@ -19821,26 +20182,29 @@ var SqliteInstalledPacksRepository = class {
19821
20182
  * attributed to Monitor, matching the Detections views.
19822
20183
  */
19823
20184
  countsByPolicyId() {
19824
- const rows = this.db.prepare(
19825
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20185
+ return countBy(
20186
+ this.db,
20187
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
19826
20188
  FROM installed_packs
19827
- GROUP BY pid`
19828
- ).all();
19829
- return new Map(rows.map((r) => [r.pid, r.n]));
20189
+ GROUP BY k`
20190
+ );
19830
20191
  }
19831
20192
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
19832
20193
  listByPolicyId(policyId) {
19833
- const rows = this.db.prepare(
19834
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20194
+ const rows = allRows(
20195
+ this.db.prepare(
20196
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
19835
20197
  FROM installed_packs
19836
20198
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
19837
20199
  ORDER BY name ASC`
19838
- ).all(policyId);
20200
+ ),
20201
+ [policyId]
20202
+ );
19839
20203
  return rows.map((r) => ({
19840
20204
  id: `${r.namespace}/${r.packId}`,
19841
20205
  name: r.name,
19842
20206
  ruleCount: parseRules(r.rulesJson).length,
19843
- enabled: r.enabled === 1
20207
+ enabled: intToBool(r.enabled)
19844
20208
  }));
19845
20209
  }
19846
20210
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -19869,14 +20233,14 @@ var SqliteInstalledPacksRepository = class {
19869
20233
  const res = this.db.prepare(
19870
20234
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
19871
20235
  WHERE namespace = :namespace AND pack_id = :packId`
19872
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
20236
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
19873
20237
  return Number(res.changes) > 0;
19874
20238
  }
19875
20239
  // Fingerprint of the recorded available mirror — compared against the
19876
20240
  // incoming inventory's signature to skip the write entirely when the running
19877
20241
  // binary's inventory hasn't changed since the last record.
19878
20242
  storedSignature() {
19879
- const rows = this.signatureStmt.all();
20243
+ const rows = allRows(this.signatureStmt);
19880
20244
  return inventorySignature(rows);
19881
20245
  }
19882
20246
  };
@@ -19904,42 +20268,48 @@ var SqliteInventoryRepository = class {
19904
20268
  upsert(input, now = Date.now()) {
19905
20269
  const id = inventoryId(input.objectType, input.identityKey);
19906
20270
  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
- });
20271
+ this.upsertStmt.run(
20272
+ bindParams({
20273
+ id: row.id,
20274
+ objectType: row.objectType,
20275
+ location: row.location,
20276
+ title: row.title,
20277
+ hostId: row.hostId,
20278
+ attributes: row.attributes,
20279
+ firstSeen: row.firstSeen,
20280
+ lastSeen: row.lastSeen
20281
+ })
20282
+ );
19917
20283
  return id;
19918
20284
  }
19919
20285
  // The full row, for round-trip assertions.
19920
20286
  findById(id) {
19921
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
19922
- return row;
20287
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
19923
20288
  }
19924
20289
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
19925
20290
  // served from the object_type index, never from audit_events.
19926
20291
  distinctTitles(objectType) {
19927
- const rows = this.db.prepare(
19928
- `SELECT DISTINCT title FROM inventory
20292
+ const rows = allRows(
20293
+ this.db.prepare(
20294
+ `SELECT DISTINCT title FROM inventory
19929
20295
  WHERE object_type = :objectType AND title IS NOT NULL
19930
20296
  ORDER BY title`
19931
- ).all({ objectType });
20297
+ ),
20298
+ { objectType }
20299
+ );
19932
20300
  return rows.map((r) => r.title);
19933
20301
  }
19934
20302
  // Distinct host os_version values — a facet served from an inventory index
19935
20303
  // over the generated column, never from the audit fact (confirm via EXPLAIN
19936
20304
  // QUERY PLAN).
19937
20305
  osVersions() {
19938
- const rows = this.db.prepare(
19939
- `SELECT DISTINCT os_version AS value FROM inventory
20306
+ const rows = allRows(
20307
+ this.db.prepare(
20308
+ `SELECT DISTINCT os_version AS value FROM inventory
19940
20309
  WHERE object_type = 'host' AND os_version IS NOT NULL
19941
20310
  ORDER BY value`
19942
- ).all();
20311
+ )
20312
+ );
19943
20313
  return rows.map((r) => r.value);
19944
20314
  }
19945
20315
  };
@@ -19959,14 +20329,6 @@ var EMPTY_PROJECT_AGG = {
19959
20329
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
19960
20330
  findingsCount: 0
19961
20331
  };
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
20332
  function resolveHarnessId(attrs, row) {
19971
20333
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
19972
20334
  return attrs.provider;
@@ -20162,30 +20524,49 @@ var SqliteInventoryAssetsRepository = class {
20162
20524
  configRowsCache;
20163
20525
  // ─── stats ─────────────────────────────────────────────────────────────────
20164
20526
  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
20527
+ const typeCounts = countBy(
20528
+ this.db,
20529
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
20530
+ );
20531
+ const byType = {
20532
+ project: 0,
20533
+ skill: typeCounts.get("skill") ?? 0,
20534
+ mcp: typeCounts.get("mcp") ?? 0,
20535
+ hook: typeCounts.get("hook") ?? 0,
20536
+ config: typeCounts.get("config") ?? 0
20537
+ };
20538
+ byType.project = countScalar(
20539
+ this.db,
20540
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
20541
+ );
20542
+ const mcpTrustCounts = countBy(
20543
+ this.db,
20544
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20173
20545
  FROM inventory_asset a
20174
20546
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20175
20547
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20176
20548
  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(
20549
+ );
20550
+ const mcpTrust = {
20551
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
20552
+ risky: mcpTrustCounts.get("risky") ?? 0,
20553
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
20554
+ };
20555
+ const harnesses = countScalar(
20556
+ this.db,
20181
20557
  `SELECT count(*) AS n FROM inventory
20182
20558
  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(
20559
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
20560
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20561
+ );
20562
+ const flaggedAssets = countScalar(
20563
+ this.db,
20564
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
20565
+ );
20566
+ const flaggedProjects = countScalar(
20567
+ this.db,
20187
20568
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20188
- ).get().n;
20569
+ );
20189
20570
  const configRows = this.configAssetRows();
20190
20571
  for (const r of configRows) {
20191
20572
  byType[r.assetType] += 1;
@@ -20442,12 +20823,15 @@ var SqliteInventoryAssetsRepository = class {
20442
20823
  }
20443
20824
  // ─── raw fetchers ────────────────────────────────────────────────────────────
20444
20825
  fetchHarnessRows() {
20445
- return this.db.prepare(
20446
- `SELECT id, title, attributes, harness_version AS harnessVersion
20826
+ return allRows(
20827
+ this.db.prepare(
20828
+ `SELECT id, title, attributes, harness_version AS harnessVersion
20447
20829
  FROM inventory
20448
20830
  WHERE object_type = 'harness'
20449
20831
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20450
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
20832
+ ),
20833
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20834
+ );
20451
20835
  }
20452
20836
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
20453
20837
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -20457,12 +20841,13 @@ var SqliteInventoryAssetsRepository = class {
20457
20841
  const params = [...harnessInvIds];
20458
20842
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
20459
20843
  if (q) {
20460
- const pat = `%${escapeLikePattern(q)}%`;
20461
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
20844
+ const pat = containsPattern(q);
20845
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
20462
20846
  params.push(pat, pat);
20463
20847
  }
20464
- const rows = this.db.prepare(
20465
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20848
+ const rows = allRows(
20849
+ this.db.prepare(
20850
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20466
20851
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20467
20852
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20468
20853
  FROM harness_asset ha
@@ -20470,7 +20855,9 @@ var SqliteInventoryAssetsRepository = class {
20470
20855
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20471
20856
  WHERE ${where}
20472
20857
  ORDER BY a.name ASC`
20473
- ).all(...params);
20858
+ ),
20859
+ params
20860
+ );
20474
20861
  for (const raw of rows) {
20475
20862
  const harnessInvId = raw.harnessInvId;
20476
20863
  const [asset] = this.mapAssetRows([raw]);
@@ -20489,21 +20876,24 @@ var SqliteInventoryAssetsRepository = class {
20489
20876
  params.push(...types);
20490
20877
  }
20491
20878
  if (q) {
20492
- const pat = `%${escapeLikePattern(q)}%`;
20493
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
20879
+ const pat = containsPattern(q);
20880
+ conditions.push(likeAny(["a.name", "a.sub"]));
20494
20881
  params.push(pat, pat);
20495
20882
  }
20496
20883
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
20497
20884
  const sampleRows = this.mapAssetRows(
20498
- this.db.prepare(
20499
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20885
+ allRows(
20886
+ this.db.prepare(
20887
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20500
20888
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20501
20889
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20502
20890
  FROM inventory_asset a
20503
20891
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20504
20892
  ${where}
20505
20893
  ORDER BY a.name ASC`
20506
- ).all(...params)
20894
+ ),
20895
+ params
20896
+ )
20507
20897
  );
20508
20898
  const configRows = this.configAssetRows(q).filter(
20509
20899
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -20512,14 +20902,17 @@ var SqliteInventoryAssetsRepository = class {
20512
20902
  }
20513
20903
  fetchAssetById(assetId) {
20514
20904
  const rows = this.mapAssetRows(
20515
- this.db.prepare(
20516
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20905
+ allRows(
20906
+ this.db.prepare(
20907
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20517
20908
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20518
20909
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20519
20910
  FROM inventory_asset a
20520
20911
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20521
20912
  WHERE a.id = ?`
20522
- ).all(assetId)
20913
+ ),
20914
+ [assetId]
20915
+ )
20523
20916
  );
20524
20917
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
20525
20918
  }
@@ -20575,37 +20968,39 @@ var SqliteInventoryAssetsRepository = class {
20575
20968
  return rows;
20576
20969
  }
20577
20970
  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;
20971
+ return latestConfigScan(this.db)?.id ?? null;
20583
20972
  }
20584
20973
  fetchProjects(q) {
20585
20974
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
20586
20975
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
20587
20976
  const params = [];
20588
20977
  if (q) {
20589
- const pat = `%${escapeLikePattern(q)}%`;
20590
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
20978
+ const pat = containsPattern(q);
20979
+ sql += ` AND ${likeAny(["name", "url"])}`;
20591
20980
  params.push(pat, pat);
20592
20981
  }
20593
20982
  sql += " ORDER BY name ASC";
20594
- return this.db.prepare(sql).all(...params);
20983
+ return allRows(this.db.prepare(sql), params);
20595
20984
  }
20596
20985
  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;
20986
+ return getRow(
20987
+ this.db.prepare(
20988
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
20989
+ ),
20990
+ [projectId]
20991
+ ) ?? null;
20600
20992
  }
20601
20993
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
20602
20994
  fetchProjectsByIds(projectIds) {
20603
20995
  const map2 = /* @__PURE__ */ new Map();
20604
20996
  if (projectIds.length === 0) return map2;
20605
- const rows = this.db.prepare(
20606
- `SELECT id, url, name, attributes, last_seen AS lastSeen
20997
+ const rows = allRows(
20998
+ this.db.prepare(
20999
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
20607
21000
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
20608
- ).all(...projectIds);
21001
+ ),
21002
+ projectIds
21003
+ );
20609
21004
  for (const r of rows) map2.set(r.id, r);
20610
21005
  return map2;
20611
21006
  }
@@ -20616,8 +21011,9 @@ var SqliteInventoryAssetsRepository = class {
20616
21011
  projectAggregates(projectIds) {
20617
21012
  const map2 = /* @__PURE__ */ new Map();
20618
21013
  if (projectIds.length === 0) return map2;
20619
- const rows = this.db.prepare(
20620
- `SELECT f.project_id AS projectId,
21014
+ const rows = allRows(
21015
+ this.db.prepare(
21016
+ `SELECT f.project_id AS projectId,
20621
21017
  coalesce(o.access, f.default_access) AS eff,
20622
21018
  count(*) AS n,
20623
21019
  coalesce(sum(f.findings_count), 0) AS findings
@@ -20625,7 +21021,9 @@ var SqliteInventoryAssetsRepository = class {
20625
21021
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
20626
21022
  WHERE f.project_id IN (${placeholders(projectIds.length)})
20627
21023
  GROUP BY f.project_id, eff`
20628
- ).all(...projectIds);
21024
+ ),
21025
+ projectIds
21026
+ );
20629
21027
  for (const r of rows) {
20630
21028
  let agg = map2.get(r.projectId);
20631
21029
  if (!agg) {
@@ -20663,37 +21061,52 @@ var SqliteInventoryAssetsRepository = class {
20663
21061
  fetchProjectFilesUnder(projectId, prefix) {
20664
21062
  if (prefix === "") {
20665
21063
  return this.mapFileRows(
20666
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21064
+ allRows(
21065
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21066
+ [projectId]
21067
+ )
20667
21068
  );
20668
21069
  }
20669
21070
  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)}/%`)
21071
+ allRows(
21072
+ this.db.prepare(
21073
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21074
+ ),
21075
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21076
+ )
20673
21077
  );
20674
21078
  }
20675
21079
  fetchProjectFilesSearch(projectId, q) {
20676
- const pat = `%${escapeLikePattern(q)}%`;
21080
+ const pat = containsPattern(q);
20677
21081
  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)
21082
+ allRows(
21083
+ this.db.prepare(
21084
+ this.fileSelect(
21085
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21086
+ )
21087
+ ),
21088
+ [projectId, pat, pat]
21089
+ )
20683
21090
  );
20684
21091
  }
20685
21092
  fetchProjectFilesBlocked(projectId) {
20686
21093
  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)
21094
+ allRows(
21095
+ this.db.prepare(
21096
+ this.fileSelect(
21097
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21098
+ )
21099
+ ),
21100
+ [projectId]
21101
+ )
20692
21102
  );
20693
21103
  }
20694
21104
  fetchProjectFile(projectId, path) {
20695
21105
  const rows = this.mapFileRows(
20696
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21106
+ allRows(
21107
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21108
+ [projectId, path]
21109
+ )
20697
21110
  );
20698
21111
  return rows[0] ?? null;
20699
21112
  }
@@ -20707,39 +21120,32 @@ var SqlitePoliciesRepository = class {
20707
21120
  }
20708
21121
  db;
20709
21122
  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
- }
21123
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21124
+ const policies = mapRowsTolerant(rows, (row) => {
21125
+ const target = JSON.parse(row.target);
21126
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21127
+ return Policy.parse({
21128
+ id: row.id,
21129
+ scope: row.scope,
21130
+ target,
21131
+ action: row.action,
21132
+ enabled: intToBool(row.enabled),
21133
+ customKeywords
21134
+ });
21135
+ });
20729
21136
  return Promise.resolve(policies);
20730
21137
  }
20731
21138
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
20732
21139
  // detection-type config exists from first run. Only when the table is empty,
20733
21140
  // so a user's edits are never clobbered.
20734
21141
  seedDefaults() {
20735
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21142
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
20736
21143
  if (count > 0) return;
20737
21144
  const stmt = this.db.prepare(
20738
21145
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
20739
21146
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
20740
21147
  );
20741
- this.db.exec("BEGIN");
20742
- try {
21148
+ failOpenTransaction(this.db, () => {
20743
21149
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
20744
21150
  stmt.run({
20745
21151
  id: randomUUID4(),
@@ -20748,10 +21154,41 @@ var SqlitePoliciesRepository = class {
20748
21154
  now: Date.now()
20749
21155
  });
20750
21156
  }
20751
- this.db.exec("COMMIT");
20752
- } catch {
20753
- this.db.exec("ROLLBACK");
20754
- }
21157
+ });
21158
+ }
21159
+ // Insert-or-update the single global per-category policy row, keyed on the
21160
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21161
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21162
+ // values), so the runtime's resolveAction reads rows written by either path
21163
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21164
+ // `id` and `created_at` are left exactly as they were.
21165
+ upsertCategoryAction(category, action) {
21166
+ const now = Date.now();
21167
+ this.db.prepare(
21168
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21169
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21170
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21171
+ ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
21172
+ }
21173
+ // Caps every global per-category policy currently set to block/redact down
21174
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21175
+ // Returns the number of rows changed.
21176
+ capCategoryActions() {
21177
+ const info = this.db.prepare(
21178
+ `UPDATE policies SET action='warn', updated_at=:now
21179
+ WHERE scope='global' AND action IN ('block','redact')
21180
+ AND json_extract(target,'$.category') IS NOT NULL`
21181
+ ).run({ now: Date.now() });
21182
+ return Number(info.changes);
21183
+ }
21184
+ // Read the current action for a single global per-category policy row, mirroring
21185
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21186
+ // row exists yet, so callers can distinguish an unset category from a set one.
21187
+ getCategoryAction(category) {
21188
+ const row = this.db.prepare(
21189
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21190
+ ).get({ category });
21191
+ return row?.action;
20755
21192
  }
20756
21193
  };
20757
21194
 
@@ -20827,7 +21264,7 @@ var SqliteProjectFilesRepository = class {
20827
21264
  maxStampStmt;
20828
21265
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
20829
21266
  replaceForProject(projectId, scan2, now) {
20830
- const { maxStamp } = this.maxStampStmt.get({ projectId });
21267
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
20831
21268
  const stamp = Math.max(now, maxStamp + 1);
20832
21269
  for (const file2 of scan2.files) {
20833
21270
  this.upsertStmt.run({
@@ -20910,7 +21347,7 @@ var SqliteResolutionsRepository = class {
20910
21347
  }
20911
21348
  /** The newest disposition recorded for a finding key, or undefined if none. */
20912
21349
  latestByKey(key) {
20913
- const row = this.latestStmt.get({ findingKey: key });
21350
+ const row = getRow(this.latestStmt, { findingKey: key });
20914
21351
  if (!row) return void 0;
20915
21352
  return {
20916
21353
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -20927,7 +21364,7 @@ var SqliteResolutionsRepository = class {
20927
21364
  * the CLI) surfaces for that file.
20928
21365
  */
20929
21366
  openAtRestKeysForPath(path) {
20930
- const rows = this.openAtRestStmt.all({ path });
21367
+ const rows = allRows(this.openAtRestStmt, { path });
20931
21368
  return rows.map((r) => r.finding_key);
20932
21369
  }
20933
21370
  /**
@@ -20938,7 +21375,7 @@ var SqliteResolutionsRepository = class {
20938
21375
  * resolution row (see scan.ts).
20939
21376
  */
20940
21377
  resolvedAtRestKeysForPath(path) {
20941
- const rows = this.resolvedAtRestStmt.all({ path });
21378
+ const rows = allRows(this.resolvedAtRestStmt, { path });
20942
21379
  return rows.map((r) => r.finding_key);
20943
21380
  }
20944
21381
  };
@@ -20967,31 +21404,25 @@ var SqliteScanLedgerRepository = class {
20967
21404
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
20968
21405
  // older ruleset are simply absent, which reads as "never scanned".
20969
21406
  entriesForRuleset(rulesetHash) {
20970
- const rows = this.readStmt.all({ rulesetHash });
21407
+ const rows = allRows(this.readStmt, {
21408
+ rulesetHash
21409
+ });
20971
21410
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
20972
21411
  }
20973
21412
  upsertEntries(entries) {
20974
21413
  if (entries.length === 0) return;
20975
21414
  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;
21415
+ failOpenTransaction(this.db, () => {
21416
+ for (const entry of entries) {
21417
+ this.upsertStmt.run({
21418
+ path: entry.path,
21419
+ mtime: entry.mtime,
21420
+ contentHash: entry.contentHash,
21421
+ rulesetHash: entry.rulesetHash,
21422
+ scannedAt
21423
+ });
20992
21424
  }
20993
- } catch {
20994
- }
21425
+ });
20995
21426
  }
20996
21427
  };
20997
21428
 
@@ -21068,8 +21499,9 @@ var SqliteSecurityRepository = class {
21068
21499
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21069
21500
  // double-counting a key that accumulated several append-only rows.
21070
21501
  severitySummary() {
21071
- const rows = this.db.prepare(
21072
- `SELECT f.severity AS severity,
21502
+ const rows = allRows(
21503
+ this.db.prepare(
21504
+ `SELECT f.severity AS severity,
21073
21505
  COUNT(*) AS count,
21074
21506
  SUM(CASE
21075
21507
  WHEN e.kind != 'code_change' THEN 1
@@ -21088,7 +21520,8 @@ var SqliteSecurityRepository = class {
21088
21520
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21089
21521
  ON latest.finding_key = f.finding_key
21090
21522
  GROUP BY f.severity`
21091
- ).all();
21523
+ )
21524
+ );
21092
21525
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21093
21526
  const bySeverity = SEVERITIES.map((severity) => ({
21094
21527
  severity,
@@ -21172,14 +21605,15 @@ var SqliteSecurityRepository = class {
21172
21605
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21173
21606
  const now = this.now();
21174
21607
  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,
21608
+ const rows = allRows(
21609
+ this.db.prepare(
21610
+ // first_detected_at is the PRESERVED first-detection time (set once on a
21611
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21612
+ // measures from first sighting not the latest re-scan's event, whose
21613
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21614
+ // the parent event's occurred_at defends against any legacy/edge row the
21615
+ // backfill left null.
21616
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21183
21617
  (
21184
21618
  SELECT fr.status FROM finding_resolution fr
21185
21619
  WHERE fr.finding_key = f.finding_key
@@ -21205,14 +21639,16 @@ var SqliteSecurityRepository = class {
21205
21639
  WHERE fr.finding_key = f.finding_key
21206
21640
  AND fr.resolved_at >= :windowStart
21207
21641
  )`
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 });
21642
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21643
+ // any resolution activity at/after the window start — a row this method
21644
+ // ultimately counts has its LATEST resolution inside the window, which
21645
+ // implies such a row exists, so nothing wanted is dropped. The exact
21646
+ // latest-wins + status/method + window gate stays in JS below,
21647
+ // dialect-agnostic. Without this, a
21648
+ // 7d request evaluated the store's entire trackable-findings history.
21649
+ ),
21650
+ { windowStart }
21651
+ );
21216
21652
  const sums = /* @__PURE__ */ new Map();
21217
21653
  const counts = /* @__PURE__ */ new Map();
21218
21654
  for (const r of rows) {
@@ -21242,8 +21678,9 @@ var SqliteSecurityRepository = class {
21242
21678
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
21243
21679
  const now = this.now();
21244
21680
  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
21681
+ const rows = allRows(
21682
+ this.db.prepare(
21683
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21247
21684
  FROM findings f JOIN events e ON e.id = f.event_id
21248
21685
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21249
21686
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -21251,7 +21688,9 @@ var SqliteSecurityRepository = class {
21251
21688
  GROUP BY repo
21252
21689
  ORDER BY c DESC, repo
21253
21690
  LIMIT :limit`
21254
- ).all({ from, to: now, limit });
21691
+ ),
21692
+ { from, to: now, limit }
21693
+ );
21255
21694
  const items = rows.map((r) => ({
21256
21695
  id: `repo_${r.repo}`,
21257
21696
  name: r.repo,
@@ -21273,8 +21712,9 @@ var SqliteSecurityRepository = class {
21273
21712
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21274
21713
  // capped at `limit`.
21275
21714
  recentlyResolved(limit = 20) {
21276
- const rows = this.db.prepare(
21277
- `SELECT f.finding_key AS finding_key,
21715
+ const rows = allRows(
21716
+ this.db.prepare(
21717
+ `SELECT f.finding_key AS finding_key,
21278
21718
  f.rule_id AS rule_id,
21279
21719
  f.severity AS severity,
21280
21720
  json_extract(e.metadata, '$.filePath') AS path,
@@ -21308,7 +21748,9 @@ var SqliteSecurityRepository = class {
21308
21748
  ) IS NOT NULL
21309
21749
  ORDER BY latest_resolved_at DESC
21310
21750
  LIMIT :limit`
21311
- ).all({ limit });
21751
+ ),
21752
+ { limit }
21753
+ );
21312
21754
  const items = rows.map((r) => ({
21313
21755
  findingKey: r.finding_key,
21314
21756
  ruleId: r.rule_id,
@@ -21325,12 +21767,15 @@ var SqliteSecurityRepository = class {
21325
21767
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
21326
21768
  // numeric and the JS aggregations bucket/split on ms directly.
21327
21769
  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
21770
+ const rows = allRows(
21771
+ this.db.prepare(
21772
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21330
21773
  FROM findings f JOIN events e ON e.id = f.event_id
21331
21774
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21332
21775
  ORDER BY e.occurred_at`
21333
- ).all({ from: fromMs, to: toMs });
21776
+ ),
21777
+ { from: fromMs, to: toMs }
21778
+ );
21334
21779
  return rows.map((r) => ({
21335
21780
  occurredAt: r.occurred_at,
21336
21781
  severity: r.severity,
@@ -21344,12 +21789,7 @@ import { randomUUID as randomUUID7 } from "crypto";
21344
21789
  var KIND_ORDER = ["provider", "internal", "ip"];
21345
21790
  var CALL_SITE_EMBED_CAP = 200;
21346
21791
  function parseNetwork(networkJson) {
21347
- if (!networkJson) return null;
21348
- try {
21349
- return JSON.parse(networkJson);
21350
- } catch {
21351
- return null;
21352
- }
21792
+ return safeJson(networkJson, null);
21353
21793
  }
21354
21794
  function toEndpointSummary(row) {
21355
21795
  return {
@@ -21436,27 +21876,39 @@ var SqliteSharesRepository = class {
21436
21876
  }
21437
21877
  db;
21438
21878
  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(
21879
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
21880
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
21881
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
21882
+ const insecure = countScalar(
21883
+ this.db,
21444
21884
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
21445
21885
  );
21446
- const needsReview = scalar(
21886
+ const needsReview = countScalar(
21887
+ this.db,
21447
21888
  `SELECT count(DISTINCT d.id) AS n
21448
21889
  FROM share_destination d
21449
21890
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
21450
21891
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
21451
21892
  );
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
- }
21893
+ const kindCounts = countBy(
21894
+ this.db,
21895
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
21896
+ );
21897
+ const byKind = {
21898
+ provider: kindCounts.get("provider") ?? 0,
21899
+ internal: kindCounts.get("internal") ?? 0,
21900
+ ip: kindCounts.get("ip") ?? 0
21901
+ };
21902
+ const trustCounts = countBy(
21903
+ this.db,
21904
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
21905
+ );
21906
+ const byTrust = {
21907
+ recognized: trustCounts.get("recognized") ?? 0,
21908
+ internal: trustCounts.get("internal") ?? 0,
21909
+ unverified: trustCounts.get("unverified") ?? 0,
21910
+ ip: trustCounts.get("ip") ?? 0
21911
+ };
21460
21912
  return Promise.resolve({
21461
21913
  destinations,
21462
21914
  endpoints,
@@ -21572,7 +22024,7 @@ var SqliteSharesRepository = class {
21572
22024
  }
21573
22025
  let sql;
21574
22026
  if (q) {
21575
- const pattern = `%${escapeLikePattern(q)}%`;
22027
+ const pattern = containsPattern(q);
21576
22028
  conditions.push(
21577
22029
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
21578
22030
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -21592,24 +22044,31 @@ var SqliteSharesRepository = class {
21592
22044
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
21593
22045
  ORDER BY d.created_at ASC, d.id ASC`;
21594
22046
  }
21595
- const rows = this.db.prepare(sql).all(...params);
22047
+ const rows = allRows(
22048
+ this.db.prepare(sql),
22049
+ params
22050
+ );
21596
22051
  return rows.map((r) => this.mapDestRow(r));
21597
22052
  }
21598
22053
  fetchDestinationById(destinationId) {
21599
- const row = this.db.prepare(
21600
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22054
+ const row = getRow(
22055
+ this.db.prepare(
22056
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
21601
22057
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
21602
22058
  o.decision AS overrideDecision
21603
22059
  FROM share_destination d
21604
22060
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
21605
22061
  WHERE d.id = ?`
21606
- ).get(destinationId);
22062
+ ),
22063
+ [destinationId]
22064
+ );
21607
22065
  return row ? this.mapDestRow(row) : null;
21608
22066
  }
21609
22067
  fetchEndpoints(destinationIds) {
21610
22068
  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,
22069
+ const rows = allRows(
22070
+ this.db.prepare(
22071
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
21613
22072
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
21614
22073
  count(c.id) AS callSiteCount
21615
22074
  FROM share_endpoint e
@@ -21617,7 +22076,9 @@ var SqliteSharesRepository = class {
21617
22076
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
21618
22077
  GROUP BY e.id
21619
22078
  ORDER BY e.created_at ASC, e.id ASC`
21620
- ).all(...destinationIds);
22079
+ ),
22080
+ destinationIds
22081
+ );
21621
22082
  return rows.map((r) => ({
21622
22083
  id: r.id,
21623
22084
  destinationId: r.destinationId,
@@ -21642,13 +22103,16 @@ var SqliteSharesRepository = class {
21642
22103
  }
21643
22104
  fetchCallSites(endpointIds) {
21644
22105
  if (endpointIds.length === 0) return [];
21645
- const rows = this.db.prepare(
21646
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22106
+ const rows = allRows(
22107
+ this.db.prepare(
22108
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
21647
22109
  project_id AS projectId
21648
22110
  FROM share_call_site
21649
22111
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
21650
22112
  ORDER BY created_at ASC, id ASC`
21651
- ).all(...endpointIds);
22113
+ ),
22114
+ endpointIds
22115
+ );
21652
22116
  return rows.map((r) => ({
21653
22117
  id: r.id,
21654
22118
  endpointId: r.endpointId,
@@ -21684,27 +22148,36 @@ var SqliteSourceProjectRepository = class {
21684
22148
  upsert(input, now = Date.now()) {
21685
22149
  const id = sourceProjectId(input.url);
21686
22150
  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
- });
22151
+ this.upsertStmt.run(
22152
+ bindParams({
22153
+ id: row.id,
22154
+ url: row.url,
22155
+ name: row.name,
22156
+ attributes: row.attributes,
22157
+ firstSeen: row.firstSeen,
22158
+ lastSeen: row.lastSeen
22159
+ })
22160
+ );
21695
22161
  return id;
21696
22162
  }
21697
22163
  findById(id) {
21698
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22164
+ return getRow(
22165
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22166
+ {
22167
+ id
22168
+ }
22169
+ );
21699
22170
  }
21700
22171
  // Distinct project names — a filter facet, served from the source_project
21701
22172
  // table, never from the audit fact table.
21702
22173
  distinctNames() {
21703
- const rows = this.db.prepare(
21704
- `SELECT DISTINCT name FROM source_project
22174
+ const rows = allRows(
22175
+ this.db.prepare(
22176
+ `SELECT DISTINCT name FROM source_project
21705
22177
  WHERE name IS NOT NULL
21706
22178
  ORDER BY name`
21707
- ).all();
22179
+ )
22180
+ );
21708
22181
  return rows.map((r) => r.name);
21709
22182
  }
21710
22183
  };
@@ -21723,8 +22196,7 @@ function hasLegacySampleRows(db) {
21723
22196
  function purgeSampleData(db) {
21724
22197
  try {
21725
22198
  if (!hasLegacySampleRows(db)) return;
21726
- db.exec("BEGIN");
21727
- try {
22199
+ withTransaction(db, () => {
21728
22200
  db.exec(
21729
22201
  `DELETE FROM share_call_site WHERE endpoint_id IN (
21730
22202
  SELECT e.id FROM share_endpoint e
@@ -21769,11 +22241,7 @@ function purgeSampleData(db) {
21769
22241
  value TEXT NOT NULL
21770
22242
  )`);
21771
22243
  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
- }
22244
+ });
21777
22245
  } catch {
21778
22246
  }
21779
22247
  }
@@ -21792,7 +22260,7 @@ function openWithPragmas(file2) {
21792
22260
  function backupLegacyStore(file2) {
21793
22261
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
21794
22262
  renameSync(file2, backup);
21795
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
22263
+ for (const sidecar of walSidecars(file2)) {
21796
22264
  if (existsSync(sidecar)) rmSync(sidecar);
21797
22265
  }
21798
22266
  return backup;
@@ -21805,9 +22273,8 @@ function openLocalDatabase(dir) {
21805
22273
  db.close();
21806
22274
  const backup = backupLegacyStore(file2);
21807
22275
  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
- `
22276
+ akaWarn(
22277
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
21811
22278
  );
21812
22279
  }
21813
22280
  applyMigrations(db);
@@ -21835,96 +22302,77 @@ function openLocalDatabase(dir) {
21835
22302
  const configInventory = new SqliteConfigInventoryRepository(db);
21836
22303
  policies.seedDefaults();
21837
22304
  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
- }
22305
+ failOpenTransaction(db, () => {
22306
+ events.insertEvent(event);
22307
+ const sessionId = event.metadata?.sessionId;
22308
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
22309
+ });
21851
22310
  }
21852
22311
  function ensureInventory(ctx) {
21853
22312
  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;
22313
+ const committed = failOpenTransaction(db, () => {
22314
+ const now = Date.now();
22315
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22316
+ if (ctx.harness) {
22317
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
22318
+ }
22319
+ resolved.accountId = inventory.upsert(
22320
+ linkHost(
22321
+ {
22322
+ objectType: "user",
22323
+ identityKey: "local",
22324
+ attributes: { source: "local" }
22325
+ },
22326
+ resolved.hostId
22327
+ ),
22328
+ now
22329
+ );
22330
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22331
+ });
22332
+ return committed ? resolved : {};
21883
22333
  }
21884
22334
  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;
22335
+ failOpenTransaction(db, () => {
22336
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
22337
+ for (const item of record2.items) inventory.upsert(item, now);
22338
+ auditEvents.insertAuditEvent(record2.scanEvent);
22339
+ const definitionIds = /* @__PURE__ */ new Map();
22340
+ for (const def of record2.definitions ?? []) {
22341
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22342
+ }
22343
+ for (const finding of record2.findings ?? []) {
22344
+ const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
22345
+ if (!definitionId) continue;
22346
+ inspectionFindings.insertFinding({
22347
+ id: randomUUID8(),
22348
+ auditEventId: record2.scanEvent.id,
22349
+ inspectionDefinitionId: definitionId,
22350
+ span: finding.span,
22351
+ maskedMatch: finding.maskedMatch,
22352
+ actionTaken: finding.actionTaken,
22353
+ confidence: finding.confidence
22354
+ });
21912
22355
  }
21913
- } catch {
21914
- }
22356
+ });
21915
22357
  }
21916
22358
  function recordProjectFiles(projectId, scan2) {
21917
22359
  if (scan2.files.length === 0) return;
22360
+ failOpenTransaction(db, () => {
22361
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
22362
+ });
22363
+ }
22364
+ async function transaction(fn) {
22365
+ db.exec("BEGIN");
21918
22366
  try {
21919
- db.exec("BEGIN");
22367
+ const result = await fn();
22368
+ db.exec("COMMIT");
22369
+ return result;
22370
+ } catch (err) {
21920
22371
  try {
21921
- projectFiles.replaceForProject(projectId, scan2, Date.now());
21922
- db.exec("COMMIT");
21923
- } catch (err) {
21924
22372
  db.exec("ROLLBACK");
21925
- throw err;
22373
+ } catch {
21926
22374
  }
21927
- } catch {
22375
+ throw err;
21928
22376
  }
21929
22377
  }
21930
22378
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -21943,8 +22391,7 @@ function openLocalDatabase(dir) {
21943
22391
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
21944
22392
  });
21945
22393
  if (stale.length === 0) return;
21946
- db.exec("BEGIN");
21947
- try {
22394
+ withTransaction(db, () => {
21948
22395
  for (const { id } of stale) {
21949
22396
  db.prepare(
21950
22397
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -21956,11 +22403,7 @@ function openLocalDatabase(dir) {
21956
22403
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
21957
22404
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
21958
22405
  }
21959
- db.exec("COMMIT");
21960
- } catch (err) {
21961
- db.exec("ROLLBACK");
21962
- throw err;
21963
- }
22406
+ });
21964
22407
  } catch {
21965
22408
  }
21966
22409
  }
@@ -22002,6 +22445,7 @@ function openLocalDatabase(dir) {
22002
22445
  purgeSampleData: () => {
22003
22446
  purgeSampleData(db);
22004
22447
  },
22448
+ transaction,
22005
22449
  close: () => {
22006
22450
  db.close();
22007
22451
  }
@@ -22094,12 +22538,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
22094
22538
  }
22095
22539
  }
22096
22540
  function readJson(file2) {
22541
+ let text;
22097
22542
  try {
22098
- const parsed = JSON.parse(readFileSync2(file2, "utf8"));
22099
- return typeof parsed === "object" && parsed !== null ? parsed : null;
22543
+ text = readFileSync2(file2, "utf8");
22100
22544
  } catch {
22101
22545
  return null;
22102
22546
  }
22547
+ return parseJsonObject(text) ?? null;
22548
+ }
22549
+
22550
+ // ../../packages/persistence/src/warn-era-cap.ts
22551
+ import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
22552
+ import { join as join5 } from "path";
22553
+ var MARKER = "warn-era-capped";
22554
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22555
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
22556
+ const marker = join5(dataDir2, MARKER);
22557
+ if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22558
+ const capped = db.policies.capCategoryActions();
22559
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
22560
+ `, { mode: DATA_FILE_MODE });
22561
+ return { capped };
22103
22562
  }
22104
22563
 
22105
22564
  // ../../packages/plugin-sdk/src/provider-env.ts
@@ -22184,7 +22643,7 @@ function resolveProviderSafe() {
22184
22643
  // ../../packages/plugin-sdk/src/config-inventory.ts
22185
22644
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22186
22645
  import { homedir as homedir2 } from "os";
22187
- import { basename as basename2, join as join6 } from "path";
22646
+ import { basename as basename2, join as join7 } from "path";
22188
22647
 
22189
22648
  // ../../packages/detections/src/matchers/keyword.ts
22190
22649
  var KeywordMatcher2 = class {
@@ -24563,8 +25022,8 @@ function scanText(text) {
24563
25022
  }
24564
25023
 
24565
25024
  // ../../packages/plugin-sdk/src/repo.ts
24566
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
24567
- import { basename, dirname, isAbsolute, join as join5, sep as sep2 } from "path";
25025
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25026
+ import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
24568
25027
  function resolveRepoIdentity(cwd) {
24569
25028
  try {
24570
25029
  const root = findGitRoot(cwd);
@@ -24597,32 +25056,32 @@ function resolveRepoNwo(cwd) {
24597
25056
  function findGitRoot(start) {
24598
25057
  let dir = start;
24599
25058
  for (; ; ) {
24600
- if (existsSync2(join5(dir, ".git"))) return dir;
25059
+ if (existsSync3(join6(dir, ".git"))) return dir;
24601
25060
  const parent = dirname(dir);
24602
25061
  if (parent === dir) return void 0;
24603
25062
  dir = parent;
24604
25063
  }
24605
25064
  }
24606
25065
  function resolveGitContext(root) {
24607
- const dotGit = join5(root, ".git");
25066
+ const dotGit = join6(root, ".git");
24608
25067
  try {
24609
25068
  if (statSync(dotGit).isDirectory()) {
24610
- return { configPath: join5(dotGit, "config"), headRoot: root };
25069
+ return { configPath: join6(dotGit, "config"), headRoot: root };
24611
25070
  }
24612
25071
  } catch {
24613
25072
  return void 0;
24614
25073
  }
24615
25074
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
24616
25075
  if (!target) return void 0;
24617
- const gitdir = isAbsolute(target) ? target : join5(root, target);
24618
- if (existsSync2(join5(gitdir, "config"))) {
24619
- return { configPath: join5(gitdir, "config"), headRoot: root };
25076
+ const gitdir = isAbsolute(target) ? target : join6(root, target);
25077
+ if (existsSync3(join6(gitdir, "config"))) {
25078
+ return { configPath: join6(gitdir, "config"), headRoot: root };
24620
25079
  }
24621
- const commonRaw = safeRead(join5(gitdir, "commondir"))?.trim();
25080
+ const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
24622
25081
  if (!commonRaw) return void 0;
24623
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join5(gitdir, commonRaw);
25082
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
24624
25083
  const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
24625
- return { configPath: join5(commonGitDir, "config"), headRoot };
25084
+ return { configPath: join6(commonGitDir, "config"), headRoot };
24626
25085
  }
24627
25086
  function safeRead(path) {
24628
25087
  try {
@@ -24712,20 +25171,23 @@ function resolveInventoryContext(input) {
24712
25171
  }
24713
25172
 
24714
25173
  // ../../packages/plugin-sdk/src/nudge.ts
24715
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
24716
- import { join as join7 } from "path";
25174
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25175
+ import { join as join8 } from "path";
24717
25176
 
24718
25177
  // ../../packages/plugin-sdk/src/project-files.ts
24719
25178
  var import_ignore = __toESM(require_ignore(), 1);
24720
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
24721
- import { basename as basename3, join as join8, relative, sep as sep3 } from "path";
25179
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25180
+ import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
24722
25181
 
24723
25182
  // ../../packages/plugin-sdk/src/runtime.ts
24724
25183
  import { randomUUID as randomUUID10 } from "crypto";
24725
25184
 
25185
+ // ../../packages/plugin-sdk/src/suppressions.ts
25186
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
25187
+
24726
25188
  // ../../packages/plugin-sdk/src/throttle.ts
24727
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
24728
- import { join as join9 } from "path";
25189
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
25190
+ import { join as join10 } from "path";
24729
25191
 
24730
25192
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
24731
25193
  import { randomUUID as randomUUID11 } from "crypto";
@@ -24918,6 +25380,14 @@ var StandaloneDataGateway = class {
24918
25380
  sweepTerminalExceptions(retentionMs) {
24919
25381
  return this.db.exceptions.sweepTerminal(retentionMs);
24920
25382
  }
25383
+ // The warn-era enforcement cap, standalone-only store maintenance invoked
25384
+ // from SessionStart, not part of the DataGateway port. Returns the number
25385
+ // of block/redact rows capped to warn (0 for a redact-policy store or an
25386
+ // already-capped one).
25387
+ capWarnEraEnforcement(policyMode) {
25388
+ const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
25389
+ return { capped };
25390
+ }
24921
25391
  // One project-file scan → the local project_file tree (one transaction inside
24922
25392
  // the LocalDatabase, fail-open there). Like the sweep above, this is
24923
25393
  // NOT part of the DataGateway port: the file tree is a local-store read model.
@@ -25023,11 +25493,11 @@ import {
25023
25493
  openSync,
25024
25494
  readFileSync as readFileSync7,
25025
25495
  readSync,
25026
- writeFileSync as writeFileSync5
25496
+ writeFileSync as writeFileSync6
25027
25497
  } from "fs";
25028
- import { join as join10 } from "path";
25498
+ import { join as join11 } from "path";
25029
25499
  function offsetsDir(dataDir2) {
25030
- return join10(dataDir2, "usage-offsets");
25500
+ return join11(dataDir2, "usage-offsets");
25031
25501
  }
25032
25502
  var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
25033
25503
  function safeSessionId(sessionId) {
@@ -25037,7 +25507,7 @@ function safeSessionId(sessionId) {
25037
25507
  return createHash5("sha256").update(sessionId).digest("hex");
25038
25508
  }
25039
25509
  function offsetPath(dataDir2, sessionId) {
25040
- return join10(offsetsDir(dataDir2), safeSessionId(sessionId));
25510
+ return join11(offsetsDir(dataDir2), safeSessionId(sessionId));
25041
25511
  }
25042
25512
  function readOffset(dataDir2, sessionId) {
25043
25513
  try {
@@ -25057,7 +25527,7 @@ function writeOffset(dataDir2, sessionId, value) {
25057
25527
  try {
25058
25528
  mkdirSync5(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
25059
25529
  const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
25060
- writeFileSync5(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
25530
+ writeFileSync6(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
25061
25531
  mode: DATA_FILE_MODE
25062
25532
  });
25063
25533
  } catch {
@@ -25101,7 +25571,7 @@ function readTail(transcriptPath, startOffset) {
25101
25571
  // src/history/transcripts.ts
25102
25572
  import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
25103
25573
  import { homedir as homedir3 } from "os";
25104
- import { join as join11 } from "path";
25574
+ import { join as join12 } from "path";
25105
25575
  function isRecord(value) {
25106
25576
  return typeof value === "object" && value !== null;
25107
25577
  }