@akasecurity/ai-tc-claude-code 0.8.1 → 0.9.0-rc1

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.
@@ -538,6 +538,10 @@ var SQLITE_MIGRATIONS = [
538
538
  {
539
539
  tag: "0009_findings_path_expression_index",
540
540
  sql: "-- Custom migration: partial expression index for the resolver's per-path reads.\n--\n-- openAtRestKeysForPath / resolvedAtRestKeysForPath (and the scanner's tier-3\n-- open-key probe) filter at-rest events by\n-- e.kind = 'code_change' AND json_extract(e.metadata, '$.filePath') = :path\n-- once per changed/deleted file on every scan \u2014 previously a full events scan\n-- per file. json_extract is deterministic, so SQLite allows it in an index;\n-- the WHERE kind = 'code_change' keeps the index to exactly the rows those\n-- queries can match (in-flight events are never path-addressed).\nCREATE INDEX `idx_events_code_change_path` ON `events` (json_extract(`metadata`, '$.filePath')) WHERE `kind` = 'code_change';\n"
541
+ },
542
+ {
543
+ tag: "0010_events_session_expression_index",
544
+ sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
541
545
  }
542
546
  ];
543
547
 
@@ -640,6 +644,12 @@ var defaultCostModel = {
640
644
  }
641
645
  };
642
646
 
647
+ // ../../packages/schema/src/token/format.ts
648
+ var COMPACT = new Intl.NumberFormat("en-US", {
649
+ notation: "compact",
650
+ maximumFractionDigits: 1
651
+ });
652
+
643
653
  // ../../packages/schema/src/token/token-report.ts
644
654
  var num = (value) => value ?? 0;
