@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
@@ -15427,6 +15452,11 @@ var AuditEventType = external_exports.enum([
15427
15452
  "prompt",
15428
15453
  "response",
15429
15454
  "code_change",
15455
+ // The events.kind of a scanned tool call, widened in to keep this a
15456
+ // superset. Narrower than 'tool_call' above and not a duplicate of it:
15457
+ // 'tool_call' is the reconciler's structural row for every call, while
15458
+ // 'tool_use' exists only where a hook enforced against the arguments.
15459
+ "tool_use",
15430
15460
  // One row per config-inventory scan, hung off the session root. It is the
15431
15461
  // fact the posture inspection findings reference (findings require an
15432
15462
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -15781,6 +15811,10 @@ var ListActivitySessionsQuery = external_exports.object({
15781
15811
  from: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
15782
15812
  /** Upper bound on startedAt; omitted defaults to now. */
15783
15813
  to: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
15814
+ /** Exclude zero-activity sessions — roots whose only recorded children are
15815
+ * bookkeeping rows (hooks, config scans), typically background `claude`
15816
+ * launches. Omitted = list everything. `z.stringbool()` per the note above. */
15817
+ excludeEmpty: external_exports.stringbool().optional(),
15784
15818
  /** Page size, 1–100; out-of-range values are a 400. `z.coerce` — query params arrive as strings. */
15785
15819
  limit: external_exports.coerce.number().int().min(1).max(100).default(50),
15786
15820
  /** Opaque pagination cursor (most-recent first). */
@@ -15789,7 +15823,11 @@ var ListActivitySessionsQuery = external_exports.object({
15789
15823
  var ListActivitySessionsResponse = external_exports.object({
15790
15824
  items: external_exports.array(ActivitySessionSummary),
15791
15825
  /** `null` once the last page is reached. */
15792
- nextCursor: external_exports.string().nullable()
15826
+ nextCursor: external_exports.string().nullable(),
15827
+ /** Zero-activity sessions matching the query's filters/range (whether or
15828
+ * not `excludeEmpty` dropped them from `items`) — the count a UI toggle
15829
+ * shows when collapsing them. */
15830
+ emptyCount: external_exports.number().int().nonnegative()
15793
15831
  }).meta({ id: "ListActivitySessionsResponse" });
15794
15832
  var ListSessionEventsQuery = external_exports.object({
15795
15833
  /** Default 100, range 1–500. */
@@ -15818,12 +15856,18 @@ var ActivityOverviewResponse = external_exports.object({
15818
15856
  }).meta({ id: "ActivityOverviewResponse" });
15819
15857
 
15820
15858
  // ../../packages/schema/src/zod/event.ts
15821
- var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
15859
+ var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15822
15860
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15823
15861
  var EventMetadata = external_exports.object({
15824
15862
  sessionId: external_exports.string().optional(),
15825
15863
  repo: external_exports.string().optional(),
15826
15864
  filePath: external_exports.string().optional(),
15865
+ // The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch'),
15866
+ // set by the tool-scanning hooks. The tool NAME only — never the tool's
15867
+ // arguments or output, which can carry the very value a finding masked
15868
+ // (metadata is stored unredacted). Gives findings on non-file captures a
15869
+ // display location ("via Bash") when no filePath exists.
15870
+ toolName: external_exports.string().optional(),
15827
15871
  // Set (true) by the worktree scanner when the file is excluded by the
15828
15872
  // repo's .gitignore. Gitignored files ARE still scanned — local scratch and
15829
15873
  // generated code can leak real secrets — but the provenance is recorded so
@@ -16131,7 +16175,7 @@ var DetectionException = external_exports.object({
16131
16175
  justification: external_exports.string().min(1),
16132
16176
  conditions: ExceptionConditions.nullable(),
16133
16177
  createdBy: external_exports.string(),
16134
- createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
16178
+ createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
16135
16179
  createdAt: external_exports.iso.datetime(),
16136
16180
  updatedAt: external_exports.iso.datetime(),
16137
16181
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16155,7 +16199,10 @@ var ExceptionBundleEntry = DetectionException.pick({
16155
16199
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16156
16200
  var KeywordMatcher = external_exports.object({
16157
16201
  type: external_exports.literal("keyword"),
16158
- keywords: external_exports.array(external_exports.string()).min(1),
16202
+ // An empty keyword matches at every position, yielding one zero-length span
16203
+ // per character. Rejected here because a keyword that matches everything is
16204
+ // never intentional.
16205
+ keywords: external_exports.array(external_exports.string().min(1)).min(1),
16159
16206
  caseSensitive: external_exports.boolean().default(false)
16160
16207
  });
16161
16208
  function isValidRegex(pattern, flags) {
@@ -16300,20 +16347,26 @@ var PolicyBundle = external_exports.object({
16300
16347
  customKeywords: external_exports.array(external_exports.string()),
16301
16348
  fetchedAt: external_exports.iso.datetime()
16302
16349
  }).meta({ id: "PolicyBundle" });
16303
- var DEFAULT_ACTIONS = {
16304
- secret: "block",
16305
- pii: "redact",
16306
- financial: "redact",
16307
- phi: "redact",
16308
- code_context: "warn",
16309
- code_flaw: "warn",
16310
- custom: "warn",
16311
- // Config-posture findings only observe today (they land in
16312
- // inspection_findings, outside the live-capture enforcement path).
16313
- config: "warn"
16314
- };
16315
16350
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16316
16351
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
16352
+ var CATEGORY_PEAK_SEVERITY = {
16353
+ secret: "critical",
16354
+ financial: "critical",
16355
+ // core-financial/credit-card
16356
+ code_flaw: "critical",
16357
+ pii: "high",
16358
+ phi: "high",
16359
+ custom: "high",
16360
+ // user-defined; conservative
16361
+ code_context: "low",
16362
+ config: "low"
16363
+ // observe-only; floors to monitor regardless
16364
+ };
16365
+ function severityFloorPolicy(category) {
16366
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
16367
+ const peak = CATEGORY_PEAK_SEVERITY[category];
16368
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
16369
+ }
16317
16370
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
16318
16371
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
16319
16372
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -16340,6 +16393,12 @@ var BUILTIN_POLICY_SPECS = {
16340
16393
  description: "Refuse the request entirely whenever any rule in this detection matches."
16341
16394
  }
16342
16395
  };
16396
+ function builtinPolicyToAction(id) {
16397
+ return BUILTIN_POLICY_SPECS[id].action;
16398
+ }
16399
+ var DEFAULT_ACTIONS = Object.fromEntries(
16400
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
16401
+ );
16343
16402
  var BUILTIN_POLICIES = Object.fromEntries(
16344
16403
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
16345
16404
  );
@@ -16392,6 +16451,10 @@ var ListEventsResponse = external_exports.object({
16392
16451
  items: external_exports.array(Event),
16393
16452
  nextCursor: external_exports.string().nullable()
16394
16453
  }).meta({ id: "ListEventsResponse" });
16454
+ var IngestResponse = external_exports.object({
16455
+ accepted: external_exports.number().int().nonnegative(),
16456
+ duplicates: external_exports.number().int().nonnegative()
16457
+ }).meta({ id: "IngestResponse" });
16395
16458
  var ListFindingsQuery = external_exports.object({
16396
16459
  cursor: external_exports.string().optional(),
16397
16460
  limit: external_exports.coerce.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).default(50),
@@ -16746,10 +16809,8 @@ function toApiProvider(sourceTool) {
16746
16809
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
16747
16810
  }
16748
16811
  var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
16749
- function deriveGroupStatus(instances) {
16750
- const statuses = new Set(
16751
- instances.map((i) => i.status).filter((s) => s !== void 0)
16752
- );
16812
+ function foldGroupStatus(instanceStatuses) {
16813
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
16753
16814
  if (statuses.size === 0) return void 0;
16754
16815
  for (const candidate of STATUS_PRECEDENCE) {
16755
16816
  if (statuses.has(candidate)) return candidate;
@@ -16767,6 +16828,7 @@ function deriveFindingStatus(row) {
16767
16828
  function buildFindingGroups(rows, opts = {}) {
16768
16829
  const overrides = opts.overrides;
16769
16830
  const packNames = opts.packNames;
16831
+ const aggregates = opts.aggregates;
16770
16832
  const byRuleId = /* @__PURE__ */ new Map();
16771
16833
  for (const row of rows) {
16772
16834
  const existing = byRuleId.get(row.ruleId);
@@ -16782,23 +16844,27 @@ function buildFindingGroups(rows, opts = {}) {
16782
16844
  provider: toApiProvider(r.sourceTool),
16783
16845
  repo: r.repo,
16784
16846
  file: r.file,
16847
+ ...r.toolName === void 0 ? {} : { toolName: r.toolName },
16785
16848
  action: toApiAction(effectiveDbAction),
16786
16849
  detectedAt: r.occurredAt,
16787
16850
  confidence: r.confidence,
16788
16851
  status: r.status
16789
16852
  };
16790
16853
  });
16791
- const latestDetectedAt = ruleRows.reduce(
16854
+ const agg = aggregates?.get(ruleId);
16855
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
16792
16856
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
16793
16857
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
16794
16858
  );
16795
16859
  const seenProviders = /* @__PURE__ */ new Set();
16796
- const providers = instances.map((i) => i.provider).filter((p) => {
16860
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
16797
16861
  if (seenProviders.has(p)) return false;
16798
16862
  seenProviders.add(p);
16799
16863
  return true;
16800
16864
  });
16801
- const actionSet = new Set(instances.map((i) => i.action));
16865
+ const actionSet = new Set(
16866
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
16867
+ );
16802
16868
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
16803
16869
  const severity = ruleRows[0]?.severity ?? "low";
16804
16870
  const detection = {
@@ -16812,8 +16878,10 @@ function buildFindingGroups(rows, opts = {}) {
16812
16878
  contextPrefix: ""
16813
16879
  // empty (pending privacy review)
16814
16880
  };
16815
- const status = deriveGroupStatus(instances);
16816
- groups.push({
16881
+ const status = foldGroupStatus(
16882
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
16883
+ );
16884
+ const group = {
16817
16885
  id: ruleId,
16818
16886
  category: apiCategory,
16819
16887
  subtype: ruleId,
@@ -16822,21 +16890,26 @@ function buildFindingGroups(rows, opts = {}) {
16822
16890
  match,
16823
16891
  detection,
16824
16892
  policy,
16825
- instanceCount: instances.length,
16893
+ instanceCount: agg?.instanceCount ?? instances.length,
16826
16894
  providers,
16827
16895
  aggregateAction,
16828
16896
  latestDetectedAt,
16829
16897
  instances,
16830
16898
  status
16831
- });
16899
+ };
16900
+ if (agg) {
16901
+ actionsCache.set(group, [...actionSet]);
16902
+ if (agg.searchText !== void 0) {
16903
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
16904
+ }
16905
+ }
16906
+ groups.push(group);
16832
16907
  }
16833
16908
  return groups;
16834
16909
  }
16835
16910
  var haystackCache = /* @__PURE__ */ new WeakMap();
16836
- function groupHaystack(g) {
16837
- const cached2 = haystackCache.get(g);
16838
- if (cached2 !== void 0) return cached2;
16839
- const haystack = [
16911
+ function buildHaystack(g, extra) {
16912
+ return [
16840
16913
  g.subtype,
16841
16914
  g.category,
16842
16915
  g.match.maskedValue,
@@ -16844,11 +16917,26 @@ function groupHaystack(g) {
16844
16917
  g.id,
16845
16918
  ...g.instances.map((i) => i.repo),
16846
16919
  ...g.instances.map((i) => i.file),
16847
- ...g.instances.map((i) => i.id)
16920
+ ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
16921
+ ...g.instances.map((i) => i.id),
16922
+ ...extra === void 0 ? [] : [extra]
16848
16923
  ].join(" ").toLowerCase();
16924
+ }
16925
+ function groupHaystack(g) {
16926
+ const cached2 = haystackCache.get(g);
16927
+ if (cached2 !== void 0) return cached2;
16928
+ const haystack = buildHaystack(g);
16849
16929
  haystackCache.set(g, haystack);
16850
16930
  return haystack;
16851
16931
  }
16932
+ var actionsCache = /* @__PURE__ */ new WeakMap();
16933
+ function groupActions(g) {
16934
+ const cached2 = actionsCache.get(g);
16935
+ if (cached2 !== void 0) return cached2;
16936
+ const actions = [...new Set(g.instances.map((i) => i.action))];
16937
+ actionsCache.set(g, actions);
16938
+ return actions;
16939
+ }
16852
16940
  function applyFindingFilters(groups, opts) {
16853
16941
  let filtered = groups;
16854
16942
  if (opts.severity && opts.severity.length > 0) {
@@ -16861,7 +16949,7 @@ function applyFindingFilters(groups, opts) {
16861
16949
  }
16862
16950
  if (opts.actions && opts.actions.length > 0) {
16863
16951
  const actionSet = new Set(opts.actions);
16864
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
16952
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
16865
16953
  }
16866
16954
  if (opts.subtype && opts.subtype.length > 0) {
16867
16955
  const subtypeSet = new Set(opts.subtype);
@@ -16913,8 +17001,7 @@ function computeFindingFacets(allGroups, opts) {
16913
17001
  });
16914
17002
  const actionMap = /* @__PURE__ */ new Map();
16915
17003
  for (const g of forAction) {
16916
- const actionSet = new Set(g.instances.map((i) => i.action));
16917
- for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
17004
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
16918
17005
  }
16919
17006
  const forSubtype = applyFindingFilters(allGroups, {
16920
17007
  providers: opts.providers,
@@ -17097,6 +17184,71 @@ var ProjectFilesScan = external_exports.object({
17097
17184
  scannedAt: external_exports.string()
17098
17185
  });
17099
17186
 
17187
+ // ../../packages/schema/src/zod/ranges.ts
17188
+ var TIME_RANGES = ["7d", "30d", "3m", "6m"];
17189
+ var TimeRange = external_exports.enum(TIME_RANGES).meta({ id: "TimeRange" });
17190
+ var DEFAULT_TIME_RANGE = "7d";
17191
+ var RANGE_DAYS = {
17192
+ "7d": 7,
17193
+ "30d": 30,
17194
+ "3m": 90,
17195
+ "6m": 180
17196
+ };
17197
+ var TIME_RANGE_OR_DEFAULT = TimeRange.catch(DEFAULT_TIME_RANGE);
17198
+
17199
+ // ../../packages/schema/src/zod/remediation.ts
17200
+ var SecretFindingState = external_exports.enum(["still-valid", "unknown", "invalid"]);
17201
+ var MaskedFindingLocation = external_exports.object({
17202
+ filePath: external_exports.string(),
17203
+ span: Span.optional()
17204
+ }).strict();
17205
+ var MaskedSecretFinding = external_exports.object({
17206
+ provider: external_exports.string(),
17207
+ maskedToken: external_exports.string(),
17208
+ where: MaskedFindingLocation,
17209
+ state: SecretFindingState,
17210
+ observedAt: external_exports.iso.datetime().optional()
17211
+ }).strict();
17212
+ var RotationChecklistEntry = external_exports.object({
17213
+ provider: external_exports.string(),
17214
+ maskedToken: external_exports.string(),
17215
+ consolePath: external_exports.string(),
17216
+ occurrenceSpread: external_exports.number().int().positive()
17217
+ }).strict();
17218
+ var RemediationOption = external_exports.enum([
17219
+ "redact-rotation-checklist",
17220
+ "redact-only",
17221
+ "set-secret-redact",
17222
+ "leave"
17223
+ ]);
17224
+ var RemediationEntrySource = external_exports.enum(["first-run", "pre-push", "secret-scan"]);
17225
+ var RemediationEntryContext = external_exports.object({
17226
+ entrySource: RemediationEntrySource
17227
+ }).strict();
17228
+ var RemediationOptionChoice = external_exports.object({
17229
+ id: RemediationOption,
17230
+ label: external_exports.string()
17231
+ });
17232
+ var BatchedRemediationDecision = external_exports.object({
17233
+ kind: external_exports.literal("decision"),
17234
+ entrySource: RemediationEntrySource,
17235
+ secretCount: external_exports.number().int().positive(),
17236
+ prompt: external_exports.string(),
17237
+ options: external_exports.tuple([
17238
+ RemediationOptionChoice.extend({ id: external_exports.literal("redact-rotation-checklist") }),
17239
+ RemediationOptionChoice.extend({ id: external_exports.literal("redact-only") }),
17240
+ RemediationOptionChoice.extend({ id: external_exports.literal("set-secret-redact") }),
17241
+ RemediationOptionChoice.extend({ id: external_exports.literal("leave") })
17242
+ ])
17243
+ });
17244
+ var NoRemediationDecision = external_exports.object({
17245
+ kind: external_exports.literal("no-decision")
17246
+ });
17247
+ var BatchedRemediation = external_exports.discriminatedUnion("kind", [
17248
+ BatchedRemediationDecision,
17249
+ NoRemediationDecision
17250
+ ]);
17251
+
17100
17252
  // ../../packages/schema/src/zod/rule-test.ts
17101
17253
  var TestRulesRequest = external_exports.object({
17102
17254
  rules: external_exports.array(Rule).min(1).max(100),
@@ -17155,10 +17307,8 @@ var SeveritySummaryResponse = external_exports.object({
17155
17307
  // All four severity levels are always present (count may be 0).
17156
17308
  bySeverity: external_exports.array(SeveritySummaryItem)
17157
17309
  }).meta({ id: "SeveritySummaryResponse" });
17158
- var SECURITY_RANGES = ["7d", "30d", "3m", "6m"];
17159
- var SecurityRange = external_exports.enum(SECURITY_RANGES).meta({ id: "SecurityRange" });
17160
17310
  var SecurityRangeQuery = external_exports.object({
17161
- range: external_exports.enum(SECURITY_RANGES).default("30d")
17311
+ range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE)
17162
17312
  });
17163
17313
  var EnforcementActionKind = external_exports.enum(["blocked", "redacted", "warned"]).meta({ id: "EnforcementActionKind" });
17164
17314
  var EnforcementAction = external_exports.object({
@@ -17168,7 +17318,7 @@ var EnforcementAction = external_exports.object({
17168
17318
  delta: external_exports.number().int()
17169
17319
  }).meta({ id: "EnforcementAction" });
17170
17320
  var EnforcementActionsResponse = external_exports.object({
17171
- range: SecurityRange,
17321
+ range: TimeRange,
17172
17322
  // Sum of actions[].count in the window.
17173
17323
  total: external_exports.number().int().nonnegative(),
17174
17324
  // One entry per kind, always all three present (count may be 0).
@@ -17183,7 +17333,7 @@ var FindingsTimeseriesPoint = external_exports.object({
17183
17333
  medium: external_exports.number().int().nonnegative()
17184
17334
  }).meta({ id: "FindingsTimeseriesPoint" });
17185
17335
  var FindingsTimeseriesResponse = external_exports.object({
17186
- range: SecurityRange,
17336
+ range: TimeRange,
17187
17337
  granularity: TimeseriesGranularity,
17188
17338
  points: external_exports.array(FindingsTimeseriesPoint)
17189
17339
  }).meta({ id: "FindingsTimeseriesResponse" });
@@ -17198,7 +17348,7 @@ var MttrTrendPoint = external_exports.object({
17198
17348
  })
17199
17349
  }).meta({ id: "MttrTrendPoint" });
17200
17350
  var MttrTrendResponse = external_exports.object({
17201
- range: SecurityRange,
17351
+ range: TimeRange,
17202
17352
  granularity: TimeseriesGranularity,
17203
17353
  points: external_exports.array(MttrTrendPoint)
17204
17354
  }).meta({ id: "MttrTrendResponse" });
@@ -17225,11 +17375,11 @@ var TopSource = external_exports.object({
17225
17375
  findingsCount: external_exports.number().int().nonnegative()
17226
17376
  }).meta({ id: "TopSource" });
17227
17377
  var TopSourcesResponse = external_exports.object({
17228
- range: SecurityRange,
17378
+ range: TimeRange,
17229
17379
  items: external_exports.array(TopSource)
17230
17380
  }).meta({ id: "TopSourcesResponse" });
17231
17381
  var TopSourcesQuery = external_exports.object({
17232
- range: external_exports.enum(SECURITY_RANGES).default("30d"),
17382
+ range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE),
17233
17383
  limit: external_exports.coerce.number().int().min(1).max(50).default(5),
17234
17384
  // Omit for both kinds.
17235
17385
  kind: external_exports.enum(SOURCE_KINDS).optional()
@@ -17242,7 +17392,7 @@ var ScanCoverageProvider = external_exports.object({
17242
17392
  supported: external_exports.boolean()
17243
17393
  }).meta({ id: "ScanCoverageProvider" });
17244
17394
  var ScanCoverageResponse = external_exports.object({
17245
- range: SecurityRange,
17395
+ range: TimeRange,
17246
17396
  providers: external_exports.array(ScanCoverageProvider)
17247
17397
  }).meta({ id: "ScanCoverageResponse" });
17248
17398
  var SubjectType = external_exports.enum(["repo", "user", "team", "policy", "share", "rule"]).meta({
@@ -17291,6 +17441,114 @@ var ApplyRecommendedActionResponse = external_exports.object({
17291
17441
  var DismissRecommendedActionResponse = external_exports.object({ id: external_exports.string(), status: external_exports.literal("dismissed") }).meta({ id: "DismissRecommendedActionResponse" });
17292
17442
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
17293
17443
 
17444
+ // ../../packages/schema/src/zod/triage.ts
17445
+ var TriageHit = external_exports.object({
17446
+ ruleId: external_exports.string(),
17447
+ category: DetectionCategory,
17448
+ severity: Severity,
17449
+ maskedMatch: external_exports.string(),
17450
+ rawMatch: external_exports.string(),
17451
+ context: external_exports.string(),
17452
+ filePath: external_exports.string().optional(),
17453
+ confidence: external_exports.number().min(0).max(1),
17454
+ id: external_exports.string().optional(),
17455
+ valueFingerprint: external_exports.string().optional(),
17456
+ keyVersion: external_exports.number().int().nonnegative().optional()
17457
+ });
17458
+ var TriagePolicy = BuiltinPolicyId;
17459
+ var TriageCategoryRec = external_exports.object({
17460
+ category: DetectionCategory,
17461
+ action: TriagePolicy,
17462
+ reasoning: external_exports.string(),
17463
+ genuineCount: external_exports.number().int().nonnegative(),
17464
+ fpCount: external_exports.number().int().nonnegative(),
17465
+ // TriageHit ids judged false-positive in this category. fpCount must equal
17466
+ // this array's length — enforced by the consumer, not this schema.
17467
+ fpIds: external_exports.array(external_exports.string())
17468
+ });
17469
+ var TriageRecommendation = external_exports.object({
17470
+ perCategory: external_exports.array(TriageCategoryRec),
17471
+ notes: external_exports.string()
17472
+ });
17473
+
17474
+ // ../../packages/schema/src/zod/setup-frame.ts
17475
+ var CalibrationCounts = external_exports.object({
17476
+ total: external_exports.number().int().nonnegative(),
17477
+ important: external_exports.number().int().nonnegative(),
17478
+ routine: external_exports.number().int().nonnegative()
17479
+ }).refine((c) => c.total === c.important + c.routine, {
17480
+ message: "total must equal important + routine",
17481
+ path: ["total"]
17482
+ });
17483
+ var FalsePositivePatternValue = external_exports.object({
17484
+ ruleId: external_exports.string(),
17485
+ category: DetectionCategory,
17486
+ valueFingerprint: external_exports.string(),
17487
+ keyVersion: external_exports.number().int().nonnegative()
17488
+ });
17489
+ var FalsePositivePatternGroup = external_exports.object({
17490
+ pattern: external_exports.string(),
17491
+ count: external_exports.number().int().nonnegative(),
17492
+ values: external_exports.array(FalsePositivePatternValue).min(1)
17493
+ });
17494
+ var CalibrationFindingKind = external_exports.object({
17495
+ category: DetectionCategory,
17496
+ count: external_exports.number().int().nonnegative(),
17497
+ egress: external_exports.boolean()
17498
+ });
17499
+ var CalibrationFrame = external_exports.object({
17500
+ counts: CalibrationCounts,
17501
+ routineCategories: external_exports.array(DetectionCategory),
17502
+ surfacedCategories: external_exports.array(DetectionCategory),
17503
+ findingKinds: external_exports.array(CalibrationFindingKind),
17504
+ posture: external_exports.record(DetectionCategory, BuiltinPolicyId),
17505
+ maskedFindings: external_exports.array(MaskedSecretFinding).optional(),
17506
+ falsePositivePatterns: external_exports.array(FalsePositivePatternGroup).optional()
17507
+ });
17508
+ var CalibrationPreviewCategory = TriageCategoryRec.pick({
17509
+ category: true,
17510
+ genuineCount: true,
17511
+ fpCount: true
17512
+ }).extend({
17513
+ egress: external_exports.boolean()
17514
+ });
17515
+ var CalibrationPreview = external_exports.object({
17516
+ categories: external_exports.array(CalibrationPreviewCategory),
17517
+ posture: external_exports.record(DetectionCategory, BuiltinPolicyId)
17518
+ });
17519
+ var CalibrationResult = external_exports.object({
17520
+ frame: CalibrationFrame,
17521
+ copy: external_exports.string()
17522
+ });
17523
+ var FirstRunCalibration = external_exports.enum(["scan", "floor"]);
17524
+ var SetupHandoffOption = external_exports.object({
17525
+ id: external_exports.enum(["enter-remediation", "open-dashboard", "not-now"]),
17526
+ label: external_exports.string()
17527
+ });
17528
+ var DashboardHandoffOptions = external_exports.tuple([
17529
+ SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
17530
+ SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
17531
+ ]);
17532
+ var ComposedRemediationOptions = external_exports.tuple([
17533
+ SetupHandoffOption.extend({ id: external_exports.literal("enter-remediation") }),
17534
+ SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
17535
+ SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
17536
+ ]);
17537
+ var SetupHandoffOffer = external_exports.object({
17538
+ worthALook: external_exports.number().int().nonnegative(),
17539
+ liveKeys: external_exports.number().int().nonnegative().optional(),
17540
+ options: external_exports.union([DashboardHandoffOptions, ComposedRemediationOptions])
17541
+ }).refine(
17542
+ (o) => o.options.some((opt) => opt.id === "enter-remediation") === (o.liveKeys ?? 0) > 0,
17543
+ {
17544
+ message: "the chain-entry option is present exactly when liveKeys > 0",
17545
+ path: ["options"]
17546
+ }
17547
+ ).refine((o) => (o.liveKeys ?? 0) <= o.worthALook, {
17548
+ message: "liveKeys is a subset of worthALook and cannot exceed it",
17549
+ path: ["liveKeys"]
17550
+ });
17551
+
17294
17552
  // ../../packages/schema/src/zod/shares.ts
17295
17553
  var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17296
17554
  var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
@@ -17476,6 +17734,102 @@ function reviewSeverityRank(reasons) {
17476
17734
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17477
17735
  }
17478
17736
 
17737
+ // ../../packages/persistence/src/internal/sql-text.ts
17738
+ function escapeLikePattern(s) {
17739
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17740
+ }
17741
+ function placeholders(n) {
17742
+ return Array.from({ length: n }, () => "?").join(", ");
17743
+ }
17744
+ function containsPattern(q) {
17745
+ return `%${escapeLikePattern(q)}%`;
17746
+ }
17747
+ function likeAny(exprs) {
17748
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
17749
+ }
17750
+
17751
+ // ../../packages/persistence/src/internal/transactions.ts
17752
+ var savepointSeq = 0;
17753
+ function withTransaction(db, fn, mode = "DEFERRED") {
17754
+ if (db.isTransaction) {
17755
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
17756
+ db.exec(`SAVEPOINT ${savepoint}`);
17757
+ try {
17758
+ fn();
17759
+ db.exec(`RELEASE ${savepoint}`);
17760
+ } catch (error51) {
17761
+ try {
17762
+ db.exec(`ROLLBACK TO ${savepoint}`);
17763
+ db.exec(`RELEASE ${savepoint}`);
17764
+ } catch {
17765
+ }
17766
+ throw error51;
17767
+ }
17768
+ return;
17769
+ }
17770
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
17771
+ try {
17772
+ fn();
17773
+ db.exec("COMMIT");
17774
+ } catch (error51) {
17775
+ try {
17776
+ db.exec("ROLLBACK");
17777
+ } catch {
17778
+ }
17779
+ throw error51;
17780
+ }
17781
+ }
17782
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
17783
+ const nested = db.isTransaction;
17784
+ try {
17785
+ withTransaction(db, fn, mode);
17786
+ return true;
17787
+ } catch (error51) {
17788
+ if (!db.isTransaction && nested) throw error51;
17789
+ return false;
17790
+ }
17791
+ }
17792
+
17793
+ // ../../packages/persistence/src/internal/warn.ts
17794
+ function akaWarn(message) {
17795
+ process.stderr.write(`[aka] ${message}
17796
+ `);
17797
+ }
17798
+
17799
+ // ../../packages/persistence/src/db/migrations/introspection.ts
17800
+ function evidenceObjects(sql) {
17801
+ const objects = [];
17802
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17803
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17804
+ objects.push({ kind: "table", name: m[1] });
17805
+ }
17806
+ }
17807
+ for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17808
+ if (m[1] !== void 0 && m[2] !== void 0) {
17809
+ objects.push({ kind: "column", table: m[1], name: m[2] });
17810
+ }
17811
+ }
17812
+ return objects;
17813
+ }
17814
+ function schemaObjectExists(db, kind, name) {
17815
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
17816
+ return row !== void 0;
17817
+ }
17818
+ function indexExists(db, name) {
17819
+ return schemaObjectExists(db, "index", name);
17820
+ }
17821
+ function columnNames(db, table2, opts) {
17822
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
17823
+ const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
17824
+ return columns.map((c) => c.name);
17825
+ }
17826
+ function evidenceExists(db, object2) {
17827
+ if (object2.kind === "column") {
17828
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
17829
+ }
17830
+ return schemaObjectExists(db, "table", object2.name);
17831
+ }
17832
+
17479
17833
  // ../../packages/persistence/src/ids.ts
17480
17834
  import { createHash } from "crypto";
17481
17835
  function sha256Hex(input) {
@@ -17512,28 +17866,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17512
17866
  }
17513
17867
 
17514
17868
  // ../../packages/persistence/src/migrations.ts
17515
- function evidenceObjects(sql) {
17516
- const objects = [];
17517
- for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17518
- if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17519
- objects.push({ kind: "table", name: m[1] });
17520
- }
17521
- }
17522
- for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17523
- if (m[1] !== void 0 && m[2] !== void 0) {
17524
- objects.push({ kind: "column", table: m[1], name: m[2] });
17525
- }
17526
- }
17527
- return objects;
17528
- }
17529
- function evidenceExists(db, object2) {
17530
- if (object2.kind === "column") {
17531
- const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
17532
- return columns.some((c) => c.name === object2.name);
17533
- }
17534
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
17535
- return row !== void 0;
17536
- }
17537
17869
  function describeObject(object2) {
17538
17870
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
17539
17871
  }
@@ -17544,10 +17876,6 @@ function createdIndexName(statement) {
17544
17876
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17545
17877
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17546
17878
  }
17547
- function indexExists(db, name) {
17548
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
17549
- return row !== void 0;
17550
- }
17551
17879
  function applyMigrations(db) {
17552
17880
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17553
17881
  db.exec(
@@ -17566,44 +17894,39 @@ function applyMigrations(db) {
17566
17894
  const present = evidence.filter((o) => evidenceExists(db, o));
17567
17895
  if (present.length > 0 && present.length < evidence.length) {
17568
17896
  const missing = evidence.filter((o) => !present.includes(o));
17569
- const message = `[aka] sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
17570
- process.stderr.write(`${message}
17571
- `);
17572
- throw new Error(message);
17897
+ 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.`;
17898
+ akaWarn(message);
17899
+ throw new Error(`[aka] ${message}`);
17573
17900
  }
17574
17901
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
17575
17902
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
17576
17903
  const statements = splitStatements(migration.sql);
17577
17904
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
17578
17905
  try {
17579
- db.exec("BEGIN IMMEDIATE");
17580
- try {
17581
- for (const statement of statements) {
17582
- const indexName = createdIndexName(statement);
17583
- if (indexName === void 0) {
17584
- if (alreadyApplied) continue;
17585
- } else if (indexExists(db, indexName)) {
17586
- continue;
17906
+ withTransaction(
17907
+ db,
17908
+ () => {
17909
+ for (const statement of statements) {
17910
+ const indexName = createdIndexName(statement);
17911
+ if (indexName === void 0) {
17912
+ if (alreadyApplied) continue;
17913
+ } else if (indexExists(db, indexName)) {
17914
+ continue;
17915
+ }
17916
+ db.exec(statement);
17587
17917
  }
17588
- db.exec(statement);
17589
- }
17590
- if (wantsFkOff && !alreadyApplied) {
17591
- const violations = db.prepare("PRAGMA foreign_key_check").all();
17592
- if (violations.length > 0) {
17593
- throw new Error(
17594
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17595
- );
17918
+ if (wantsFkOff && !alreadyApplied) {
17919
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
17920
+ if (violations.length > 0) {
17921
+ throw new Error(
17922
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17923
+ );
17924
+ }
17596
17925
  }
17597
- }
17598
- record2.run(migration.tag, Date.now());
17599
- db.exec("COMMIT");
17600
- } catch (error51) {
17601
- try {
17602
- db.exec("ROLLBACK");
17603
- } catch {
17604
- }
17605
- throw error51;
17606
- }
17926
+ record2.run(migration.tag, Date.now());
17927
+ },
17928
+ "IMMEDIATE"
17929
+ );
17607
17930
  } finally {
17608
17931
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
17609
17932
  }
@@ -17646,8 +17969,7 @@ var TOKEN_USAGE_COLUMNS = [
17646
17969
  }
17647
17970
  ];
17648
17971
  function ensureTokenUsageColumns(db) {
17649
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
17650
- const existing = new Set(columns.map((c) => c.name));
17972
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17651
17973
  for (const column of TOKEN_USAGE_COLUMNS) {
17652
17974
  if (!existing.has(column.name)) {
17653
17975
  db.exec(column.ddl);
@@ -17685,47 +18007,39 @@ function reconcileSourceProjectIds(db) {
17685
18007
  repoint: db.prepare(`UPDATE ${table2} SET project_id = ? WHERE project_id = ?`)
17686
18008
  }));
17687
18009
  const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
17688
- db.exec("BEGIN IMMEDIATE");
17689
- try {
17690
- for (const { row, canonicalId } of legacy) {
17691
- foldProject.run(
17692
- canonicalId,
17693
- row.url,
17694
- row.name,
17695
- row.attributes,
17696
- row.firstSeen,
17697
- row.lastSeen
17698
- );
17699
- repointAudit.run(canonicalId, row.id);
17700
- for (const { dropCollisions, repoint } of pathTables) {
17701
- dropCollisions.run(row.id, canonicalId);
17702
- repoint.run(canonicalId, row.id);
18010
+ withTransaction(
18011
+ db,
18012
+ () => {
18013
+ for (const { row, canonicalId } of legacy) {
18014
+ foldProject.run(
18015
+ canonicalId,
18016
+ row.url,
18017
+ row.name,
18018
+ row.attributes,
18019
+ row.firstSeen,
18020
+ row.lastSeen
18021
+ );
18022
+ repointAudit.run(canonicalId, row.id);
18023
+ for (const { dropCollisions, repoint } of pathTables) {
18024
+ dropCollisions.run(row.id, canonicalId);
18025
+ repoint.run(canonicalId, row.id);
18026
+ }
18027
+ repointCallSite.run(canonicalId, row.id);
18028
+ deleteLegacy.run(row.id);
17703
18029
  }
17704
- repointCallSite.run(canonicalId, row.id);
17705
- deleteLegacy.run(row.id);
17706
- }
17707
- db.exec("COMMIT");
17708
- } catch (error51) {
17709
- try {
17710
- db.exec("ROLLBACK");
17711
- } catch {
17712
- }
17713
- throw error51;
17714
- }
18030
+ },
18031
+ "IMMEDIATE"
18032
+ );
17715
18033
  } catch (error51) {
17716
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
17717
- `);
18034
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
17718
18035
  }
17719
18036
  }
17720
18037
  function isForeignSqliteLineage(db) {
17721
- const tenantsTable = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tenants' LIMIT 1").get();
17722
- if (tenantsTable !== void 0) return true;
17723
- const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
17724
- return eventsColumns.some((c) => c.name === "tenant_id");
18038
+ if (schemaObjectExists(db, "table", "tenants")) return true;
18039
+ return columnNames(db, "events").includes("tenant_id");
17725
18040
  }
17726
18041
  function ensureSyncedAtColumn(db, table2) {
17727
- const columns = db.prepare(`PRAGMA table_info(${table2})`).all();
17728
- if (!columns.some((c) => c.name === "synced_at")) {
18042
+ if (!columnNames(db, table2).includes("synced_at")) {
17729
18043
  db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
17730
18044
  }
17731
18045
  }
@@ -17776,8 +18090,11 @@ function ensureDataDirSync(dir) {
17776
18090
  } catch {
17777
18091
  }
17778
18092
  }
18093
+ function walSidecars(file2) {
18094
+ return [`${file2}-wal`, `${file2}-shm`];
18095
+ }
17779
18096
  function tightenPerms(file2) {
17780
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
18097
+ for (const path of [file2, ...walSidecars(file2)]) {
17781
18098
  try {
17782
18099
  chmodSync(path, DATA_FILE_MODE);
17783
18100
  } catch {
@@ -17785,12 +18102,68 @@ function tightenPerms(file2) {
17785
18102
  }
17786
18103
  }
17787
18104
 
17788
- // ../../packages/persistence/src/repositories/sql-utils.ts
17789
- function escapeLikePattern(s) {
17790
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
18105
+ // ../../packages/persistence/src/internal/json.ts
18106
+ function safeJson(s, fallback) {
18107
+ if (s == null) return fallback;
18108
+ try {
18109
+ return JSON.parse(s);
18110
+ } catch {
18111
+ return fallback;
18112
+ }
17791
18113
  }
17792
- function placeholders(n) {
17793
- return Array.from({ length: n }, () => "?").join(", ");
18114
+ function parseJsonObject(s) {
18115
+ if (s == null) return void 0;
18116
+ try {
18117
+ const parsed = JSON.parse(s);
18118
+ if (typeof parsed === "object" && parsed !== null) return parsed;
18119
+ } catch {
18120
+ }
18121
+ return void 0;
18122
+ }
18123
+
18124
+ // ../../packages/persistence/src/internal/rows.ts
18125
+ function allRows(stmt, params) {
18126
+ if (params === void 0) return stmt.all();
18127
+ if (Array.isArray(params)) return stmt.all(...params);
18128
+ return stmt.all(params);
18129
+ }
18130
+ function getRow(stmt, params) {
18131
+ if (params === void 0) return stmt.get();
18132
+ if (Array.isArray(params)) return stmt.get(...params);
18133
+ return stmt.get(params);
18134
+ }
18135
+ function intToBool(raw) {
18136
+ return raw === 1 || raw === true;
18137
+ }
18138
+ function boolToInt(b) {
18139
+ return b ? 1 : 0;
18140
+ }
18141
+ function bindParams(row) {
18142
+ const out = {};
18143
+ for (const [key, value] of Object.entries(row)) {
18144
+ out[key] = value === void 0 ? null : value;
18145
+ }
18146
+ return out;
18147
+ }
18148
+ function countScalar(db, sql, params) {
18149
+ return getRow(db.prepare(sql), params)?.n ?? 0;
18150
+ }
18151
+ function countBy(db, sql, params) {
18152
+ const map2 = /* @__PURE__ */ new Map();
18153
+ for (const row of allRows(db.prepare(sql), params)) {
18154
+ map2.set(row.k, row.n);
18155
+ }
18156
+ return map2;
18157
+ }
18158
+ function mapRowsTolerant(rows, map2) {
18159
+ const out = [];
18160
+ for (const row of rows) {
18161
+ try {
18162
+ out.push(map2(row));
18163
+ } catch {
18164
+ }
18165
+ }
18166
+ return out;
17794
18167
  }
17795
18168
 
17796
18169
  // ../../packages/persistence/src/repositories/activity.ts
@@ -17842,15 +18215,11 @@ function encodeCursor(payload) {
17842
18215
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
17843
18216
  }
17844
18217
  function decodeCursor(cursor) {
17845
- try {
17846
- const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
17847
- if (parsed !== null && typeof parsed === "object" && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
17848
- return parsed;
17849
- }
17850
- return null;
17851
- } catch {
17852
- return null;
18218
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18219
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18220
+ return parsed;
17853
18221
  }
18222
+ return null;
17854
18223
  }
17855
18224
  var DB_EVENT_TYPE_TO_KIND = {
17856
18225
  session: "session",
@@ -17867,15 +18236,8 @@ var DB_EVENT_TYPE_TO_KIND = {
17867
18236
  };
17868
18237
  function safeParseStringArray(raw) {
17869
18238
  if (!raw) return [];
17870
- try {
17871
- const parsed = JSON.parse(raw);
17872
- return Array.isArray(parsed) ? parsed : [];
17873
- } catch {
17874
- return [];
17875
- }
17876
- }
17877
- function toBool(raw) {
17878
- return raw === 1 || raw === true;
18239
+ const parsed = safeJson(raw, null);
18240
+ return Array.isArray(parsed) ? parsed : [];
17879
18241
  }
17880
18242
  function toHarness(raw) {
17881
18243
  const parsed = Harness.safeParse(raw);
@@ -17928,8 +18290,8 @@ function buildAuditEvent(row) {
17928
18290
  severity: severityParsed?.success ? severityParsed.data : null,
17929
18291
  link: linkParsed?.success ? linkParsed.data : null,
17930
18292
  targetId: row.target_id,
17931
- internal: toBool(row.internal),
17932
- flagged: toBool(row.flagged)
18293
+ internal: intToBool(row.internal),
18294
+ flagged: intToBool(row.flagged)
17933
18295
  };
17934
18296
  }
17935
18297
  var TIMELINE_COLUMNS = `
@@ -17945,6 +18307,10 @@ var TIMELINE_COLUMNS = `
17945
18307
  json_extract(attributes, '$.internal') AS internal,
17946
18308
  json_extract(attributes, '$.flagged') AS flagged`;
17947
18309
  var SESSION_ROOT = `event_type = 'session'`;
18310
+ var HAS_ACTIVITY = `EXISTS (
18311
+ SELECT 1 FROM audit_events c
18312
+ WHERE c.root_session_id = audit_events.id
18313
+ AND c.event_type NOT IN ('hook', 'config_scan'))`;
17948
18314
  var SqliteActivityRepository = class {
17949
18315
  constructor(db, now = () => Date.now()) {
17950
18316
  this.db = db;
@@ -17955,12 +18321,16 @@ var SqliteActivityRepository = class {
17955
18321
  stats(tz) {
17956
18322
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
17957
18323
  const { startMs, endMs } = window;
17958
- const sessionsToday = this.db.prepare(
18324
+ const sessionsToday = countScalar(
18325
+ this.db,
17959
18326
  `SELECT count(*) AS n FROM audit_events
17960
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
17961
- ).get(startMs, endMs).n;
18327
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?
18328
+ AND ${HAS_ACTIVITY}`,
18329
+ [startMs, endMs]
18330
+ );
17962
18331
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
17963
- const liveNow = this.db.prepare(
18332
+ const liveNow = countScalar(
18333
+ this.db,
17964
18334
  `SELECT count(*) AS n FROM audit_events s
17965
18335
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
17966
18336
  AND max(
@@ -17969,22 +18339,29 @@ var SqliteActivityRepository = class {
17969
18339
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
17970
18340
  s.started_at
17971
18341
  )
17972
- ) >= ?`
17973
- ).get(liveThreshold).n;
17974
- const toolCallsToday = this.db.prepare(
18342
+ ) >= ?`,
18343
+ [liveThreshold]
18344
+ );
18345
+ const toolCallsToday = countScalar(
18346
+ this.db,
17975
18347
  `SELECT count(*) AS n FROM audit_events
17976
- WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`
17977
- ).get(startMs, endMs).n;
17978
- const findingsToday = this.db.prepare(
18348
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18349
+ [startMs, endMs]
18350
+ );
18351
+ const findingsToday = countScalar(
18352
+ this.db,
17979
18353
  `SELECT count(*) AS n FROM inspection_findings f
17980
18354
  JOIN audit_events e ON e.id = f.audit_event_id
17981
- WHERE e.started_at >= ? AND e.started_at < ?`
17982
- ).get(startMs, endMs).n;
17983
- const egressToday = this.db.prepare(
18355
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18356
+ [startMs, endMs]
18357
+ );
18358
+ const egressToday = countScalar(
18359
+ this.db,
17984
18360
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
17985
18361
  FROM audit_events
17986
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
17987
- ).get(startMs, endMs).n;
18362
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18363
+ [startMs, endMs]
18364
+ );
17988
18365
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
17989
18366
  }
17990
18367
  listSessions(query) {
@@ -18006,7 +18383,7 @@ var SqliteActivityRepository = class {
18006
18383
  conditions.push("started_at <= ?");
18007
18384
  params.push(toMs);
18008
18385
  if (query.q) {
18009
- const pattern = `%${escapeLikePattern(query.q)}%`;
18386
+ const pattern = containsPattern(query.q);
18010
18387
  conditions.push(
18011
18388
  `(content LIKE ? ESCAPE '\\'
18012
18389
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18020,13 +18397,21 @@ var SqliteActivityRepository = class {
18020
18397
  );
18021
18398
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
18022
18399
  }
18400
+ const emptyCount = countScalar(
18401
+ this.db,
18402
+ `SELECT count(*) AS n FROM audit_events
18403
+ WHERE ${[...conditions, `NOT ${HAS_ACTIVITY}`].join(" AND ")}`,
18404
+ params
18405
+ );
18406
+ if (query.excludeEmpty) conditions.push(HAS_ACTIVITY);
18023
18407
  if (cursor) {
18024
18408
  conditions.push("(started_at < ? OR (started_at = ? AND id < ?))");
18025
18409
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18026
18410
  }
18027
18411
  const limit = query.limit;
18028
- const rows = this.db.prepare(
18029
- `SELECT id,
18412
+ const rows = allRows(
18413
+ this.db.prepare(
18414
+ `SELECT id,
18030
18415
  json_extract(attributes, '$.harness') AS harness,
18031
18416
  content AS title,
18032
18417
  json_extract(attributes, '$.project') AS project,
@@ -18039,7 +18424,9 @@ var SqliteActivityRepository = class {
18039
18424
  WHERE ${conditions.join(" AND ")}
18040
18425
  ORDER BY started_at DESC, id DESC
18041
18426
  LIMIT ?`
18042
- ).all(...params, limit + 1);
18427
+ ),
18428
+ [...params, limit + 1]
18429
+ );
18043
18430
  const hasMore = rows.length > limit;
18044
18431
  const page = hasMore ? rows.slice(0, limit) : rows;
18045
18432
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18053,11 +18440,12 @@ var SqliteActivityRepository = class {
18053
18440
  );
18054
18441
  const last = page[page.length - 1];
18055
18442
  const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
18056
- return Promise.resolve({ items, nextCursor });
18443
+ return Promise.resolve({ items, nextCursor, emptyCount });
18057
18444
  }
18058
18445
  getSession(sessionId) {
18059
- const rootRow = this.db.prepare(
18060
- `SELECT id,
18446
+ const rootRow = getRow(
18447
+ this.db.prepare(
18448
+ `SELECT id,
18061
18449
  json_extract(attributes, '$.harness') AS harness,
18062
18450
  content AS title,
18063
18451
  json_extract(attributes, '$.project') AS project,
@@ -18074,47 +18462,66 @@ var SqliteActivityRepository = class {
18074
18462
  FROM audit_events
18075
18463
  WHERE id = ? AND event_type = 'session'
18076
18464
  LIMIT 1`
18077
- ).get(sessionId);
18465
+ ),
18466
+ [sessionId]
18467
+ );
18078
18468
  if (!rootRow) return Promise.resolve(null);
18079
- const timelineRows = this.db.prepare(
18080
- `SELECT ${TIMELINE_COLUMNS}
18469
+ const timelineRows = allRows(
18470
+ this.db.prepare(
18471
+ `SELECT ${TIMELINE_COLUMNS}
18081
18472
  FROM audit_events
18082
18473
  WHERE id = ? OR root_session_id = ?
18083
18474
  ORDER BY started_at ASC, id ASC`
18084
- ).all(sessionId, sessionId);
18475
+ ),
18476
+ [sessionId, sessionId]
18477
+ );
18085
18478
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18086
- const tokenRow = this.db.prepare(
18087
- `SELECT
18479
+ const tokenRow = getRow(
18480
+ this.db.prepare(
18481
+ `SELECT
18088
18482
  coalesce(sum(input_tokens), 0) AS input,
18089
18483
  coalesce(sum(output_tokens), 0) AS output,
18090
18484
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18091
18485
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18092
18486
  FROM audit_events
18093
18487
  WHERE root_session_id = ? AND event_type = 'llm_call'`
18094
- ).get(sessionId);
18095
- const primaryModel = this.db.prepare(
18096
- `SELECT model, provider FROM audit_events
18488
+ ),
18489
+ [sessionId]
18490
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
18491
+ const primaryModel = getRow(
18492
+ this.db.prepare(
18493
+ `SELECT model, provider FROM audit_events
18097
18494
  WHERE root_session_id = ? AND event_type = 'llm_call'
18098
18495
  ORDER BY started_at ASC, id ASC
18099
18496
  LIMIT 1`
18100
- ).get(sessionId);
18101
- const toolRows = this.db.prepare(
18102
- `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18497
+ ),
18498
+ [sessionId]
18499
+ );
18500
+ const toolRows = allRows(
18501
+ this.db.prepare(
18502
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18103
18503
  count(*) AS n
18104
18504
  FROM audit_events
18105
18505
  WHERE root_session_id = ? AND event_type = 'tool_call'
18106
18506
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
18107
- ).all(sessionId);
18108
- const modelRows = this.db.prepare(
18109
- `SELECT DISTINCT model FROM audit_events
18507
+ ),
18508
+ [sessionId]
18509
+ );
18510
+ const modelRows = allRows(
18511
+ this.db.prepare(
18512
+ `SELECT DISTINCT model FROM audit_events
18110
18513
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18111
18514
  ORDER BY model`
18112
- ).all(sessionId);
18515
+ ),
18516
+ [sessionId]
18517
+ );
18113
18518
  const derivedModels = modelRows.map((r) => r.model);
18114
- const commits = this.db.prepare(
18519
+ const commits = countScalar(
18520
+ this.db,
18115
18521
  `SELECT count(*) AS n FROM audit_events
18116
- WHERE root_session_id = ? AND event_type = 'commit'`
18117
- ).get(sessionId).n;
18522
+ WHERE root_session_id = ? AND event_type = 'commit'`,
18523
+ [sessionId]
18524
+ );
18118
18525
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18119
18526
  turns: 0,
18120
18527
  findings: 0,
@@ -18181,7 +18588,10 @@ var SqliteActivityRepository = class {
18181
18588
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18182
18589
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18183
18590
  );
18184
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
18591
+ const rows = allRows(
18592
+ stmt,
18593
+ fromMs === void 0 ? void 0 : [fromMs]
18594
+ );
18185
18595
  const seen = /* @__PURE__ */ new Set();
18186
18596
  for (const row of rows) seen.add(toHarness(row.harness));
18187
18597
  return Promise.resolve([...seen]);
@@ -18204,23 +18614,23 @@ var SqliteActivityRepository = class {
18204
18614
  conditions.push("started_at >= ?");
18205
18615
  params.push(opts.fromMs);
18206
18616
  }
18207
- const rows = this.db.prepare(
18208
- `SELECT root_session_id AS sessionId, attributes
18617
+ const rows = allRows(
18618
+ this.db.prepare(
18619
+ `SELECT root_session_id AS sessionId, attributes
18209
18620
  FROM audit_events
18210
18621
  WHERE ${conditions.join(" AND ")}`
18211
- ).all(...params);
18212
- const leaves = [];
18213
- for (const row of rows) {
18214
- if (row.sessionId === null) continue;
18215
- try {
18216
- leaves.push({
18217
- sessionId: row.sessionId,
18218
- attributes: JSON.parse(row.attributes)
18219
- });
18220
- } catch {
18221
- }
18222
- }
18223
- return leaves;
18622
+ ),
18623
+ params
18624
+ );
18625
+ return mapRowsTolerant(
18626
+ rows.filter(
18627
+ (row) => row.sessionId !== null
18628
+ ),
18629
+ (row) => ({
18630
+ sessionId: row.sessionId,
18631
+ attributes: JSON.parse(row.attributes)
18632
+ })
18633
+ );
18224
18634
  }
18225
18635
  /**
18226
18636
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -18234,57 +18644,72 @@ var SqliteActivityRepository = class {
18234
18644
  );
18235
18645
  if (sessionIds.length === 0) return result;
18236
18646
  const inClause = placeholders(sessionIds.length);
18237
- const lastActivityRows = this.db.prepare(
18238
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18647
+ const lastActivityRows = allRows(
18648
+ this.db.prepare(
18649
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18239
18650
  WHERE root_session_id IN (${inClause})
18240
18651
  GROUP BY root_session_id`
18241
- ).all(...sessionIds);
18652
+ ),
18653
+ sessionIds
18654
+ );
18242
18655
  for (const row of lastActivityRows) {
18243
18656
  if (row.id === null) continue;
18244
18657
  const entry = result.get(row.id);
18245
18658
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
18246
18659
  }
18247
- const turnsRows = this.db.prepare(
18248
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18660
+ const turnsRows = allRows(
18661
+ this.db.prepare(
18662
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18249
18663
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
18250
18664
  GROUP BY root_session_id`
18251
- ).all(...sessionIds);
18665
+ ),
18666
+ sessionIds
18667
+ );
18252
18668
  for (const row of turnsRows) {
18253
18669
  if (row.id === null) continue;
18254
18670
  const entry = result.get(row.id);
18255
18671
  if (entry) entry.turns = row.n;
18256
18672
  }
18257
- const runKeyRows = this.db.prepare(
18258
- `SELECT root_session_id AS id,
18673
+ const runKeyRows = allRows(
18674
+ this.db.prepare(
18675
+ `SELECT root_session_id AS id,
18259
18676
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
18260
18677
  FROM audit_events
18261
18678
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
18262
18679
  AND json_extract(attributes, '$.run_key') IS NOT NULL
18263
18680
  GROUP BY root_session_id`
18264
- ).all(...sessionIds);
18681
+ ),
18682
+ sessionIds
18683
+ );
18265
18684
  for (const row of runKeyRows) {
18266
18685
  if (row.id === null) continue;
18267
18686
  const entry = result.get(row.id);
18268
18687
  if (entry) entry.turns = Math.max(entry.turns, row.n);
18269
18688
  }
18270
- const findingsRows = this.db.prepare(
18271
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18689
+ const findingsRows = allRows(
18690
+ this.db.prepare(
18691
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18272
18692
  JOIN audit_events e ON e.id = f.audit_event_id
18273
18693
  WHERE e.root_session_id IN (${inClause})
18274
18694
  GROUP BY e.root_session_id`
18275
- ).all(...sessionIds);
18695
+ ),
18696
+ sessionIds
18697
+ );
18276
18698
  for (const row of findingsRows) {
18277
18699
  if (row.id === null) continue;
18278
18700
  const entry = result.get(row.id);
18279
18701
  if (entry) entry.findings = row.n;
18280
18702
  }
18281
- const sharesRows = this.db.prepare(
18282
- `SELECT root_session_id AS id,
18703
+ const sharesRows = allRows(
18704
+ this.db.prepare(
18705
+ `SELECT root_session_id AS id,
18283
18706
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
18284
18707
  FROM audit_events
18285
18708
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
18286
18709
  GROUP BY root_session_id`
18287
- ).all(...sessionIds);
18710
+ ),
18711
+ sessionIds
18712
+ );
18288
18713
  for (const row of sharesRows) {
18289
18714
  if (row.id === null) continue;
18290
18715
  const entry = result.get(row.id);
@@ -18334,33 +18759,28 @@ var SqliteAuditEventsRepository = class {
18334
18759
  // the caller fails open and drops the whole pass — recovered idempotently on the
18335
18760
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
18336
18761
  runInTransaction(fn) {
18337
- this.db.exec("BEGIN");
18338
- try {
18339
- fn();
18340
- this.db.exec("COMMIT");
18341
- } catch (err) {
18342
- this.db.exec("ROLLBACK");
18343
- throw err;
18344
- }
18762
+ withTransaction(this.db, fn);
18345
18763
  }
18346
18764
  insertAuditEvent(input) {
18347
18765
  const row = toAuditEventRow(input);
18348
- this.insertStmt.run({
18349
- id: row.id,
18350
- parentId: row.parentId ?? null,
18351
- rootSessionId: row.rootSessionId ?? null,
18352
- eventType: row.eventType,
18353
- hostId: row.hostId ?? null,
18354
- harnessId: row.harnessId ?? null,
18355
- sourceProjectId: row.sourceProjectId ?? null,
18356
- startedAt: row.startedAt,
18357
- endedAt: row.endedAt ?? null,
18358
- severity: row.severity ?? null,
18359
- priority: row.priority ?? null,
18360
- content: row.content ?? null,
18361
- contentHash: row.contentHash ?? null,
18362
- attributes: row.attributes ?? null
18363
- });
18766
+ this.insertStmt.run(
18767
+ bindParams({
18768
+ id: row.id,
18769
+ parentId: row.parentId,
18770
+ rootSessionId: row.rootSessionId,
18771
+ eventType: row.eventType,
18772
+ hostId: row.hostId,
18773
+ harnessId: row.harnessId,
18774
+ sourceProjectId: row.sourceProjectId,
18775
+ startedAt: row.startedAt,
18776
+ endedAt: row.endedAt,
18777
+ severity: row.severity,
18778
+ priority: row.priority,
18779
+ content: row.content,
18780
+ contentHash: row.contentHash,
18781
+ attributes: row.attributes
18782
+ })
18783
+ );
18364
18784
  }
18365
18785
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18366
18786
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -18374,22 +18794,24 @@ var SqliteAuditEventsRepository = class {
18374
18794
  const startedAt = isoToEpochMillis(input.startedAt);
18375
18795
  if (!Number.isFinite(startedAt)) return;
18376
18796
  const id = llmCallId(input.sessionId, input.messageId);
18377
- this.upsertLlmCallStmt.run({
18378
- id,
18379
- parentId: input.parentId,
18380
- rootSessionId: input.rootSessionId,
18381
- eventType: "llm_call",
18382
- hostId: null,
18383
- harnessId: null,
18384
- sourceProjectId: null,
18385
- startedAt,
18386
- endedAt: null,
18387
- severity: null,
18388
- priority: null,
18389
- content: null,
18390
- contentHash: null,
18391
- attributes: JSON.stringify(input.attributes)
18392
- });
18797
+ this.upsertLlmCallStmt.run(
18798
+ bindParams({
18799
+ id,
18800
+ parentId: input.parentId,
18801
+ rootSessionId: input.rootSessionId,
18802
+ eventType: "llm_call",
18803
+ hostId: null,
18804
+ harnessId: null,
18805
+ sourceProjectId: null,
18806
+ startedAt,
18807
+ endedAt: null,
18808
+ severity: null,
18809
+ priority: null,
18810
+ content: null,
18811
+ contentHash: null,
18812
+ attributes: JSON.stringify(input.attributes)
18813
+ })
18814
+ );
18393
18815
  }
18394
18816
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
18395
18817
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -18413,25 +18835,29 @@ var SqliteAuditEventsRepository = class {
18413
18835
  const startedAt = isoToEpochMillis(input.startedAt);
18414
18836
  if (!Number.isFinite(startedAt)) return;
18415
18837
  const id = toolCallId(input.sessionId, input.toolUseId);
18416
- this.insertStmt.run({
18417
- id,
18418
- parentId: input.parentId,
18419
- rootSessionId: input.rootSessionId,
18420
- eventType: "tool_call",
18421
- hostId: null,
18422
- harnessId: null,
18423
- sourceProjectId: null,
18424
- startedAt,
18425
- endedAt: null,
18426
- severity: null,
18427
- priority: null,
18428
- content: null,
18429
- contentHash: null,
18430
- attributes: JSON.stringify(input.attributes)
18431
- });
18838
+ this.insertStmt.run(
18839
+ bindParams({
18840
+ id,
18841
+ parentId: input.parentId,
18842
+ rootSessionId: input.rootSessionId,
18843
+ eventType: "tool_call",
18844
+ hostId: null,
18845
+ harnessId: null,
18846
+ sourceProjectId: null,
18847
+ startedAt,
18848
+ endedAt: null,
18849
+ severity: null,
18850
+ priority: null,
18851
+ content: null,
18852
+ contentHash: null,
18853
+ attributes: JSON.stringify(input.attributes)
18854
+ })
18855
+ );
18432
18856
  }
18433
18857
  findById(id) {
18434
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
18858
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
18859
+ id
18860
+ });
18435
18861
  }
18436
18862
  // Read the `provider` snapshotted onto a session root's attributes.
18437
18863
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -18441,14 +18867,8 @@ var SqliteAuditEventsRepository = class {
18441
18867
  sessionProvider(sessionId) {
18442
18868
  const row = this.findById(sessionId);
18443
18869
  if (!row?.attributes) return void 0;
18444
- try {
18445
- const parsed = JSON.parse(row.attributes);
18446
- if (typeof parsed === "object" && parsed !== null) {
18447
- const provider = parsed.provider;
18448
- if (typeof provider === "string") return provider;
18449
- }
18450
- } catch {
18451
- }
18870
+ const provider = parseJsonObject(row.attributes)?.provider;
18871
+ if (typeof provider === "string") return provider;
18452
18872
  return void 0;
18453
18873
  }
18454
18874
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -18457,11 +18877,13 @@ var SqliteAuditEventsRepository = class {
18457
18877
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
18458
18878
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
18459
18879
  llmCallLeaves() {
18460
- return this.db.prepare(
18461
- `SELECT root_session_id AS sessionId, attributes
18880
+ return allRows(
18881
+ this.db.prepare(
18882
+ `SELECT root_session_id AS sessionId, attributes
18462
18883
  FROM audit_events
18463
18884
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
18464
- ).all();
18885
+ )
18886
+ );
18465
18887
  }
18466
18888
  };
18467
18889
 
@@ -18480,16 +18902,29 @@ var SqliteClassifiedDataRepository = class {
18480
18902
  upsert(input) {
18481
18903
  const id = classifiedDataId(input.class);
18482
18904
  const row = toClassifiedDataRow(input, id);
18483
- this.insertStmt.run({
18484
- id: row.id,
18485
- class: row.class,
18486
- label: row.label ?? null,
18487
- attributes: row.attributes ?? null
18488
- });
18905
+ this.insertStmt.run(
18906
+ bindParams({
18907
+ id: row.id,
18908
+ class: row.class,
18909
+ label: row.label,
18910
+ attributes: row.attributes
18911
+ })
18912
+ );
18489
18913
  return id;
18490
18914
  }
18491
18915
  };
18492
18916
 
18917
+ // ../../packages/persistence/src/repositories/config-scan.ts
18918
+ function latestConfigScan(db) {
18919
+ return getRow(
18920
+ db.prepare(
18921
+ `SELECT id, started_at, attributes FROM audit_events
18922
+ WHERE event_type = 'config_scan'
18923
+ ORDER BY started_at DESC, id DESC LIMIT 1`
18924
+ )
18925
+ );
18926
+ }
18927
+
18493
18928
  // ../../packages/persistence/src/repositories/config-inventory.ts
18494
18929
  var SqliteConfigInventoryRepository = class {
18495
18930
  constructor(db) {
@@ -18497,7 +18932,7 @@ var SqliteConfigInventoryRepository = class {
18497
18932
  }
18498
18933
  db;
18499
18934
  report() {
18500
- const scan2 = this.latestScan();
18935
+ const scan2 = latestConfigScan(this.db);
18501
18936
  if (!scan2) {
18502
18937
  return {
18503
18938
  scannedAt: null,
@@ -18508,17 +18943,23 @@ var SqliteConfigInventoryRepository = class {
18508
18943
  topics: []
18509
18944
  };
18510
18945
  }
18511
- const rows = this.db.prepare(
18512
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18946
+ const rows = allRows(
18947
+ this.db.prepare(
18948
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18513
18949
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
18514
18950
  ORDER BY object_type, title`
18515
- ).all({ startedAt: scan2.started_at });
18516
- const findings = this.db.prepare(
18517
- `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18951
+ ),
18952
+ { startedAt: scan2.started_at }
18953
+ );
18954
+ const findings = allRows(
18955
+ this.db.prepare(
18956
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18518
18957
  FROM inspection_findings f
18519
18958
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
18520
18959
  WHERE f.audit_event_id = :scanId`
18521
- ).all({ scanId: scan2.id });
18960
+ ),
18961
+ { scanId: scan2.id }
18962
+ );
18522
18963
  const skills = [];
18523
18964
  const hooks = [];
18524
18965
  const mcpServers = [];
@@ -18547,7 +18988,9 @@ var SqliteConfigInventoryRepository = class {
18547
18988
  // schema note); an override whose asset is gone simply never matches. A row
18548
18989
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
18549
18990
  trustOverrides() {
18550
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
18991
+ const rows = allRows(
18992
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
18993
+ );
18551
18994
  const map2 = /* @__PURE__ */ new Map();
18552
18995
  for (const row of rows) {
18553
18996
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -18556,13 +18999,6 @@ var SqliteConfigInventoryRepository = class {
18556
18999
  }
18557
19000
  return map2;
18558
19001
  }
18559
- latestScan() {
18560
- return this.db.prepare(
18561
- `SELECT id, started_at, attributes FROM audit_events
18562
- WHERE event_type = 'config_scan'
18563
- ORDER BY started_at DESC, id DESC LIMIT 1`
18564
- ).get();
18565
- }
18566
19002
  };
18567
19003
  function toSkillItem(row, bag) {
18568
19004
  const item = {
@@ -18673,22 +19109,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
18673
19109
  return topics;
18674
19110
  }
18675
19111
  function countScanErrors(attributes) {
18676
- if (!attributes) return 0;
18677
- try {
18678
- const parsed = JSON.parse(attributes);
18679
- const errors = parsed?.errors;
18680
- return typeof errors === "number" ? errors : 0;
18681
- } catch {
18682
- return 0;
18683
- }
19112
+ const errors = parseJsonObject(attributes)?.errors;
19113
+ return typeof errors === "number" ? errors : 0;
18684
19114
  }
18685
19115
  function parseBag(raw) {
18686
- try {
18687
- const parsed = JSON.parse(raw);
18688
- if (typeof parsed === "object" && parsed !== null) return parsed;
18689
- } catch {
18690
- }
18691
- return void 0;
19116
+ return parseJsonObject(raw);
18692
19117
  }
18693
19118
  function str(value) {
18694
19119
  return typeof value === "string" ? value : void 0;
@@ -18697,12 +19122,7 @@ function str(value) {
18697
19122
  // ../../packages/persistence/src/repositories/detections.ts
18698
19123
  var DAY_MS2 = 864e5;
18699
19124
  function parseRules(rulesJson) {
18700
- let raw;
18701
- try {
18702
- raw = JSON.parse(rulesJson);
18703
- } catch {
18704
- return [];
18705
- }
19125
+ const raw = safeJson(rulesJson, []);
18706
19126
  if (!Array.isArray(raw)) return [];
18707
19127
  const rules = [];
18708
19128
  for (const entry of raw) {
@@ -18721,11 +19141,13 @@ var SqliteDetectionsRepository = class {
18721
19141
  db;
18722
19142
  now;
18723
19143
  listDetections(query) {
18724
- const rows = this.db.prepare(
18725
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19144
+ const rows = allRows(
19145
+ this.db.prepare(
19146
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18726
19147
  rules_json AS rulesJson
18727
19148
  FROM installed_packs`
18728
- ).all();
19149
+ )
19150
+ );
18729
19151
  const available = this.availableByPack();
18730
19152
  const summaries = rows.map((r) => {
18731
19153
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -18734,7 +19156,7 @@ var SqliteDetectionsRepository = class {
18734
19156
  packId: r.packId,
18735
19157
  version: r.version,
18736
19158
  name: r.name,
18737
- enabled: r.enabled === 1,
19159
+ enabled: intToBool(r.enabled),
18738
19160
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
18739
19161
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
18740
19162
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -18751,19 +19173,23 @@ var SqliteDetectionsRepository = class {
18751
19173
  // available_packs keyed by the "namespace/packId" slug (one read per list /
18752
19174
  // detail call; the table is a handful of rows).
18753
19175
  availableByPack() {
18754
- const rows = this.db.prepare(
18755
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
19176
+ const rows = allRows(
19177
+ this.db.prepare(
19178
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18756
19179
  FROM available_packs`
18757
- ).all();
19180
+ )
19181
+ );
18758
19182
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
18759
19183
  }
18760
19184
  getDetectionStats() {
18761
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
19185
+ const rows = allRows(
19186
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
19187
+ );
18762
19188
  let rules = 0;
18763
19189
  let active = 0;
18764
19190
  const ruleIds = /* @__PURE__ */ new Set();
18765
19191
  for (const r of rows) {
18766
- if (r.enabled === 1) active += 1;
19192
+ if (intToBool(r.enabled)) active += 1;
18767
19193
  const parsed = parseRules(r.rulesJson);
18768
19194
  rules += parsed.length;
18769
19195
  for (const rule of parsed) {
@@ -18781,12 +19207,15 @@ var SqliteDetectionsRepository = class {
18781
19207
  const parts = splitDetectionId(id);
18782
19208
  if (!parts) return Promise.resolve(null);
18783
19209
  const { namespace, packId } = parts;
18784
- const row = this.db.prepare(
18785
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19210
+ const row = getRow(
19211
+ this.db.prepare(
19212
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18786
19213
  rules_json AS rulesJson, updated_at AS updatedAt
18787
19214
  FROM installed_packs
18788
19215
  WHERE namespace = ? AND pack_id = ?`
18789
- ).get(namespace, packId);
19216
+ ),
19217
+ [namespace, packId]
19218
+ );
18790
19219
  if (!row) return Promise.resolve(null);
18791
19220
  const rules = parseRules(row.rulesJson);
18792
19221
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -18804,7 +19233,7 @@ var SqliteDetectionsRepository = class {
18804
19233
  packId: row.packId,
18805
19234
  version: row.version,
18806
19235
  name: row.name,
18807
- enabled: row.enabled === 1,
19236
+ enabled: intToBool(row.enabled),
18808
19237
  rules,
18809
19238
  updatedAt: new Date(row.updatedAt),
18810
19239
  policyId: row.policyId
@@ -18819,13 +19248,14 @@ var SqliteDetectionsRepository = class {
18819
19248
  countFindingsLast30d(ruleIds) {
18820
19249
  if (ruleIds.length === 0) return 0;
18821
19250
  const since = this.now() - 30 * DAY_MS2;
18822
- const placeholders2 = ruleIds.map(() => "?").join(", ");
18823
- const row = this.db.prepare(
18824
- `SELECT count(*) AS c
19251
+ const inClause = placeholders(ruleIds.length);
19252
+ return countScalar(
19253
+ this.db,
19254
+ `SELECT count(*) AS n
18825
19255
  FROM findings f JOIN events e ON e.id = f.event_id
18826
- WHERE e.occurred_at >= ? AND f.rule_id IN (${placeholders2})`
18827
- ).get(since, ...ruleIds);
18828
- return row.c;
19256
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19257
+ [since, ...ruleIds]
19258
+ );
18829
19259
  }
18830
19260
  };
18831
19261
 
@@ -18842,16 +19272,17 @@ var SqliteEventsRepository = class {
18842
19272
  insertStmt;
18843
19273
  insertEvent(event) {
18844
19274
  const row = toEventRow(event);
18845
- this.insertStmt.run({
18846
- id: row.id,
18847
- sourceTool: row.sourceTool,
18848
- kind: row.kind,
18849
- occurredAt: row.occurredAt,
18850
- contentHash: row.contentHash,
18851
- content: row.content,
18852
- // exactOptionalPropertyTypes: the column is nullable, never undefined.
18853
- metadata: row.metadata ?? null
18854
- });
19275
+ this.insertStmt.run(
19276
+ bindParams({
19277
+ id: row.id,
19278
+ sourceTool: row.sourceTool,
19279
+ kind: row.kind,
19280
+ occurredAt: row.occurredAt,
19281
+ contentHash: row.contentHash,
19282
+ content: row.content,
19283
+ metadata: row.metadata
19284
+ })
19285
+ );
18855
19286
  }
18856
19287
  // Every recorded event's content hash — the historical backfill loads this once
18857
19288
  // to skip transcript messages it has already stored, so re-running the scan
@@ -18859,13 +19290,23 @@ var SqliteEventsRepository = class {
18859
19290
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
18860
19291
  // async EventsReadPort contract.
18861
19292
  contentHashes() {
18862
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19293
+ const rows = allRows(
19294
+ this.db.prepare("SELECT content_hash FROM events")
19295
+ );
18863
19296
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
18864
19297
  }
18865
19298
  };
18866
19299
 
18867
19300
  // ../../packages/persistence/src/repositories/exceptions.ts
18868
19301
  import { randomUUID } from "crypto";
19302
+
19303
+ // ../../packages/persistence/src/internal/sqlite-errors.ts
19304
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19305
+ function isUniqueConstraintError(err) {
19306
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19307
+ }
19308
+
19309
+ // ../../packages/persistence/src/repositories/exceptions.ts
18869
19310
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
18870
19311
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
18871
19312
  var DuplicateActiveExceptionError = class extends Error {
@@ -18886,10 +19327,6 @@ var AmbiguousExceptionIdError = class extends Error {
18886
19327
  this.name = "AmbiguousExceptionIdError";
18887
19328
  }
18888
19329
  };
18889
- var SQLITE_CONSTRAINT_UNIQUE = 2067;
18890
- function isUniqueConstraintError(err) {
18891
- return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
18892
- }
18893
19330
  var ACTIVE_PREDICATE = `revoked_at IS NULL
18894
19331
  AND (expires_at IS NULL OR expires_at > :now)
18895
19332
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -18945,10 +19382,11 @@ var SqliteExceptionsRepository = class {
18945
19382
  this.insertExceptionRow(id, input, now);
18946
19383
  } catch (err) {
18947
19384
  if (!isUniqueConstraintError(err)) throw err;
18948
- this.db.exec("BEGIN IMMEDIATE");
18949
- try {
18950
- const superseded = this.db.prepare(
18951
- `UPDATE exceptions
19385
+ withTransaction(
19386
+ this.db,
19387
+ () => {
19388
+ const superseded = this.db.prepare(
19389
+ `UPDATE exceptions
18952
19390
  SET revoked_at = :now, revoked_by = :revokedBy,
18953
19391
  revoke_reason = 'superseded by a new grant for the same value',
18954
19392
  updated_at = :now
@@ -18956,24 +19394,27 @@ var SqliteExceptionsRepository = class {
18956
19394
  AND key_version = :keyVersion AND revoked_at IS NULL
18957
19395
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
18958
19396
  OR (max_uses IS NOT NULL AND use_count >= max_uses))`
18959
- ).run({
18960
- now,
18961
- revokedBy: input.createdBy,
18962
- ruleId: input.ruleId,
18963
- valueFingerprint: input.valueFingerprint,
18964
- keyVersion: input.keyVersion
18965
- });
18966
- if (Number(superseded.changes) !== 1) {
18967
- throw new DuplicateActiveExceptionError(input.ruleId);
18968
- }
18969
- this.insertExceptionRow(id, input, now);
18970
- this.db.exec("COMMIT");
18971
- } catch (retryErr) {
18972
- this.db.exec("ROLLBACK");
18973
- throw retryErr;
18974
- }
19397
+ ).run({
19398
+ now,
19399
+ revokedBy: input.createdBy,
19400
+ ruleId: input.ruleId,
19401
+ valueFingerprint: input.valueFingerprint,
19402
+ keyVersion: input.keyVersion
19403
+ });
19404
+ if (Number(superseded.changes) !== 1) {
19405
+ throw new DuplicateActiveExceptionError(input.ruleId);
19406
+ }
19407
+ this.insertExceptionRow(id, input, now);
19408
+ },
19409
+ "IMMEDIATE"
19410
+ );
19411
+ }
19412
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
19413
+ id
19414
+ });
19415
+ if (row === void 0) {
19416
+ throw new Error("exception row not found immediately after insert");
18975
19417
  }
18976
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
18977
19418
  return parseExceptionRow(row);
18978
19419
  }
18979
19420
  insertExceptionRow(id, input, now) {
@@ -19011,14 +19452,11 @@ var SqliteExceptionsRepository = class {
19011
19452
  */
19012
19453
  list(opts) {
19013
19454
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19014
- const rows = this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`).all(opts?.includeTerminal ? {} : { now: Date.now() });
19015
- const exceptions = [];
19016
- for (const row of rows) {
19017
- try {
19018
- exceptions.push(parseExceptionRow(row));
19019
- } catch {
19020
- }
19021
- }
19455
+ const rows = allRows(
19456
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19457
+ opts?.includeTerminal ? {} : { now: Date.now() }
19458
+ );
19459
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19022
19460
  return Promise.resolve(exceptions);
19023
19461
  }
19024
19462
  /**
@@ -19028,7 +19466,12 @@ var SqliteExceptionsRepository = class {
19028
19466
  */
19029
19467
  getByIdPrefix(prefix) {
19030
19468
  if (prefix.length === 0) return Promise.resolve(void 0);
19031
- const rows = this.db.prepare(String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`).all({ pattern: `${escapeLikePattern(prefix)}%` });
19469
+ const rows = allRows(
19470
+ this.db.prepare(
19471
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
19472
+ ),
19473
+ { pattern: `${escapeLikePattern(prefix)}%` }
19474
+ );
19032
19475
  if (rows.length > 1) {
19033
19476
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19034
19477
  }
@@ -19070,30 +19513,27 @@ var SqliteExceptionsRepository = class {
19070
19513
  * a different (rotated-away) key never match, so they are excluded at read.
19071
19514
  */
19072
19515
  activeBundleEntries(keyVersion, now = Date.now()) {
19073
- const rows = this.db.prepare(
19074
- `SELECT * FROM exceptions
19516
+ const rows = allRows(
19517
+ this.db.prepare(
19518
+ `SELECT * FROM exceptions
19075
19519
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19076
19520
  ORDER BY created_at DESC, rowid DESC`
19077
- ).all({ keyVersion, now });
19078
- const entries = [];
19079
- for (const row of rows) {
19080
- try {
19081
- const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19082
- entries.push(
19083
- ExceptionBundleEntry.parse({
19084
- id: row.id,
19085
- ruleId: row.rule_id,
19086
- valueFingerprint: row.value_fingerprint,
19087
- keyVersion: row.key_version,
19088
- expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19089
- maxUses: row.max_uses,
19090
- useCount: row.use_count,
19091
- conditions
19092
- })
19093
- );
19094
- } catch {
19095
- }
19096
- }
19521
+ ),
19522
+ { keyVersion, now }
19523
+ );
19524
+ const entries = mapRowsTolerant(rows, (row) => {
19525
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19526
+ return ExceptionBundleEntry.parse({
19527
+ id: row.id,
19528
+ ruleId: row.rule_id,
19529
+ valueFingerprint: row.value_fingerprint,
19530
+ keyVersion: row.key_version,
19531
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19532
+ maxUses: row.max_uses,
19533
+ useCount: row.use_count,
19534
+ conditions
19535
+ });
19536
+ });
19097
19537
  return Promise.resolve(entries);
19098
19538
  }
19099
19539
  /**
@@ -19120,11 +19560,14 @@ var SqliteExceptionsRepository = class {
19120
19560
  }
19121
19561
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19122
19562
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19123
- const rows = this.db.prepare(
19124
- `SELECT * FROM blocked_detections
19563
+ const rows = allRows(
19564
+ this.db.prepare(
19565
+ `SELECT * FROM blocked_detections
19125
19566
  WHERE blocked_at > :cutoff
19126
19567
  ORDER BY blocked_at DESC, rowid DESC`
19127
- ).all({ cutoff: Date.now() - windowMs });
19568
+ ),
19569
+ { cutoff: Date.now() - windowMs }
19570
+ );
19128
19571
  return Promise.resolve(
19129
19572
  rows.map((row) => ({
19130
19573
  reference: row.reference,
@@ -19204,7 +19647,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
19204
19647
  )`;
19205
19648
 
19206
19649
  // ../../packages/persistence/src/repositories/findings.ts
19207
- var GROUP_ROW_CAP = 2e3;
19650
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
19651
+ var CONCAT_SEP = ",";
19652
+ var TUPLE_SEP = "|";
19653
+ function splitConcat(value) {
19654
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
19655
+ }
19208
19656
  function deriveInstanceStatus(row) {
19209
19657
  return deriveFindingStatus({
19210
19658
  kind: row.kind,
@@ -19272,13 +19720,16 @@ var SqliteFindingsRepository = class {
19272
19720
  }
19273
19721
  recentFindings(opts) {
19274
19722
  const limit = opts?.limit ?? 50;
19275
- const rows = this.db.prepare(
19276
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19723
+ const rows = allRows(
19724
+ this.db.prepare(
19725
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19277
19726
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19278
19727
  FROM findings f JOIN events e ON e.id = f.event_id
19279
19728
  ORDER BY e.occurred_at DESC, f.rowid DESC
19280
19729
  LIMIT :limit`
19281
- ).all({ limit });
19730
+ ),
19731
+ { limit }
19732
+ );
19282
19733
  return Promise.resolve(
19283
19734
  rows.map((r) => ({
19284
19735
  id: r.id,
@@ -19295,26 +19746,94 @@ var SqliteFindingsRepository = class {
19295
19746
  }))
19296
19747
  );
19297
19748
  }
19749
+ /** Live-enforced findings recorded for one session — a bare COUNT over the
19750
+ * session-stamped events (served by idx_events_session_id), so the Activity
19751
+ * page can label its findings link without the grouped pipeline. */
19752
+ sessionFindingsCount(sessionId) {
19753
+ if (!sessionId) return Promise.resolve(0);
19754
+ return Promise.resolve(
19755
+ countScalar(
19756
+ this.db,
19757
+ `SELECT count(*) AS n FROM findings f
19758
+ JOIN events e ON e.id = f.event_id
19759
+ WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
19760
+ { sessionId }
19761
+ )
19762
+ );
19763
+ }
19764
+ /** Per-rule transcript firing tally for one session — reads the OTHER finding
19765
+ * store (inspection_findings, keyed to audit_events): every detection the
19766
+ * transcript pass recorded, counted per firing rather than per unique value.
19767
+ * Rides on session-scoped grouped responses so the findings view can
19768
+ * reconcile the Activity page's tally with the deduped groups it lists. */
19769
+ sessionFirings(sessionId) {
19770
+ return Object.fromEntries(
19771
+ countBy(
19772
+ this.db,
19773
+ `SELECT d.rule_id AS k, count(*) AS n
19774
+ FROM inspection_findings f
19775
+ JOIN audit_events e ON e.id = f.audit_event_id
19776
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19777
+ WHERE e.root_session_id = :sessionId
19778
+ GROUP BY d.rule_id`,
19779
+ { sessionId }
19780
+ )
19781
+ );
19782
+ }
19298
19783
  /**
19299
- * Grouped findings for the dashboard — joins findings⋈events (repo/file
19300
- * from event metadata), groups by ruleId, computes per-filter-excluded facets,
19784
+ * Grouped findings for the dashboard — joins findings⋈events (repo/file/
19785
+ * toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
19301
19786
  * applies the requested filters, and sorts by severity then recency. Filtering
19302
19787
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19303
19788
  * reflect the full filtered set; `items` is the requested
19304
- * page (default 100); no cursor (nextCursor is always null).
19789
+ * page (default 50); no cursor (nextCursor is always null).
19790
+ *
19791
+ * Two reads, neither of which materializes a row per finding:
19792
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
19793
+ * the group and the filters need (count, providers, actions, statuses,
19794
+ * latest, search text);
19795
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
19796
+ * populate `instances` for the table's expanded rows.
19797
+ * The aggregates carry raw DB values and are translated by the same
19798
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
19799
+ * rule is ever restated in SQL.
19305
19800
  */
19306
19801
  listGroupedFindings(query) {
19307
- const rows = this.db.prepare(
19308
- `SELECT f.id, f.rule_id, f.category, f.severity, f.masked_match,
19309
- f.action_taken, f.confidence, e.occurred_at, e.source_tool,
19310
- json_extract(e.metadata, '$.repo') AS repo,
19311
- json_extract(e.metadata, '$.filePath') AS file,
19312
- e.kind AS kind, f.finding_key AS finding_key,
19313
- ${latestResolutionStatusSql("f")} AS latest_status
19314
- FROM findings f JOIN events e ON e.id = f.event_id
19315
- ORDER BY e.occurred_at DESC, f.id DESC
19316
- LIMIT :cap`
19317
- ).all({ cap: GROUP_ROW_CAP });
19802
+ const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
19803
+ const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
19804
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
19805
+ predicate: sessionPredicate,
19806
+ params: sessionParams
19807
+ });
19808
+ const rows = allRows(
19809
+ this.db.prepare(
19810
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19811
+ occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
19812
+ FROM (
19813
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19814
+ f.severity AS severity, f.masked_match AS masked_match,
19815
+ f.action_taken AS action_taken, f.confidence AS confidence,
19816
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19817
+ json_extract(e.metadata, '$.repo') AS repo,
19818
+ json_extract(e.metadata, '$.filePath') AS file,
19819
+ json_extract(e.metadata, '$.toolName') AS tool_name,
19820
+ e.kind AS kind, f.finding_key AS finding_key,
19821
+ latest.status AS latest_status,
19822
+ ROW_NUMBER() OVER (
19823
+ PARTITION BY f.rule_id
19824
+ ORDER BY e.occurred_at DESC, f.id DESC
19825
+ ) AS rn
19826
+ FROM findings f
19827
+ JOIN events e ON e.id = f.event_id
19828
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19829
+ ON latest.finding_key = f.finding_key
19830
+ ${sessionPredicate}
19831
+ )
19832
+ WHERE rn <= :cap
19833
+ ORDER BY occurred_at DESC, id DESC`
19834
+ ),
19835
+ { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
19836
+ );
19318
19837
  const groupable = rows.map((r) => ({
19319
19838
  id: r.id,
19320
19839
  ruleId: r.rule_id,
@@ -19327,9 +19846,10 @@ var SqliteFindingsRepository = class {
19327
19846
  sourceTool: r.source_tool,
19328
19847
  repo: r.repo ?? "",
19329
19848
  file: r.file ?? "",
19849
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
19330
19850
  status: deriveInstanceStatus(r)
19331
19851
  }));
19332
- const allGroups = buildFindingGroups(groupable);
19852
+ const allGroups = buildFindingGroups(groupable, { aggregates });
19333
19853
  const filterOpts = {
19334
19854
  severity: query.severity,
19335
19855
  providers: query.provider,
@@ -19345,44 +19865,134 @@ var SqliteFindingsRepository = class {
19345
19865
  };
19346
19866
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
19347
19867
  const items = sorted.slice(0, limit);
19348
- return Promise.resolve({ totals, facets, items, nextCursor: null });
19868
+ return Promise.resolve({
19869
+ totals,
19870
+ facets,
19871
+ items,
19872
+ nextCursor: null,
19873
+ ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
19874
+ });
19875
+ }
19876
+ /**
19877
+ * One row per rule_id, folding EVERY instance of the group into the values
19878
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
19879
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
19880
+ *
19881
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
19882
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19883
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19884
+ * status keeps the classifier itself in @akasecurity/schema, where
19885
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
19886
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
19887
+ * a group's row stays small however many findings it holds.
19888
+ *
19889
+ * `withSearchText` is the exception, and the one column here that does NOT
19890
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
19891
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19892
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
19893
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
19894
+ * match an instance outside the preview, which searching the preview alone
19895
+ * would silently lose, so it is fetched only when the request actually
19896
+ * carries a `q`.
19897
+ */
19898
+ groupAggregates(withSearchText, scope) {
19899
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19900
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
19901
+ group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
19902
+ const rows = this.db.prepare(
19903
+ `SELECT f.rule_id AS rule_id,
19904
+ count(*) AS instance_count,
19905
+ max(e.occurred_at) AS latest_at,
19906
+ group_concat(DISTINCT e.source_tool) AS source_tools,
19907
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
19908
+ group_concat(DISTINCT (
19909
+ e.kind || '${TUPLE_SEP}' ||
19910
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19911
+ coalesce(latest.status, '')
19912
+ )) AS status_inputs
19913
+ ${searchTextColumns}
19914
+ FROM findings f
19915
+ JOIN events e ON e.id = f.event_id
19916
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19917
+ ON latest.finding_key = f.finding_key
19918
+ ${scope.predicate}
19919
+ GROUP BY f.rule_id`
19920
+ ).all(scope.params);
19921
+ return new Map(
19922
+ rows.map((r) => [
19923
+ r.rule_id,
19924
+ {
19925
+ instanceCount: r.instance_count,
19926
+ sourceTools: splitConcat(r.source_tools),
19927
+ actionsTaken: splitConcat(r.actions_taken),
19928
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19929
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
19930
+ return {
19931
+ // deriveFindingStatus only distinguishes null from non-null here,
19932
+ // so the marker stands in for the key itself (never rendered).
19933
+ kind,
19934
+ findingKey: keyMarker === "" ? null : keyMarker,
19935
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
19936
+ };
19937
+ }),
19938
+ latestDetectedAt: epochMillisToIso(r.latest_at),
19939
+ // Free text only — joined and substring-matched, so group_concat's
19940
+ // commas need no unpicking (a repo/path containing one still matches).
19941
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
19942
+ // tell "no q this request" from "a group with no repo/file at all"
19943
+ // and skip priming a haystack nothing will read.
19944
+ ...withSearchText ? {
19945
+ searchText: [r.repos ?? "", r.files ?? "", r.tool_names ?? ""].filter((s) => s !== "").join(" ")
19946
+ } : {}
19947
+ }
19948
+ ])
19949
+ );
19349
19950
  }
19350
19951
  healthSummary() {
19351
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
19952
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
19352
19953
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
19353
- const grouped = this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken").all();
19954
+ const grouped = allRows(
19955
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
19956
+ );
19354
19957
  for (const row of grouped) {
19355
19958
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
19356
19959
  }
19357
19960
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
19358
- const sevRows = this.db.prepare(
19359
- `SELECT f.severity AS severity, count(*) AS c
19961
+ const sevRows = allRows(
19962
+ this.db.prepare(
19963
+ `SELECT f.severity AS severity, count(*) AS c
19360
19964
  FROM findings f
19361
19965
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19362
19966
  ON latest.finding_key = f.finding_key
19363
19967
  WHERE latest.status IS NULL OR latest.status != 'resolved'
19364
19968
  GROUP BY f.severity`
19365
- ).all();
19969
+ )
19970
+ );
19366
19971
  for (const row of sevRows) {
19367
19972
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
19368
19973
  }
19369
19974
  const categories = ENFORCEABLE_CATEGORIES;
19370
- const enabledRows = this.db.prepare(
19371
- `SELECT DISTINCT json_extract(target, '$.category') AS category
19975
+ const enabledRows = allRows(
19976
+ this.db.prepare(
19977
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
19372
19978
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
19373
- ).all();
19979
+ )
19980
+ );
19374
19981
  const enabled = new Set(enabledRows.map((r) => r.category));
19375
19982
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
19376
19983
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
19377
19984
  }
19378
19985
  activityByDay(days = 7) {
19379
19986
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
19380
- const rows = this.db.prepare(
19381
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19987
+ const rows = allRows(
19988
+ this.db.prepare(
19989
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19382
19990
  FROM findings f JOIN events e ON e.id = f.event_id
19383
19991
  WHERE e.occurred_at >= :since
19384
19992
  GROUP BY day, f.action_taken`
19385
- ).all({ since });
19993
+ ),
19994
+ { since }
19995
+ );
19386
19996
  const buckets = /* @__PURE__ */ new Map();
19387
19997
  for (let i = 0; i < days; i++) {
19388
19998
  const day = isoDay(since + i * DAY_MS3);
@@ -19454,17 +20064,19 @@ var SqliteInspectionFindingsRepository = class {
19454
20064
  insertStmt;
19455
20065
  insertFinding(input) {
19456
20066
  const row = toInspectionFindingRow(input);
19457
- this.insertStmt.run({
19458
- id: row.id,
19459
- auditEventId: row.auditEventId,
19460
- inspectionDefinitionId: row.inspectionDefinitionId,
19461
- classifiedDataId: row.classifiedDataId ?? null,
19462
- spanStart: row.spanStart,
19463
- spanEnd: row.spanEnd,
19464
- maskedMatch: row.maskedMatch,
19465
- actionTaken: row.actionTaken,
19466
- confidence: row.confidence
19467
- });
20067
+ this.insertStmt.run(
20068
+ bindParams({
20069
+ id: row.id,
20070
+ auditEventId: row.auditEventId,
20071
+ inspectionDefinitionId: row.inspectionDefinitionId,
20072
+ classifiedDataId: row.classifiedDataId,
20073
+ spanStart: row.spanStart,
20074
+ spanEnd: row.spanEnd,
20075
+ maskedMatch: row.maskedMatch,
20076
+ actionTaken: row.actionTaken,
20077
+ confidence: row.confidence
20078
+ })
20079
+ );
19468
20080
  }
19469
20081
  };
19470
20082
 
@@ -19540,12 +20152,7 @@ function isMirrorDowngrade(incoming, stored) {
19540
20152
  }
19541
20153
  function ruleIdsOf(rulesJson) {
19542
20154
  const ids = /* @__PURE__ */ new Set();
19543
- let raw;
19544
- try {
19545
- raw = JSON.parse(rulesJson);
19546
- } catch {
19547
- return ids;
19548
- }
20155
+ const raw = safeJson(rulesJson, []);
19549
20156
  if (!Array.isArray(raw)) return ids;
19550
20157
  for (const entry of raw) {
19551
20158
  if (entry && typeof entry === "object") {
@@ -19618,47 +20225,48 @@ var SqliteInstalledPacksRepository = class {
19618
20225
  }));
19619
20226
  if (this.storedSignature() === inventorySignature(rows)) return;
19620
20227
  const now = Date.now();
19621
- this.db.exec("BEGIN IMMEDIATE");
19622
- try {
19623
- const mirror = this.mirrorState();
19624
- let behind = false;
19625
- for (const row of rows) {
19626
- const params = {
19627
- id: randomUUID2(),
19628
- namespace: row.namespace,
19629
- packId: row.packId,
19630
- version: row.version,
19631
- name: row.name,
19632
- rulesJson: row.rulesJson,
19633
- now
19634
- };
19635
- const stored = mirror.get(`${row.namespace}/${row.packId}`);
19636
- if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
19637
- this.upsertAvailableStmt.run({
19638
- ...params,
20228
+ withTransaction(
20229
+ this.db,
20230
+ () => {
20231
+ const mirror = this.mirrorState();
20232
+ let behind = false;
20233
+ for (const row of rows) {
20234
+ const params = {
19639
20235
  id: randomUUID2(),
19640
- recordedBy: meta3?.recordedBy ?? null
19641
- });
19642
- } else {
19643
- behind = true;
20236
+ namespace: row.namespace,
20237
+ packId: row.packId,
20238
+ version: row.version,
20239
+ name: row.name,
20240
+ rulesJson: row.rulesJson,
20241
+ now
20242
+ };
20243
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
20244
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20245
+ this.upsertAvailableStmt.run({
20246
+ ...params,
20247
+ id: randomUUID2(),
20248
+ recordedBy: meta3?.recordedBy ?? null
20249
+ });
20250
+ } else {
20251
+ behind = true;
20252
+ }
20253
+ this.insertMissingStmt.run(params);
19644
20254
  }
19645
- this.insertMissingStmt.run(params);
19646
- }
19647
- if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
19648
- this.db.exec("COMMIT");
19649
- } catch (err) {
19650
- this.db.exec("ROLLBACK");
19651
- throw err;
19652
- }
20255
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20256
+ },
20257
+ "IMMEDIATE"
20258
+ );
19653
20259
  } catch {
19654
20260
  }
19655
20261
  }
19656
20262
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
19657
20263
  // input to the downgrade guard. Read INSIDE the write transaction.
19658
20264
  mirrorState() {
19659
- const rows = this.db.prepare(
19660
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
19661
- ).all();
20265
+ const rows = allRows(
20266
+ this.db.prepare(
20267
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20268
+ )
20269
+ );
19662
20270
  return new Map(
19663
20271
  rows.map((r) => [
19664
20272
  `${r.namespace}/${r.packId}`,
@@ -19670,7 +20278,9 @@ var SqliteInstalledPacksRepository = class {
19670
20278
  // (keys joined with '/', matching the detection id slug encoding — packId may
19671
20279
  // itself contain '/', but namespace may not, so the join is unambiguous).
19672
20280
  pruneAvailable(keep) {
19673
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20281
+ const rows = allRows(
20282
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20283
+ );
19674
20284
  const keepSet = new Set(keep);
19675
20285
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
19676
20286
  for (const r of rows) {
@@ -19697,11 +20307,13 @@ var SqliteInstalledPacksRepository = class {
19697
20307
  if (this.db.isTransaction) {
19698
20308
  throw new Error("applyUpdate must not be called inside an open transaction");
19699
20309
  }
19700
- this.db.exec("BEGIN IMMEDIATE");
19701
- try {
19702
- this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
19703
- const res = this.db.prepare(
19704
- `UPDATE installed_packs SET
20310
+ let changed = false;
20311
+ withTransaction(
20312
+ this.db,
20313
+ () => {
20314
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20315
+ const res = this.db.prepare(
20316
+ `UPDATE installed_packs SET
19705
20317
  version = (SELECT a.version FROM available_packs a
19706
20318
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
19707
20319
  name = (SELECT a.name FROM available_packs a
@@ -19712,17 +20324,13 @@ var SqliteInstalledPacksRepository = class {
19712
20324
  WHERE namespace = :namespace AND pack_id = :packId
19713
20325
  AND EXISTS (SELECT 1 FROM available_packs a
19714
20326
  WHERE a.namespace = :namespace AND a.pack_id = :packId)`
19715
- ).run({ namespace, packId, now: Date.now() });
19716
- this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
19717
- this.db.exec("COMMIT");
19718
- return Number(res.changes) > 0;
19719
- } catch (err) {
19720
- try {
19721
- this.db.exec("ROLLBACK");
19722
- } catch {
19723
- }
19724
- throw err;
19725
- }
20327
+ ).run({ namespace, packId, now: Date.now() });
20328
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20329
+ changed = Number(res.changes) > 0;
20330
+ },
20331
+ "IMMEDIATE"
20332
+ );
20333
+ return changed;
19726
20334
  }
19727
20335
  /**
19728
20336
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -19736,9 +20344,11 @@ var SqliteInstalledPacksRepository = class {
19736
20344
  * JSON-level failure therefore counts as invalid.
19737
20345
  */
19738
20346
  installedRuleset() {
19739
- const rows = this.db.prepare(
19740
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
19741
- ).all();
20347
+ const rows = allRows(
20348
+ this.db.prepare(
20349
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20350
+ )
20351
+ );
19742
20352
  const out = {
19743
20353
  installedPacks: rows.length,
19744
20354
  enabledPacks: 0,
@@ -19747,7 +20357,7 @@ var SqliteInstalledPacksRepository = class {
19747
20357
  ruleActions: /* @__PURE__ */ new Map()
19748
20358
  };
19749
20359
  for (const row of rows) {
19750
- if (row.enabled !== 1) continue;
20360
+ if (!intToBool(row.enabled)) continue;
19751
20361
  out.enabledPacks += 1;
19752
20362
  const action = policyIdToAction(row.policyId);
19753
20363
  let raw;
@@ -19782,9 +20392,11 @@ var SqliteInstalledPacksRepository = class {
19782
20392
  * running max would mask a genuinely-newer parseable stamp.
19783
20393
  */
19784
20394
  newestRecordedBinary() {
19785
- const rows = this.db.prepare(
19786
- `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
19787
- ).all();
20395
+ const rows = allRows(
20396
+ this.db.prepare(
20397
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20398
+ )
20399
+ );
19788
20400
  let newest = null;
19789
20401
  for (const row of rows) {
19790
20402
  const at = row.recordedBy.lastIndexOf("@");
@@ -19799,13 +20411,15 @@ var SqliteInstalledPacksRepository = class {
19799
20411
  return newest;
19800
20412
  }
19801
20413
  counts() {
19802
- const row = this.db.prepare(
19803
- `SELECT count(*) AS packs,
20414
+ const row = getRow(
20415
+ this.db.prepare(
20416
+ `SELECT count(*) AS packs,
19804
20417
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
19805
20418
  coalesce(sum(enabled), 0) AS enabled
19806
20419
  FROM installed_packs`
19807
- ).get();
19808
- return Promise.resolve(row);
20420
+ )
20421
+ );
20422
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
19809
20423
  }
19810
20424
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
19811
20425
  // Back the Policies page's built-in catalog: how many
@@ -19818,26 +20432,29 @@ var SqliteInstalledPacksRepository = class {
19818
20432
  * attributed to Monitor, matching the Detections views.
19819
20433
  */
19820
20434
  countsByPolicyId() {
19821
- const rows = this.db.prepare(
19822
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20435
+ return countBy(
20436
+ this.db,
20437
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
19823
20438
  FROM installed_packs
19824
- GROUP BY pid`
19825
- ).all();
19826
- return new Map(rows.map((r) => [r.pid, r.n]));
20439
+ GROUP BY k`
20440
+ );
19827
20441
  }
19828
20442
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
19829
20443
  listByPolicyId(policyId) {
19830
- const rows = this.db.prepare(
19831
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20444
+ const rows = allRows(
20445
+ this.db.prepare(
20446
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
19832
20447
  FROM installed_packs
19833
20448
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
19834
20449
  ORDER BY name ASC`
19835
- ).all(policyId);
20450
+ ),
20451
+ [policyId]
20452
+ );
19836
20453
  return rows.map((r) => ({
19837
20454
  id: `${r.namespace}/${r.packId}`,
19838
20455
  name: r.name,
19839
20456
  ruleCount: parseRules(r.rulesJson).length,
19840
- enabled: r.enabled === 1
20457
+ enabled: intToBool(r.enabled)
19841
20458
  }));
19842
20459
  }
19843
20460
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -19866,14 +20483,14 @@ var SqliteInstalledPacksRepository = class {
19866
20483
  const res = this.db.prepare(
19867
20484
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
19868
20485
  WHERE namespace = :namespace AND pack_id = :packId`
19869
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
20486
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
19870
20487
  return Number(res.changes) > 0;
19871
20488
  }
19872
20489
  // Fingerprint of the recorded available mirror — compared against the
19873
20490
  // incoming inventory's signature to skip the write entirely when the running
19874
20491
  // binary's inventory hasn't changed since the last record.
19875
20492
  storedSignature() {
19876
- const rows = this.signatureStmt.all();
20493
+ const rows = allRows(this.signatureStmt);
19877
20494
  return inventorySignature(rows);
19878
20495
  }
19879
20496
  };
@@ -19901,42 +20518,48 @@ var SqliteInventoryRepository = class {
19901
20518
  upsert(input, now = Date.now()) {
19902
20519
  const id = inventoryId(input.objectType, input.identityKey);
19903
20520
  const row = toInventoryRow(input, id, now);
19904
- this.upsertStmt.run({
19905
- id: row.id,
19906
- objectType: row.objectType,
19907
- location: row.location ?? null,
19908
- title: row.title ?? null,
19909
- hostId: row.hostId ?? null,
19910
- attributes: row.attributes,
19911
- firstSeen: row.firstSeen,
19912
- lastSeen: row.lastSeen
19913
- });
20521
+ this.upsertStmt.run(
20522
+ bindParams({
20523
+ id: row.id,
20524
+ objectType: row.objectType,
20525
+ location: row.location,
20526
+ title: row.title,
20527
+ hostId: row.hostId,
20528
+ attributes: row.attributes,
20529
+ firstSeen: row.firstSeen,
20530
+ lastSeen: row.lastSeen
20531
+ })
20532
+ );
19914
20533
  return id;
19915
20534
  }
19916
20535
  // The full row, for round-trip assertions.
19917
20536
  findById(id) {
19918
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
19919
- return row;
20537
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
19920
20538
  }
19921
20539
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
19922
20540
  // served from the object_type index, never from audit_events.
19923
20541
  distinctTitles(objectType) {
19924
- const rows = this.db.prepare(
19925
- `SELECT DISTINCT title FROM inventory
20542
+ const rows = allRows(
20543
+ this.db.prepare(
20544
+ `SELECT DISTINCT title FROM inventory
19926
20545
  WHERE object_type = :objectType AND title IS NOT NULL
19927
20546
  ORDER BY title`
19928
- ).all({ objectType });
20547
+ ),
20548
+ { objectType }
20549
+ );
19929
20550
  return rows.map((r) => r.title);
19930
20551
  }
19931
20552
  // Distinct host os_version values — a facet served from an inventory index
19932
20553
  // over the generated column, never from the audit fact (confirm via EXPLAIN
19933
20554
  // QUERY PLAN).
19934
20555
  osVersions() {
19935
- const rows = this.db.prepare(
19936
- `SELECT DISTINCT os_version AS value FROM inventory
20556
+ const rows = allRows(
20557
+ this.db.prepare(
20558
+ `SELECT DISTINCT os_version AS value FROM inventory
19937
20559
  WHERE object_type = 'host' AND os_version IS NOT NULL
19938
20560
  ORDER BY value`
19939
- ).all();
20561
+ )
20562
+ );
19940
20563
  return rows.map((r) => r.value);
19941
20564
  }
19942
20565
  };
@@ -19956,14 +20579,6 @@ var EMPTY_PROJECT_AGG = {
19956
20579
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
19957
20580
  findingsCount: 0
19958
20581
  };
19959
- function safeJson(s, fallback) {
19960
- if (s == null) return fallback;
19961
- try {
19962
- return JSON.parse(s);
19963
- } catch {
19964
- return fallback;
19965
- }
19966
- }
19967
20582
  function resolveHarnessId(attrs, row) {
19968
20583
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
19969
20584
  return attrs.provider;
@@ -20159,30 +20774,49 @@ var SqliteInventoryAssetsRepository = class {
20159
20774
  configRowsCache;
20160
20775
  // ─── stats ─────────────────────────────────────────────────────────────────
20161
20776
  getInventoryStats() {
20162
- const byType = { project: 0, skill: 0, mcp: 0, hook: 0, config: 0 };
20163
- for (const r of this.db.prepare("SELECT asset_type AS t, count(*) AS n FROM inventory_asset GROUP BY asset_type").all()) {
20164
- if (r.t in byType) byType[r.t] = r.n;
20165
- }
20166
- byType.project = this.db.prepare(`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`).get().n;
20167
- const mcpTrust = { "known-good": 0, risky: 0, unapproved: 0 };
20168
- for (const r of this.db.prepare(
20169
- `SELECT coalesce(o.trust, a.trust) AS trust, count(*) AS n
20777
+ const typeCounts = countBy(
20778
+ this.db,
20779
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
20780
+ );
20781
+ const byType = {
20782
+ project: 0,
20783
+ skill: typeCounts.get("skill") ?? 0,
20784
+ mcp: typeCounts.get("mcp") ?? 0,
20785
+ hook: typeCounts.get("hook") ?? 0,
20786
+ config: typeCounts.get("config") ?? 0
20787
+ };
20788
+ byType.project = countScalar(
20789
+ this.db,
20790
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
20791
+ );
20792
+ const mcpTrustCounts = countBy(
20793
+ this.db,
20794
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20170
20795
  FROM inventory_asset a
20171
20796
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20172
20797
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20173
20798
  GROUP BY coalesce(o.trust, a.trust)`
20174
- ).all()) {
20175
- if (r.trust in mcpTrust) mcpTrust[r.trust] = r.n;
20176
- }
20177
- const harnesses = this.db.prepare(
20799
+ );
20800
+ const mcpTrust = {
20801
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
20802
+ risky: mcpTrustCounts.get("risky") ?? 0,
20803
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
20804
+ };
20805
+ const harnesses = countScalar(
20806
+ this.db,
20178
20807
  `SELECT count(*) AS n FROM inventory
20179
20808
  WHERE object_type = 'harness'
20180
- AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20181
- ).get({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }).n;
20182
- const flaggedAssets = this.db.prepare("SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'").get().n;
20183
- const flaggedProjects = this.db.prepare(
20809
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
20810
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20811
+ );
20812
+ const flaggedAssets = countScalar(
20813
+ this.db,
20814
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
20815
+ );
20816
+ const flaggedProjects = countScalar(
20817
+ this.db,
20184
20818
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20185
- ).get().n;
20819
+ );
20186
20820
  const configRows = this.configAssetRows();
20187
20821
  for (const r of configRows) {
20188
20822
  byType[r.assetType] += 1;
@@ -20439,12 +21073,15 @@ var SqliteInventoryAssetsRepository = class {
20439
21073
  }
20440
21074
  // ─── raw fetchers ────────────────────────────────────────────────────────────
20441
21075
  fetchHarnessRows() {
20442
- return this.db.prepare(
20443
- `SELECT id, title, attributes, harness_version AS harnessVersion
21076
+ return allRows(
21077
+ this.db.prepare(
21078
+ `SELECT id, title, attributes, harness_version AS harnessVersion
20444
21079
  FROM inventory
20445
21080
  WHERE object_type = 'harness'
20446
21081
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20447
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
21082
+ ),
21083
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
21084
+ );
20448
21085
  }
20449
21086
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
20450
21087
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -20454,12 +21091,13 @@ var SqliteInventoryAssetsRepository = class {
20454
21091
  const params = [...harnessInvIds];
20455
21092
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
20456
21093
  if (q) {
20457
- const pat = `%${escapeLikePattern(q)}%`;
20458
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
21094
+ const pat = containsPattern(q);
21095
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
20459
21096
  params.push(pat, pat);
20460
21097
  }
20461
- const rows = this.db.prepare(
20462
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
21098
+ const rows = allRows(
21099
+ this.db.prepare(
21100
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20463
21101
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20464
21102
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20465
21103
  FROM harness_asset ha
@@ -20467,7 +21105,9 @@ var SqliteInventoryAssetsRepository = class {
20467
21105
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20468
21106
  WHERE ${where}
20469
21107
  ORDER BY a.name ASC`
20470
- ).all(...params);
21108
+ ),
21109
+ params
21110
+ );
20471
21111
  for (const raw of rows) {
20472
21112
  const harnessInvId = raw.harnessInvId;
20473
21113
  const [asset] = this.mapAssetRows([raw]);
@@ -20486,21 +21126,24 @@ var SqliteInventoryAssetsRepository = class {
20486
21126
  params.push(...types);
20487
21127
  }
20488
21128
  if (q) {
20489
- const pat = `%${escapeLikePattern(q)}%`;
20490
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
21129
+ const pat = containsPattern(q);
21130
+ conditions.push(likeAny(["a.name", "a.sub"]));
20491
21131
  params.push(pat, pat);
20492
21132
  }
20493
21133
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
20494
21134
  const sampleRows = this.mapAssetRows(
20495
- this.db.prepare(
20496
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21135
+ allRows(
21136
+ this.db.prepare(
21137
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20497
21138
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20498
21139
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20499
21140
  FROM inventory_asset a
20500
21141
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20501
21142
  ${where}
20502
21143
  ORDER BY a.name ASC`
20503
- ).all(...params)
21144
+ ),
21145
+ params
21146
+ )
20504
21147
  );
20505
21148
  const configRows = this.configAssetRows(q).filter(
20506
21149
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -20509,14 +21152,17 @@ var SqliteInventoryAssetsRepository = class {
20509
21152
  }
20510
21153
  fetchAssetById(assetId) {
20511
21154
  const rows = this.mapAssetRows(
20512
- this.db.prepare(
20513
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21155
+ allRows(
21156
+ this.db.prepare(
21157
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20514
21158
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20515
21159
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20516
21160
  FROM inventory_asset a
20517
21161
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20518
21162
  WHERE a.id = ?`
20519
- ).all(assetId)
21163
+ ),
21164
+ [assetId]
21165
+ )
20520
21166
  );
20521
21167
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
20522
21168
  }
@@ -20572,37 +21218,39 @@ var SqliteInventoryAssetsRepository = class {
20572
21218
  return rows;
20573
21219
  }
20574
21220
  latestConfigScanId() {
20575
- const row = this.db.prepare(
20576
- `SELECT id FROM audit_events WHERE event_type = 'config_scan'
20577
- ORDER BY started_at DESC, id DESC LIMIT 1`
20578
- ).get();
20579
- return row?.id ?? null;
21221
+ return latestConfigScan(this.db)?.id ?? null;
20580
21222
  }
20581
21223
  fetchProjects(q) {
20582
21224
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
20583
21225
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
20584
21226
  const params = [];
20585
21227
  if (q) {
20586
- const pat = `%${escapeLikePattern(q)}%`;
20587
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
21228
+ const pat = containsPattern(q);
21229
+ sql += ` AND ${likeAny(["name", "url"])}`;
20588
21230
  params.push(pat, pat);
20589
21231
  }
20590
21232
  sql += " ORDER BY name ASC";
20591
- return this.db.prepare(sql).all(...params);
21233
+ return allRows(this.db.prepare(sql), params);
20592
21234
  }
20593
21235
  fetchProjectById(projectId) {
20594
- return this.db.prepare(
20595
- "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
20596
- ).get(projectId) ?? null;
21236
+ return getRow(
21237
+ this.db.prepare(
21238
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
21239
+ ),
21240
+ [projectId]
21241
+ ) ?? null;
20597
21242
  }
20598
21243
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
20599
21244
  fetchProjectsByIds(projectIds) {
20600
21245
  const map2 = /* @__PURE__ */ new Map();
20601
21246
  if (projectIds.length === 0) return map2;
20602
- const rows = this.db.prepare(
20603
- `SELECT id, url, name, attributes, last_seen AS lastSeen
21247
+ const rows = allRows(
21248
+ this.db.prepare(
21249
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
20604
21250
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
20605
- ).all(...projectIds);
21251
+ ),
21252
+ projectIds
21253
+ );
20606
21254
  for (const r of rows) map2.set(r.id, r);
20607
21255
  return map2;
20608
21256
  }
@@ -20613,8 +21261,9 @@ var SqliteInventoryAssetsRepository = class {
20613
21261
  projectAggregates(projectIds) {
20614
21262
  const map2 = /* @__PURE__ */ new Map();
20615
21263
  if (projectIds.length === 0) return map2;
20616
- const rows = this.db.prepare(
20617
- `SELECT f.project_id AS projectId,
21264
+ const rows = allRows(
21265
+ this.db.prepare(
21266
+ `SELECT f.project_id AS projectId,
20618
21267
  coalesce(o.access, f.default_access) AS eff,
20619
21268
  count(*) AS n,
20620
21269
  coalesce(sum(f.findings_count), 0) AS findings
@@ -20622,7 +21271,9 @@ var SqliteInventoryAssetsRepository = class {
20622
21271
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
20623
21272
  WHERE f.project_id IN (${placeholders(projectIds.length)})
20624
21273
  GROUP BY f.project_id, eff`
20625
- ).all(...projectIds);
21274
+ ),
21275
+ projectIds
21276
+ );
20626
21277
  for (const r of rows) {
20627
21278
  let agg = map2.get(r.projectId);
20628
21279
  if (!agg) {
@@ -20660,37 +21311,52 @@ var SqliteInventoryAssetsRepository = class {
20660
21311
  fetchProjectFilesUnder(projectId, prefix) {
20661
21312
  if (prefix === "") {
20662
21313
  return this.mapFileRows(
20663
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21314
+ allRows(
21315
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21316
+ [projectId]
21317
+ )
20664
21318
  );
20665
21319
  }
20666
21320
  return this.mapFileRows(
20667
- this.db.prepare(
20668
- this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
20669
- ).all(projectId, `${escapeLikePattern(prefix)}/%`)
21321
+ allRows(
21322
+ this.db.prepare(
21323
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21324
+ ),
21325
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21326
+ )
20670
21327
  );
20671
21328
  }
20672
21329
  fetchProjectFilesSearch(projectId, q) {
20673
- const pat = `%${escapeLikePattern(q)}%`;
21330
+ const pat = containsPattern(q);
20674
21331
  return this.mapFileRows(
20675
- this.db.prepare(
20676
- this.fileSelect(
20677
- "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
20678
- )
20679
- ).all(projectId, pat, pat)
21332
+ allRows(
21333
+ this.db.prepare(
21334
+ this.fileSelect(
21335
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21336
+ )
21337
+ ),
21338
+ [projectId, pat, pat]
21339
+ )
20680
21340
  );
20681
21341
  }
20682
21342
  fetchProjectFilesBlocked(projectId) {
20683
21343
  return this.mapFileRows(
20684
- this.db.prepare(
20685
- this.fileSelect(
20686
- "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
20687
- )
20688
- ).all(projectId)
21344
+ allRows(
21345
+ this.db.prepare(
21346
+ this.fileSelect(
21347
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21348
+ )
21349
+ ),
21350
+ [projectId]
21351
+ )
20689
21352
  );
20690
21353
  }
20691
21354
  fetchProjectFile(projectId, path) {
20692
21355
  const rows = this.mapFileRows(
20693
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21356
+ allRows(
21357
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21358
+ [projectId, path]
21359
+ )
20694
21360
  );
20695
21361
  return rows[0] ?? null;
20696
21362
  }
@@ -20704,39 +21370,32 @@ var SqlitePoliciesRepository = class {
20704
21370
  }
20705
21371
  db;
20706
21372
  readPolicies() {
20707
- const rows = this.db.prepare("SELECT * FROM policies").all();
20708
- const policies = [];
20709
- for (const row of rows) {
20710
- try {
20711
- const target = JSON.parse(row.target);
20712
- const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
20713
- policies.push(
20714
- Policy.parse({
20715
- id: row.id,
20716
- scope: row.scope,
20717
- target,
20718
- action: row.action,
20719
- enabled: row.enabled === 1,
20720
- customKeywords
20721
- })
20722
- );
20723
- } catch {
20724
- }
20725
- }
21373
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21374
+ const policies = mapRowsTolerant(rows, (row) => {
21375
+ const target = JSON.parse(row.target);
21376
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21377
+ return Policy.parse({
21378
+ id: row.id,
21379
+ scope: row.scope,
21380
+ target,
21381
+ action: row.action,
21382
+ enabled: intToBool(row.enabled),
21383
+ customKeywords
21384
+ });
21385
+ });
20726
21386
  return Promise.resolve(policies);
20727
21387
  }
20728
21388
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
20729
21389
  // detection-type config exists from first run. Only when the table is empty,
20730
21390
  // so a user's edits are never clobbered.
20731
21391
  seedDefaults() {
20732
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21392
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
20733
21393
  if (count > 0) return;
20734
21394
  const stmt = this.db.prepare(
20735
21395
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
20736
21396
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
20737
21397
  );
20738
- this.db.exec("BEGIN");
20739
- try {
21398
+ failOpenTransaction(this.db, () => {
20740
21399
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
20741
21400
  stmt.run({
20742
21401
  id: randomUUID4(),
@@ -20745,10 +21404,41 @@ var SqlitePoliciesRepository = class {
20745
21404
  now: Date.now()
20746
21405
  });
20747
21406
  }
20748
- this.db.exec("COMMIT");
20749
- } catch {
20750
- this.db.exec("ROLLBACK");
20751
- }
21407
+ });
21408
+ }
21409
+ // Insert-or-update the single global per-category policy row, keyed on the
21410
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21411
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21412
+ // values), so the runtime's resolveAction reads rows written by either path
21413
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21414
+ // `id` and `created_at` are left exactly as they were.
21415
+ upsertCategoryAction(category, action) {
21416
+ const now = Date.now();
21417
+ this.db.prepare(
21418
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21419
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21420
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21421
+ ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
21422
+ }
21423
+ // Caps every global per-category policy currently set to block/redact down
21424
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21425
+ // Returns the number of rows changed.
21426
+ capCategoryActions() {
21427
+ const info = this.db.prepare(
21428
+ `UPDATE policies SET action='warn', updated_at=:now
21429
+ WHERE scope='global' AND action IN ('block','redact')
21430
+ AND json_extract(target,'$.category') IS NOT NULL`
21431
+ ).run({ now: Date.now() });
21432
+ return Number(info.changes);
21433
+ }
21434
+ // Read the current action for a single global per-category policy row, mirroring
21435
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21436
+ // row exists yet, so callers can distinguish an unset category from a set one.
21437
+ getCategoryAction(category) {
21438
+ const row = this.db.prepare(
21439
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21440
+ ).get({ category });
21441
+ return row?.action;
20752
21442
  }
20753
21443
  };
20754
21444
 
@@ -20824,7 +21514,7 @@ var SqliteProjectFilesRepository = class {
20824
21514
  maxStampStmt;
20825
21515
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
20826
21516
  replaceForProject(projectId, scan2, now) {
20827
- const { maxStamp } = this.maxStampStmt.get({ projectId });
21517
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
20828
21518
  const stamp = Math.max(now, maxStamp + 1);
20829
21519
  for (const file2 of scan2.files) {
20830
21520
  this.upsertStmt.run({
@@ -20907,7 +21597,7 @@ var SqliteResolutionsRepository = class {
20907
21597
  }
20908
21598
  /** The newest disposition recorded for a finding key, or undefined if none. */
20909
21599
  latestByKey(key) {
20910
- const row = this.latestStmt.get({ findingKey: key });
21600
+ const row = getRow(this.latestStmt, { findingKey: key });
20911
21601
  if (!row) return void 0;
20912
21602
  return {
20913
21603
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -20924,7 +21614,7 @@ var SqliteResolutionsRepository = class {
20924
21614
  * the CLI) surfaces for that file.
20925
21615
  */
20926
21616
  openAtRestKeysForPath(path) {
20927
- const rows = this.openAtRestStmt.all({ path });
21617
+ const rows = allRows(this.openAtRestStmt, { path });
20928
21618
  return rows.map((r) => r.finding_key);
20929
21619
  }
20930
21620
  /**
@@ -20935,7 +21625,7 @@ var SqliteResolutionsRepository = class {
20935
21625
  * resolution row (see scan.ts).
20936
21626
  */
20937
21627
  resolvedAtRestKeysForPath(path) {
20938
- const rows = this.resolvedAtRestStmt.all({ path });
21628
+ const rows = allRows(this.resolvedAtRestStmt, { path });
20939
21629
  return rows.map((r) => r.finding_key);
20940
21630
  }
20941
21631
  };
@@ -20964,38 +21654,31 @@ var SqliteScanLedgerRepository = class {
20964
21654
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
20965
21655
  // older ruleset are simply absent, which reads as "never scanned".
20966
21656
  entriesForRuleset(rulesetHash) {
20967
- const rows = this.readStmt.all({ rulesetHash });
21657
+ const rows = allRows(this.readStmt, {
21658
+ rulesetHash
21659
+ });
20968
21660
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
20969
21661
  }
20970
21662
  upsertEntries(entries) {
20971
21663
  if (entries.length === 0) return;
20972
21664
  const scannedAt = Date.now();
20973
- try {
20974
- this.db.exec("BEGIN");
20975
- try {
20976
- for (const entry of entries) {
20977
- this.upsertStmt.run({
20978
- path: entry.path,
20979
- mtime: entry.mtime,
20980
- contentHash: entry.contentHash,
20981
- rulesetHash: entry.rulesetHash,
20982
- scannedAt
20983
- });
20984
- }
20985
- this.db.exec("COMMIT");
20986
- } catch (err) {
20987
- this.db.exec("ROLLBACK");
20988
- throw err;
21665
+ failOpenTransaction(this.db, () => {
21666
+ for (const entry of entries) {
21667
+ this.upsertStmt.run({
21668
+ path: entry.path,
21669
+ mtime: entry.mtime,
21670
+ contentHash: entry.contentHash,
21671
+ rulesetHash: entry.rulesetHash,
21672
+ scannedAt
21673
+ });
20989
21674
  }
20990
- } catch {
20991
- }
21675
+ });
20992
21676
  }
20993
21677
  };
20994
21678
 
20995
21679
  // ../../packages/persistence/src/repositories/security.ts
20996
21680
  var DAY_MS4 = 864e5;
20997
21681
  var SEVERITIES = ["critical", "high", "medium", "low"];
20998
- var RANGE_DAYS = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
20999
21682
  var ACTION_TO_KIND = {
21000
21683
  block: "blocked",
21001
21684
  redact: "redacted",
@@ -21010,8 +21693,14 @@ var SCAN_COVERAGE = [
21010
21693
  { provider: "copilot", coverage: 0, supported: false },
21011
21694
  { provider: "api", coverage: 0, supported: false }
21012
21695
  ];
21696
+ var GRANULARITY = {
21697
+ "7d": "day",
21698
+ "30d": "day",
21699
+ "3m": "week",
21700
+ "6m": "week"
21701
+ };
21013
21702
  function granularityFor(range) {
21014
- return range === "7d" || range === "30d" ? "day" : "week";
21703
+ return GRANULARITY[range];
21015
21704
  }
21016
21705
  function startOfUtcDay2(ms) {
21017
21706
  return Math.floor(ms / DAY_MS4) * DAY_MS4;
@@ -21065,8 +21754,9 @@ var SqliteSecurityRepository = class {
21065
21754
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21066
21755
  // double-counting a key that accumulated several append-only rows.
21067
21756
  severitySummary() {
21068
- const rows = this.db.prepare(
21069
- `SELECT f.severity AS severity,
21757
+ const rows = allRows(
21758
+ this.db.prepare(
21759
+ `SELECT f.severity AS severity,
21070
21760
  COUNT(*) AS count,
21071
21761
  SUM(CASE
21072
21762
  WHEN e.kind != 'code_change' THEN 1
@@ -21085,7 +21775,8 @@ var SqliteSecurityRepository = class {
21085
21775
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21086
21776
  ON latest.finding_key = f.finding_key
21087
21777
  GROUP BY f.severity`
21088
- ).all();
21778
+ )
21779
+ );
21089
21780
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21090
21781
  const bySeverity = SEVERITIES.map((severity) => ({
21091
21782
  severity,
@@ -21169,14 +21860,15 @@ var SqliteSecurityRepository = class {
21169
21860
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21170
21861
  const now = this.now();
21171
21862
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
21172
- const rows = this.db.prepare(
21173
- // first_detected_at is the PRESERVED first-detection time (set once on a
21174
- // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21175
- // measures from first sighting — not the latest re-scan's event, whose
21176
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21177
- // the parent event's occurred_at defends against any legacy/edge row the
21178
- // backfill left null.
21179
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21863
+ const rows = allRows(
21864
+ this.db.prepare(
21865
+ // first_detected_at is the PRESERVED first-detection time (set once on a
21866
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21867
+ // measures from first sighting — not the latest re-scan's event, whose
21868
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21869
+ // the parent event's occurred_at defends against any legacy/edge row the
21870
+ // backfill left null.
21871
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21180
21872
  (
21181
21873
  SELECT fr.status FROM finding_resolution fr
21182
21874
  WHERE fr.finding_key = f.finding_key
@@ -21202,14 +21894,16 @@ var SqliteSecurityRepository = class {
21202
21894
  WHERE fr.finding_key = f.finding_key
21203
21895
  AND fr.resolved_at >= :windowStart
21204
21896
  )`
21205
- // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21206
- // any resolution activity at/after the window start — a row this method
21207
- // ultimately counts has its LATEST resolution inside the window, which
21208
- // implies such a row exists, so nothing wanted is dropped. The exact
21209
- // latest-wins + status/method + window gate stays in JS below,
21210
- // dialect-agnostic. Without this, a
21211
- // 7d request evaluated the store's entire trackable-findings history.
21212
- ).all({ windowStart });
21897
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21898
+ // any resolution activity at/after the window start — a row this method
21899
+ // ultimately counts has its LATEST resolution inside the window, which
21900
+ // implies such a row exists, so nothing wanted is dropped. The exact
21901
+ // latest-wins + status/method + window gate stays in JS below,
21902
+ // dialect-agnostic. Without this, a
21903
+ // 7d request evaluated the store's entire trackable-findings history.
21904
+ ),
21905
+ { windowStart }
21906
+ );
21213
21907
  const sums = /* @__PURE__ */ new Map();
21214
21908
  const counts = /* @__PURE__ */ new Map();
21215
21909
  for (const r of rows) {
@@ -21239,8 +21933,9 @@ var SqliteSecurityRepository = class {
21239
21933
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
21240
21934
  const now = this.now();
21241
21935
  const from = now - RANGE_DAYS[range] * DAY_MS4;
21242
- const rows = this.db.prepare(
21243
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21936
+ const rows = allRows(
21937
+ this.db.prepare(
21938
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21244
21939
  FROM findings f JOIN events e ON e.id = f.event_id
21245
21940
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21246
21941
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -21248,7 +21943,9 @@ var SqliteSecurityRepository = class {
21248
21943
  GROUP BY repo
21249
21944
  ORDER BY c DESC, repo
21250
21945
  LIMIT :limit`
21251
- ).all({ from, to: now, limit });
21946
+ ),
21947
+ { from, to: now, limit }
21948
+ );
21252
21949
  const items = rows.map((r) => ({
21253
21950
  id: `repo_${r.repo}`,
21254
21951
  name: r.repo,
@@ -21270,8 +21967,9 @@ var SqliteSecurityRepository = class {
21270
21967
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21271
21968
  // capped at `limit`.
21272
21969
  recentlyResolved(limit = 20) {
21273
- const rows = this.db.prepare(
21274
- `SELECT f.finding_key AS finding_key,
21970
+ const rows = allRows(
21971
+ this.db.prepare(
21972
+ `SELECT f.finding_key AS finding_key,
21275
21973
  f.rule_id AS rule_id,
21276
21974
  f.severity AS severity,
21277
21975
  json_extract(e.metadata, '$.filePath') AS path,
@@ -21305,7 +22003,9 @@ var SqliteSecurityRepository = class {
21305
22003
  ) IS NOT NULL
21306
22004
  ORDER BY latest_resolved_at DESC
21307
22005
  LIMIT :limit`
21308
- ).all({ limit });
22006
+ ),
22007
+ { limit }
22008
+ );
21309
22009
  const items = rows.map((r) => ({
21310
22010
  findingKey: r.finding_key,
21311
22011
  ruleId: r.rule_id,
@@ -21322,12 +22022,15 @@ var SqliteSecurityRepository = class {
21322
22022
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
21323
22023
  // numeric and the JS aggregations bucket/split on ms directly.
21324
22024
  findingsInRange(fromMs, toMs) {
21325
- const rows = this.db.prepare(
21326
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
22025
+ const rows = allRows(
22026
+ this.db.prepare(
22027
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21327
22028
  FROM findings f JOIN events e ON e.id = f.event_id
21328
22029
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21329
22030
  ORDER BY e.occurred_at`
21330
- ).all({ from: fromMs, to: toMs });
22031
+ ),
22032
+ { from: fromMs, to: toMs }
22033
+ );
21331
22034
  return rows.map((r) => ({
21332
22035
  occurredAt: r.occurred_at,
21333
22036
  severity: r.severity,
@@ -21341,12 +22044,7 @@ import { randomUUID as randomUUID7 } from "crypto";
21341
22044
  var KIND_ORDER = ["provider", "internal", "ip"];
21342
22045
  var CALL_SITE_EMBED_CAP = 200;
21343
22046
  function parseNetwork(networkJson) {
21344
- if (!networkJson) return null;
21345
- try {
21346
- return JSON.parse(networkJson);
21347
- } catch {
21348
- return null;
21349
- }
22047
+ return safeJson(networkJson, null);
21350
22048
  }
21351
22049
  function toEndpointSummary(row) {
21352
22050
  return {
@@ -21433,27 +22131,39 @@ var SqliteSharesRepository = class {
21433
22131
  }
21434
22132
  db;
21435
22133
  stats() {
21436
- const scalar = (sql) => this.db.prepare(sql).get()?.n ?? 0;
21437
- const destinations = scalar("SELECT count(*) AS n FROM share_destination");
21438
- const endpoints = scalar("SELECT count(*) AS n FROM share_endpoint");
21439
- const callSites = scalar("SELECT count(*) AS n FROM share_call_site");
21440
- const insecure = scalar(
22134
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
22135
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
22136
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22137
+ const insecure = countScalar(
22138
+ this.db,
21441
22139
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
21442
22140
  );
21443
- const needsReview = scalar(
22141
+ const needsReview = countScalar(
22142
+ this.db,
21444
22143
  `SELECT count(DISTINCT d.id) AS n
21445
22144
  FROM share_destination d
21446
22145
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
21447
22146
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
21448
22147
  );
21449
- const byKind = { provider: 0, internal: 0, ip: 0 };
21450
- for (const row of this.db.prepare("SELECT kind, count(*) AS n FROM share_destination GROUP BY kind").all()) {
21451
- byKind[row.kind] = row.n;
21452
- }
21453
- const byTrust = { recognized: 0, internal: 0, unverified: 0, ip: 0 };
21454
- for (const row of this.db.prepare("SELECT trust, count(*) AS n FROM share_destination GROUP BY trust").all()) {
21455
- byTrust[row.trust] = row.n;
21456
- }
22148
+ const kindCounts = countBy(
22149
+ this.db,
22150
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
22151
+ );
22152
+ const byKind = {
22153
+ provider: kindCounts.get("provider") ?? 0,
22154
+ internal: kindCounts.get("internal") ?? 0,
22155
+ ip: kindCounts.get("ip") ?? 0
22156
+ };
22157
+ const trustCounts = countBy(
22158
+ this.db,
22159
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
22160
+ );
22161
+ const byTrust = {
22162
+ recognized: trustCounts.get("recognized") ?? 0,
22163
+ internal: trustCounts.get("internal") ?? 0,
22164
+ unverified: trustCounts.get("unverified") ?? 0,
22165
+ ip: trustCounts.get("ip") ?? 0
22166
+ };
21457
22167
  return Promise.resolve({
21458
22168
  destinations,
21459
22169
  endpoints,
@@ -21569,7 +22279,7 @@ var SqliteSharesRepository = class {
21569
22279
  }
21570
22280
  let sql;
21571
22281
  if (q) {
21572
- const pattern = `%${escapeLikePattern(q)}%`;
22282
+ const pattern = containsPattern(q);
21573
22283
  conditions.push(
21574
22284
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
21575
22285
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -21589,24 +22299,31 @@ var SqliteSharesRepository = class {
21589
22299
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
21590
22300
  ORDER BY d.created_at ASC, d.id ASC`;
21591
22301
  }
21592
- const rows = this.db.prepare(sql).all(...params);
22302
+ const rows = allRows(
22303
+ this.db.prepare(sql),
22304
+ params
22305
+ );
21593
22306
  return rows.map((r) => this.mapDestRow(r));
21594
22307
  }
21595
22308
  fetchDestinationById(destinationId) {
21596
- const row = this.db.prepare(
21597
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22309
+ const row = getRow(
22310
+ this.db.prepare(
22311
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
21598
22312
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
21599
22313
  o.decision AS overrideDecision
21600
22314
  FROM share_destination d
21601
22315
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
21602
22316
  WHERE d.id = ?`
21603
- ).get(destinationId);
22317
+ ),
22318
+ [destinationId]
22319
+ );
21604
22320
  return row ? this.mapDestRow(row) : null;
21605
22321
  }
21606
22322
  fetchEndpoints(destinationIds) {
21607
22323
  if (destinationIds.length === 0) return [];
21608
- const rows = this.db.prepare(
21609
- `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
22324
+ const rows = allRows(
22325
+ this.db.prepare(
22326
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
21610
22327
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
21611
22328
  count(c.id) AS callSiteCount
21612
22329
  FROM share_endpoint e
@@ -21614,7 +22331,9 @@ var SqliteSharesRepository = class {
21614
22331
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
21615
22332
  GROUP BY e.id
21616
22333
  ORDER BY e.created_at ASC, e.id ASC`
21617
- ).all(...destinationIds);
22334
+ ),
22335
+ destinationIds
22336
+ );
21618
22337
  return rows.map((r) => ({
21619
22338
  id: r.id,
21620
22339
  destinationId: r.destinationId,
@@ -21639,13 +22358,16 @@ var SqliteSharesRepository = class {
21639
22358
  }
21640
22359
  fetchCallSites(endpointIds) {
21641
22360
  if (endpointIds.length === 0) return [];
21642
- const rows = this.db.prepare(
21643
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22361
+ const rows = allRows(
22362
+ this.db.prepare(
22363
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
21644
22364
  project_id AS projectId
21645
22365
  FROM share_call_site
21646
22366
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
21647
22367
  ORDER BY created_at ASC, id ASC`
21648
- ).all(...endpointIds);
22368
+ ),
22369
+ endpointIds
22370
+ );
21649
22371
  return rows.map((r) => ({
21650
22372
  id: r.id,
21651
22373
  endpointId: r.endpointId,
@@ -21681,27 +22403,36 @@ var SqliteSourceProjectRepository = class {
21681
22403
  upsert(input, now = Date.now()) {
21682
22404
  const id = sourceProjectId(input.url);
21683
22405
  const row = toSourceProjectRow(input, id, now);
21684
- this.upsertStmt.run({
21685
- id: row.id,
21686
- url: row.url ?? null,
21687
- name: row.name ?? null,
21688
- attributes: row.attributes,
21689
- firstSeen: row.firstSeen,
21690
- lastSeen: row.lastSeen
21691
- });
22406
+ this.upsertStmt.run(
22407
+ bindParams({
22408
+ id: row.id,
22409
+ url: row.url,
22410
+ name: row.name,
22411
+ attributes: row.attributes,
22412
+ firstSeen: row.firstSeen,
22413
+ lastSeen: row.lastSeen
22414
+ })
22415
+ );
21692
22416
  return id;
21693
22417
  }
21694
22418
  findById(id) {
21695
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22419
+ return getRow(
22420
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22421
+ {
22422
+ id
22423
+ }
22424
+ );
21696
22425
  }
21697
22426
  // Distinct project names — a filter facet, served from the source_project
21698
22427
  // table, never from the audit fact table.
21699
22428
  distinctNames() {
21700
- const rows = this.db.prepare(
21701
- `SELECT DISTINCT name FROM source_project
22429
+ const rows = allRows(
22430
+ this.db.prepare(
22431
+ `SELECT DISTINCT name FROM source_project
21702
22432
  WHERE name IS NOT NULL
21703
22433
  ORDER BY name`
21704
- ).all();
22434
+ )
22435
+ );
21705
22436
  return rows.map((r) => r.name);
21706
22437
  }
21707
22438
  };
@@ -21720,8 +22451,7 @@ function hasLegacySampleRows(db) {
21720
22451
  function purgeSampleData(db) {
21721
22452
  try {
21722
22453
  if (!hasLegacySampleRows(db)) return;
21723
- db.exec("BEGIN");
21724
- try {
22454
+ withTransaction(db, () => {
21725
22455
  db.exec(
21726
22456
  `DELETE FROM share_call_site WHERE endpoint_id IN (
21727
22457
  SELECT e.id FROM share_endpoint e
@@ -21766,11 +22496,7 @@ function purgeSampleData(db) {
21766
22496
  value TEXT NOT NULL
21767
22497
  )`);
21768
22498
  db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
21769
- db.exec("COMMIT");
21770
- } catch (err) {
21771
- db.exec("ROLLBACK");
21772
- throw err;
21773
- }
22499
+ });
21774
22500
  } catch {
21775
22501
  }
21776
22502
  }
@@ -21789,7 +22515,7 @@ function openWithPragmas(file2) {
21789
22515
  function backupLegacyStore(file2) {
21790
22516
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
21791
22517
  renameSync(file2, backup);
21792
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
22518
+ for (const sidecar of walSidecars(file2)) {
21793
22519
  if (existsSync(sidecar)) rmSync(sidecar);
21794
22520
  }
21795
22521
  return backup;
@@ -21802,9 +22528,8 @@ function openLocalDatabase(dir) {
21802
22528
  db.close();
21803
22529
  const backup = backupLegacyStore(file2);
21804
22530
  db = openWithPragmas(file2);
21805
- process.stderr.write(
21806
- `[aka] Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.
21807
- `
22531
+ akaWarn(
22532
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
21808
22533
  );
21809
22534
  }
21810
22535
  applyMigrations(db);
@@ -21832,96 +22557,77 @@ function openLocalDatabase(dir) {
21832
22557
  const configInventory = new SqliteConfigInventoryRepository(db);
21833
22558
  policies.seedDefaults();
21834
22559
  function recordCapture(event, detected) {
21835
- try {
21836
- db.exec("BEGIN");
21837
- try {
21838
- events.insertEvent(event);
21839
- const sessionId = event.metadata?.sessionId;
21840
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
21841
- db.exec("COMMIT");
21842
- } catch (err) {
21843
- db.exec("ROLLBACK");
21844
- throw err;
21845
- }
21846
- } catch {
21847
- }
22560
+ failOpenTransaction(db, () => {
22561
+ events.insertEvent(event);
22562
+ const sessionId = event.metadata?.sessionId;
22563
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
22564
+ });
21848
22565
  }
21849
22566
  function ensureInventory(ctx) {
21850
22567
  const resolved = {};
21851
- try {
21852
- db.exec("BEGIN");
21853
- try {
21854
- const now = Date.now();
21855
- if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
21856
- if (ctx.harness) {
21857
- resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
21858
- }
21859
- resolved.accountId = inventory.upsert(
21860
- linkHost(
21861
- {
21862
- objectType: "user",
21863
- identityKey: "local",
21864
- attributes: { source: "local" }
21865
- },
21866
- resolved.hostId
21867
- ),
21868
- now
21869
- );
21870
- if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
21871
- db.exec("COMMIT");
21872
- } catch (err) {
21873
- db.exec("ROLLBACK");
21874
- throw err;
21875
- }
21876
- } catch {
21877
- return {};
21878
- }
21879
- return resolved;
22568
+ const committed = failOpenTransaction(db, () => {
22569
+ const now = Date.now();
22570
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22571
+ if (ctx.harness) {
22572
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
22573
+ }
22574
+ resolved.accountId = inventory.upsert(
22575
+ linkHost(
22576
+ {
22577
+ objectType: "user",
22578
+ identityKey: "local",
22579
+ attributes: { source: "local" }
22580
+ },
22581
+ resolved.hostId
22582
+ ),
22583
+ now
22584
+ );
22585
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22586
+ });
22587
+ return committed ? resolved : {};
21880
22588
  }
21881
22589
  function recordConfigScan(record2) {
21882
- try {
21883
- db.exec("BEGIN");
21884
- try {
21885
- const now = isoToEpochMillis(record2.scanEvent.startedAt);
21886
- for (const item of record2.items) inventory.upsert(item, now);
21887
- auditEvents.insertAuditEvent(record2.scanEvent);
21888
- const definitionIds = /* @__PURE__ */ new Map();
21889
- for (const def of record2.definitions ?? []) {
21890
- definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
21891
- }
21892
- for (const finding of record2.findings ?? []) {
21893
- const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
21894
- if (!definitionId) continue;
21895
- inspectionFindings.insertFinding({
21896
- id: randomUUID8(),
21897
- auditEventId: record2.scanEvent.id,
21898
- inspectionDefinitionId: definitionId,
21899
- span: finding.span,
21900
- maskedMatch: finding.maskedMatch,
21901
- actionTaken: finding.actionTaken,
21902
- confidence: finding.confidence
21903
- });
21904
- }
21905
- db.exec("COMMIT");
21906
- } catch (err) {
21907
- db.exec("ROLLBACK");
21908
- throw err;
22590
+ failOpenTransaction(db, () => {
22591
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
22592
+ for (const item of record2.items) inventory.upsert(item, now);
22593
+ auditEvents.insertAuditEvent(record2.scanEvent);
22594
+ const definitionIds = /* @__PURE__ */ new Map();
22595
+ for (const def of record2.definitions ?? []) {
22596
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22597
+ }
22598
+ for (const finding of record2.findings ?? []) {
22599
+ const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
22600
+ if (!definitionId) continue;
22601
+ inspectionFindings.insertFinding({
22602
+ id: randomUUID8(),
22603
+ auditEventId: record2.scanEvent.id,
22604
+ inspectionDefinitionId: definitionId,
22605
+ span: finding.span,
22606
+ maskedMatch: finding.maskedMatch,
22607
+ actionTaken: finding.actionTaken,
22608
+ confidence: finding.confidence
22609
+ });
21909
22610
  }
21910
- } catch {
21911
- }
22611
+ });
21912
22612
  }
21913
22613
  function recordProjectFiles(projectId, scan2) {
21914
22614
  if (scan2.files.length === 0) return;
22615
+ failOpenTransaction(db, () => {
22616
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
22617
+ });
22618
+ }
22619
+ async function transaction(fn) {
22620
+ db.exec("BEGIN");
21915
22621
  try {
21916
- db.exec("BEGIN");
22622
+ const result = await fn();
22623
+ db.exec("COMMIT");
22624
+ return result;
22625
+ } catch (err) {
21917
22626
  try {
21918
- projectFiles.replaceForProject(projectId, scan2, Date.now());
21919
- db.exec("COMMIT");
21920
- } catch (err) {
21921
22627
  db.exec("ROLLBACK");
21922
- throw err;
22628
+ } catch {
21923
22629
  }
21924
- } catch {
22630
+ throw err;
21925
22631
  }
21926
22632
  }
21927
22633
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -21940,8 +22646,7 @@ function openLocalDatabase(dir) {
21940
22646
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
21941
22647
  });
21942
22648
  if (stale.length === 0) return;
21943
- db.exec("BEGIN");
21944
- try {
22649
+ withTransaction(db, () => {
21945
22650
  for (const { id } of stale) {
21946
22651
  db.prepare(
21947
22652
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -21953,11 +22658,7 @@ function openLocalDatabase(dir) {
21953
22658
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
21954
22659
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
21955
22660
  }
21956
- db.exec("COMMIT");
21957
- } catch (err) {
21958
- db.exec("ROLLBACK");
21959
- throw err;
21960
- }
22661
+ });
21961
22662
  } catch {
21962
22663
  }
21963
22664
  }
@@ -21999,6 +22700,7 @@ function openLocalDatabase(dir) {
21999
22700
  purgeSampleData: () => {
22000
22701
  purgeSampleData(db);
22001
22702
  },
22703
+ transaction,
22002
22704
  close: () => {
22003
22705
  db.close();
22004
22706
  }
@@ -22119,12 +22821,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
22119
22821
  }
22120
22822
  }
22121
22823
  function readJson(file2) {
22824
+ let text;
22122
22825
  try {
22123
- const parsed = JSON.parse(readFileSync2(file2, "utf8"));
22124
- return typeof parsed === "object" && parsed !== null ? parsed : null;
22826
+ text = readFileSync2(file2, "utf8");
22125
22827
  } catch {
22126
22828
  return null;
22127
22829
  }
22830
+ return parseJsonObject(text) ?? null;
22831
+ }
22832
+
22833
+ // ../../packages/persistence/src/warn-era-cap.ts
22834
+ import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
22835
+ import { join as join5 } from "path";
22836
+ var MARKER = "warn-era-capped";
22837
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22838
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
22839
+ const marker = join5(dataDir2, MARKER);
22840
+ if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22841
+ const capped = db.policies.capCategoryActions();
22842
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
22843
+ `, { mode: DATA_FILE_MODE });
22844
+ return { capped };
22128
22845
  }
22129
22846
 
22130
22847
  // ../../packages/plugin-sdk/src/provider-env.ts
@@ -22199,23 +22916,30 @@ function resolveProviderSafe() {
22199
22916
  // ../../packages/plugin-sdk/src/config-inventory.ts
22200
22917
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22201
22918
  import { homedir as homedir2 } from "os";
22202
- import { basename as basename2, join as join6 } from "path";
22919
+ import { basename as basename2, join as join7 } from "path";
22920
+
22921
+ // ../../packages/detections/src/escape-regexp.ts
22922
+ function escapeRegExp(value) {
22923
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22924
+ }
22925
+
22926
+ // ../../packages/detections/src/matchers/limits.ts
22927
+ var MAX_MATCHES_PER_RULE = 1e4;
22203
22928
 
22204
22929
  // ../../packages/detections/src/matchers/keyword.ts
22205
22930
  var KeywordMatcher2 = class {
22206
22931
  match(text, rule) {
22207
22932
  if (rule.matcher.type !== "keyword") return [];
22208
22933
  const { keywords, caseSensitive } = rule.matcher;
22209
- const haystack = caseSensitive ? text : text.toLowerCase();
22210
22934
  const spans = [];
22211
22935
  for (const kw of keywords) {
22212
- const needle = caseSensitive ? kw : kw.toLowerCase();
22213
- let idx = 0;
22214
- while (idx < haystack.length) {
22215
- const pos = haystack.indexOf(needle, idx);
22216
- if (pos === -1) break;
22217
- spans.push({ start: pos, end: pos + kw.length });
22218
- idx = pos + 1;
22936
+ if (kw.length === 0) continue;
22937
+ if (spans.length >= MAX_MATCHES_PER_RULE) break;
22938
+ const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
22939
+ let m;
22940
+ while ((m = re.exec(text)) !== null) {
22941
+ spans.push({ start: m.index, end: m.index + m[0].length });
22942
+ if (spans.length >= MAX_MATCHES_PER_RULE) break;
22219
22943
  }
22220
22944
  }
22221
22945
  return spans;
@@ -22223,7 +22947,6 @@ var KeywordMatcher2 = class {
22223
22947
  };
22224
22948
 
22225
22949
  // ../../packages/detections/src/matchers/regex.ts
22226
- var MAX_MATCHES_PER_RULE = 1e4;
22227
22950
  var RegexMatcher2 = class {
22228
22951
  match(text, rule) {
22229
22952
  if (rule.matcher.type !== "regex") return [];
@@ -22309,9 +23032,6 @@ function registerPack(pack) {
22309
23032
  function getLoadedRules() {
22310
23033
  return [...packs.values()].flatMap((p) => p.rules);
22311
23034
  }
22312
- function escapeRegExp(value) {
22313
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22314
- }
22315
23035
  function isCorroborated(candidate, candidates, text) {
22316
23036
  const req = candidate.rule.requiresNearby;
22317
23037
  if (!req) return true;
@@ -23093,7 +23813,6 @@ var db_table_name_default = {
23093
23813
  "SELECT * FROM ",
23094
23814
  "SELECT COUNT(*) FROM ",
23095
23815
  "INSERT INTO ",
23096
- "UPDATE ",
23097
23816
  "DELETE FROM ",
23098
23817
  "CREATE TABLE ",
23099
23818
  "ALTER TABLE ",
@@ -24537,8 +25256,8 @@ function bundledDetections() {
24537
25256
  }
24538
25257
 
24539
25258
  // ../../packages/plugin-sdk/src/repo.ts
24540
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
24541
- import { basename, dirname, isAbsolute, join as join5, sep as sep2 } from "path";
25259
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25260
+ import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
24542
25261
 
24543
25262
  // ../../packages/plugin-sdk/src/events.ts
24544
25263
  import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
@@ -24577,16 +25296,17 @@ function computeFindingKey(input) {
24577
25296
  import { arch, hostname as hostname3, platform, release } from "os";
24578
25297
 
24579
25298
  // ../../packages/plugin-sdk/src/nudge.ts
24580
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
24581
- import { join as join7 } from "path";
25299
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25300
+ import { join as join8 } from "path";
24582
25301
 
24583
25302
  // ../../packages/plugin-sdk/src/project-files.ts
24584
25303
  var import_ignore = __toESM(require_ignore(), 1);
24585
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
24586
- import { basename as basename3, join as join8, relative, sep as sep3 } from "path";
25304
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25305
+ import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
24587
25306
 
24588
25307
  // ../../packages/plugin-sdk/src/runtime.ts
24589
25308
  import { randomUUID as randomUUID10 } from "crypto";
25309
+ var ENFORCEMENT_CEILING_ENABLED = false;
24590
25310
  var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
24591
25311
  function entryIsActive(entry, now) {
24592
25312
  if (entry.expiresAt !== null && Date.parse(entry.expiresAt) <= now) return false;
@@ -24659,17 +25379,22 @@ function createPluginRuntime(gateway, settings, opts) {
24659
25379
  const fallback = DEFAULT_ACTIONS[category];
24660
25380
  return fallback ?? "log";
24661
25381
  }
25382
+ function actionForFinding(finding, excepted) {
25383
+ if (excepted?.has(finding)) return "allow";
25384
+ const action = resolveAction(finding.ruleId, finding.category);
25385
+ if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (action === "block" || action === "redact")) {
25386
+ return "warn";
25387
+ }
25388
+ return action;
25389
+ }
24662
25390
  function decide(findings, text, excepted) {
24663
25391
  if (findings.length === 0) return { action: "log", text, findings: [] };
24664
- const actionFor = (finding) => excepted?.has(finding) ? "allow" : resolveAction(finding.ruleId, finding.category);
25392
+ const actionFor = (finding) => actionForFinding(finding, excepted);
24665
25393
  let worst = "log";
24666
25394
  for (const finding of findings) {
24667
25395
  const action = actionFor(finding);
24668
25396
  if (ACTION_PRIORITY.indexOf(action) < ACTION_PRIORITY.indexOf(worst)) worst = action;
24669
25397
  }
24670
- if (policyMode === "warn" && (worst === "block" || worst === "redact")) {
24671
- return { action: "warn", text, findings };
24672
- }
24673
25398
  if (worst === "block") return { action: "block", text: null, findings };
24674
25399
  if (worst === "redact") {
24675
25400
  const redactFindings = findings.filter((f) => actionFor(f) === "redact");
@@ -24689,7 +25414,7 @@ function createPluginRuntime(gateway, settings, opts) {
24689
25414
  const excepted = /* @__PURE__ */ new Set();
24690
25415
  const exceptionIds = [];
24691
25416
  try {
24692
- if (policyMode === "warn") return { excepted, exceptionIds };
25417
+ if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn") return { excepted, exceptionIds };
24693
25418
  const enforced = findings.filter((f) => {
24694
25419
  const action = resolveAction(f.ruleId, f.category);
24695
25420
  return action === "block" || action === "redact";
@@ -24742,8 +25467,8 @@ function createPluginRuntime(gateway, settings, opts) {
24742
25467
  if (!key) return references;
24743
25468
  const seen = /* @__PURE__ */ new Set();
24744
25469
  for (const finding of decision.findings) {
24745
- const action = resolveAction(finding.ruleId, finding.category);
24746
- if (action !== "block" && action !== "redact" || excepted.has(finding)) continue;
25470
+ const action = actionForFinding(finding, excepted);
25471
+ if (action !== "block" && action !== "redact") continue;
24747
25472
  const fp = fingerprintOf(key, finding, fpCache);
24748
25473
  const pair = `${finding.ruleId}:${fp}`;
24749
25474
  if (seen.has(pair)) continue;
@@ -24832,7 +25557,7 @@ function createPluginRuntime(gateway, settings, opts) {
24832
25557
  severity: match.severity,
24833
25558
  span: match.span,
24834
25559
  maskedMatch,
24835
- actionTaken: excepted.has(match) ? "allow" : decision.action,
25560
+ actionTaken: actionForFinding(match, excepted),
24836
25561
  confidence: match.confidence,
24837
25562
  ...findingKey ? { findingKey } : {}
24838
25563
  };
@@ -24860,13 +25585,16 @@ function createPluginRuntime(gateway, settings, opts) {
24860
25585
  return { processText, capture, rulesetFingerprint, close };
24861
25586
  }
24862
25587
 
25588
+ // ../../packages/plugin-sdk/src/suppressions.ts
25589
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
25590
+
24863
25591
  // ../../packages/plugin-sdk/src/throttle.ts
24864
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
24865
- import { join as join9 } from "path";
25592
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
25593
+ import { join as join10 } from "path";
24866
25594
 
24867
25595
  // ../../packages/scanner/src/discover.ts
24868
25596
  import { readdirSync as readdirSync3 } from "fs";
24869
- import { join as join10 } from "path";
25597
+ import { join as join11 } from "path";
24870
25598
 
24871
25599
  // ../../packages/scanner/src/constants.ts
24872
25600
  var COMMON_SKIP_DIRS = ["node_modules", "__pycache__", ".venv", "venv", ".cache"];
@@ -24911,7 +25639,7 @@ function discoverGitRepos(opts) {
24911
25639
  if (!entry.isDirectory()) continue;
24912
25640
  if (DISCOVER_SKIP.has(entry.name)) continue;
24913
25641
  if (entry.name.startsWith(".")) continue;
24914
- visit(join10(dir, entry.name), depth + 1);
25642
+ visit(join11(dir, entry.name), depth + 1);
24915
25643
  }
24916
25644
  }
24917
25645
  for (const root of searchRoots) {
@@ -25014,7 +25742,7 @@ function renderMultiRepoSummary(summary, opts = {}) {
25014
25742
  }
25015
25743
 
25016
25744
  // ../../packages/scanner/src/scan.ts
25017
- import { existsSync as existsSync4 } from "fs";
25745
+ import { existsSync as existsSync5 } from "fs";
25018
25746
  import { isAbsolute as isAbsolute2, relative as relative4 } from "path";
25019
25747
 
25020
25748
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
@@ -25208,6 +25936,14 @@ var StandaloneDataGateway = class {
25208
25936
  sweepTerminalExceptions(retentionMs) {
25209
25937
  return this.db.exceptions.sweepTerminal(retentionMs);
25210
25938
  }
25939
+ // The warn-era enforcement cap, standalone-only store maintenance invoked
25940
+ // from SessionStart, not part of the DataGateway port. Returns the number
25941
+ // of block/redact rows capped to warn (0 for a redact-policy store or an
25942
+ // already-capped one).
25943
+ capWarnEraEnforcement(policyMode) {
25944
+ const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
25945
+ return { capped };
25946
+ }
25211
25947
  // One project-file scan → the local project_file tree (one transaction inside
25212
25948
  // the LocalDatabase, fail-open there). Like the sweep above, this is
25213
25949
  // NOT part of the DataGateway port: the file tree is a local-store read model.
@@ -25313,7 +26049,7 @@ function computeResolutions(prior, current) {
25313
26049
  // ../../packages/scanner/src/walk.ts
25314
26050
  var import_ignore2 = __toESM(require_ignore(), 1);
25315
26051
  import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
25316
- import { extname, join as join11, relative as relative3, sep as sep4 } from "path";
26052
+ import { extname, join as join12, relative as relative3, sep as sep4 } from "path";
25317
26053
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
25318
26054
  ".ts",
25319
26055
  ".tsx",
@@ -25345,7 +26081,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
25345
26081
  var DEFAULT_MAX_BYTES = 512 * 1024;
25346
26082
  function readIgnoreLayer(dir, filename) {
25347
26083
  try {
25348
- const content = readFileSync7(join11(dir, filename), "utf8");
26084
+ const content = readFileSync7(join12(dir, filename), "utf8");
25349
26085
  return { base: dir, matcher: (0, import_ignore2.default)().add(content) };
25350
26086
  } catch {
25351
26087
  return void 0;
@@ -25379,7 +26115,7 @@ function* walkSourceFiles(opts = {}) {
25379
26115
  const dirSkipLayers = skipLayer ? [...skipLayers, skipLayer] : skipLayers;
25380
26116
  for (const entry of dirents) {
25381
26117
  const name = entry.name;
25382
- const fullPath = join11(dir, name);
26118
+ const fullPath = join12(dir, name);
25383
26119
  if (entry.isDirectory()) {
25384
26120
  const skipState = evaluate(dirSkipLayers, fullPath, true);
25385
26121
  if (skipState !== "unignored" && (SKIP_DIRS.has(name) || skipState === "ignored")) {
@@ -25472,7 +26208,7 @@ function isUnderRoot(path, rootDir) {
25472
26208
  }
25473
26209
  async function sweepDeletedFiles(gateway, rootDir, previous) {
25474
26210
  for (const path of previous.keys()) {
25475
- if (!isUnderRoot(path, rootDir) || existsSync4(path)) continue;
26211
+ if (!isUnderRoot(path, rootDir) || existsSync5(path)) continue;
25476
26212
  await resolveRemovedFindings(gateway, path, [], { deleted: true });
25477
26213
  }
25478
26214
  }