645
655
  function costUsageOf(a) {
@@ -15311,6 +15321,12 @@ var FindingInstance = external_exports.object({
15311
15321
  provider: FindingProvider,
15312
15322
  repo: external_exports.string(),
15313
15323
  file: external_exports.string(),
15324
+ // Host tool that produced the scanned text (event metadata's toolName).
15325
+ // Present whenever the capturing hook recorded one — including
15326
+ // file-attributed captures (views prefer `file`); its display value is
15327
+ // the location fallback ("via Bash") when no filePath exists. Absent for
15328
+ // legacy rows and non-tool captures (prompts, worktree scans).
15329
+ toolName: external_exports.string().optional(),
15314
15330
  // Effective action: override.action ?? actionTaken, translated to FindingAction.
15315
15331
  action: FindingAction,
15316
15332
  detectedAt: external_exports.iso.datetime(),
@@ -15367,6 +15383,9 @@ var ListGroupedFindingsQuery = external_exports.object({
15367
15383
  provider: external_exports.array(FindingProvider).optional(),
15368
15384
  action: external_exports.array(FindingAction).optional(),
15369
15385
  q: external_exports.string().optional(),
15386
+ // Scope to findings whose event carries this session id (the Activity page's
15387
+ // session → findings drilldown). Findings without a session never match.
15388
+ sessionId: external_exports.string().optional(),
15370
15389
  groupBy: external_exports.literal("type").optional(),
15371
15390
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
15372
15391
  cursor: external_exports.string().optional()
@@ -15378,7 +15397,13 @@ var ListGroupedFindingsResponse = external_exports.object({
15378
15397
  }),
15379
15398
  facets: FindingFacets,
15380
15399
  items: external_exports.array(FindingGroup),
15381
- nextCursor: external_exports.string().nullable()
15400
+ nextCursor: external_exports.string().nullable(),
15401
+ // Present only on session-scoped queries (`sessionId` set): per ruleId, how
15402
+ // many times that rule fired in the session's persisted transcript. Findings
15403
+ // here are deduplicated to unique values while the transcript tally counts
15404
+ // every firing, so the two numbers legitimately differ — this map lets a
15405
+ // session-scoped view show both.
15406
+ sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
15382
15407
  }).meta({ id: "ListGroupedFindingsResponse" });
15383
15408
  var ApplyFindingActionRequest = external_exports.object({
15384
15409
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
@@ -15430,6 +15455,11 @@ var AuditEventType = external_exports.enum([
15430
15455
  "prompt",
15431
15456
  "response",
15432
15457
  "code_change",
15458
+ // The events.kind of a scanned tool call, widened in to keep this a
15459
+ // superset. Narrower than 'tool_call' above and not a duplicate of it:
15460
+ // 'tool_call' is the reconciler's structural row for every call, while
15461
+ // 'tool_use' exists only where a hook enforced against the arguments.
15462
+ "tool_use",
15433
15463
  // One row per config-inventory scan, hung off the session root. It is the
15434
15464
  // fact the posture inspection findings reference (findings require an
15435
15465
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -15784,6 +15814,10 @@ var ListActivitySessionsQuery = external_exports.object({
15784
15814
  from: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
15785
15815
  /** Upper bound on startedAt; omitted defaults to now. */
15786
15816
  to: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
15817
+ /** Exclude zero-activity sessions — roots whose only recorded children are
15818
+ * bookkeeping rows (hooks, config scans), typically background `claude`
15819
+ * launches. Omitted = list everything. `z.stringbool()` per the note above. */
15820
+ excludeEmpty: external_exports.stringbool().optional(),
15787
15821
  /** Page size, 1–100; out-of-range values are a 400. `z.coerce` — query params arrive as strings. */
15788
15822
  limit: external_exports.coerce.number().int().min(1).max(100).default(50),
15789
15823
  /** Opaque pagination cursor (most-recent first). */
@@ -15792,7 +15826,11 @@ var ListActivitySessionsQuery = external_exports.object({
15792
15826
  var ListActivitySessionsResponse = external_exports.object({
15793
15827
  items: external_exports.array(ActivitySessionSummary),
15794
15828
  /** `null` once the last page is reached. */
15795
- nextCursor: external_exports.string().nullable()
15829
+ nextCursor: external_exports.string().nullable(),
15830
+ /** Zero-activity sessions matching the query's filters/range (whether or
15831
+ * not `excludeEmpty` dropped them from `items`) — the count a UI toggle
15832
+ * shows when collapsing them. */
15833
+ emptyCount: external_exports.number().int().nonnegative()
15796
15834
  }).meta({ id: "ListActivitySessionsResponse" });
15797
15835
  var ListSessionEventsQuery = external_exports.object({
15798
15836
  /** Default 100, range 1–500. */
@@ -15821,12 +15859,18 @@ var ActivityOverviewResponse = external_exports.object({
15821
15859
  }).meta({ id: "ActivityOverviewResponse" });
15822
15860
 
15823
15861
  // ../../packages/schema/src/zod/event.ts
15824
- var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
15862
+ var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15825
15863
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15826
15864
  var EventMetadata = external_exports.object({
15827
15865
  sessionId: external_exports.string().optional(),
15828
15866
  repo: external_exports.string().optional(),
15829
15867
  filePath: external_exports.string().optional(),
15868
+ // The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch'),
15869
+ // set by the tool-scanning hooks. The tool NAME only — never the tool's
15870
+ // arguments or output, which can carry the very value a finding masked
15871
+ // (metadata is stored unredacted). Gives findings on non-file captures a
15872
+ // display location ("via Bash") when no filePath exists.
15873
+ toolName: external_exports.string().optional(),
15830
15874
  // Set (true) by the worktree scanner when the file is excluded by the
15831
15875
  // repo's .gitignore. Gitignored files ARE still scanned — local scratch and
15832
15876
  // generated code can leak real secrets — but the provenance is recorded so
@@ -16134,7 +16178,7 @@ var DetectionException = external_exports.object({
16134
16178
  justification: external_exports.string().min(1),
16135
16179
  conditions: ExceptionConditions.nullable(),
16136
16180
  createdBy: external_exports.string(),
16137
- createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
16181
+ createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
16138
16182
  createdAt: external_exports.iso.datetime(),
16139
16183
  updatedAt: external_exports.iso.datetime(),
16140
16184
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16158,7 +16202,10 @@ var ExceptionBundleEntry = DetectionException.pick({
16158
16202
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16159
16203
  var KeywordMatcher = external_exports.object({
16160
16204
  type: external_exports.literal("keyword"),
16161
- keywords: external_exports.array(external_exports.string()).min(1),
16205
+ // An empty keyword matches at every position, yielding one zero-length span
16206
+ // per character. Rejected here because a keyword that matches everything is
16207
+ // never intentional.
16208
+ keywords: external_exports.array(external_exports.string().min(1)).min(1),
16162
16209
  caseSensitive: external_exports.boolean().default(false)
16163
16210
  });
16164
16211
  function isValidRegex(pattern, flags) {
@@ -16303,20 +16350,26 @@ var PolicyBundle = external_exports.object({
16303
16350
  customKeywords: external_exports.array(external_exports.string()),
16304
16351
  fetchedAt: external_exports.iso.datetime()
16305
16352
  }).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
16353
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16319
16354
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
16355
+ var CATEGORY_PEAK_SEVERITY = {
16356
+ secret: "critical",
16357
+ financial: "critical",
16358
+ // core-financial/credit-card
16359
+ code_flaw: "critical",
16360
+ pii: "high",
16361
+ phi: "high",
16362
+ custom: "high",
16363
+ // user-defined; conservative
16364
+ code_context: "low",
16365
+ config: "low"
16366
+ // observe-only; floors to monitor regardless
16367
+ };
16368
+ function severityFloorPolicy(category) {
16369
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
16370
+ const peak = CATEGORY_PEAK_SEVERITY[category];
16371
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
16372
+ }
16320
16373
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
16321
16374
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
16322
16375
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -16343,6 +16396,12 @@ var BUILTIN_POLICY_SPECS = {
16343
16396
  description: "Refuse the request entirely whenever any rule in this detection matches."
16344
16397
  }
16345
16398
  };
16399
+ function builtinPolicyToAction(id) {
16400
+ return BUILTIN_POLICY_SPECS[id].action;
16401
+ }
16402
+ var DEFAULT_ACTIONS = Object.fromEntries(
16403
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
16404
+ );
16346
16405
  var BUILTIN_POLICIES = Object.fromEntries(
16347
16406
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
16348
16407
  );
@@ -16395,6 +16454,10 @@ var ListEventsResponse = external_exports.object({
16395
16454
  items: external_exports.array(Event),
16396
16455
  nextCursor: external_exports.string().nullable()
16397
16456
  }).meta({ id: "ListEventsResponse" });
16457
+ var IngestResponse = external_exports.object({
16458
+ accepted: external_exports.number().int().nonnegative(),
16459
+ duplicates: external_exports.number().int().nonnegative()
16460
+ }).meta({ id: "IngestResponse" });
16398
16461
  var ListFindingsQuery = external_exports.object({
16399
16462
  cursor: external_exports.string().optional(),
16400
16463
  limit: external_exports.coerce.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).default(50),
@@ -16749,10 +16812,8 @@ function toApiProvider(sourceTool) {
16749
16812
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
16750
16813
  }
16751
16814
  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
- );
16815
+ function foldGroupStatus(instanceStatuses) {
16816
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
16756
16817
  if (statuses.size === 0) return void 0;
16757
16818
  for (const candidate of STATUS_PRECEDENCE) {
16758
16819
  if (statuses.has(candidate)) return candidate;
@@ -16770,6 +16831,7 @@ function deriveFindingStatus(row) {
16770
16831
  function buildFindingGroups(rows, opts = {}) {
16771
16832
  const overrides = opts.overrides;
16772
16833
  const packNames = opts.packNames;
16834
+ const aggregates = opts.aggregates;
16773
16835
  const byRuleId = /* @__PURE__ */ new Map();
16774
16836
  for (const row of rows) {
16775
16837
  const existing = byRuleId.get(row.ruleId);
@@ -16785,23 +16847,27 @@ function buildFindingGroups(rows, opts = {}) {
16785
16847
  provider: toApiProvider(r.sourceTool),
16786
16848
  repo: r.repo,
16787
16849
  file: r.file,
16850
+ ...r.toolName === void 0 ? {} : { toolName: r.toolName },
16788
16851
  action: toApiAction(effectiveDbAction),
16789
16852
  detectedAt: r.occurredAt,
16790
16853
  confidence: r.confidence,
16791
16854
  status: r.status
16792
16855
  };
16793
16856
  });
16794
- const latestDetectedAt = ruleRows.reduce(
16857
+ const agg = aggregates?.get(ruleId);
16858
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
16795
16859
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
16796
16860
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
16797
16861
  );
16798
16862
  const seenProviders = /* @__PURE__ */ new Set();
16799
- const providers = instances.map((i) => i.provider).filter((p) => {
16863
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
16800
16864
  if (seenProviders.has(p)) return false;
16801
16865
  seenProviders.add(p);
16802
16866
  return true;
16803
16867
  });
16804
- const actionSet = new Set(instances.map((i) => i.action));
16868
+ const actionSet = new Set(
16869
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
16870
+ );
16805
16871
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
16806
16872
  const severity = ruleRows[0]?.severity ?? "low";
16807
16873
  const detection = {
@@ -16815,8 +16881,10 @@ function buildFindingGroups(rows, opts = {}) {
16815
16881
  contextPrefix: ""
16816
16882
  // empty (pending privacy review)
16817
16883
  };
16818
- const status = deriveGroupStatus(instances);
16819
- groups.push({
16884
+ const status = foldGroupStatus(
16885
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
16886
+ );
16887
+ const group = {
16820
16888
  id: ruleId,
16821
16889
  category: apiCategory,
16822
16890
  subtype: ruleId,
@@ -16825,21 +16893,26 @@ function buildFindingGroups(rows, opts = {}) {
16825
16893
  match,
16826
16894
  detection,
16827
16895
  policy,
16828
- instanceCount: instances.length,
16896
+ instanceCount: agg?.instanceCount ?? instances.length,
16829
16897
  providers,
16830
16898
  aggregateAction,
16831
16899
  latestDetectedAt,
16832
16900
  instances,
16833
16901
  status
16834
- });
16902
+ };
16903
+ if (agg) {
16904
+ actionsCache.set(group, [...actionSet]);
16905
+ if (agg.searchText !== void 0) {
16906
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
16907
+ }
16908
+ }
16909
+ groups.push(group);
16835
16910
  }
16836
16911
  return groups;
16837
16912
  }
16838
16913
  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 = [
16914
+ function buildHaystack(g, extra) {
16915
+ return [
16843
16916
  g.subtype,
16844
16917
  g.category,
16845
16918
  g.match.maskedValue,
@@ -16847,11 +16920,26 @@ function groupHaystack(g) {
16847
16920
  g.id,
16848
16921
  ...g.instances.map((i) => i.repo),
16849
16922
  ...g.instances.map((i) => i.file),
16850
- ...g.instances.map((i) => i.id)
16923
+ ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
16924
+ ...g.instances.map((i) => i.id),
16925
+ ...extra === void 0 ? [] : [extra]
16851
16926
  ].join(" ").toLowerCase();
16927
+ }
16928
+ function groupHaystack(g) {
16929
+ const cached2 = haystackCache.get(g);
16930
+ if (cached2 !== void 0) return cached2;
16931
+ const haystack = buildHaystack(g);
16852
16932
  haystackCache.set(g, haystack);
16853
16933
  return haystack;
16854
16934
  }
16935
+ var actionsCache = /* @__PURE__ */ new WeakMap();
16936
+ function groupActions(g) {
16937
+ const cached2 = actionsCache.get(g);
16938
+ if (cached2 !== void 0) return cached2;
16939
+ const actions = [...new Set(g.instances.map((i) => i.action))];
16940
+ actionsCache.set(g, actions);
16941
+ return actions;
16942
+ }
16855
16943
  function applyFindingFilters(groups, opts) {
16856
16944
  let filtered = groups;
16857
16945
  if (opts.severity && opts.severity.length > 0) {
@@ -16864,7 +16952,7 @@ function applyFindingFilters(groups, opts) {
16864
16952
  }
16865
16953
  if (opts.actions && opts.actions.length > 0) {
16866
16954
  const actionSet = new Set(opts.actions);
16867
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
16955
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
16868
16956
  }
16869
16957
  if (opts.subtype && opts.subtype.length > 0) {
16870
16958
  const subtypeSet = new Set(opts.subtype);
@@ -16916,8 +17004,7 @@ function computeFindingFacets(allGroups, opts) {
16916
17004
  });
16917
17005
  const actionMap = /* @__PURE__ */ new Map();
16918
17006
  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);
17007
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
16921
17008
  }
16922
17009
  const forSubtype = applyFindingFilters(allGroups, {
16923
17010
  providers: opts.providers,
@@ -17100,6 +17187,71 @@ var ProjectFilesScan = external_exports.object({
17100
17187
  scannedAt: external_exports.string()
17101
17188
  });
17102
17189
 
17190
+ // ../../packages/schema/src/zod/ranges.ts
17191
+ var TIME_RANGES = ["7d", "30d", "3m", "6m"];
17192
+ var TimeRange = external_exports.enum(TIME_RANGES).meta({ id: "TimeRange" });
17193
+ var DEFAULT_TIME_RANGE = "7d";
17194
+ var RANGE_DAYS = {
17195
+ "7d": 7,
17196
+ "30d": 30,
17197
+ "3m": 90,
17198
+ "6m": 180
17199
+ };
17200
+ var TIME_RANGE_OR_DEFAULT = TimeRange.catch(DEFAULT_TIME_RANGE);
17201
+
17202
+ // ../../packages/schema/src/zod/remediation.ts
17203
+ var SecretFindingState = external_exports.enum(["still-valid", "unknown", "invalid"]);
17204
+ var MaskedFindingLocation = external_exports.object({
17205
+ filePath: external_exports.string(),
17206
+ span: Span.optional()
17207
+ }).strict();
17208
+ var MaskedSecretFinding = external_exports.object({
17209
+ provider: external_exports.string(),
17210
+ maskedToken: external_exports.string(),
17211
+ where: MaskedFindingLocation,
17212
+ state: SecretFindingState,
17213
+ observedAt: external_exports.iso.datetime().optional()
17214
+ }).strict();
17215
+ var RotationChecklistEntry = external_exports.object({
17216
+ provider: external_exports.string(),
17217
+ maskedToken: external_exports.string(),
17218
+ consolePath: external_exports.string(),
17219
+ occurrenceSpread: external_exports.number().int().positive()
17220
+ }).strict();
17221
+ var RemediationOption = external_exports.enum([
17222
+ "redact-rotation-checklist",
17223
+ "redact-only",
17224
+ "set-secret-redact",
17225
+ "leave"
17226
+ ]);
17227
+ var RemediationEntrySource = external_exports.enum(["first-run", "pre-push", "secret-scan"]);
17228
+ var RemediationEntryContext = external_exports.object({
17229
+ entrySource: RemediationEntrySource
17230
+ }).strict();
17231
+ var RemediationOptionChoice = external_exports.object({
17232
+ id: RemediationOption,
17233
+ label: external_exports.string()
17234
+ });
17235
+ var BatchedRemediationDecision = external_exports.object({
17236
+ kind: external_exports.literal("decision"),
17237
+ entrySource: RemediationEntrySource,
17238
+ secretCount: external_exports.number().int().positive(),
17239
+ prompt: external_exports.string(),
17240
+ options: external_exports.tuple([
17241
+ RemediationOptionChoice.extend({ id: external_exports.literal("redact-rotation-checklist") }),
17242
+ RemediationOptionChoice.extend({ id: external_exports.literal("redact-only") }),
17243
+ RemediationOptionChoice.extend({ id: external_exports.literal("set-secret-redact") }),
17244
+ RemediationOptionChoice.extend({ id: external_exports.literal("leave") })
17245
+ ])
17246
+ });
17247
+ var NoRemediationDecision = external_exports.object({
17248
+ kind: external_exports.literal("no-decision")
17249
+ });
17250
+ var BatchedRemediation = external_exports.discriminatedUnion("kind", [
17251
+ BatchedRemediationDecision,
17252
+ NoRemediationDecision
17253
+ ]);
17254
+
17103
17255
  // ../../packages/schema/src/zod/rule-test.ts
17104
17256
  var TestRulesRequest = external_exports.object({
17105
17257
  rules: external_exports.array(Rule).min(1).max(100),
@@ -17158,10 +17310,8 @@ var SeveritySummaryResponse = external_exports.object({
17158
17310
  // All four severity levels are always present (count may be 0).
17159
17311
  bySeverity: external_exports.array(SeveritySummaryItem)
17160
17312
  }).meta({ id: "SeveritySummaryResponse" });
17161
- var SECURITY_RANGES = ["7d", "30d", "3m", "6m"];
17162
- var SecurityRange = external_exports.enum(SECURITY_RANGES).meta({ id: "SecurityRange" });
17163
17313
  var SecurityRangeQuery = external_exports.object({
17164
- range: external_exports.enum(SECURITY_RANGES).default("30d")
17314
+ range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE)
17165
17315
  });
17166
17316
  var EnforcementActionKind = external_exports.enum(["blocked", "redacted", "warned"]).meta({ id: "EnforcementActionKind" });
17167
17317
  var EnforcementAction = external_exports.object({
@@ -17171,7 +17321,7 @@ var EnforcementAction = external_exports.object({
17171
17321
  delta: external_exports.number().int()
17172
17322
  }).meta({ id: "EnforcementAction" });
17173
17323
  var EnforcementActionsResponse = external_exports.object({
17174
- range: SecurityRange,
17324
+ range: TimeRange,
17175
17325
  // Sum of actions[].count in the window.
17176
17326
  total: external_exports.number().int().nonnegative(),
17177
17327
  // One entry per kind, always all three present (count may be 0).
@@ -17186,7 +17336,7 @@ var FindingsTimeseriesPoint = external_exports.object({
17186
17336
  medium: external_exports.number().int().nonnegative()
17187
17337
  }).meta({ id: "FindingsTimeseriesPoint" });
17188
17338
  var FindingsTimeseriesResponse = external_exports.object({
17189
- range: SecurityRange,
17339
+ range: TimeRange,
17190
17340
  granularity: TimeseriesGranularity,
17191
17341
  points: external_exports.array(FindingsTimeseriesPoint)
17192
17342
  }).meta({ id: "FindingsTimeseriesResponse" });
@@ -17201,7 +17351,7 @@ var MttrTrendPoint = external_exports.object({
17201
17351
  })
17202
17352
  }).meta({ id: "MttrTrendPoint" });
17203
17353
  var MttrTrendResponse = external_exports.object({
17204
- range: SecurityRange,
17354
+ range: TimeRange,
17205
17355
  granularity: TimeseriesGranularity,
17206
17356
  points: external_exports.array(MttrTrendPoint)
17207
17357
  }).meta({ id: "MttrTrendResponse" });
@@ -17228,11 +17378,11 @@ var TopSource = external_exports.object({
17228
17378
  findingsCount: external_exports.number().int().nonnegative()
17229
17379
  }).meta({ id: "TopSource" });
17230
17380
  var TopSourcesResponse = external_exports.object({
17231
- range: SecurityRange,
17381
+ range: TimeRange,
17232
17382
  items: external_exports.array(TopSource)
17233
17383
  }).meta({ id: "TopSourcesResponse" });
17234
17384
  var TopSourcesQuery = external_exports.object({
17235
- range: external_exports.enum(SECURITY_RANGES).default("30d"),
17385
+ range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE),
17236
17386
  limit: external_exports.coerce.number().int().min(1).max(50).default(5),
17237
17387
  // Omit for both kinds.
17238
17388
  kind: external_exports.enum(SOURCE_KINDS).optional()
@@ -17245,7 +17395,7 @@ var ScanCoverageProvider = external_exports.object({
17245
17395
  supported: external_exports.boolean()
17246
17396
  }).meta({ id: "ScanCoverageProvider" });
17247
17397
  var ScanCoverageResponse = external_exports.object({
17248
- range: SecurityRange,
17398
+ range: TimeRange,
17249
17399
  providers: external_exports.array(ScanCoverageProvider)
17250
17400
  }).meta({ id: "ScanCoverageResponse" });
17251
17401
  var SubjectType = external_exports.enum(["repo", "user", "team", "policy", "share", "rule"]).meta({
@@ -17294,6 +17444,114 @@ var ApplyRecommendedActionResponse = external_exports.object({
17294
17444
  var DismissRecommendedActionResponse = external_exports.object({ id: external_exports.string(), status: external_exports.literal("dismissed") }).meta({ id: "DismissRecommendedActionResponse" });
17295
17445
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
17296
17446
 
17447
+ // ../../packages/schema/src/zod/triage.ts
17448
+ var TriageHit = external_exports.object({
17449
+ ruleId: external_exports.string(),
17450
+ category: DetectionCategory,
17451
+ severity: Severity,
17452
+ maskedMatch: external_exports.string(),
17453
+ rawMatch: external_exports.string(),
17454
+ context: external_exports.string(),
17455
+ filePath: external_exports.string().optional(),
17456
+ confidence: external_exports.number().min(0).max(1),
17457
+ id: external_exports.string().optional(),
17458
+ valueFingerprint: external_exports.string().optional(),
17459
+ keyVersion: external_exports.number().int().nonnegative().optional()
17460
+ });
17461
+ var TriagePolicy = BuiltinPolicyId;
17462
+ var TriageCategoryRec = external_exports.object({
17463
+ category: DetectionCategory,
17464
+ action: TriagePolicy,
17465
+ reasoning: external_exports.string(),
17466
+ genuineCount: external_exports.number().int().nonnegative(),
17467
+ fpCount: external_exports.number().int().nonnegative(),
17468
+ // TriageHit ids judged false-positive in this category. fpCount must equal
17469
+ // this array's length — enforced by the consumer, not this schema.
17470
+ fpIds: external_exports.array(external_exports.string())
17471
+ });
17472
+ var TriageRecommendation = external_exports.object({
17473
+ perCategory: external_exports.array(TriageCategoryRec),
17474
+ notes: external_exports.string()
17475
+ });
17476
+
17477
+ // ../../packages/schema/src/zod/setup-frame.ts
17478
+ var CalibrationCounts = external_exports.object({
17479
+ total: external_exports.number().int().nonnegative(),
17480
+ important: external_exports.number().int().nonnegative(),
17481
+ routine: external_exports.number().int().nonnegative()
17482
+ }).refine((c) => c.total === c.important + c.routine, {
17483
+ message: "total must equal important + routine",
17484
+ path: ["total"]
17485
+ });
17486
+ var FalsePositivePatternValue = external_exports.object({
17487
+ ruleId: external_exports.string(),
17488
+ category: DetectionCategory,
17489
+ valueFingerprint: external_exports.string(),
17490
+ keyVersion: external_exports.number().int().nonnegative()
17491
+ });
17492
+ var FalsePositivePatternGroup = external_exports.object({
17493
+ pattern: external_exports.string(),
17494
+ count: external_exports.number().int().nonnegative(),
17495
+ values: external_exports.array(FalsePositivePatternValue).min(1)
17496
+ });
17497
+ var CalibrationFindingKind = external_exports.object({
17498
+ category: DetectionCategory,
17499
+ count: external_exports.number().int().nonnegative(),
17500
+ egress: external_exports.boolean()
17501
+ });
17502
+ var CalibrationFrame = external_exports.object({
17503
+ counts: CalibrationCounts,
17504
+ routineCategories: external_exports.array(DetectionCategory),
17505
+ surfacedCategories: external_exports.array(DetectionCategory),
17506
+ findingKinds: external_exports.array(CalibrationFindingKind),
17507
+ posture: external_exports.record(DetectionCategory, BuiltinPolicyId),
17508
+ maskedFindings: external_exports.array(MaskedSecretFinding).optional(),
17509
+ falsePositivePatterns: external_exports.array(FalsePositivePatternGroup).optional()
17510
+ });
17511
+ var CalibrationPreviewCategory = TriageCategoryRec.pick({
17512
+ category: true,
17513
+ genuineCount: true,
17514
+ fpCount: true
17515
+ }).extend({
17516
+ egress: external_exports.boolean()
17517
+ });
17518
+ var CalibrationPreview = external_exports.object({
17519
+ categories: external_exports.array(CalibrationPreviewCategory),
17520
+ posture: external_exports.record(DetectionCategory, BuiltinPolicyId)
17521
+ });
17522
+ var CalibrationResult = external_exports.object({
17523
+ frame: CalibrationFrame,
17524
+ copy: external_exports.string()
17525
+ });
17526
+ var FirstRunCalibration = external_exports.enum(["scan", "floor"]);
17527
+ var SetupHandoffOption = external_exports.object({
17528
+ id: external_exports.enum(["enter-remediation", "open-dashboard", "not-now"]),
17529
+ label: external_exports.string()
17530
+ });
17531
+ var DashboardHandoffOptions = external_exports.tuple([
17532
+ SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
17533
+ SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
17534
+ ]);
17535
+ var ComposedRemediationOptions = external_exports.tuple([
17536
+ SetupHandoffOption.extend({ id: external_exports.literal("enter-remediation") }),
17537
+ SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
17538
+ SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
17539
+ ]);
17540
+ var SetupHandoffOffer = external_exports.object({
17541
+ worthALook: external_exports.number().int().nonnegative(),
17542
+ liveKeys: external_exports.number().int().nonnegative().optional(),
17543
+ options: external_exports.union([DashboardHandoffOptions, ComposedRemediationOptions])
17544
+ }).refine(
17545
+ (o) => o.options.some((opt) => opt.id === "enter-remediation") === (o.liveKeys ?? 0) > 0,
17546
+ {
17547
+ message: "the chain-entry option is present exactly when liveKeys > 0",
17548
+ path: ["options"]
17549
+ }
17550
+ ).refine((o) => (o.liveKeys ?? 0) <= o.worthALook, {
17551
+ message: "liveKeys is a subset of worthALook and cannot exceed it",
17552
+ path: ["liveKeys"]
17553
+ });
17554
+
17297
17555
  // ../../packages/schema/src/zod/shares.ts
17298
17556
  var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17299
17557
  var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
@@ -17479,6 +17737,102 @@ function reviewSeverityRank(reasons) {
17479
17737
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17480
17738
  }
17481
17739
 
17740
+ // ../../packages/persistence/src/internal/sql-text.ts
17741
+ function escapeLikePattern(s) {
17742
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17743
+ }
17744
+ function placeholders(n) {
17745
+ return Array.from({ length: n }, () => "?").join(", ");
17746
+ }
17747
+ function containsPattern(q) {
17748
+ return `%${escapeLikePattern(q)}%`;
17749
+ }
17750
+ function likeAny(exprs) {
17751
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
17752
+ }
17753
+
17754
+ // ../../packages/persistence/src/internal/transactions.ts
17755
+ var savepointSeq = 0;
17756
+ function withTransaction(db, fn, mode = "DEFERRED") {
17757
+ if (db.isTransaction) {
17758
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
17759
+ db.exec(`SAVEPOINT ${savepoint}`);
17760
+ try {
17761
+ fn();
17762
+ db.exec(`RELEASE ${savepoint}`);
17763
+ } catch (error51) {
17764
+ try {
17765
+ db.exec(`ROLLBACK TO ${savepoint}`);
17766
+ db.exec(`RELEASE ${savepoint}`);
17767
+ } catch {
17768
+ }
17769
+ throw error51;
17770
+ }
17771
+ return;
17772
+ }
17773
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
17774
+ try {
17775
+ fn();
17776
+ db.exec("COMMIT");
17777
+ } catch (error51) {
17778
+ try {
17779
+ db.exec("ROLLBACK");
17780
+ } catch {
17781
+ }
17782
+ throw error51;
17783
+ }
17784
+ }
17785
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
17786
+ const nested = db.isTransaction;
17787
+ try {
17788
+ withTransaction(db, fn, mode);
17789
+ return true;
17790
+ } catch (error51) {
17791
+ if (!db.isTransaction && nested) throw error51;
17792
+ return false;
17793
+ }
17794
+ }
17795
+
17796
+ // ../../packages/persistence/src/internal/warn.ts
17797
+ function akaWarn(message) {
17798
+ process.stderr.write(`[aka] ${message}
17799
+ `);
17800
+ }
17801
+
17802
+ // ../../packages/persistence/src/db/migrations/introspection.ts
17803
+ function evidenceObjects(sql) {
17804
+ const objects = [];
17805
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17806
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17807
+ objects.push({ kind: "table", name: m[1] });
17808
+ }
17809
+ }
17810
+ for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17811
+ if (m[1] !== void 0 && m[2] !== void 0) {
17812
+ objects.push({ kind: "column", table: m[1], name: m[2] });
17813
+ }
17814
+ }
17815
+ return objects;
17816
+ }
17817
+ function schemaObjectExists(db, kind, name) {
17818
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
17819
+ return row !== void 0;
17820
+ }
17821
+ function indexExists(db, name) {
17822
+ return schemaObjectExists(db, "index", name);
17823
+ }
17824
+ function columnNames(db, table, opts) {
17825
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
17826
+ const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
17827
+ return columns.map((c) => c.name);
17828
+ }
17829
+ function evidenceExists(db, object2) {
17830
+ if (object2.kind === "column") {
17831
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
17832
+ }
17833
+ return schemaObjectExists(db, "table", object2.name);
17834
+ }
17835
+
17482
17836
  // ../../packages/persistence/src/ids.ts
17483
17837
  import { createHash } from "crypto";
17484
17838
  function sha256Hex(input) {
@@ -17515,28 +17869,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17515
17869
  }
17516
17870
 
17517
17871
  // ../../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
17872
  function describeObject(object2) {
17541
17873
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
17542
17874
  }
@@ -17547,10 +17879,6 @@ function createdIndexName(statement) {
17547
17879
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17548
17880
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17549
17881
  }
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
17882
  function applyMigrations(db) {
17555
17883
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17556
17884
  db.exec(
@@ -17569,44 +17897,39 @@ function applyMigrations(db) {
17569
17897
  const present = evidence.filter((o) => evidenceExists(db, o));
17570
17898
  if (present.length > 0 && present.length < evidence.length) {
17571
17899
  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);
17900
+ 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.`;
17901
+ akaWarn(message);
17902
+ throw new Error(`[aka] ${message}`);
17576
17903
  }
17577
17904
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
17578
17905
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
17579
17906
  const statements = splitStatements(migration.sql);
17580
17907
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
17581
17908
  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;
17909
+ withTransaction(
17910
+ db,
17911
+ () => {
17912
+ for (const statement of statements) {
17913
+ const indexName = createdIndexName(statement);
17914
+ if (indexName === void 0) {
17915
+ if (alreadyApplied) continue;
17916
+ } else if (indexExists(db, indexName)) {
17917
+ continue;
17918
+ }
17919
+ db.exec(statement);
17590
17920
  }
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
- );
17921
+ if (wantsFkOff && !alreadyApplied) {
17922
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
17923
+ if (violations.length > 0) {
17924
+ throw new Error(
17925
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17926
+ );
17927
+ }
17599
17928
  }
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
- }
17929
+ record2.run(migration.tag, Date.now());
17930
+ },
17931
+ "IMMEDIATE"
17932
+ );
17610
17933
  } finally {
17611
17934
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
17612
17935
  }
@@ -17649,8 +17972,7 @@ var TOKEN_USAGE_COLUMNS = [
17649
17972
  }
17650
17973
  ];
17651
17974
  function ensureTokenUsageColumns(db) {
17652
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
17653
- const existing = new Set(columns.map((c) => c.name));
17975
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17654
17976
  for (const column of TOKEN_USAGE_COLUMNS) {
17655
17977
  if (!existing.has(column.name)) {
17656
17978
  db.exec(column.ddl);
@@ -17688,47 +18010,39 @@ function reconcileSourceProjectIds(db) {
17688
18010
  repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
17689
18011
  }));
17690
18012
  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);
18013
+ withTransaction(
18014
+ db,
18015
+ () => {
18016
+ for (const { row, canonicalId } of legacy) {
18017
+ foldProject.run(
18018
+ canonicalId,
18019
+ row.url,
18020
+ row.name,
18021
+ row.attributes,
18022
+ row.firstSeen,
18023
+ row.lastSeen
18024
+ );
18025
+ repointAudit.run(canonicalId, row.id);
18026
+ for (const { dropCollisions, repoint } of pathTables) {
18027
+ dropCollisions.run(row.id, canonicalId);
18028
+ repoint.run(canonicalId, row.id);
18029
+ }
18030
+ repointCallSite.run(canonicalId, row.id);
18031
+ deleteLegacy.run(row.id);
17706
18032
  }
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
- }
18033
+ },
18034
+ "IMMEDIATE"
18035
+ );
17718
18036
  } catch (error51) {
17719
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
17720
- `);
18037
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
17721
18038
  }
17722
18039
  }
17723
18040
  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");
18041
+ if (schemaObjectExists(db, "table", "tenants")) return true;
18042
+ return columnNames(db, "events").includes("tenant_id");
17728
18043
  }
17729
18044
  function ensureSyncedAtColumn(db, table) {
17730
- const columns = db.prepare(`PRAGMA table_info(${table})`).all();
17731
- if (!columns.some((c) => c.name === "synced_at")) {
18045
+ if (!columnNames(db, table).includes("synced_at")) {
17732
18046
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
17733
18047
  }
17734
18048
  }
@@ -17779,8 +18093,11 @@ function ensureDataDirSync(dir) {
17779
18093
  } catch {
17780
18094
  }
17781
18095
  }
18096
+ function walSidecars(file2) {
18097
+ return [`${file2}-wal`, `${file2}-shm`];
18098
+ }
17782
18099
  function tightenPerms(file2) {
17783
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
18100
+ for (const path of [file2, ...walSidecars(file2)]) {
17784
18101
  try {
17785
18102
  chmodSync(path, DATA_FILE_MODE);
17786
18103
  } catch {
@@ -17788,12 +18105,68 @@ function tightenPerms(file2) {
17788
18105
  }
17789
18106
  }
17790
18107
 
17791
- // ../../packages/persistence/src/repositories/sql-utils.ts
17792
- function escapeLikePattern(s) {
17793
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
18108
+ // ../../packages/persistence/src/internal/json.ts
18109
+ function safeJson(s, fallback) {
18110
+ if (s == null) return fallback;
18111
+ try {
18112
+ return JSON.parse(s);
18113
+ } catch {
18114
+ return fallback;
18115
+ }
17794
18116
  }
17795
- function placeholders(n) {
17796
- return Array.from({ length: n }, () => "?").join(", ");
18117
+ function parseJsonObject(s) {
18118
+ if (s == null) return void 0;
18119
+ try {
18120
+ const parsed = JSON.parse(s);
18121
+ if (typeof parsed === "object" && parsed !== null) return parsed;
18122
+ } catch {
18123
+ }
18124
+ return void 0;
18125
+ }
18126
+
18127
+ // ../../packages/persistence/src/internal/rows.ts
18128
+ function allRows(stmt, params) {
18129
+ if (params === void 0) return stmt.all();
18130
+ if (Array.isArray(params)) return stmt.all(...params);
18131
+ return stmt.all(params);
18132
+ }
18133
+ function getRow(stmt, params) {
18134
+ if (params === void 0) return stmt.get();
18135
+ if (Array.isArray(params)) return stmt.get(...params);
18136
+ return stmt.get(params);
18137
+ }
18138
+ function intToBool(raw) {
18139
+ return raw === 1 || raw === true;
18140
+ }
18141
+ function boolToInt(b) {
18142
+ return b ? 1 : 0;
18143
+ }
18144
+ function bindParams(row) {
18145
+ const out = {};
18146
+ for (const [key, value] of Object.entries(row)) {
18147
+ out[key] = value === void 0 ? null : value;
18148
+ }
18149
+ return out;
18150
+ }
18151
+ function countScalar(db, sql, params) {
18152
+ return getRow(db.prepare(sql), params)?.n ?? 0;
18153
+ }
18154
+ function countBy(db, sql, params) {
18155
+ const map2 = /* @__PURE__ */ new Map();
18156
+ for (const row of allRows(db.prepare(sql), params)) {
18157
+ map2.set(row.k, row.n);
18158
+ }
18159
+ return map2;
18160
+ }
18161
+ function mapRowsTolerant(rows, map2) {
18162
+ const out = [];
18163
+ for (const row of rows) {
18164
+ try {
18165
+ out.push(map2(row));
18166
+ } catch {
18167
+ }
18168
+ }
18169
+ return out;
17797
18170
  }
17798
18171
 
17799
18172
  // ../../packages/persistence/src/repositories/activity.ts
@@ -17845,15 +18218,11 @@ function encodeCursor(payload) {
17845
18218
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
17846
18219
  }
17847
18220
  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;
18221
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18222
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18223
+ return parsed;
17856
18224
  }
18225
+ return null;
17857
18226
  }
17858
18227
  var DB_EVENT_TYPE_TO_KIND = {
17859
18228
  session: "session",
@@ -17870,15 +18239,8 @@ var DB_EVENT_TYPE_TO_KIND = {
17870
18239
  };
17871
18240
  function safeParseStringArray(raw) {
17872
18241
  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;
18242
+ const parsed = safeJson(raw, null);
18243
+ return Array.isArray(parsed) ? parsed : [];
17882
18244
  }
17883
18245
  function toHarness(raw) {
17884
18246
  const parsed = Harness.safeParse(raw);
@@ -17931,8 +18293,8 @@ function buildAuditEvent(row) {
17931
18293
  severity: severityParsed?.success ? severityParsed.data : null,
17932
18294
  link: linkParsed?.success ? linkParsed.data : null,
17933
18295
  targetId: row.target_id,
17934
- internal: toBool(row.internal),
17935
- flagged: toBool(row.flagged)
18296
+ internal: intToBool(row.internal),
18297
+ flagged: intToBool(row.flagged)
17936
18298
  };
17937
18299
  }
17938
18300
  var TIMELINE_COLUMNS = `
@@ -17948,6 +18310,10 @@ var TIMELINE_COLUMNS = `
17948
18310
  json_extract(attributes, '$.internal') AS internal,
17949
18311
  json_extract(attributes, '$.flagged') AS flagged`;
17950
18312
  var SESSION_ROOT = `event_type = 'session'`;
18313
+ var HAS_ACTIVITY = `EXISTS (
18314
+ SELECT 1 FROM audit_events c
18315
+ WHERE c.root_session_id = audit_events.id
18316
+ AND c.event_type NOT IN ('hook', 'config_scan'))`;
17951
18317
  var SqliteActivityRepository = class {
17952
18318
  constructor(db, now = () => Date.now()) {
17953
18319
  this.db = db;
@@ -17958,12 +18324,16 @@ var SqliteActivityRepository = class {
17958
18324
  stats(tz) {
17959
18325
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
17960
18326
  const { startMs, endMs } = window;
17961
- const sessionsToday = this.db.prepare(
18327
+ const sessionsToday = countScalar(
18328
+ this.db,
17962
18329
  `SELECT count(*) AS n FROM audit_events
17963
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
17964
- ).get(startMs, endMs).n;
18330
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?
18331
+ AND ${HAS_ACTIVITY}`,
18332
+ [startMs, endMs]
18333
+ );
17965
18334
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
17966
- const liveNow = this.db.prepare(
18335
+ const liveNow = countScalar(
18336
+ this.db,
17967
18337
  `SELECT count(*) AS n FROM audit_events s
17968
18338
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
17969
18339
  AND max(
@@ -17972,22 +18342,29 @@ var SqliteActivityRepository = class {
17972
18342
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
17973
18343
  s.started_at
17974
18344
  )
17975
- ) >= ?`
17976
- ).get(liveThreshold).n;
17977
- const toolCallsToday = this.db.prepare(
18345
+ ) >= ?`,
18346
+ [liveThreshold]
18347
+ );
18348
+ const toolCallsToday = countScalar(
18349
+ this.db,
17978
18350
  `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(
18351
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18352
+ [startMs, endMs]
18353
+ );
18354
+ const findingsToday = countScalar(
18355
+ this.db,
17982
18356
  `SELECT count(*) AS n FROM inspection_findings f
17983
18357
  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(
18358
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18359
+ [startMs, endMs]
18360
+ );
18361
+ const egressToday = countScalar(
18362
+ this.db,
17987
18363
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
17988
18364
  FROM audit_events
17989
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
17990
- ).get(startMs, endMs).n;
18365
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18366
+ [startMs, endMs]
18367
+ );
17991
18368
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
17992
18369
  }
17993
18370
  listSessions(query) {
@@ -18009,7 +18386,7 @@ var SqliteActivityRepository = class {
18009
18386
  conditions.push("started_at <= ?");
18010
18387
  params.push(toMs);
18011
18388
  if (query.q) {
18012
- const pattern = `%${escapeLikePattern(query.q)}%`;
18389
+ const pattern = containsPattern(query.q);
18013
18390
  conditions.push(
18014
18391
  `(content LIKE ? ESCAPE '\\'
18015
18392
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18023,13 +18400,21 @@ var SqliteActivityRepository = class {
18023
18400
  );
18024
18401
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
18025
18402
  }
18403
+ const emptyCount = countScalar(
18404
+ this.db,
18405
+ `SELECT count(*) AS n FROM audit_events
18406
+ WHERE ${[...conditions, `NOT ${HAS_ACTIVITY}`].join(" AND ")}`,
18407
+ params
18408
+ );
18409
+ if (query.excludeEmpty) conditions.push(HAS_ACTIVITY);
18026
18410
  if (cursor) {
18027
18411
  conditions.push("(started_at < ? OR (started_at = ? AND id < ?))");
18028
18412
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18029
18413
  }
18030
18414
  const limit = query.limit;
18031
- const rows = this.db.prepare(
18032
- `SELECT id,
18415
+ const rows = allRows(
18416
+ this.db.prepare(
18417
+ `SELECT id,
18033
18418
  json_extract(attributes, '$.harness') AS harness,
18034
18419
  content AS title,
18035
18420
  json_extract(attributes, '$.project') AS project,
@@ -18042,7 +18427,9 @@ var SqliteActivityRepository = class {
18042
18427
  WHERE ${conditions.join(" AND ")}
18043
18428
  ORDER BY started_at DESC, id DESC
18044
18429
  LIMIT ?`
18045
- ).all(...params, limit + 1);
18430
+ ),
18431
+ [...params, limit + 1]
18432
+ );
18046
18433
  const hasMore = rows.length > limit;
18047
18434
  const page = hasMore ? rows.slice(0, limit) : rows;
18048
18435
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18056,11 +18443,12 @@ var SqliteActivityRepository = class {
18056
18443
  );
18057
18444
  const last = page[page.length - 1];
18058
18445
  const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
18059
- return Promise.resolve({ items, nextCursor });
18446
+ return Promise.resolve({ items, nextCursor, emptyCount });
18060
18447
  }
18061
18448
  getSession(sessionId) {
18062
- const rootRow = this.db.prepare(
18063
- `SELECT id,
18449
+ const rootRow = getRow(
18450
+ this.db.prepare(
18451
+ `SELECT id,
18064
18452
  json_extract(attributes, '$.harness') AS harness,
18065
18453
  content AS title,
18066
18454
  json_extract(attributes, '$.project') AS project,
@@ -18077,47 +18465,66 @@ var SqliteActivityRepository = class {
18077
18465
  FROM audit_events
18078
18466
  WHERE id = ? AND event_type = 'session'
18079
18467
  LIMIT 1`
18080
- ).get(sessionId);
18468
+ ),
18469
+ [sessionId]
18470
+ );
18081
18471
  if (!rootRow) return Promise.resolve(null);
18082
- const timelineRows = this.db.prepare(
18083
- `SELECT ${TIMELINE_COLUMNS}
18472
+ const timelineRows = allRows(
18473
+ this.db.prepare(
18474
+ `SELECT ${TIMELINE_COLUMNS}
18084
18475
  FROM audit_events
18085
18476
  WHERE id = ? OR root_session_id = ?
18086
18477
  ORDER BY started_at ASC, id ASC`
18087
- ).all(sessionId, sessionId);
18478
+ ),
18479
+ [sessionId, sessionId]
18480
+ );
18088
18481
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18089
- const tokenRow = this.db.prepare(
18090
- `SELECT
18482
+ const tokenRow = getRow(
18483
+ this.db.prepare(
18484
+ `SELECT
18091
18485
  coalesce(sum(input_tokens), 0) AS input,
18092
18486
  coalesce(sum(output_tokens), 0) AS output,
18093
18487
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18094
18488
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18095
18489
  FROM audit_events
18096
18490
  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
18491
+ ),
18492
+ [sessionId]
18493
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
18494
+ const primaryModel = getRow(
18495
+ this.db.prepare(
18496
+ `SELECT model, provider FROM audit_events
18100
18497
  WHERE root_session_id = ? AND event_type = 'llm_call'
18101
18498
  ORDER BY started_at ASC, id ASC
18102
18499
  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,
18106
- count(*) AS n
18107
- FROM audit_events
18500
+ ),
18501
+ [sessionId]
18502
+ );
18503
+ const toolRows = allRows(
18504
+ this.db.prepare(
18505
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18506
+ count(*) AS n
18507
+ FROM audit_events
18108
18508
  WHERE root_session_id = ? AND event_type = 'tool_call'
18109
18509
  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
18510
+ ),
18511
+ [sessionId]
18512
+ );
18513
+ const modelRows = allRows(
18514
+ this.db.prepare(
18515
+ `SELECT DISTINCT model FROM audit_events
18113
18516
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18114
18517
  ORDER BY model`
18115
- ).all(sessionId);
18518
+ ),
18519
+ [sessionId]
18520
+ );
18116
18521
  const derivedModels = modelRows.map((r) => r.model);
18117
- const commits = this.db.prepare(
18522
+ const commits = countScalar(
18523
+ this.db,
18118
18524
  `SELECT count(*) AS n FROM audit_events
18119
- WHERE root_session_id = ? AND event_type = 'commit'`
18120
- ).get(sessionId).n;
18525
+ WHERE root_session_id = ? AND event_type = 'commit'`,
18526
+ [sessionId]
18527
+ );
18121
18528
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18122
18529
  turns: 0,
18123
18530
  findings: 0,
@@ -18184,7 +18591,10 @@ var SqliteActivityRepository = class {
18184
18591
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18185
18592
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18186
18593
  );
18187
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
18594
+ const rows = allRows(
18595
+ stmt,
18596
+ fromMs === void 0 ? void 0 : [fromMs]
18597
+ );
18188
18598
  const seen = /* @__PURE__ */ new Set();
18189
18599
  for (const row of rows) seen.add(toHarness(row.harness));
18190
18600
  return Promise.resolve([...seen]);
@@ -18207,23 +18617,23 @@ var SqliteActivityRepository = class {
18207
18617
  conditions.push("started_at >= ?");
18208
18618
  params.push(opts.fromMs);
18209
18619
  }
18210
- const rows = this.db.prepare(
18211
- `SELECT root_session_id AS sessionId, attributes
18620
+ const rows = allRows(
18621
+ this.db.prepare(
18622
+ `SELECT root_session_id AS sessionId, attributes
18212
18623
  FROM audit_events
18213
18624
  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;
18625
+ ),
18626
+ params
18627
+ );
18628
+ return mapRowsTolerant(
18629
+ rows.filter(
18630
+ (row) => row.sessionId !== null
18631
+ ),
18632
+ (row) => ({
18633
+ sessionId: row.sessionId,
18634
+ attributes: JSON.parse(row.attributes)
18635
+ })
18636
+ );
18227
18637
  }
18228
18638
  /**
18229
18639
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -18237,57 +18647,72 @@ var SqliteActivityRepository = class {
18237
18647
  );
18238
18648
  if (sessionIds.length === 0) return result;
18239
18649
  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
18650
+ const lastActivityRows = allRows(
18651
+ this.db.prepare(
18652
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18242
18653
  WHERE root_session_id IN (${inClause})
18243
18654
  GROUP BY root_session_id`
18244
- ).all(...sessionIds);
18655
+ ),
18656
+ sessionIds
18657
+ );
18245
18658
  for (const row of lastActivityRows) {
18246
18659
  if (row.id === null) continue;
18247
18660
  const entry = result.get(row.id);
18248
18661
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
18249
18662
  }
18250
- const turnsRows = this.db.prepare(
18251
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18663
+ const turnsRows = allRows(
18664
+ this.db.prepare(
18665
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18252
18666
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
18253
18667
  GROUP BY root_session_id`
18254
- ).all(...sessionIds);
18668
+ ),
18669
+ sessionIds
18670
+ );
18255
18671
  for (const row of turnsRows) {
18256
18672
  if (row.id === null) continue;
18257
18673
  const entry = result.get(row.id);
18258
18674
  if (entry) entry.turns = row.n;
18259
18675
  }
18260
- const runKeyRows = this.db.prepare(
18261
- `SELECT root_session_id AS id,
18676
+ const runKeyRows = allRows(
18677
+ this.db.prepare(
18678
+ `SELECT root_session_id AS id,
18262
18679
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
18263
18680
  FROM audit_events
18264
18681
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
18265
18682
  AND json_extract(attributes, '$.run_key') IS NOT NULL
18266
18683
  GROUP BY root_session_id`
18267
- ).all(...sessionIds);
18684
+ ),
18685
+ sessionIds
18686
+ );
18268
18687
  for (const row of runKeyRows) {
18269
18688
  if (row.id === null) continue;
18270
18689
  const entry = result.get(row.id);
18271
18690
  if (entry) entry.turns = Math.max(entry.turns, row.n);
18272
18691
  }
18273
- const findingsRows = this.db.prepare(
18274
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18692
+ const findingsRows = allRows(
18693
+ this.db.prepare(
18694
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18275
18695
  JOIN audit_events e ON e.id = f.audit_event_id
18276
18696
  WHERE e.root_session_id IN (${inClause})
18277
18697
  GROUP BY e.root_session_id`
18278
- ).all(...sessionIds);
18698
+ ),
18699
+ sessionIds
18700
+ );
18279
18701
  for (const row of findingsRows) {
18280
18702
  if (row.id === null) continue;
18281
18703
  const entry = result.get(row.id);
18282
18704
  if (entry) entry.findings = row.n;
18283
18705
  }
18284
- const sharesRows = this.db.prepare(
18285
- `SELECT root_session_id AS id,
18706
+ const sharesRows = allRows(
18707
+ this.db.prepare(
18708
+ `SELECT root_session_id AS id,
18286
18709
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
18287
18710
  FROM audit_events
18288
18711
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
18289
18712
  GROUP BY root_session_id`
18290
- ).all(...sessionIds);
18713
+ ),
18714
+ sessionIds
18715
+ );
18291
18716
  for (const row of sharesRows) {
18292
18717
  if (row.id === null) continue;
18293
18718
  const entry = result.get(row.id);
@@ -18337,33 +18762,28 @@ var SqliteAuditEventsRepository = class {
18337
18762
  // the caller fails open and drops the whole pass — recovered idempotently on the
18338
18763
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
18339
18764
  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
- }
18765
+ withTransaction(this.db, fn);
18348
18766
  }
18349
18767
  insertAuditEvent(input) {
18350
18768
  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
- });
18769
+ this.insertStmt.run(
18770
+ bindParams({
18771
+ id: row.id,
18772
+ parentId: row.parentId,
18773
+ rootSessionId: row.rootSessionId,
18774
+ eventType: row.eventType,
18775
+ hostId: row.hostId,
18776
+ harnessId: row.harnessId,
18777
+ sourceProjectId: row.sourceProjectId,
18778
+ startedAt: row.startedAt,
18779
+ endedAt: row.endedAt,
18780
+ severity: row.severity,
18781
+ priority: row.priority,
18782
+ content: row.content,
18783
+ contentHash: row.contentHash,
18784
+ attributes: row.attributes
18785
+ })
18786
+ );
18367
18787
  }
18368
18788
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18369
18789
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -18377,22 +18797,24 @@ var SqliteAuditEventsRepository = class {
18377
18797
  const startedAt = isoToEpochMillis(input.startedAt);
18378
18798
  if (!Number.isFinite(startedAt)) return;
18379
18799
  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
- });
18800
+ this.upsertLlmCallStmt.run(
18801
+ bindParams({
18802
+ id,
18803
+ parentId: input.parentId,
18804
+ rootSessionId: input.rootSessionId,
18805
+ eventType: "llm_call",
18806
+ hostId: null,
18807
+ harnessId: null,
18808
+ sourceProjectId: null,
18809
+ startedAt,
18810
+ endedAt: null,
18811
+ severity: null,
18812
+ priority: null,
18813
+ content: null,
18814
+ contentHash: null,
18815
+ attributes: JSON.stringify(input.attributes)
18816
+ })
18817
+ );
18396
18818
  }
18397
18819
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
18398
18820
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -18416,25 +18838,29 @@ var SqliteAuditEventsRepository = class {
18416
18838
  const startedAt = isoToEpochMillis(input.startedAt);
18417
18839
  if (!Number.isFinite(startedAt)) return;
18418
18840
  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
- });
18841
+ this.insertStmt.run(
18842
+ bindParams({
18843
+ id,
18844
+ parentId: input.parentId,
18845
+ rootSessionId: input.rootSessionId,
18846
+ eventType: "tool_call",
18847
+ hostId: null,
18848
+ harnessId: null,
18849
+ sourceProjectId: null,
18850
+ startedAt,
18851
+ endedAt: null,
18852
+ severity: null,
18853
+ priority: null,
18854
+ content: null,
18855
+ contentHash: null,
18856
+ attributes: JSON.stringify(input.attributes)
18857
+ })
18858
+ );
18435
18859
  }
18436
18860
  findById(id) {
18437
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
18861
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
18862
+ id
18863
+ });
18438
18864
  }
18439
18865
  // Read the `provider` snapshotted onto a session root's attributes.
18440
18866
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -18444,14 +18870,8 @@ var SqliteAuditEventsRepository = class {
18444
18870
  sessionProvider(sessionId) {
18445
18871
  const row = this.findById(sessionId);
18446
18872
  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
- }
18873
+ const provider = parseJsonObject(row.attributes)?.provider;
18874
+ if (typeof provider === "string") return provider;
18455
18875
  return void 0;
18456
18876
  }
18457
18877
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -18460,11 +18880,13 @@ var SqliteAuditEventsRepository = class {
18460
18880
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
18461
18881
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
18462
18882
  llmCallLeaves() {
18463
- return this.db.prepare(
18464
- `SELECT root_session_id AS sessionId, attributes
18883
+ return allRows(
18884
+ this.db.prepare(
18885
+ `SELECT root_session_id AS sessionId, attributes
18465
18886
  FROM audit_events
18466
18887
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
18467
- ).all();
18888
+ )
18889
+ );
18468
18890
  }
18469
18891
  };
18470
18892
 
@@ -18483,16 +18905,29 @@ var SqliteClassifiedDataRepository = class {
18483
18905
  upsert(input) {
18484
18906
  const id = classifiedDataId(input.class);
18485
18907
  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
- });
18908
+ this.insertStmt.run(
18909
+ bindParams({
18910
+ id: row.id,
18911
+ class: row.class,
18912
+ label: row.label,
18913
+ attributes: row.attributes
18914
+ })
18915
+ );
18492
18916
  return id;
18493
18917
  }
18494
18918
  };
18495
18919
 
18920
+ // ../../packages/persistence/src/repositories/config-scan.ts
18921
+ function latestConfigScan(db) {
18922
+ return getRow(
18923
+ db.prepare(
18924
+ `SELECT id, started_at, attributes FROM audit_events
18925
+ WHERE event_type = 'config_scan'
18926
+ ORDER BY started_at DESC, id DESC LIMIT 1`
18927
+ )
18928
+ );
18929
+ }
18930
+
18496
18931
  // ../../packages/persistence/src/repositories/config-inventory.ts
18497
18932
  var SqliteConfigInventoryRepository = class {
18498
18933
  constructor(db) {
@@ -18500,7 +18935,7 @@ var SqliteConfigInventoryRepository = class {
18500
18935
  }
18501
18936
  db;
18502
18937
  report() {
18503
- const scan2 = this.latestScan();
18938
+ const scan2 = latestConfigScan(this.db);
18504
18939
  if (!scan2) {
18505
18940
  return {
18506
18941
  scannedAt: null,
@@ -18511,17 +18946,23 @@ var SqliteConfigInventoryRepository = class {
18511
18946
  topics: []
18512
18947
  };
18513
18948
  }
18514
- const rows = this.db.prepare(
18515
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18949
+ const rows = allRows(
18950
+ this.db.prepare(
18951
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18516
18952
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
18517
18953
  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
18954
+ ),
18955
+ { startedAt: scan2.started_at }
18956
+ );
18957
+ const findings = allRows(
18958
+ this.db.prepare(
18959
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18521
18960
  FROM inspection_findings f
18522
18961
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
18523
18962
  WHERE f.audit_event_id = :scanId`
18524
- ).all({ scanId: scan2.id });
18963
+ ),
18964
+ { scanId: scan2.id }
18965
+ );
18525
18966
  const skills = [];
18526
18967
  const hooks = [];
18527
18968
  const mcpServers = [];
@@ -18550,7 +18991,9 @@ var SqliteConfigInventoryRepository = class {
18550
18991
  // schema note); an override whose asset is gone simply never matches. A row
18551
18992
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
18552
18993
  trustOverrides() {
18553
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
18994
+ const rows = allRows(
18995
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
18996
+ );
18554
18997
  const map2 = /* @__PURE__ */ new Map();
18555
18998
  for (const row of rows) {
18556
18999
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -18559,13 +19002,6 @@ var SqliteConfigInventoryRepository = class {
18559
19002
  }
18560
19003
  return map2;
18561
19004
  }
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
19005
  };
18570
19006
  function toSkillItem(row, bag) {
18571
19007
  const item = {
@@ -18676,22 +19112,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
18676
19112
  return topics;
18677
19113
  }
18678
19114
  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
- }
19115
+ const errors = parseJsonObject(attributes)?.errors;
19116
+ return typeof errors === "number" ? errors : 0;
18687
19117
  }
18688
19118
  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;
19119
+ return parseJsonObject(raw);
18695
19120
  }
18696
19121
  function str(value) {
18697
19122
  return typeof value === "string" ? value : void 0;
@@ -18700,12 +19125,7 @@ function str(value) {
18700
19125
  // ../../packages/persistence/src/repositories/detections.ts
18701
19126
  var DAY_MS2 = 864e5;
18702
19127
  function parseRules(rulesJson) {
18703
- let raw;
18704
- try {
18705
- raw = JSON.parse(rulesJson);
18706
- } catch {
18707
- return [];
18708
- }
19128
+ const raw = safeJson(rulesJson, []);
18709
19129
  if (!Array.isArray(raw)) return [];
18710
19130
  const rules = [];
18711
19131
  for (const entry of raw) {
@@ -18724,11 +19144,13 @@ var SqliteDetectionsRepository = class {
18724
19144
  db;
18725
19145
  now;
18726
19146
  listDetections(query) {
18727
- const rows = this.db.prepare(
18728
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19147
+ const rows = allRows(
19148
+ this.db.prepare(
19149
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18729
19150
  rules_json AS rulesJson
18730
19151
  FROM installed_packs`
18731
- ).all();
19152
+ )
19153
+ );
18732
19154
  const available = this.availableByPack();
18733
19155
  const summaries = rows.map((r) => {
18734
19156
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -18737,7 +19159,7 @@ var SqliteDetectionsRepository = class {
18737
19159
  packId: r.packId,
18738
19160
  version: r.version,
18739
19161
  name: r.name,
18740
- enabled: r.enabled === 1,
19162
+ enabled: intToBool(r.enabled),
18741
19163
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
18742
19164
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
18743
19165
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -18754,19 +19176,23 @@ var SqliteDetectionsRepository = class {
18754
19176
  // available_packs keyed by the "namespace/packId" slug (one read per list /
18755
19177
  // detail call; the table is a handful of rows).
18756
19178
  availableByPack() {
18757
- const rows = this.db.prepare(
18758
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
19179
+ const rows = allRows(
19180
+ this.db.prepare(
19181
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18759
19182
  FROM available_packs`
18760
- ).all();
19183
+ )
19184
+ );
18761
19185
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
18762
19186
  }
18763
19187
  getDetectionStats() {
18764
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
19188
+ const rows = allRows(
19189
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
19190
+ );
18765
19191
  let rules = 0;
18766
19192
  let active = 0;
18767
19193
  const ruleIds = /* @__PURE__ */ new Set();
18768
19194
  for (const r of rows) {
18769
- if (r.enabled === 1) active += 1;
19195
+ if (intToBool(r.enabled)) active += 1;
18770
19196
  const parsed = parseRules(r.rulesJson);
18771
19197
  rules += parsed.length;
18772
19198
  for (const rule of parsed) {
@@ -18784,12 +19210,15 @@ var SqliteDetectionsRepository = class {
18784
19210
  const parts = splitDetectionId(id);
18785
19211
  if (!parts) return Promise.resolve(null);
18786
19212
  const { namespace, packId } = parts;
18787
- const row = this.db.prepare(
18788
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19213
+ const row = getRow(
19214
+ this.db.prepare(
19215
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18789
19216
  rules_json AS rulesJson, updated_at AS updatedAt
18790
19217
  FROM installed_packs
18791
19218
  WHERE namespace = ? AND pack_id = ?`
18792
- ).get(namespace, packId);
19219
+ ),
19220
+ [namespace, packId]
19221
+ );
18793
19222
  if (!row) return Promise.resolve(null);
18794
19223
  const rules = parseRules(row.rulesJson);
18795
19224
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -18807,7 +19236,7 @@ var SqliteDetectionsRepository = class {
18807
19236
  packId: row.packId,
18808
19237
  version: row.version,
18809
19238
  name: row.name,
18810
- enabled: row.enabled === 1,
19239
+ enabled: intToBool(row.enabled),
18811
19240
  rules,
18812
19241
  updatedAt: new Date(row.updatedAt),
18813
19242
  policyId: row.policyId
@@ -18822,13 +19251,14 @@ var SqliteDetectionsRepository = class {
18822
19251
  countFindingsLast30d(ruleIds) {
18823
19252
  if (ruleIds.length === 0) return 0;
18824
19253
  const since = this.now() - 30 * DAY_MS2;
18825
- const placeholders2 = ruleIds.map(() => "?").join(", ");
18826
- const row = this.db.prepare(
18827
- `SELECT count(*) AS c
19254
+ const inClause = placeholders(ruleIds.length);
19255
+ return countScalar(
19256
+ this.db,
19257
+ `SELECT count(*) AS n
18828
19258
  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;
19259
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19260
+ [since, ...ruleIds]
19261
+ );
18832
19262
  }
18833
19263
  };
18834
19264
 
@@ -18845,16 +19275,17 @@ var SqliteEventsRepository = class {
18845
19275
  insertStmt;
18846
19276
  insertEvent(event) {
18847
19277
  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
- });
19278
+ this.insertStmt.run(
19279
+ bindParams({
19280
+ id: row.id,
19281
+ sourceTool: row.sourceTool,
19282
+ kind: row.kind,
19283
+ occurredAt: row.occurredAt,
19284
+ contentHash: row.contentHash,
19285
+ content: row.content,
19286
+ metadata: row.metadata
19287
+ })
19288
+ );
18858
19289
  }
18859
19290
  // Every recorded event's content hash — the historical backfill loads this once
18860
19291
  // to skip transcript messages it has already stored, so re-running the scan
@@ -18862,13 +19293,23 @@ var SqliteEventsRepository = class {
18862
19293
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
18863
19294
  // async EventsReadPort contract.
18864
19295
  contentHashes() {
18865
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19296
+ const rows = allRows(
19297
+ this.db.prepare("SELECT content_hash FROM events")
19298
+ );
18866
19299
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
18867
19300
  }
18868
19301
  };
18869
19302
 
18870
19303
  // ../../packages/persistence/src/repositories/exceptions.ts
18871
19304
  import { randomUUID } from "crypto";
19305
+
19306
+ // ../../packages/persistence/src/internal/sqlite-errors.ts
19307
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19308
+ function isUniqueConstraintError(err) {
19309
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19310
+ }
19311
+
19312
+ // ../../packages/persistence/src/repositories/exceptions.ts
18872
19313
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
18873
19314
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
18874
19315
  var DuplicateActiveExceptionError = class extends Error {
@@ -18889,10 +19330,6 @@ var AmbiguousExceptionIdError = class extends Error {
18889
19330
  this.name = "AmbiguousExceptionIdError";
18890
19331
  }
18891
19332
  };
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
19333
  var ACTIVE_PREDICATE = `revoked_at IS NULL
18897
19334
  AND (expires_at IS NULL OR expires_at > :now)
18898
19335
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -18948,10 +19385,11 @@ var SqliteExceptionsRepository = class {
18948
19385
  this.insertExceptionRow(id, input, now);
18949
19386
  } catch (err) {
18950
19387
  if (!isUniqueConstraintError(err)) throw err;
18951
- this.db.exec("BEGIN IMMEDIATE");
18952
- try {
18953
- const superseded = this.db.prepare(
18954
- `UPDATE exceptions
19388
+ withTransaction(
19389
+ this.db,
19390
+ () => {
19391
+ const superseded = this.db.prepare(
19392
+ `UPDATE exceptions
18955
19393
  SET revoked_at = :now, revoked_by = :revokedBy,
18956
19394
  revoke_reason = 'superseded by a new grant for the same value',
18957
19395
  updated_at = :now
@@ -18959,24 +19397,27 @@ var SqliteExceptionsRepository = class {
18959
19397
  AND key_version = :keyVersion AND revoked_at IS NULL
18960
19398
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
18961
19399
  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
- }
19400
+ ).run({
19401
+ now,
19402
+ revokedBy: input.createdBy,
19403
+ ruleId: input.ruleId,
19404
+ valueFingerprint: input.valueFingerprint,
19405
+ keyVersion: input.keyVersion
19406
+ });
19407
+ if (Number(superseded.changes) !== 1) {
19408
+ throw new DuplicateActiveExceptionError(input.ruleId);
19409
+ }
19410
+ this.insertExceptionRow(id, input, now);
19411
+ },
19412
+ "IMMEDIATE"
19413
+ );
19414
+ }
19415
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
19416
+ id
19417
+ });
19418
+ if (row === void 0) {
19419
+ throw new Error("exception row not found immediately after insert");
18978
19420
  }
18979
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
18980
19421
  return parseExceptionRow(row);
18981
19422
  }
18982
19423
  insertExceptionRow(id, input, now) {
@@ -19014,14 +19455,11 @@ var SqliteExceptionsRepository = class {
19014
19455
  */
19015
19456
  list(opts) {
19016
19457
  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
- }
19458
+ const rows = allRows(
19459
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19460
+ opts?.includeTerminal ? {} : { now: Date.now() }
19461
+ );
19462
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19025
19463
  return Promise.resolve(exceptions);
19026
19464
  }
19027
19465
  /**
@@ -19031,7 +19469,12 @@ var SqliteExceptionsRepository = class {
19031
19469
  */
19032
19470
  getByIdPrefix(prefix) {
19033
19471
  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)}%` });
19472
+ const rows = allRows(
19473
+ this.db.prepare(
19474
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
19475
+ ),
19476
+ { pattern: `${escapeLikePattern(prefix)}%` }
19477
+ );
19035
19478
  if (rows.length > 1) {
19036
19479
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19037
19480
  }
@@ -19073,30 +19516,27 @@ var SqliteExceptionsRepository = class {
19073
19516
  * a different (rotated-away) key never match, so they are excluded at read.
19074
19517
  */
19075
19518
  activeBundleEntries(keyVersion, now = Date.now()) {
19076
- const rows = this.db.prepare(
19077
- `SELECT * FROM exceptions
19519
+ const rows = allRows(
19520
+ this.db.prepare(
19521
+ `SELECT * FROM exceptions
19078
19522
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19079
19523
  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
- }
19524
+ ),
19525
+ { keyVersion, now }
19526
+ );
19527
+ const entries = mapRowsTolerant(rows, (row) => {
19528
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19529
+ return ExceptionBundleEntry.parse({
19530
+ id: row.id,
19531
+ ruleId: row.rule_id,
19532
+ valueFingerprint: row.value_fingerprint,
19533
+ keyVersion: row.key_version,
19534
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19535
+ maxUses: row.max_uses,
19536
+ useCount: row.use_count,
19537
+ conditions
19538
+ });
19539
+ });
19100
19540
  return Promise.resolve(entries);
19101
19541
  }
19102
19542
  /**
@@ -19123,11 +19563,14 @@ var SqliteExceptionsRepository = class {
19123
19563
  }
19124
19564
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19125
19565
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19126
- const rows = this.db.prepare(
19127
- `SELECT * FROM blocked_detections
19566
+ const rows = allRows(
19567
+ this.db.prepare(
19568
+ `SELECT * FROM blocked_detections
19128
19569
  WHERE blocked_at > :cutoff
19129
19570
  ORDER BY blocked_at DESC, rowid DESC`
19130
- ).all({ cutoff: Date.now() - windowMs });
19571
+ ),
19572
+ { cutoff: Date.now() - windowMs }
19573
+ );
19131
19574
  return Promise.resolve(
19132
19575
  rows.map((row) => ({
19133
19576
  reference: row.reference,
@@ -19207,7 +19650,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
19207
19650
  )`;
19208
19651
 
19209
19652
  // ../../packages/persistence/src/repositories/findings.ts
19210
- var GROUP_ROW_CAP = 2e3;
19653
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
19654
+ var CONCAT_SEP = ",";
19655
+ var TUPLE_SEP = "|";
19656
+ function splitConcat(value) {
19657
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
19658
+ }
19211
19659
  function deriveInstanceStatus(row) {
19212
19660
  return deriveFindingStatus({
19213
19661
  kind: row.kind,
@@ -19275,13 +19723,16 @@ var SqliteFindingsRepository = class {
19275
19723
  }
19276
19724
  recentFindings(opts) {
19277
19725
  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,
19726
+ const rows = allRows(
19727
+ this.db.prepare(
19728
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19280
19729
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19281
19730
  FROM findings f JOIN events e ON e.id = f.event_id
19282
19731
  ORDER BY e.occurred_at DESC, f.rowid DESC
19283
19732
  LIMIT :limit`
19284
- ).all({ limit });
19733
+ ),
19734
+ { limit }
19735
+ );
19285
19736
  return Promise.resolve(
19286
19737
  rows.map((r) => ({
19287
19738
  id: r.id,
@@ -19298,26 +19749,94 @@ var SqliteFindingsRepository = class {
19298
19749
  }))
19299
19750
  );
19300
19751
  }
19752
+ /** Live-enforced findings recorded for one session — a bare COUNT over the
19753
+ * session-stamped events (served by idx_events_session_id), so the Activity
19754
+ * page can label its findings link without the grouped pipeline. */
19755
+ sessionFindingsCount(sessionId) {
19756
+ if (!sessionId) return Promise.resolve(0);
19757
+ return Promise.resolve(
19758
+ countScalar(
19759
+ this.db,
19760
+ `SELECT count(*) AS n FROM findings f
19761
+ JOIN events e ON e.id = f.event_id
19762
+ WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
19763
+ { sessionId }
19764
+ )
19765
+ );
19766
+ }
19767
+ /** Per-rule transcript firing tally for one session — reads the OTHER finding
19768
+ * store (inspection_findings, keyed to audit_events): every detection the
19769
+ * transcript pass recorded, counted per firing rather than per unique value.
19770
+ * Rides on session-scoped grouped responses so the findings view can
19771
+ * reconcile the Activity page's tally with the deduped groups it lists. */
19772
+ sessionFirings(sessionId) {
19773
+ return Object.fromEntries(
19774
+ countBy(
19775
+ this.db,
19776
+ `SELECT d.rule_id AS k, count(*) AS n
19777
+ FROM inspection_findings f
19778
+ JOIN audit_events e ON e.id = f.audit_event_id
19779
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19780
+ WHERE e.root_session_id = :sessionId
19781
+ GROUP BY d.rule_id`,
19782
+ { sessionId }
19783
+ )
19784
+ );
19785
+ }
19301
19786
  /**
19302
- * Grouped findings for the dashboard — joins findings⋈events (repo/file
19303
- * from event metadata), groups by ruleId, computes per-filter-excluded facets,
19787
+ * Grouped findings for the dashboard — joins findings⋈events (repo/file/
19788
+ * toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
19304
19789
  * applies the requested filters, and sorts by severity then recency. Filtering
19305
19790
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19306
19791
  * reflect the full filtered set; `items` is the requested
19307
- * page (default 100); no cursor (nextCursor is always null).
19792
+ * page (default 50); no cursor (nextCursor is always null).
19793
+ *
19794
+ * Two reads, neither of which materializes a row per finding:
19795
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
19796
+ * the group and the filters need (count, providers, actions, statuses,
19797
+ * latest, search text);
19798
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
19799
+ * populate `instances` for the table's expanded rows.
19800
+ * The aggregates carry raw DB values and are translated by the same
19801
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
19802
+ * rule is ever restated in SQL.
19308
19803
  */
19309
19804
  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 });
19805
+ const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
19806
+ const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
19807
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
19808
+ predicate: sessionPredicate,
19809
+ params: sessionParams
19810
+ });
19811
+ const rows = allRows(
19812
+ this.db.prepare(
19813
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19814
+ occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
19815
+ FROM (
19816
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19817
+ f.severity AS severity, f.masked_match AS masked_match,
19818
+ f.action_taken AS action_taken, f.confidence AS confidence,
19819
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19820
+ json_extract(e.metadata, '$.repo') AS repo,
19821
+ json_extract(e.metadata, '$.filePath') AS file,
19822
+ json_extract(e.metadata, '$.toolName') AS tool_name,
19823
+ e.kind AS kind, f.finding_key AS finding_key,
19824
+ latest.status AS latest_status,
19825
+ ROW_NUMBER() OVER (
19826
+ PARTITION BY f.rule_id
19827
+ ORDER BY e.occurred_at DESC, f.id DESC
19828
+ ) AS rn
19829
+ FROM findings f
19830
+ JOIN events e ON e.id = f.event_id
19831
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19832
+ ON latest.finding_key = f.finding_key
19833
+ ${sessionPredicate}
19834
+ )
19835
+ WHERE rn <= :cap
19836
+ ORDER BY occurred_at DESC, id DESC`
19837
+ ),
19838
+ { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
19839
+ );
19321
19840
  const groupable = rows.map((r) => ({
19322
19841
  id: r.id,
19323
19842
  ruleId: r.rule_id,
@@ -19330,9 +19849,10 @@ var SqliteFindingsRepository = class {
19330
19849
  sourceTool: r.source_tool,
19331
19850
  repo: r.repo ?? "",
19332
19851
  file: r.file ?? "",
19852
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
19333
19853
  status: deriveInstanceStatus(r)
19334
19854
  }));
19335
- const allGroups = buildFindingGroups(groupable);
19855
+ const allGroups = buildFindingGroups(groupable, { aggregates });
19336
19856
  const filterOpts = {
19337
19857
  severity: query.severity,
19338
19858
  providers: query.provider,
@@ -19348,44 +19868,134 @@ var SqliteFindingsRepository = class {
19348
19868
  };
19349
19869
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
19350
19870
  const items = sorted.slice(0, limit);
19351
- return Promise.resolve({ totals, facets, items, nextCursor: null });
19871
+ return Promise.resolve({
19872
+ totals,
19873
+ facets,
19874
+ items,
19875
+ nextCursor: null,
19876
+ ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
19877
+ });
19878
+ }
19879
+ /**
19880
+ * One row per rule_id, folding EVERY instance of the group into the values
19881
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
19882
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
19883
+ *
19884
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
19885
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19886
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19887
+ * status keeps the classifier itself in @akasecurity/schema, where
19888
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
19889
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
19890
+ * a group's row stays small however many findings it holds.
19891
+ *
19892
+ * `withSearchText` is the exception, and the one column here that does NOT
19893
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
19894
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19895
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
19896
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
19897
+ * match an instance outside the preview, which searching the preview alone
19898
+ * would silently lose, so it is fetched only when the request actually
19899
+ * carries a `q`.
19900
+ */
19901
+ groupAggregates(withSearchText, scope) {
19902
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19903
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
19904
+ group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
19905
+ const rows = this.db.prepare(
19906
+ `SELECT f.rule_id AS rule_id,
19907
+ count(*) AS instance_count,
19908
+ max(e.occurred_at) AS latest_at,
19909
+ group_concat(DISTINCT e.source_tool) AS source_tools,
19910
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
19911
+ group_concat(DISTINCT (
19912
+ e.kind || '${TUPLE_SEP}' ||
19913
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19914
+ coalesce(latest.status, '')
19915
+ )) AS status_inputs
19916
+ ${searchTextColumns}
19917
+ FROM findings f
19918
+ JOIN events e ON e.id = f.event_id
19919
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19920
+ ON latest.finding_key = f.finding_key
19921
+ ${scope.predicate}
19922
+ GROUP BY f.rule_id`
19923
+ ).all(scope.params);
19924
+ return new Map(
19925
+ rows.map((r) => [
19926
+ r.rule_id,
19927
+ {
19928
+ instanceCount: r.instance_count,
19929
+ sourceTools: splitConcat(r.source_tools),
19930
+ actionsTaken: splitConcat(r.actions_taken),
19931
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19932
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
19933
+ return {
19934
+ // deriveFindingStatus only distinguishes null from non-null here,
19935
+ // so the marker stands in for the key itself (never rendered).
19936
+ kind,
19937
+ findingKey: keyMarker === "" ? null : keyMarker,
19938
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
19939
+ };
19940
+ }),
19941
+ latestDetectedAt: epochMillisToIso(r.latest_at),
19942
+ // Free text only — joined and substring-matched, so group_concat's
19943
+ // commas need no unpicking (a repo/path containing one still matches).
19944
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
19945
+ // tell "no q this request" from "a group with no repo/file at all"
19946
+ // and skip priming a haystack nothing will read.
19947
+ ...withSearchText ? {
19948
+ searchText: [r.repos ?? "", r.files ?? "", r.tool_names ?? ""].filter((s) => s !== "").join(" ")
19949
+ } : {}
19950
+ }
19951
+ ])
19952
+ );
19352
19953
  }
19353
19954
  healthSummary() {
19354
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
19955
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
19355
19956
  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();
19957
+ const grouped = allRows(
19958
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
19959
+ );
19357
19960
  for (const row of grouped) {
19358
19961
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
19359
19962
  }
19360
19963
  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
19964
+ const sevRows = allRows(
19965
+ this.db.prepare(
19966
+ `SELECT f.severity AS severity, count(*) AS c
19363
19967
  FROM findings f
19364
19968
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19365
19969
  ON latest.finding_key = f.finding_key
19366
19970
  WHERE latest.status IS NULL OR latest.status != 'resolved'
19367
19971
  GROUP BY f.severity`
19368
- ).all();
19972
+ )
19973
+ );
19369
19974
  for (const row of sevRows) {
19370
19975
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
19371
19976
  }
19372
19977
  const categories = ENFORCEABLE_CATEGORIES;
19373
- const enabledRows = this.db.prepare(
19374
- `SELECT DISTINCT json_extract(target, '$.category') AS category
19978
+ const enabledRows = allRows(
19979
+ this.db.prepare(
19980
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
19375
19981
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
19376
- ).all();
19982
+ )
19983
+ );
19377
19984
  const enabled = new Set(enabledRows.map((r) => r.category));
19378
19985
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
19379
19986
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
19380
19987
  }
19381
19988
  activityByDay(days = 7) {
19382
19989
  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
19990
+ const rows = allRows(
19991
+ this.db.prepare(
19992
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19385
19993
  FROM findings f JOIN events e ON e.id = f.event_id
19386
19994
  WHERE e.occurred_at >= :since
19387
19995
  GROUP BY day, f.action_taken`
19388
- ).all({ since });
19996
+ ),
19997
+ { since }
19998
+ );
19389
19999
  const buckets = /* @__PURE__ */ new Map();
19390
20000
  for (let i = 0; i < days; i++) {
19391
20001
  const day = isoDay(since + i * DAY_MS3);
@@ -19457,17 +20067,19 @@ var SqliteInspectionFindingsRepository = class {
19457
20067
  insertStmt;
19458
20068
  insertFinding(input) {
19459
20069
  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
- });
20070
+ this.insertStmt.run(
20071
+ bindParams({
20072
+ id: row.id,
20073
+ auditEventId: row.auditEventId,
20074
+ inspectionDefinitionId: row.inspectionDefinitionId,
20075
+ classifiedDataId: row.classifiedDataId,
20076
+ spanStart: row.spanStart,
20077
+ spanEnd: row.spanEnd,
20078
+ maskedMatch: row.maskedMatch,
20079
+ actionTaken: row.actionTaken,
20080
+ confidence: row.confidence
20081
+ })
20082
+ );
19471
20083
  }
19472
20084
  };
19473
20085
 
@@ -19543,12 +20155,7 @@ function isMirrorDowngrade(incoming, stored) {
19543
20155
  }
19544
20156
  function ruleIdsOf(rulesJson) {
19545
20157
  const ids = /* @__PURE__ */ new Set();
19546
- let raw;
19547
- try {
19548
- raw = JSON.parse(rulesJson);
19549
- } catch {
19550
- return ids;
19551
- }
20158
+ const raw = safeJson(rulesJson, []);
19552
20159
  if (!Array.isArray(raw)) return ids;
19553
20160
  for (const entry of raw) {
19554
20161
  if (entry && typeof entry === "object") {
@@ -19621,47 +20228,48 @@ var SqliteInstalledPacksRepository = class {
19621
20228
  }));
19622
20229
  if (this.storedSignature() === inventorySignature(rows)) return;
19623
20230
  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,
20231
+ withTransaction(
20232
+ this.db,
20233
+ () => {
20234
+ const mirror = this.mirrorState();
20235
+ let behind = false;
20236
+ for (const row of rows) {
20237
+ const params = {
19642
20238
  id: randomUUID2(),
19643
- recordedBy: meta3?.recordedBy ?? null
19644
- });
19645
- } else {
19646
- behind = true;
20239
+ namespace: row.namespace,
20240
+ packId: row.packId,
20241
+ version: row.version,
20242
+ name: row.name,
20243
+ rulesJson: row.rulesJson,
20244
+ now
20245
+ };
20246
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
20247
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20248
+ this.upsertAvailableStmt.run({
20249
+ ...params,
20250
+ id: randomUUID2(),
20251
+ recordedBy: meta3?.recordedBy ?? null
20252
+ });
20253
+ } else {
20254
+ behind = true;
20255
+ }
20256
+ this.insertMissingStmt.run(params);
19647
20257
  }
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
- }
20258
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20259
+ },
20260
+ "IMMEDIATE"
20261
+ );
19656
20262
  } catch {
19657
20263
  }
19658
20264
  }
19659
20265
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
19660
20266
  // input to the downgrade guard. Read INSIDE the write transaction.
19661
20267
  mirrorState() {
19662
- const rows = this.db.prepare(
19663
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
19664
- ).all();
20268
+ const rows = allRows(
20269
+ this.db.prepare(
20270
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20271
+ )
20272
+ );
19665
20273
  return new Map(
19666
20274
  rows.map((r) => [
19667
20275
  `${r.namespace}/${r.packId}`,
@@ -19673,7 +20281,9 @@ var SqliteInstalledPacksRepository = class {
19673
20281
  // (keys joined with '/', matching the detection id slug encoding — packId may
19674
20282
  // itself contain '/', but namespace may not, so the join is unambiguous).
19675
20283
  pruneAvailable(keep) {
19676
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20284
+ const rows = allRows(
20285
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20286
+ );
19677
20287
  const keepSet = new Set(keep);
19678
20288
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
19679
20289
  for (const r of rows) {
@@ -19700,11 +20310,13 @@ var SqliteInstalledPacksRepository = class {
19700
20310
  if (this.db.isTransaction) {
19701
20311
  throw new Error("applyUpdate must not be called inside an open transaction");
19702
20312
  }
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
20313
+ let changed = false;
20314
+ withTransaction(
20315
+ this.db,
20316
+ () => {
20317
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20318
+ const res = this.db.prepare(
20319
+ `UPDATE installed_packs SET
19708
20320
  version = (SELECT a.version FROM available_packs a
19709
20321
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
19710
20322
  name = (SELECT a.name FROM available_packs a
@@ -19715,17 +20327,13 @@ var SqliteInstalledPacksRepository = class {
19715
20327
  WHERE namespace = :namespace AND pack_id = :packId
19716
20328
  AND EXISTS (SELECT 1 FROM available_packs a
19717
20329
  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
- }
20330
+ ).run({ namespace, packId, now: Date.now() });
20331
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20332
+ changed = Number(res.changes) > 0;
20333
+ },
20334
+ "IMMEDIATE"
20335
+ );
20336
+ return changed;
19729
20337
  }
19730
20338
  /**
19731
20339
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -19739,9 +20347,11 @@ var SqliteInstalledPacksRepository = class {
19739
20347
  * JSON-level failure therefore counts as invalid.
19740
20348
  */
19741
20349
  installedRuleset() {
19742
- const rows = this.db.prepare(
19743
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
19744
- ).all();
20350
+ const rows = allRows(
20351
+ this.db.prepare(
20352
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20353
+ )
20354
+ );
19745
20355
  const out = {
19746
20356
  installedPacks: rows.length,
19747
20357
  enabledPacks: 0,
@@ -19750,7 +20360,7 @@ var SqliteInstalledPacksRepository = class {
19750
20360
  ruleActions: /* @__PURE__ */ new Map()
19751
20361
  };
19752
20362
  for (const row of rows) {
19753
- if (row.enabled !== 1) continue;
20363
+ if (!intToBool(row.enabled)) continue;
19754
20364
  out.enabledPacks += 1;
19755
20365
  const action = policyIdToAction(row.policyId);
19756
20366
  let raw;
@@ -19785,9 +20395,11 @@ var SqliteInstalledPacksRepository = class {
19785
20395
  * running max would mask a genuinely-newer parseable stamp.
19786
20396
  */
19787
20397
  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();
20398
+ const rows = allRows(
20399
+ this.db.prepare(
20400
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20401
+ )
20402
+ );
19791
20403
  let newest = null;
19792
20404
  for (const row of rows) {
19793
20405
  const at = row.recordedBy.lastIndexOf("@");
@@ -19802,13 +20414,15 @@ var SqliteInstalledPacksRepository = class {
19802
20414
  return newest;
19803
20415
  }
19804
20416
  counts() {
19805
- const row = this.db.prepare(
19806
- `SELECT count(*) AS packs,
20417
+ const row = getRow(
20418
+ this.db.prepare(
20419
+ `SELECT count(*) AS packs,
19807
20420
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
19808
20421
  coalesce(sum(enabled), 0) AS enabled
19809
20422
  FROM installed_packs`
19810
- ).get();
19811
- return Promise.resolve(row);
20423
+ )
20424
+ );
20425
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
19812
20426
  }
19813
20427
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
19814
20428
  // Back the Policies page's built-in catalog: how many
@@ -19821,26 +20435,29 @@ var SqliteInstalledPacksRepository = class {
19821
20435
  * attributed to Monitor, matching the Detections views.
19822
20436
  */
19823
20437
  countsByPolicyId() {
19824
- const rows = this.db.prepare(
19825
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20438
+ return countBy(
20439
+ this.db,
20440
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
19826
20441
  FROM installed_packs
19827
- GROUP BY pid`
19828
- ).all();
19829
- return new Map(rows.map((r) => [r.pid, r.n]));
20442
+ GROUP BY k`
20443
+ );
19830
20444
  }
19831
20445
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
19832
20446
  listByPolicyId(policyId) {
19833
- const rows = this.db.prepare(
19834
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20447
+ const rows = allRows(
20448
+ this.db.prepare(
20449
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
19835
20450
  FROM installed_packs
19836
20451
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
19837
20452
  ORDER BY name ASC`
19838
- ).all(policyId);
20453
+ ),
20454
+ [policyId]
20455
+ );
19839
20456
  return rows.map((r) => ({
19840
20457
  id: `${r.namespace}/${r.packId}`,
19841
20458
  name: r.name,
19842
20459
  ruleCount: parseRules(r.rulesJson).length,
19843
- enabled: r.enabled === 1
20460
+ enabled: intToBool(r.enabled)
19844
20461
  }));
19845
20462
  }
19846
20463
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -19869,14 +20486,14 @@ var SqliteInstalledPacksRepository = class {
19869
20486
  const res = this.db.prepare(
19870
20487
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
19871
20488
  WHERE namespace = :namespace AND pack_id = :packId`
19872
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
20489
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
19873
20490
  return Number(res.changes) > 0;
19874
20491
  }
19875
20492
  // Fingerprint of the recorded available mirror — compared against the
19876
20493
  // incoming inventory's signature to skip the write entirely when the running
19877
20494
  // binary's inventory hasn't changed since the last record.
19878
20495
  storedSignature() {
19879
- const rows = this.signatureStmt.all();
20496
+ const rows = allRows(this.signatureStmt);
19880
20497
  return inventorySignature(rows);
19881
20498
  }
19882
20499
  };
@@ -19904,42 +20521,48 @@ var SqliteInventoryRepository = class {
19904
20521
  upsert(input, now = Date.now()) {
19905
20522
  const id = inventoryId(input.objectType, input.identityKey);
19906
20523
  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
- });
20524
+ this.upsertStmt.run(
20525
+ bindParams({
20526
+ id: row.id,
20527
+ objectType: row.objectType,
20528
+ location: row.location,
20529
+ title: row.title,
20530
+ hostId: row.hostId,
20531
+ attributes: row.attributes,
20532
+ firstSeen: row.firstSeen,
20533
+ lastSeen: row.lastSeen
20534
+ })
20535
+ );
19917
20536
  return id;
19918
20537
  }
19919
20538
  // The full row, for round-trip assertions.
19920
20539
  findById(id) {
19921
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
19922
- return row;
20540
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
19923
20541
  }
19924
20542
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
19925
20543
  // served from the object_type index, never from audit_events.
19926
20544
  distinctTitles(objectType) {
19927
- const rows = this.db.prepare(
19928
- `SELECT DISTINCT title FROM inventory
20545
+ const rows = allRows(
20546
+ this.db.prepare(
20547
+ `SELECT DISTINCT title FROM inventory
19929
20548
  WHERE object_type = :objectType AND title IS NOT NULL
19930
20549
  ORDER BY title`
19931
- ).all({ objectType });
20550
+ ),
20551
+ { objectType }
20552
+ );
19932
20553
  return rows.map((r) => r.title);
19933
20554
  }
19934
20555
  // Distinct host os_version values — a facet served from an inventory index
19935
20556
  // over the generated column, never from the audit fact (confirm via EXPLAIN
19936
20557
  // QUERY PLAN).
19937
20558
  osVersions() {
19938
- const rows = this.db.prepare(
19939
- `SELECT DISTINCT os_version AS value FROM inventory
20559
+ const rows = allRows(
20560
+ this.db.prepare(
20561
+ `SELECT DISTINCT os_version AS value FROM inventory
19940
20562
  WHERE object_type = 'host' AND os_version IS NOT NULL
19941
20563
  ORDER BY value`
19942
- ).all();
20564
+ )
20565
+ );
19943
20566
  return rows.map((r) => r.value);
19944
20567
  }
19945
20568
  };
@@ -19959,14 +20582,6 @@ var EMPTY_PROJECT_AGG = {
19959
20582
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
19960
20583
  findingsCount: 0
19961
20584
  };
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
20585
  function resolveHarnessId(attrs, row) {
19971
20586
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
19972
20587
  return attrs.provider;
@@ -20162,30 +20777,49 @@ var SqliteInventoryAssetsRepository = class {
20162
20777
  configRowsCache;
20163
20778
  // ─── stats ─────────────────────────────────────────────────────────────────
20164
20779
  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
20780
+ const typeCounts = countBy(
20781
+ this.db,
20782
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
20783
+ );
20784
+ const byType = {
20785
+ project: 0,
20786
+ skill: typeCounts.get("skill") ?? 0,
20787
+ mcp: typeCounts.get("mcp") ?? 0,
20788
+ hook: typeCounts.get("hook") ?? 0,
20789
+ config: typeCounts.get("config") ?? 0
20790
+ };
20791
+ byType.project = countScalar(
20792
+ this.db,
20793
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
20794
+ );
20795
+ const mcpTrustCounts = countBy(
20796
+ this.db,
20797
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20173
20798
  FROM inventory_asset a
20174
20799
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20175
20800
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20176
20801
  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(
20802
+ );
20803
+ const mcpTrust = {
20804
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
20805
+ risky: mcpTrustCounts.get("risky") ?? 0,
20806
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
20807
+ };
20808
+ const harnesses = countScalar(
20809
+ this.db,
20181
20810
  `SELECT count(*) AS n FROM inventory
20182
20811
  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(
20812
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
20813
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20814
+ );
20815
+ const flaggedAssets = countScalar(
20816
+ this.db,
20817
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
20818
+ );
20819
+ const flaggedProjects = countScalar(
20820
+ this.db,
20187
20821
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20188
- ).get().n;
20822
+ );
20189
20823
  const configRows = this.configAssetRows();
20190
20824
  for (const r of configRows) {
20191
20825
  byType[r.assetType] += 1;
@@ -20442,12 +21076,15 @@ var SqliteInventoryAssetsRepository = class {
20442
21076
  }
20443
21077
  // ─── raw fetchers ────────────────────────────────────────────────────────────
20444
21078
  fetchHarnessRows() {
20445
- return this.db.prepare(
20446
- `SELECT id, title, attributes, harness_version AS harnessVersion
21079
+ return allRows(
21080
+ this.db.prepare(
21081
+ `SELECT id, title, attributes, harness_version AS harnessVersion
20447
21082
  FROM inventory
20448
21083
  WHERE object_type = 'harness'
20449
21084
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20450
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
21085
+ ),
21086
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
21087
+ );
20451
21088
  }
20452
21089
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
20453
21090
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -20457,12 +21094,13 @@ var SqliteInventoryAssetsRepository = class {
20457
21094
  const params = [...harnessInvIds];
20458
21095
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
20459
21096
  if (q) {
20460
- const pat = `%${escapeLikePattern(q)}%`;
20461
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
21097
+ const pat = containsPattern(q);
21098
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
20462
21099
  params.push(pat, pat);
20463
21100
  }
20464
- const rows = this.db.prepare(
20465
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
21101
+ const rows = allRows(
21102
+ this.db.prepare(
21103
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20466
21104
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20467
21105
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20468
21106
  FROM harness_asset ha
@@ -20470,7 +21108,9 @@ var SqliteInventoryAssetsRepository = class {
20470
21108
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20471
21109
  WHERE ${where}
20472
21110
  ORDER BY a.name ASC`
20473
- ).all(...params);
21111
+ ),
21112
+ params
21113
+ );
20474
21114
  for (const raw of rows) {
20475
21115
  const harnessInvId = raw.harnessInvId;
20476
21116
  const [asset] = this.mapAssetRows([raw]);
@@ -20489,21 +21129,24 @@ var SqliteInventoryAssetsRepository = class {
20489
21129
  params.push(...types);
20490
21130
  }
20491
21131
  if (q) {
20492
- const pat = `%${escapeLikePattern(q)}%`;
20493
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
21132
+ const pat = containsPattern(q);
21133
+ conditions.push(likeAny(["a.name", "a.sub"]));
20494
21134
  params.push(pat, pat);
20495
21135
  }
20496
21136
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
20497
21137
  const sampleRows = this.mapAssetRows(
20498
- this.db.prepare(
20499
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21138
+ allRows(
21139
+ this.db.prepare(
21140
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20500
21141
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20501
21142
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20502
21143
  FROM inventory_asset a
20503
21144
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20504
21145
  ${where}
20505
21146
  ORDER BY a.name ASC`
20506
- ).all(...params)
21147
+ ),
21148
+ params
21149
+ )
20507
21150
  );
20508
21151
  const configRows = this.configAssetRows(q).filter(
20509
21152
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -20512,14 +21155,17 @@ var SqliteInventoryAssetsRepository = class {
20512
21155
  }
20513
21156
  fetchAssetById(assetId) {
20514
21157
  const rows = this.mapAssetRows(
20515
- this.db.prepare(
20516
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21158
+ allRows(
21159
+ this.db.prepare(
21160
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20517
21161
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20518
21162
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20519
21163
  FROM inventory_asset a
20520
21164
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20521
21165
  WHERE a.id = ?`
20522
- ).all(assetId)
21166
+ ),
21167
+ [assetId]
21168
+ )
20523
21169
  );
20524
21170
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
20525
21171
  }
@@ -20575,37 +21221,39 @@ var SqliteInventoryAssetsRepository = class {
20575
21221
  return rows;
20576
21222
  }
20577
21223
  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;
21224
+ return latestConfigScan(this.db)?.id ?? null;
20583
21225
  }
20584
21226
  fetchProjects(q) {
20585
21227
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
20586
21228
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
20587
21229
  const params = [];
20588
21230
  if (q) {
20589
- const pat = `%${escapeLikePattern(q)}%`;
20590
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
21231
+ const pat = containsPattern(q);
21232
+ sql += ` AND ${likeAny(["name", "url"])}`;
20591
21233
  params.push(pat, pat);
20592
21234
  }
20593
21235
  sql += " ORDER BY name ASC";
20594
- return this.db.prepare(sql).all(...params);
21236
+ return allRows(this.db.prepare(sql), params);
20595
21237
  }
20596
21238
  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;
21239
+ return getRow(
21240
+ this.db.prepare(
21241
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
21242
+ ),
21243
+ [projectId]
21244
+ ) ?? null;
20600
21245
  }
20601
21246
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
20602
21247
  fetchProjectsByIds(projectIds) {
20603
21248
  const map2 = /* @__PURE__ */ new Map();
20604
21249
  if (projectIds.length === 0) return map2;
20605
- const rows = this.db.prepare(
20606
- `SELECT id, url, name, attributes, last_seen AS lastSeen
21250
+ const rows = allRows(
21251
+ this.db.prepare(
21252
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
20607
21253
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
20608
- ).all(...projectIds);
21254
+ ),
21255
+ projectIds
21256
+ );
20609
21257
  for (const r of rows) map2.set(r.id, r);
20610
21258
  return map2;
20611
21259
  }
@@ -20616,8 +21264,9 @@ var SqliteInventoryAssetsRepository = class {
20616
21264
  projectAggregates(projectIds) {
20617
21265
  const map2 = /* @__PURE__ */ new Map();
20618
21266
  if (projectIds.length === 0) return map2;
20619
- const rows = this.db.prepare(
20620
- `SELECT f.project_id AS projectId,
21267
+ const rows = allRows(
21268
+ this.db.prepare(
21269
+ `SELECT f.project_id AS projectId,
20621
21270
  coalesce(o.access, f.default_access) AS eff,
20622
21271
  count(*) AS n,
20623
21272
  coalesce(sum(f.findings_count), 0) AS findings
@@ -20625,7 +21274,9 @@ var SqliteInventoryAssetsRepository = class {
20625
21274
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
20626
21275
  WHERE f.project_id IN (${placeholders(projectIds.length)})
20627
21276
  GROUP BY f.project_id, eff`
20628
- ).all(...projectIds);
21277
+ ),
21278
+ projectIds
21279
+ );
20629
21280
  for (const r of rows) {
20630
21281
  let agg = map2.get(r.projectId);
20631
21282
  if (!agg) {
@@ -20663,37 +21314,52 @@ var SqliteInventoryAssetsRepository = class {
20663
21314
  fetchProjectFilesUnder(projectId, prefix) {
20664
21315
  if (prefix === "") {
20665
21316
  return this.mapFileRows(
20666
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21317
+ allRows(
21318
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21319
+ [projectId]
21320
+ )
20667
21321
  );
20668
21322
  }
20669
21323
  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)}/%`)
21324
+ allRows(
21325
+ this.db.prepare(
21326
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21327
+ ),
21328
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21329
+ )
20673
21330
  );
20674
21331
  }
20675
21332
  fetchProjectFilesSearch(projectId, q) {
20676
- const pat = `%${escapeLikePattern(q)}%`;
21333
+ const pat = containsPattern(q);
20677
21334
  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)
21335
+ allRows(
21336
+ this.db.prepare(
21337
+ this.fileSelect(
21338
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21339
+ )
21340
+ ),
21341
+ [projectId, pat, pat]
21342
+ )
20683
21343
  );
20684
21344
  }
20685
21345
  fetchProjectFilesBlocked(projectId) {
20686
21346
  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)
21347
+ allRows(
21348
+ this.db.prepare(
21349
+ this.fileSelect(
21350
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21351
+ )
21352
+ ),
21353
+ [projectId]
21354
+ )
20692
21355
  );
20693
21356
  }
20694
21357
  fetchProjectFile(projectId, path) {
20695
21358
  const rows = this.mapFileRows(
20696
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21359
+ allRows(
21360
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21361
+ [projectId, path]
21362
+ )
20697
21363
  );
20698
21364
  return rows[0] ?? null;
20699
21365
  }
@@ -20707,39 +21373,32 @@ var SqlitePoliciesRepository = class {
20707
21373
  }
20708
21374
  db;
20709
21375
  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
- }
21376
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21377
+ const policies = mapRowsTolerant(rows, (row) => {
21378
+ const target = JSON.parse(row.target);
21379
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21380
+ return Policy.parse({
21381
+ id: row.id,
21382
+ scope: row.scope,
21383
+ target,
21384
+ action: row.action,
21385
+ enabled: intToBool(row.enabled),
21386
+ customKeywords
21387
+ });
21388
+ });
20729
21389
  return Promise.resolve(policies);
20730
21390
  }
20731
21391
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
20732
21392
  // detection-type config exists from first run. Only when the table is empty,
20733
21393
  // so a user's edits are never clobbered.
20734
21394
  seedDefaults() {
20735
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21395
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
20736
21396
  if (count > 0) return;
20737
21397
  const stmt = this.db.prepare(
20738
21398
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
20739
21399
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
20740
21400
  );
20741
- this.db.exec("BEGIN");
20742
- try {
21401
+ failOpenTransaction(this.db, () => {
20743
21402
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
20744
21403
  stmt.run({
20745
21404
  id: randomUUID4(),
@@ -20748,10 +21407,41 @@ var SqlitePoliciesRepository = class {
20748
21407
  now: Date.now()
20749
21408
  });
20750
21409
  }
20751
- this.db.exec("COMMIT");
20752
- } catch {
20753
- this.db.exec("ROLLBACK");
20754
- }
21410
+ });
21411
+ }
21412
+ // Insert-or-update the single global per-category policy row, keyed on the
21413
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21414
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21415
+ // values), so the runtime's resolveAction reads rows written by either path
21416
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21417
+ // `id` and `created_at` are left exactly as they were.
21418
+ upsertCategoryAction(category, action) {
21419
+ const now = Date.now();
21420
+ this.db.prepare(
21421
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21422
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21423
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21424
+ ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
21425
+ }
21426
+ // Caps every global per-category policy currently set to block/redact down
21427
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21428
+ // Returns the number of rows changed.
21429
+ capCategoryActions() {
21430
+ const info = this.db.prepare(
21431
+ `UPDATE policies SET action='warn', updated_at=:now
21432
+ WHERE scope='global' AND action IN ('block','redact')
21433
+ AND json_extract(target,'$.category') IS NOT NULL`
21434
+ ).run({ now: Date.now() });
21435
+ return Number(info.changes);
21436
+ }
21437
+ // Read the current action for a single global per-category policy row, mirroring
21438
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21439
+ // row exists yet, so callers can distinguish an unset category from a set one.
21440
+ getCategoryAction(category) {
21441
+ const row = this.db.prepare(
21442
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21443
+ ).get({ category });
21444
+ return row?.action;
20755
21445
  }
20756
21446
  };
20757
21447
 
@@ -20827,7 +21517,7 @@ var SqliteProjectFilesRepository = class {
20827
21517
  maxStampStmt;
20828
21518
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
20829
21519
  replaceForProject(projectId, scan2, now) {
20830
- const { maxStamp } = this.maxStampStmt.get({ projectId });
21520
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
20831
21521
  const stamp = Math.max(now, maxStamp + 1);
20832
21522
  for (const file2 of scan2.files) {
20833
21523
  this.upsertStmt.run({
@@ -20910,7 +21600,7 @@ var SqliteResolutionsRepository = class {
20910
21600
  }
20911
21601
  /** The newest disposition recorded for a finding key, or undefined if none. */
20912
21602
  latestByKey(key) {
20913
- const row = this.latestStmt.get({ findingKey: key });
21603
+ const row = getRow(this.latestStmt, { findingKey: key });
20914
21604
  if (!row) return void 0;
20915
21605
  return {
20916
21606
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -20927,7 +21617,7 @@ var SqliteResolutionsRepository = class {
20927
21617
  * the CLI) surfaces for that file.
20928
21618
  */
20929
21619
  openAtRestKeysForPath(path) {
20930
- const rows = this.openAtRestStmt.all({ path });
21620
+ const rows = allRows(this.openAtRestStmt, { path });
20931
21621
  return rows.map((r) => r.finding_key);
20932
21622
  }
20933
21623
  /**
@@ -20938,7 +21628,7 @@ var SqliteResolutionsRepository = class {
20938
21628
  * resolution row (see scan.ts).
20939
21629
  */
20940
21630
  resolvedAtRestKeysForPath(path) {
20941
- const rows = this.resolvedAtRestStmt.all({ path });
21631
+ const rows = allRows(this.resolvedAtRestStmt, { path });
20942
21632
  return rows.map((r) => r.finding_key);
20943
21633
  }
20944
21634
  };
@@ -20967,38 +21657,31 @@ var SqliteScanLedgerRepository = class {
20967
21657
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
20968
21658
  // older ruleset are simply absent, which reads as "never scanned".
20969
21659
  entriesForRuleset(rulesetHash) {
20970
- const rows = this.readStmt.all({ rulesetHash });
21660
+ const rows = allRows(this.readStmt, {
21661
+ rulesetHash
21662
+ });
20971
21663
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
20972
21664
  }
20973
21665
  upsertEntries(entries) {
20974
21666
  if (entries.length === 0) return;
20975
21667
  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;
21668
+ failOpenTransaction(this.db, () => {
21669
+ for (const entry of entries) {
21670
+ this.upsertStmt.run({
21671
+ path: entry.path,
21672
+ mtime: entry.mtime,
21673
+ contentHash: entry.contentHash,
21674
+ rulesetHash: entry.rulesetHash,
21675
+ scannedAt
21676
+ });
20992
21677
  }
20993
- } catch {
20994
- }
21678
+ });
20995
21679
  }
20996
21680
  };
20997
21681
 
20998
21682
  // ../../packages/persistence/src/repositories/security.ts
20999
21683
  var DAY_MS4 = 864e5;
21000
21684
  var SEVERITIES = ["critical", "high", "medium", "low"];
21001
- var RANGE_DAYS = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
21002
21685
  var ACTION_TO_KIND = {
21003
21686
  block: "blocked",
21004
21687
  redact: "redacted",
@@ -21013,8 +21696,14 @@ var SCAN_COVERAGE = [
21013
21696
  { provider: "copilot", coverage: 0, supported: false },
21014
21697
  { provider: "api", coverage: 0, supported: false }
21015
21698
  ];
21699
+ var GRANULARITY = {
21700
+ "7d": "day",
21701
+ "30d": "day",
21702
+ "3m": "week",
21703
+ "6m": "week"
21704
+ };
21016
21705
  function granularityFor(range) {
21017
- return range === "7d" || range === "30d" ? "day" : "week";
21706
+ return GRANULARITY[range];
21018
21707
  }
21019
21708
  function startOfUtcDay2(ms) {
21020
21709
  return Math.floor(ms / DAY_MS4) * DAY_MS4;
@@ -21068,8 +21757,9 @@ var SqliteSecurityRepository = class {
21068
21757
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21069
21758
  // double-counting a key that accumulated several append-only rows.
21070
21759
  severitySummary() {
21071
- const rows = this.db.prepare(
21072
- `SELECT f.severity AS severity,
21760
+ const rows = allRows(
21761
+ this.db.prepare(
21762
+ `SELECT f.severity AS severity,
21073
21763
  COUNT(*) AS count,
21074
21764
  SUM(CASE
21075
21765
  WHEN e.kind != 'code_change' THEN 1
@@ -21088,7 +21778,8 @@ var SqliteSecurityRepository = class {
21088
21778
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21089
21779
  ON latest.finding_key = f.finding_key
21090
21780
  GROUP BY f.severity`
21091
- ).all();
21781
+ )
21782
+ );
21092
21783
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21093
21784
  const bySeverity = SEVERITIES.map((severity) => ({
21094
21785
  severity,
@@ -21172,14 +21863,15 @@ var SqliteSecurityRepository = class {
21172
21863
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21173
21864
  const now = this.now();
21174
21865
  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,
21866
+ const rows = allRows(
21867
+ this.db.prepare(
21868
+ // first_detected_at is the PRESERVED first-detection time (set once on a
21869
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21870
+ // measures from first sighting — not the latest re-scan's event, whose
21871
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21872
+ // the parent event's occurred_at defends against any legacy/edge row the
21873
+ // backfill left null.
21874
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21183
21875
  (
21184
21876
  SELECT fr.status FROM finding_resolution fr
21185
21877
  WHERE fr.finding_key = f.finding_key
@@ -21205,14 +21897,16 @@ var SqliteSecurityRepository = class {
21205
21897
  WHERE fr.finding_key = f.finding_key
21206
21898
  AND fr.resolved_at >= :windowStart
21207
21899
  )`
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 });
21900
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21901
+ // any resolution activity at/after the window start — a row this method
21902
+ // ultimately counts has its LATEST resolution inside the window, which
21903
+ // implies such a row exists, so nothing wanted is dropped. The exact
21904
+ // latest-wins + status/method + window gate stays in JS below,
21905
+ // dialect-agnostic. Without this, a
21906
+ // 7d request evaluated the store's entire trackable-findings history.
21907
+ ),
21908
+ { windowStart }
21909
+ );
21216
21910
  const sums = /* @__PURE__ */ new Map();
21217
21911
  const counts = /* @__PURE__ */ new Map();
21218
21912
  for (const r of rows) {
@@ -21242,8 +21936,9 @@ var SqliteSecurityRepository = class {
21242
21936
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
21243
21937
  const now = this.now();
21244
21938
  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
21939
+ const rows = allRows(
21940
+ this.db.prepare(
21941
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21247
21942
  FROM findings f JOIN events e ON e.id = f.event_id
21248
21943
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21249
21944
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -21251,7 +21946,9 @@ var SqliteSecurityRepository = class {
21251
21946
  GROUP BY repo
21252
21947
  ORDER BY c DESC, repo
21253
21948
  LIMIT :limit`
21254
- ).all({ from, to: now, limit });
21949
+ ),
21950
+ { from, to: now, limit }
21951
+ );
21255
21952
  const items = rows.map((r) => ({
21256
21953
  id: `repo_${r.repo}`,
21257
21954
  name: r.repo,
@@ -21273,8 +21970,9 @@ var SqliteSecurityRepository = class {
21273
21970
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21274
21971
  // capped at `limit`.
21275
21972
  recentlyResolved(limit = 20) {
21276
- const rows = this.db.prepare(
21277
- `SELECT f.finding_key AS finding_key,
21973
+ const rows = allRows(
21974
+ this.db.prepare(
21975
+ `SELECT f.finding_key AS finding_key,
21278
21976
  f.rule_id AS rule_id,
21279
21977
  f.severity AS severity,
21280
21978
  json_extract(e.metadata, '$.filePath') AS path,
@@ -21308,7 +22006,9 @@ var SqliteSecurityRepository = class {
21308
22006
  ) IS NOT NULL
21309
22007
  ORDER BY latest_resolved_at DESC
21310
22008
  LIMIT :limit`
21311
- ).all({ limit });
22009
+ ),
22010
+ { limit }
22011
+ );
21312
22012
  const items = rows.map((r) => ({
21313
22013
  findingKey: r.finding_key,
21314
22014
  ruleId: r.rule_id,
@@ -21325,12 +22025,15 @@ var SqliteSecurityRepository = class {
21325
22025
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
21326
22026
  // numeric and the JS aggregations bucket/split on ms directly.
21327
22027
  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
22028
+ const rows = allRows(
22029
+ this.db.prepare(
22030
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21330
22031
  FROM findings f JOIN events e ON e.id = f.event_id
21331
22032
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21332
22033
  ORDER BY e.occurred_at`
21333
- ).all({ from: fromMs, to: toMs });
22034
+ ),
22035
+ { from: fromMs, to: toMs }
22036
+ );
21334
22037
  return rows.map((r) => ({
21335
22038
  occurredAt: r.occurred_at,
21336
22039
  severity: r.severity,
@@ -21344,12 +22047,7 @@ import { randomUUID as randomUUID7 } from "crypto";
21344
22047
  var KIND_ORDER = ["provider", "internal", "ip"];
21345
22048
  var CALL_SITE_EMBED_CAP = 200;
21346
22049
  function parseNetwork(networkJson) {
21347
- if (!networkJson) return null;
21348
- try {
21349
- return JSON.parse(networkJson);
21350
- } catch {
21351
- return null;
21352
- }
22050
+ return safeJson(networkJson, null);
21353
22051
  }
21354
22052
  function toEndpointSummary(row) {
21355
22053
  return {
@@ -21436,27 +22134,39 @@ var SqliteSharesRepository = class {
21436
22134
  }
21437
22135
  db;
21438
22136
  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(
22137
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
22138
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
22139
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22140
+ const insecure = countScalar(
22141
+ this.db,
21444
22142
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
21445
22143
  );
21446
- const needsReview = scalar(
22144
+ const needsReview = countScalar(
22145
+ this.db,
21447
22146
  `SELECT count(DISTINCT d.id) AS n
21448
22147
  FROM share_destination d
21449
22148
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
21450
22149
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
21451
22150
  );
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
- }
22151
+ const kindCounts = countBy(
22152
+ this.db,
22153
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
22154
+ );
22155
+ const byKind = {
22156
+ provider: kindCounts.get("provider") ?? 0,
22157
+ internal: kindCounts.get("internal") ?? 0,
22158
+ ip: kindCounts.get("ip") ?? 0
22159
+ };
22160
+ const trustCounts = countBy(
22161
+ this.db,
22162
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
22163
+ );
22164
+ const byTrust = {
22165
+ recognized: trustCounts.get("recognized") ?? 0,
22166
+ internal: trustCounts.get("internal") ?? 0,
22167
+ unverified: trustCounts.get("unverified") ?? 0,
22168
+ ip: trustCounts.get("ip") ?? 0
22169
+ };
21460
22170
  return Promise.resolve({
21461
22171
  destinations,
21462
22172
  endpoints,
@@ -21572,7 +22282,7 @@ var SqliteSharesRepository = class {
21572
22282
  }
21573
22283
  let sql;
21574
22284
  if (q) {
21575
- const pattern = `%${escapeLikePattern(q)}%`;
22285
+ const pattern = containsPattern(q);
21576
22286
  conditions.push(
21577
22287
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
21578
22288
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -21592,24 +22302,31 @@ var SqliteSharesRepository = class {
21592
22302
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
21593
22303
  ORDER BY d.created_at ASC, d.id ASC`;
21594
22304
  }
21595
- const rows = this.db.prepare(sql).all(...params);
22305
+ const rows = allRows(
22306
+ this.db.prepare(sql),
22307
+ params
22308
+ );
21596
22309
  return rows.map((r) => this.mapDestRow(r));
21597
22310
  }
21598
22311
  fetchDestinationById(destinationId) {
21599
- const row = this.db.prepare(
21600
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22312
+ const row = getRow(
22313
+ this.db.prepare(
22314
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
21601
22315
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
21602
22316
  o.decision AS overrideDecision
21603
22317
  FROM share_destination d
21604
22318
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
21605
22319
  WHERE d.id = ?`
21606
- ).get(destinationId);
22320
+ ),
22321
+ [destinationId]
22322
+ );
21607
22323
  return row ? this.mapDestRow(row) : null;
21608
22324
  }
21609
22325
  fetchEndpoints(destinationIds) {
21610
22326
  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,
22327
+ const rows = allRows(
22328
+ this.db.prepare(
22329
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
21613
22330
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
21614
22331
  count(c.id) AS callSiteCount
21615
22332
  FROM share_endpoint e
@@ -21617,7 +22334,9 @@ var SqliteSharesRepository = class {
21617
22334
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
21618
22335
  GROUP BY e.id
21619
22336
  ORDER BY e.created_at ASC, e.id ASC`
21620
- ).all(...destinationIds);
22337
+ ),
22338
+ destinationIds
22339
+ );
21621
22340
  return rows.map((r) => ({
21622
22341
  id: r.id,
21623
22342
  destinationId: r.destinationId,
@@ -21642,13 +22361,16 @@ var SqliteSharesRepository = class {
21642
22361
  }
21643
22362
  fetchCallSites(endpointIds) {
21644
22363
  if (endpointIds.length === 0) return [];
21645
- const rows = this.db.prepare(
21646
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22364
+ const rows = allRows(
22365
+ this.db.prepare(
22366
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
21647
22367
  project_id AS projectId
21648
22368
  FROM share_call_site
21649
22369
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
21650
22370
  ORDER BY created_at ASC, id ASC`
21651
- ).all(...endpointIds);
22371
+ ),
22372
+ endpointIds
22373
+ );
21652
22374
  return rows.map((r) => ({
21653
22375
  id: r.id,
21654
22376
  endpointId: r.endpointId,
@@ -21684,27 +22406,36 @@ var SqliteSourceProjectRepository = class {
21684
22406
  upsert(input, now = Date.now()) {
21685
22407
  const id = sourceProjectId(input.url);
21686
22408
  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
- });
22409
+ this.upsertStmt.run(
22410
+ bindParams({
22411
+ id: row.id,
22412
+ url: row.url,
22413
+ name: row.name,
22414
+ attributes: row.attributes,
22415
+ firstSeen: row.firstSeen,
22416
+ lastSeen: row.lastSeen
22417
+ })
22418
+ );
21695
22419
  return id;
21696
22420
  }
21697
22421
  findById(id) {
21698
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22422
+ return getRow(
22423
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22424
+ {
22425
+ id
22426
+ }
22427
+ );
21699
22428
  }
21700
22429
  // Distinct project names — a filter facet, served from the source_project
21701
22430
  // table, never from the audit fact table.
21702
22431
  distinctNames() {
21703
- const rows = this.db.prepare(
21704
- `SELECT DISTINCT name FROM source_project
22432
+ const rows = allRows(
22433
+ this.db.prepare(
22434
+ `SELECT DISTINCT name FROM source_project
21705
22435
  WHERE name IS NOT NULL
21706
22436
  ORDER BY name`
21707
- ).all();
22437
+ )
22438
+ );
21708
22439
  return rows.map((r) => r.name);
21709
22440
  }
21710
22441
  };
@@ -21723,8 +22454,7 @@ function hasLegacySampleRows(db) {
21723
22454
  function purgeSampleData(db) {
21724
22455
  try {
21725
22456
  if (!hasLegacySampleRows(db)) return;
21726
- db.exec("BEGIN");
21727
- try {
22457
+ withTransaction(db, () => {
21728
22458
  db.exec(
21729
22459
  `DELETE FROM share_call_site WHERE endpoint_id IN (
21730
22460
  SELECT e.id FROM share_endpoint e
@@ -21769,11 +22499,7 @@ function purgeSampleData(db) {
21769
22499
  value TEXT NOT NULL
21770
22500
  )`);
21771
22501
  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
- }
22502
+ });
21777
22503
  } catch {
21778
22504
  }
21779
22505
  }
@@ -21792,7 +22518,7 @@ function openWithPragmas(file2) {
21792
22518
  function backupLegacyStore(file2) {
21793
22519
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
21794
22520
  renameSync(file2, backup);
21795
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
22521
+ for (const sidecar of walSidecars(file2)) {
21796
22522
  if (existsSync(sidecar)) rmSync(sidecar);
21797
22523
  }
21798
22524
  return backup;
@@ -21805,9 +22531,8 @@ function openLocalDatabase(dir) {
21805
22531
  db.close();
21806
22532
  const backup = backupLegacyStore(file2);
21807
22533
  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
- `
22534
+ akaWarn(
22535
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
21811
22536
  );
21812
22537
  }
21813
22538
  applyMigrations(db);
@@ -21835,96 +22560,77 @@ function openLocalDatabase(dir) {
21835
22560
  const configInventory = new SqliteConfigInventoryRepository(db);
21836
22561
  policies.seedDefaults();
21837
22562
  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
- }
22563
+ failOpenTransaction(db, () => {
22564
+ events.insertEvent(event);
22565
+ const sessionId = event.metadata?.sessionId;
22566
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
22567
+ });
21851
22568
  }
21852
22569
  function ensureInventory(ctx) {
21853
22570
  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;
22571
+ const committed = failOpenTransaction(db, () => {
22572
+ const now = Date.now();
22573
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22574
+ if (ctx.harness) {
22575
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
22576
+ }
22577
+ resolved.accountId = inventory.upsert(
22578
+ linkHost(
22579
+ {
22580
+ objectType: "user",
22581
+ identityKey: "local",
22582
+ attributes: { source: "local" }
22583
+ },
22584
+ resolved.hostId
22585
+ ),
22586
+ now
22587
+ );
22588
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22589
+ });
22590
+ return committed ? resolved : {};
21883
22591
  }
21884
22592
  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;
22593
+ failOpenTransaction(db, () => {
22594
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
22595
+ for (const item of record2.items) inventory.upsert(item, now);
22596
+ auditEvents.insertAuditEvent(record2.scanEvent);
22597
+ const definitionIds = /* @__PURE__ */ new Map();
22598
+ for (const def of record2.definitions ?? []) {
22599
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22600
+ }
22601
+ for (const finding of record2.findings ?? []) {
22602
+ const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
22603
+ if (!definitionId) continue;
22604
+ inspectionFindings.insertFinding({
22605
+ id: randomUUID8(),
22606
+ auditEventId: record2.scanEvent.id,
22607
+ inspectionDefinitionId: definitionId,
22608
+ span: finding.span,
22609
+ maskedMatch: finding.maskedMatch,
22610
+ actionTaken: finding.actionTaken,
22611
+ confidence: finding.confidence
22612
+ });
21912
22613
  }
21913
- } catch {
21914
- }
22614
+ });
21915
22615
  }
21916
22616
  function recordProjectFiles(projectId, scan2) {
21917
22617
  if (scan2.files.length === 0) return;
22618
+ failOpenTransaction(db, () => {
22619
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
22620
+ });
22621
+ }
22622
+ async function transaction(fn) {
22623
+ db.exec("BEGIN");
21918
22624
  try {
21919
- db.exec("BEGIN");
22625
+ const result = await fn();
22626
+ db.exec("COMMIT");
22627
+ return result;
22628
+ } catch (err) {
21920
22629
  try {
21921
- projectFiles.replaceForProject(projectId, scan2, Date.now());
21922
- db.exec("COMMIT");
21923
- } catch (err) {
21924
22630
  db.exec("ROLLBACK");
21925
- throw err;
22631
+ } catch {
21926
22632
  }
21927
- } catch {
22633
+ throw err;
21928
22634
  }
21929
22635
  }
21930
22636
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -21943,8 +22649,7 @@ function openLocalDatabase(dir) {
21943
22649
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
21944
22650
  });
21945
22651
  if (stale.length === 0) return;
21946
- db.exec("BEGIN");
21947
- try {
22652
+ withTransaction(db, () => {
21948
22653
  for (const { id } of stale) {
21949
22654
  db.prepare(
21950
22655
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -21956,11 +22661,7 @@ function openLocalDatabase(dir) {
21956
22661
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
21957
22662
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
21958
22663
  }
21959
- db.exec("COMMIT");
21960
- } catch (err) {
21961
- db.exec("ROLLBACK");
21962
- throw err;
21963
- }
22664
+ });
21964
22665
  } catch {
21965
22666
  }
21966
22667
  }
@@ -22002,6 +22703,7 @@ function openLocalDatabase(dir) {
22002
22703
  purgeSampleData: () => {
22003
22704
  purgeSampleData(db);
22004
22705
  },
22706
+ transaction,
22005
22707
  close: () => {
22006
22708
  db.close();
22007
22709
  }
@@ -22094,12 +22796,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
22094
22796
  }
22095
22797
  }
22096
22798
  function readJson(file2) {
22799
+ let text;
22097
22800
  try {
22098
- const parsed = JSON.parse(readFileSync2(file2, "utf8"));
22099
- return typeof parsed === "object" && parsed !== null ? parsed : null;
22801
+ text = readFileSync2(file2, "utf8");
22100
22802
  } catch {
22101
22803
  return null;
22102
22804
  }
22805
+ return parseJsonObject(text) ?? null;
22806
+ }
22807
+
22808
+ // ../../packages/persistence/src/warn-era-cap.ts
22809
+ import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
22810
+ import { join as join5 } from "path";
22811
+ var MARKER = "warn-era-capped";
22812
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22813
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
22814
+ const marker = join5(dataDir2, MARKER);
22815
+ if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22816
+ const capped = db.policies.capCategoryActions();
22817
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
22818
+ `, { mode: DATA_FILE_MODE });
22819
+ return { capped };
22103
22820
  }
22104
22821
 
22105
22822
  // ../../packages/plugin-sdk/src/provider-env.ts
@@ -22184,23 +22901,30 @@ function resolveProviderSafe() {
22184
22901
  // ../../packages/plugin-sdk/src/config-inventory.ts
22185
22902
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22186
22903
  import { homedir as homedir2 } from "os";
22187
- import { basename as basename2, join as join6 } from "path";
22904
+ import { basename as basename2, join as join7 } from "path";
22905
+
22906
+ // ../../packages/detections/src/escape-regexp.ts
22907
+ function escapeRegExp(value) {
22908
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22909
+ }
22910
+
22911
+ // ../../packages/detections/src/matchers/limits.ts
22912
+ var MAX_MATCHES_PER_RULE = 1e4;
22188
22913
 
22189
22914
  // ../../packages/detections/src/matchers/keyword.ts
22190
22915
  var KeywordMatcher2 = class {
22191
22916
  match(text, rule) {
22192
22917
  if (rule.matcher.type !== "keyword") return [];
22193
22918
  const { keywords, caseSensitive } = rule.matcher;
22194
- const haystack = caseSensitive ? text : text.toLowerCase();
22195
22919
  const spans = [];
22196
22920
  for (const kw of keywords) {
22197
- const needle = caseSensitive ? kw : kw.toLowerCase();
22198
- let idx = 0;
22199
- while (idx < haystack.length) {
22200
- const pos = haystack.indexOf(needle, idx);
22201
- if (pos === -1) break;
22202
- spans.push({ start: pos, end: pos + kw.length });
22203
- idx = pos + 1;
22921
+ if (kw.length === 0) continue;
22922
+ if (spans.length >= MAX_MATCHES_PER_RULE) break;
22923
+ const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
22924
+ let m;
22925
+ while ((m = re.exec(text)) !== null) {
22926
+ spans.push({ start: m.index, end: m.index + m[0].length });
22927
+ if (spans.length >= MAX_MATCHES_PER_RULE) break;
22204
22928
  }
22205
22929
  }
22206
22930
  return spans;
@@ -22208,7 +22932,6 @@ var KeywordMatcher2 = class {
22208
22932
  };
22209
22933
 
22210
22934
  // ../../packages/detections/src/matchers/regex.ts
22211
- var MAX_MATCHES_PER_RULE = 1e4;
22212
22935
  var RegexMatcher2 = class {
22213
22936
  match(text, rule) {
22214
22937
  if (rule.matcher.type !== "regex") return [];
@@ -22294,9 +23017,6 @@ function registerPack(pack) {
22294
23017
  function getLoadedRules() {
22295
23018
  return [...packs.values()].flatMap((p) => p.rules);
22296
23019
  }
22297
- function escapeRegExp(value) {
22298
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22299
- }
22300
23020
  function isCorroborated(candidate, candidates, text) {
22301
23021
  const req = candidate.rule.requiresNearby;
22302
23022
  if (!req) return true;
@@ -23078,7 +23798,6 @@ var db_table_name_default = {
23078
23798
  "SELECT * FROM ",
23079
23799
  "SELECT COUNT(*) FROM ",
23080
23800
  "INSERT INTO ",
23081
- "UPDATE ",
23082
23801
  "DELETE FROM ",
23083
23802
  "CREATE TABLE ",
23084
23803
  "ALTER TABLE ",
@@ -24563,8 +25282,8 @@ function scanText(text) {
24563
25282
  }
24564
25283
 
24565
25284
  // ../../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";
25285
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25286
+ import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
24568
25287
  function resolveRepoIdentity(cwd) {
24569
25288
  try {
24570
25289
  const root = findGitRoot(cwd);
@@ -24597,32 +25316,32 @@ function resolveRepoNwo(cwd) {
24597
25316
  function findGitRoot(start) {
24598
25317
  let dir = start;
24599
25318
  for (; ; ) {
24600
- if (existsSync2(join5(dir, ".git"))) return dir;
25319
+ if (existsSync3(join6(dir, ".git"))) return dir;
24601
25320
  const parent = dirname(dir);
24602
25321
  if (parent === dir) return void 0;
24603
25322
  dir = parent;
24604
25323
  }
24605
25324
  }
24606
25325
  function resolveGitContext(root) {
24607
- const dotGit = join5(root, ".git");
25326
+ const dotGit = join6(root, ".git");
24608
25327
  try {
24609
25328
  if (statSync(dotGit).isDirectory()) {
24610
- return { configPath: join5(dotGit, "config"), headRoot: root };
25329
+ return { configPath: join6(dotGit, "config"), headRoot: root };
24611
25330
  }
24612
25331
  } catch {
24613
25332
  return void 0;
24614
25333
  }
24615
25334
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
24616
25335
  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 };
25336
+ const gitdir = isAbsolute(target) ? target : join6(root, target);
25337
+ if (existsSync3(join6(gitdir, "config"))) {
25338
+ return { configPath: join6(gitdir, "config"), headRoot: root };
24620
25339
  }
24621
- const commonRaw = safeRead(join5(gitdir, "commondir"))?.trim();
25340
+ const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
24622
25341
  if (!commonRaw) return void 0;
24623
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join5(gitdir, commonRaw);
25342
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
24624
25343
  const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
24625
- return { configPath: join5(commonGitDir, "config"), headRoot };
25344
+ return { configPath: join6(commonGitDir, "config"), headRoot };
24626
25345
  }
24627
25346
  function safeRead(path) {
24628
25347
  try {
@@ -24712,20 +25431,23 @@ function resolveInventoryContext(input) {
24712
25431
  }
24713
25432
 
24714
25433
  // ../../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";
25434
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25435
+ import { join as join8 } from "path";
24717
25436
 
24718
25437
  // ../../packages/plugin-sdk/src/project-files.ts
24719
25438
  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";
25439
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25440
+ import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
24722
25441
 
24723
25442
  // ../../packages/plugin-sdk/src/runtime.ts
24724
25443
  import { randomUUID as randomUUID10 } from "crypto";
24725
25444
 
25445
+ // ../../packages/plugin-sdk/src/suppressions.ts
25446
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
25447
+
24726
25448
  // ../../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";
25449
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
25450
+ import { join as join10 } from "path";
24729
25451
 
24730
25452
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
24731
25453
  import { randomUUID as randomUUID11 } from "crypto";
@@ -24918,6 +25640,14 @@ var StandaloneDataGateway = class {
24918
25640
  sweepTerminalExceptions(retentionMs) {
24919
25641
  return this.db.exceptions.sweepTerminal(retentionMs);
24920
25642
  }
25643
+ // The warn-era enforcement cap, standalone-only store maintenance invoked
25644
+ // from SessionStart, not part of the DataGateway port. Returns the number
25645
+ // of block/redact rows capped to warn (0 for a redact-policy store or an
25646
+ // already-capped one).
25647
+ capWarnEraEnforcement(policyMode) {
25648
+ const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
25649
+ return { capped };
25650
+ }
24921
25651
  // One project-file scan → the local project_file tree (one transaction inside
24922
25652
  // the LocalDatabase, fail-open there). Like the sweep above, this is
24923
25653
  // NOT part of the DataGateway port: the file tree is a local-store read model.
@@ -25023,11 +25753,11 @@ import {
25023
25753
  openSync,
25024
25754
  readFileSync as readFileSync7,
25025
25755
  readSync,
25026
- writeFileSync as writeFileSync5
25756
+ writeFileSync as writeFileSync6
25027
25757
  } from "fs";
25028
- import { join as join10 } from "path";
25758
+ import { join as join11 } from "path";
25029
25759
  function offsetsDir(dataDir2) {
25030
- return join10(dataDir2, "usage-offsets");
25760
+ return join11(dataDir2, "usage-offsets");
25031
25761
  }
25032
25762
  var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
25033
25763
  function safeSessionId(sessionId) {
@@ -25037,7 +25767,7 @@ function safeSessionId(sessionId) {
25037
25767
  return createHash5("sha256").update(sessionId).digest("hex");
25038
25768
  }
25039
25769
  function offsetPath(dataDir2, sessionId) {
25040
- return join10(offsetsDir(dataDir2), safeSessionId(sessionId));
25770
+ return join11(offsetsDir(dataDir2), safeSessionId(sessionId));
25041
25771
  }
25042
25772
  function readOffset(dataDir2, sessionId) {
25043
25773
  try {
@@ -25057,7 +25787,7 @@ function writeOffset(dataDir2, sessionId, value) {
25057
25787
  try {
25058
25788
  mkdirSync5(offsetsDir(dataDir2), { recursive: true, mode: DATA_DIR_MODE });
25059
25789
  const payload = value.lastPromptId !== void 0 ? { offset: value.offset, lastPromptId: value.lastPromptId } : { offset: value.offset };
25060
- writeFileSync5(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
25790
+ writeFileSync6(offsetPath(dataDir2, sessionId), JSON.stringify(payload), {
25061
25791
  mode: DATA_FILE_MODE
25062
25792
  });
25063
25793
  } catch {
@@ -25101,7 +25831,7 @@ function readTail(transcriptPath, startOffset) {
25101
25831
  // src/history/transcripts.ts
25102
25832
  import { readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
25103
25833
  import { homedir as homedir3 } from "os";
25104
- import { join as join11 } from "path";
25834
+ import { join as join12 } from "path";
25105
25835
  function isRecord(value) {
25106
25836
  return typeof value === "object" && value !== null;
25107
25837
  }