@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.
package/scripts/query.js CHANGED
@@ -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
 
@@ -641,6 +645,10 @@ var defaultCostModel = {
641
645
  };
642
646
 
643
647
  // ../../packages/schema/src/token/format.ts
648
+ var COMPACT = new Intl.NumberFormat("en-US", {
649
+ notation: "compact",
650
+ maximumFractionDigits: 1
651
+ });
644
652
  function formatUsd(value) {
645
653
  return value > 0 && value < 0.1 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
646
654
  }
@@ -15373,6 +15381,12 @@ var FindingInstance = external_exports.object({
15373
15381
  provider: FindingProvider,
15374
15382
  repo: external_exports.string(),
15375
15383
  file: external_exports.string(),
15384
+ // Host tool that produced the scanned text (event metadata's toolName).
15385
+ // Present whenever the capturing hook recorded one — including
15386
+ // file-attributed captures (views prefer `file`); its display value is
15387
+ // the location fallback ("via Bash") when no filePath exists. Absent for
15388
+ // legacy rows and non-tool captures (prompts, worktree scans).
15389
+ toolName: external_exports.string().optional(),
15376
15390
  // Effective action: override.action ?? actionTaken, translated to FindingAction.
15377
15391
  action: FindingAction,
15378
15392
  detectedAt: external_exports.iso.datetime(),
@@ -15429,6 +15443,9 @@ var ListGroupedFindingsQuery = external_exports.object({
15429
15443
  provider: external_exports.array(FindingProvider).optional(),
15430
15444
  action: external_exports.array(FindingAction).optional(),
15431
15445
  q: external_exports.string().optional(),
15446
+ // Scope to findings whose event carries this session id (the Activity page's
15447
+ // session → findings drilldown). Findings without a session never match.
15448
+ sessionId: external_exports.string().optional(),
15432
15449
  groupBy: external_exports.literal("type").optional(),
15433
15450
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
15434
15451
  cursor: external_exports.string().optional()
@@ -15440,7 +15457,13 @@ var ListGroupedFindingsResponse = external_exports.object({
15440
15457
  }),
15441
15458
  facets: FindingFacets,
15442
15459
  items: external_exports.array(FindingGroup),
15443
- nextCursor: external_exports.string().nullable()
15460
+ nextCursor: external_exports.string().nullable(),
15461
+ // Present only on session-scoped queries (`sessionId` set): per ruleId, how
15462
+ // many times that rule fired in the session's persisted transcript. Findings
15463
+ // here are deduplicated to unique values while the transcript tally counts
15464
+ // every firing, so the two numbers legitimately differ — this map lets a
15465
+ // session-scoped view show both.
15466
+ sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
15444
15467
  }).meta({ id: "ListGroupedFindingsResponse" });
15445
15468
  var ApplyFindingActionRequest = external_exports.object({
15446
15469
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
@@ -15489,6 +15512,11 @@ var AuditEventType = external_exports.enum([
15489
15512
  "prompt",
15490
15513
  "response",
15491
15514
  "code_change",
15515
+ // The events.kind of a scanned tool call, widened in to keep this a
15516
+ // superset. Narrower than 'tool_call' above and not a duplicate of it:
15517
+ // 'tool_call' is the reconciler's structural row for every call, while
15518
+ // 'tool_use' exists only where a hook enforced against the arguments.
15519
+ "tool_use",
15492
15520
  // One row per config-inventory scan, hung off the session root. It is the
15493
15521
  // fact the posture inspection findings reference (findings require an
15494
15522
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -15843,6 +15871,10 @@ var ListActivitySessionsQuery = external_exports.object({
15843
15871
  from: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
15844
15872
  /** Upper bound on startedAt; omitted defaults to now. */
15845
15873
  to: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
15874
+ /** Exclude zero-activity sessions — roots whose only recorded children are
15875
+ * bookkeeping rows (hooks, config scans), typically background `claude`
15876
+ * launches. Omitted = list everything. `z.stringbool()` per the note above. */
15877
+ excludeEmpty: external_exports.stringbool().optional(),
15846
15878
  /** Page size, 1–100; out-of-range values are a 400. `z.coerce` — query params arrive as strings. */
15847
15879
  limit: external_exports.coerce.number().int().min(1).max(100).default(50),
15848
15880
  /** Opaque pagination cursor (most-recent first). */
@@ -15851,7 +15883,11 @@ var ListActivitySessionsQuery = external_exports.object({
15851
15883
  var ListActivitySessionsResponse = external_exports.object({
15852
15884
  items: external_exports.array(ActivitySessionSummary),
15853
15885
  /** `null` once the last page is reached. */
15854
- nextCursor: external_exports.string().nullable()
15886
+ nextCursor: external_exports.string().nullable(),
15887
+ /** Zero-activity sessions matching the query's filters/range (whether or
15888
+ * not `excludeEmpty` dropped them from `items`) — the count a UI toggle
15889
+ * shows when collapsing them. */
15890
+ emptyCount: external_exports.number().int().nonnegative()
15855
15891
  }).meta({ id: "ListActivitySessionsResponse" });
15856
15892
  var ListSessionEventsQuery = external_exports.object({
15857
15893
  /** Default 100, range 1–500. */
@@ -15880,12 +15916,18 @@ var ActivityOverviewResponse = external_exports.object({
15880
15916
  }).meta({ id: "ActivityOverviewResponse" });
15881
15917
 
15882
15918
  // ../../packages/schema/src/zod/event.ts
15883
- var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
15919
+ var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15884
15920
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15885
15921
  var EventMetadata = external_exports.object({
15886
15922
  sessionId: external_exports.string().optional(),
15887
15923
  repo: external_exports.string().optional(),
15888
15924
  filePath: external_exports.string().optional(),
15925
+ // The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch'),
15926
+ // set by the tool-scanning hooks. The tool NAME only — never the tool's
15927
+ // arguments or output, which can carry the very value a finding masked
15928
+ // (metadata is stored unredacted). Gives findings on non-file captures a
15929
+ // display location ("via Bash") when no filePath exists.
15930
+ toolName: external_exports.string().optional(),
15889
15931
  // Set (true) by the worktree scanner when the file is excluded by the
15890
15932
  // repo's .gitignore. Gitignored files ARE still scanned — local scratch and
15891
15933
  // generated code can leak real secrets — but the provenance is recorded so
@@ -16193,7 +16235,7 @@ var DetectionException = external_exports.object({
16193
16235
  justification: external_exports.string().min(1),
16194
16236
  conditions: ExceptionConditions.nullable(),
16195
16237
  createdBy: external_exports.string(),
16196
- createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
16238
+ createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
16197
16239
  createdAt: external_exports.iso.datetime(),
16198
16240
  updatedAt: external_exports.iso.datetime(),
16199
16241
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16217,7 +16259,10 @@ var ExceptionBundleEntry = DetectionException.pick({
16217
16259
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16218
16260
  var KeywordMatcher = external_exports.object({
16219
16261
  type: external_exports.literal("keyword"),
16220
- keywords: external_exports.array(external_exports.string()).min(1),
16262
+ // An empty keyword matches at every position, yielding one zero-length span
16263
+ // per character. Rejected here because a keyword that matches everything is
16264
+ // never intentional.
16265
+ keywords: external_exports.array(external_exports.string().min(1)).min(1),
16221
16266
  caseSensitive: external_exports.boolean().default(false)
16222
16267
  });
16223
16268
  function isValidRegex(pattern, flags) {
@@ -16362,20 +16407,26 @@ var PolicyBundle = external_exports.object({
16362
16407
  customKeywords: external_exports.array(external_exports.string()),
16363
16408
  fetchedAt: external_exports.iso.datetime()
16364
16409
  }).meta({ id: "PolicyBundle" });
16365
- var DEFAULT_ACTIONS = {
16366
- secret: "block",
16367
- pii: "redact",
16368
- financial: "redact",
16369
- phi: "redact",
16370
- code_context: "warn",
16371
- code_flaw: "warn",
16372
- custom: "warn",
16373
- // Config-posture findings only observe today (they land in
16374
- // inspection_findings, outside the live-capture enforcement path).
16375
- config: "warn"
16376
- };
16377
16410
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16378
16411
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
16412
+ var CATEGORY_PEAK_SEVERITY = {
16413
+ secret: "critical",
16414
+ financial: "critical",
16415
+ // core-financial/credit-card
16416
+ code_flaw: "critical",
16417
+ pii: "high",
16418
+ phi: "high",
16419
+ custom: "high",
16420
+ // user-defined; conservative
16421
+ code_context: "low",
16422
+ config: "low"
16423
+ // observe-only; floors to monitor regardless
16424
+ };
16425
+ function severityFloorPolicy(category) {
16426
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
16427
+ const peak = CATEGORY_PEAK_SEVERITY[category];
16428
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
16429
+ }
16379
16430
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
16380
16431
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
16381
16432
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -16402,6 +16453,12 @@ var BUILTIN_POLICY_SPECS = {
16402
16453
  description: "Refuse the request entirely whenever any rule in this detection matches."
16403
16454
  }
16404
16455
  };
16456
+ function builtinPolicyToAction(id) {
16457
+ return BUILTIN_POLICY_SPECS[id].action;
16458
+ }
16459
+ var DEFAULT_ACTIONS = Object.fromEntries(
16460
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
16461
+ );
16405
16462
  var BUILTIN_POLICIES = Object.fromEntries(
16406
16463
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
16407
16464
  );
@@ -16454,6 +16511,10 @@ var ListEventsResponse = external_exports.object({
16454
16511
  items: external_exports.array(Event),
16455
16512
  nextCursor: external_exports.string().nullable()
16456
16513
  }).meta({ id: "ListEventsResponse" });
16514
+ var IngestResponse = external_exports.object({
16515
+ accepted: external_exports.number().int().nonnegative(),
16516
+ duplicates: external_exports.number().int().nonnegative()
16517
+ }).meta({ id: "IngestResponse" });
16457
16518
  var ListFindingsQuery = external_exports.object({
16458
16519
  cursor: external_exports.string().optional(),
16459
16520
  limit: external_exports.coerce.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).default(50),
@@ -16808,10 +16869,8 @@ function toApiProvider(sourceTool) {
16808
16869
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
16809
16870
  }
16810
16871
  var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
16811
- function deriveGroupStatus(instances) {
16812
- const statuses = new Set(
16813
- instances.map((i) => i.status).filter((s) => s !== void 0)
16814
- );
16872
+ function foldGroupStatus(instanceStatuses) {
16873
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
16815
16874
  if (statuses.size === 0) return void 0;
16816
16875
  for (const candidate of STATUS_PRECEDENCE) {
16817
16876
  if (statuses.has(candidate)) return candidate;
@@ -16829,6 +16888,7 @@ function deriveFindingStatus(row) {
16829
16888
  function buildFindingGroups(rows, opts = {}) {
16830
16889
  const overrides = opts.overrides;
16831
16890
  const packNames = opts.packNames;
16891
+ const aggregates = opts.aggregates;
16832
16892
  const byRuleId = /* @__PURE__ */ new Map();
16833
16893
  for (const row of rows) {
16834
16894
  const existing = byRuleId.get(row.ruleId);
@@ -16844,23 +16904,27 @@ function buildFindingGroups(rows, opts = {}) {
16844
16904
  provider: toApiProvider(r.sourceTool),
16845
16905
  repo: r.repo,
16846
16906
  file: r.file,
16907
+ ...r.toolName === void 0 ? {} : { toolName: r.toolName },
16847
16908
  action: toApiAction(effectiveDbAction),
16848
16909
  detectedAt: r.occurredAt,
16849
16910
  confidence: r.confidence,
16850
16911
  status: r.status
16851
16912
  };
16852
16913
  });
16853
- const latestDetectedAt = ruleRows.reduce(
16914
+ const agg = aggregates?.get(ruleId);
16915
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
16854
16916
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
16855
16917
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
16856
16918
  );
16857
16919
  const seenProviders = /* @__PURE__ */ new Set();
16858
- const providers = instances.map((i) => i.provider).filter((p) => {
16920
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
16859
16921
  if (seenProviders.has(p)) return false;
16860
16922
  seenProviders.add(p);
16861
16923
  return true;
16862
16924
  });
16863
- const actionSet = new Set(instances.map((i) => i.action));
16925
+ const actionSet = new Set(
16926
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
16927
+ );
16864
16928
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
16865
16929
  const severity = ruleRows[0]?.severity ?? "low";
16866
16930
  const detection = {
@@ -16874,8 +16938,10 @@ function buildFindingGroups(rows, opts = {}) {
16874
16938
  contextPrefix: ""
16875
16939
  // empty (pending privacy review)
16876
16940
  };
16877
- const status = deriveGroupStatus(instances);
16878
- groups.push({
16941
+ const status = foldGroupStatus(
16942
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
16943
+ );
16944
+ const group = {
16879
16945
  id: ruleId,
16880
16946
  category: apiCategory,
16881
16947
  subtype: ruleId,
@@ -16884,21 +16950,26 @@ function buildFindingGroups(rows, opts = {}) {
16884
16950
  match,
16885
16951
  detection,
16886
16952
  policy,
16887
- instanceCount: instances.length,
16953
+ instanceCount: agg?.instanceCount ?? instances.length,
16888
16954
  providers,
16889
16955
  aggregateAction,
16890
16956
  latestDetectedAt,
16891
16957
  instances,
16892
16958
  status
16893
- });
16959
+ };
16960
+ if (agg) {
16961
+ actionsCache.set(group, [...actionSet]);
16962
+ if (agg.searchText !== void 0) {
16963
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
16964
+ }
16965
+ }
16966
+ groups.push(group);
16894
16967
  }
16895
16968
  return groups;
16896
16969
  }
16897
16970
  var haystackCache = /* @__PURE__ */ new WeakMap();
16898
- function groupHaystack(g) {
16899
- const cached2 = haystackCache.get(g);
16900
- if (cached2 !== void 0) return cached2;
16901
- const haystack = [
16971
+ function buildHaystack(g, extra) {
16972
+ return [
16902
16973
  g.subtype,
16903
16974
  g.category,
16904
16975
  g.match.maskedValue,
@@ -16906,11 +16977,26 @@ function groupHaystack(g) {
16906
16977
  g.id,
16907
16978
  ...g.instances.map((i) => i.repo),
16908
16979
  ...g.instances.map((i) => i.file),
16909
- ...g.instances.map((i) => i.id)
16980
+ ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
16981
+ ...g.instances.map((i) => i.id),
16982
+ ...extra === void 0 ? [] : [extra]
16910
16983
  ].join(" ").toLowerCase();
16984
+ }
16985
+ function groupHaystack(g) {
16986
+ const cached2 = haystackCache.get(g);
16987
+ if (cached2 !== void 0) return cached2;
16988
+ const haystack = buildHaystack(g);
16911
16989
  haystackCache.set(g, haystack);
16912
16990
  return haystack;
16913
16991
  }
16992
+ var actionsCache = /* @__PURE__ */ new WeakMap();
16993
+ function groupActions(g) {
16994
+ const cached2 = actionsCache.get(g);
16995
+ if (cached2 !== void 0) return cached2;
16996
+ const actions = [...new Set(g.instances.map((i) => i.action))];
16997
+ actionsCache.set(g, actions);
16998
+ return actions;
16999
+ }
16914
17000
  function applyFindingFilters(groups, opts) {
16915
17001
  let filtered = groups;
16916
17002
  if (opts.severity && opts.severity.length > 0) {
@@ -16923,7 +17009,7 @@ function applyFindingFilters(groups, opts) {
16923
17009
  }
16924
17010
  if (opts.actions && opts.actions.length > 0) {
16925
17011
  const actionSet = new Set(opts.actions);
16926
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
17012
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
16927
17013
  }
16928
17014
  if (opts.subtype && opts.subtype.length > 0) {
16929
17015
  const subtypeSet = new Set(opts.subtype);
@@ -16975,8 +17061,7 @@ function computeFindingFacets(allGroups, opts) {
16975
17061
  });
16976
17062
  const actionMap = /* @__PURE__ */ new Map();
16977
17063
  for (const g of forAction) {
16978
- const actionSet = new Set(g.instances.map((i) => i.action));
16979
- for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
17064
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
16980
17065
  }
16981
17066
  const forSubtype = applyFindingFilters(allGroups, {
16982
17067
  providers: opts.providers,
@@ -17159,6 +17244,71 @@ var ProjectFilesScan = external_exports.object({
17159
17244
  scannedAt: external_exports.string()
17160
17245
  });
17161
17246
 
17247
+ // ../../packages/schema/src/zod/ranges.ts
17248
+ var TIME_RANGES = ["7d", "30d", "3m", "6m"];
17249
+ var TimeRange = external_exports.enum(TIME_RANGES).meta({ id: "TimeRange" });
17250
+ var DEFAULT_TIME_RANGE = "7d";
17251
+ var RANGE_DAYS = {
17252
+ "7d": 7,
17253
+ "30d": 30,
17254
+ "3m": 90,
17255
+ "6m": 180
17256
+ };
17257
+ var TIME_RANGE_OR_DEFAULT = TimeRange.catch(DEFAULT_TIME_RANGE);
17258
+
17259
+ // ../../packages/schema/src/zod/remediation.ts
17260
+ var SecretFindingState = external_exports.enum(["still-valid", "unknown", "invalid"]);
17261
+ var MaskedFindingLocation = external_exports.object({
17262
+ filePath: external_exports.string(),
17263
+ span: Span.optional()
17264
+ }).strict();
17265
+ var MaskedSecretFinding = external_exports.object({
17266
+ provider: external_exports.string(),
17267
+ maskedToken: external_exports.string(),
17268
+ where: MaskedFindingLocation,
17269
+ state: SecretFindingState,
17270
+ observedAt: external_exports.iso.datetime().optional()
17271
+ }).strict();
17272
+ var RotationChecklistEntry = external_exports.object({
17273
+ provider: external_exports.string(),
17274
+ maskedToken: external_exports.string(),
17275
+ consolePath: external_exports.string(),
17276
+ occurrenceSpread: external_exports.number().int().positive()
17277
+ }).strict();
17278
+ var RemediationOption = external_exports.enum([
17279
+ "redact-rotation-checklist",
17280
+ "redact-only",
17281
+ "set-secret-redact",
17282
+ "leave"
17283
+ ]);
17284
+ var RemediationEntrySource = external_exports.enum(["first-run", "pre-push", "secret-scan"]);
17285
+ var RemediationEntryContext = external_exports.object({
17286
+ entrySource: RemediationEntrySource
17287
+ }).strict();
17288
+ var RemediationOptionChoice = external_exports.object({
17289
+ id: RemediationOption,
17290
+ label: external_exports.string()
17291
+ });
17292
+ var BatchedRemediationDecision = external_exports.object({
17293
+ kind: external_exports.literal("decision"),
17294
+ entrySource: RemediationEntrySource,
17295
+ secretCount: external_exports.number().int().positive(),
17296
+ prompt: external_exports.string(),
17297
+ options: external_exports.tuple([
17298
+ RemediationOptionChoice.extend({ id: external_exports.literal("redact-rotation-checklist") }),
17299
+ RemediationOptionChoice.extend({ id: external_exports.literal("redact-only") }),
17300
+ RemediationOptionChoice.extend({ id: external_exports.literal("set-secret-redact") }),
17301
+ RemediationOptionChoice.extend({ id: external_exports.literal("leave") })
17302
+ ])
17303
+ });
17304
+ var NoRemediationDecision = external_exports.object({
17305
+ kind: external_exports.literal("no-decision")
17306
+ });
17307
+ var BatchedRemediation = external_exports.discriminatedUnion("kind", [
17308
+ BatchedRemediationDecision,
17309
+ NoRemediationDecision
17310
+ ]);
17311
+
17162
17312
  // ../../packages/schema/src/zod/rule-test.ts
17163
17313
  var TestRulesRequest = external_exports.object({
17164
17314
  rules: external_exports.array(Rule).min(1).max(100),
@@ -17217,10 +17367,8 @@ var SeveritySummaryResponse = external_exports.object({
17217
17367
  // All four severity levels are always present (count may be 0).
17218
17368
  bySeverity: external_exports.array(SeveritySummaryItem)
17219
17369
  }).meta({ id: "SeveritySummaryResponse" });
17220
- var SECURITY_RANGES = ["7d", "30d", "3m", "6m"];
17221
- var SecurityRange = external_exports.enum(SECURITY_RANGES).meta({ id: "SecurityRange" });
17222
17370
  var SecurityRangeQuery = external_exports.object({
17223
- range: external_exports.enum(SECURITY_RANGES).default("30d")
17371
+ range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE)
17224
17372
  });
17225
17373
  var EnforcementActionKind = external_exports.enum(["blocked", "redacted", "warned"]).meta({ id: "EnforcementActionKind" });
17226
17374
  var EnforcementAction = external_exports.object({
@@ -17230,7 +17378,7 @@ var EnforcementAction = external_exports.object({
17230
17378
  delta: external_exports.number().int()
17231
17379
  }).meta({ id: "EnforcementAction" });
17232
17380
  var EnforcementActionsResponse = external_exports.object({
17233
- range: SecurityRange,
17381
+ range: TimeRange,
17234
17382
  // Sum of actions[].count in the window.
17235
17383
  total: external_exports.number().int().nonnegative(),
17236
17384
  // One entry per kind, always all three present (count may be 0).
@@ -17245,7 +17393,7 @@ var FindingsTimeseriesPoint = external_exports.object({
17245
17393
  medium: external_exports.number().int().nonnegative()
17246
17394
  }).meta({ id: "FindingsTimeseriesPoint" });
17247
17395
  var FindingsTimeseriesResponse = external_exports.object({
17248
- range: SecurityRange,
17396
+ range: TimeRange,
17249
17397
  granularity: TimeseriesGranularity,
17250
17398
  points: external_exports.array(FindingsTimeseriesPoint)
17251
17399
  }).meta({ id: "FindingsTimeseriesResponse" });
@@ -17260,7 +17408,7 @@ var MttrTrendPoint = external_exports.object({
17260
17408
  })
17261
17409
  }).meta({ id: "MttrTrendPoint" });
17262
17410
  var MttrTrendResponse = external_exports.object({
17263
- range: SecurityRange,
17411
+ range: TimeRange,
17264
17412
  granularity: TimeseriesGranularity,
17265
17413
  points: external_exports.array(MttrTrendPoint)
17266
17414
  }).meta({ id: "MttrTrendResponse" });
@@ -17287,11 +17435,11 @@ var TopSource = external_exports.object({
17287
17435
  findingsCount: external_exports.number().int().nonnegative()
17288
17436
  }).meta({ id: "TopSource" });
17289
17437
  var TopSourcesResponse = external_exports.object({
17290
- range: SecurityRange,
17438
+ range: TimeRange,
17291
17439
  items: external_exports.array(TopSource)
17292
17440
  }).meta({ id: "TopSourcesResponse" });
17293
17441
  var TopSourcesQuery = external_exports.object({
17294
- range: external_exports.enum(SECURITY_RANGES).default("30d"),
17442
+ range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE),
17295
17443
  limit: external_exports.coerce.number().int().min(1).max(50).default(5),
17296
17444
  // Omit for both kinds.
17297
17445
  kind: external_exports.enum(SOURCE_KINDS).optional()
@@ -17304,7 +17452,7 @@ var ScanCoverageProvider = external_exports.object({
17304
17452
  supported: external_exports.boolean()
17305
17453
  }).meta({ id: "ScanCoverageProvider" });
17306
17454
  var ScanCoverageResponse = external_exports.object({
17307
- range: SecurityRange,
17455
+ range: TimeRange,
17308
17456
  providers: external_exports.array(ScanCoverageProvider)
17309
17457
  }).meta({ id: "ScanCoverageResponse" });
17310
17458
  var SubjectType = external_exports.enum(["repo", "user", "team", "policy", "share", "rule"]).meta({
@@ -17353,6 +17501,114 @@ var ApplyRecommendedActionResponse = external_exports.object({
17353
17501
  var DismissRecommendedActionResponse = external_exports.object({ id: external_exports.string(), status: external_exports.literal("dismissed") }).meta({ id: "DismissRecommendedActionResponse" });
17354
17502
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
17355
17503
 
17504
+ // ../../packages/schema/src/zod/triage.ts
17505
+ var TriageHit = external_exports.object({
17506
+ ruleId: external_exports.string(),
17507
+ category: DetectionCategory,
17508
+ severity: Severity,
17509
+ maskedMatch: external_exports.string(),
17510
+ rawMatch: external_exports.string(),
17511
+ context: external_exports.string(),
17512
+ filePath: external_exports.string().optional(),
17513
+ confidence: external_exports.number().min(0).max(1),
17514
+ id: external_exports.string().optional(),
17515
+ valueFingerprint: external_exports.string().optional(),
17516
+ keyVersion: external_exports.number().int().nonnegative().optional()
17517
+ });
17518
+ var TriagePolicy = BuiltinPolicyId;
17519
+ var TriageCategoryRec = external_exports.object({
17520
+ category: DetectionCategory,
17521
+ action: TriagePolicy,
17522
+ reasoning: external_exports.string(),
17523
+ genuineCount: external_exports.number().int().nonnegative(),
17524
+ fpCount: external_exports.number().int().nonnegative(),
17525
+ // TriageHit ids judged false-positive in this category. fpCount must equal
17526
+ // this array's length — enforced by the consumer, not this schema.
17527
+ fpIds: external_exports.array(external_exports.string())
17528
+ });
17529
+ var TriageRecommendation = external_exports.object({
17530
+ perCategory: external_exports.array(TriageCategoryRec),
17531
+ notes: external_exports.string()
17532
+ });
17533
+
17534
+ // ../../packages/schema/src/zod/setup-frame.ts
17535
+ var CalibrationCounts = external_exports.object({
17536
+ total: external_exports.number().int().nonnegative(),
17537
+ important: external_exports.number().int().nonnegative(),
17538
+ routine: external_exports.number().int().nonnegative()
17539
+ }).refine((c) => c.total === c.important + c.routine, {
17540
+ message: "total must equal important + routine",
17541
+ path: ["total"]
17542
+ });
17543
+ var FalsePositivePatternValue = external_exports.object({
17544
+ ruleId: external_exports.string(),
17545
+ category: DetectionCategory,
17546
+ valueFingerprint: external_exports.string(),
17547
+ keyVersion: external_exports.number().int().nonnegative()
17548
+ });
17549
+ var FalsePositivePatternGroup = external_exports.object({
17550
+ pattern: external_exports.string(),
17551
+ count: external_exports.number().int().nonnegative(),
17552
+ values: external_exports.array(FalsePositivePatternValue).min(1)
17553
+ });
17554
+ var CalibrationFindingKind = external_exports.object({
17555
+ category: DetectionCategory,
17556
+ count: external_exports.number().int().nonnegative(),
17557
+ egress: external_exports.boolean()
17558
+ });
17559
+ var CalibrationFrame = external_exports.object({
17560
+ counts: CalibrationCounts,
17561
+ routineCategories: external_exports.array(DetectionCategory),
17562
+ surfacedCategories: external_exports.array(DetectionCategory),
17563
+ findingKinds: external_exports.array(CalibrationFindingKind),
17564
+ posture: external_exports.record(DetectionCategory, BuiltinPolicyId),
17565
+ maskedFindings: external_exports.array(MaskedSecretFinding).optional(),
17566
+ falsePositivePatterns: external_exports.array(FalsePositivePatternGroup).optional()
17567
+ });
17568
+ var CalibrationPreviewCategory = TriageCategoryRec.pick({
17569
+ category: true,
17570
+ genuineCount: true,
17571
+ fpCount: true
17572
+ }).extend({
17573
+ egress: external_exports.boolean()
17574
+ });
17575
+ var CalibrationPreview = external_exports.object({
17576
+ categories: external_exports.array(CalibrationPreviewCategory),
17577
+ posture: external_exports.record(DetectionCategory, BuiltinPolicyId)
17578
+ });
17579
+ var CalibrationResult = external_exports.object({
17580
+ frame: CalibrationFrame,
17581
+ copy: external_exports.string()
17582
+ });
17583
+ var FirstRunCalibration = external_exports.enum(["scan", "floor"]);
17584
+ var SetupHandoffOption = external_exports.object({
17585
+ id: external_exports.enum(["enter-remediation", "open-dashboard", "not-now"]),
17586
+ label: external_exports.string()
17587
+ });
17588
+ var DashboardHandoffOptions = external_exports.tuple([
17589
+ SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
17590
+ SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
17591
+ ]);
17592
+ var ComposedRemediationOptions = external_exports.tuple([
17593
+ SetupHandoffOption.extend({ id: external_exports.literal("enter-remediation") }),
17594
+ SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
17595
+ SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
17596
+ ]);
17597
+ var SetupHandoffOffer = external_exports.object({
17598
+ worthALook: external_exports.number().int().nonnegative(),
17599
+ liveKeys: external_exports.number().int().nonnegative().optional(),
17600
+ options: external_exports.union([DashboardHandoffOptions, ComposedRemediationOptions])
17601
+ }).refine(
17602
+ (o) => o.options.some((opt) => opt.id === "enter-remediation") === (o.liveKeys ?? 0) > 0,
17603
+ {
17604
+ message: "the chain-entry option is present exactly when liveKeys > 0",
17605
+ path: ["options"]
17606
+ }
17607
+ ).refine((o) => (o.liveKeys ?? 0) <= o.worthALook, {
17608
+ message: "liveKeys is a subset of worthALook and cannot exceed it",
17609
+ path: ["liveKeys"]
17610
+ });
17611
+
17356
17612
  // ../../packages/schema/src/zod/shares.ts
17357
17613
  var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17358
17614
  var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
@@ -17538,6 +17794,102 @@ function reviewSeverityRank(reasons) {
17538
17794
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17539
17795
  }
17540
17796
 
17797
+ // ../../packages/persistence/src/internal/sql-text.ts
17798
+ function escapeLikePattern(s) {
17799
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17800
+ }
17801
+ function placeholders(n) {
17802
+ return Array.from({ length: n }, () => "?").join(", ");
17803
+ }
17804
+ function containsPattern(q) {
17805
+ return `%${escapeLikePattern(q)}%`;
17806
+ }
17807
+ function likeAny(exprs) {
17808
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
17809
+ }
17810
+
17811
+ // ../../packages/persistence/src/internal/transactions.ts
17812
+ var savepointSeq = 0;
17813
+ function withTransaction(db, fn, mode = "DEFERRED") {
17814
+ if (db.isTransaction) {
17815
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
17816
+ db.exec(`SAVEPOINT ${savepoint}`);
17817
+ try {
17818
+ fn();
17819
+ db.exec(`RELEASE ${savepoint}`);
17820
+ } catch (error51) {
17821
+ try {
17822
+ db.exec(`ROLLBACK TO ${savepoint}`);
17823
+ db.exec(`RELEASE ${savepoint}`);
17824
+ } catch {
17825
+ }
17826
+ throw error51;
17827
+ }
17828
+ return;
17829
+ }
17830
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
17831
+ try {
17832
+ fn();
17833
+ db.exec("COMMIT");
17834
+ } catch (error51) {
17835
+ try {
17836
+ db.exec("ROLLBACK");
17837
+ } catch {
17838
+ }
17839
+ throw error51;
17840
+ }
17841
+ }
17842
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
17843
+ const nested = db.isTransaction;
17844
+ try {
17845
+ withTransaction(db, fn, mode);
17846
+ return true;
17847
+ } catch (error51) {
17848
+ if (!db.isTransaction && nested) throw error51;
17849
+ return false;
17850
+ }
17851
+ }
17852
+
17853
+ // ../../packages/persistence/src/internal/warn.ts
17854
+ function akaWarn(message) {
17855
+ process.stderr.write(`[aka] ${message}
17856
+ `);
17857
+ }
17858
+
17859
+ // ../../packages/persistence/src/db/migrations/introspection.ts
17860
+ function evidenceObjects(sql) {
17861
+ const objects = [];
17862
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17863
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17864
+ objects.push({ kind: "table", name: m[1] });
17865
+ }
17866
+ }
17867
+ for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17868
+ if (m[1] !== void 0 && m[2] !== void 0) {
17869
+ objects.push({ kind: "column", table: m[1], name: m[2] });
17870
+ }
17871
+ }
17872
+ return objects;
17873
+ }
17874
+ function schemaObjectExists(db, kind, name) {
17875
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
17876
+ return row !== void 0;
17877
+ }
17878
+ function indexExists(db, name) {
17879
+ return schemaObjectExists(db, "index", name);
17880
+ }
17881
+ function columnNames(db, table2, opts) {
17882
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
17883
+ const columns = db.prepare(`PRAGMA ${pragma}(${table2})`).all();
17884
+ return columns.map((c) => c.name);
17885
+ }
17886
+ function evidenceExists(db, object2) {
17887
+ if (object2.kind === "column") {
17888
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
17889
+ }
17890
+ return schemaObjectExists(db, "table", object2.name);
17891
+ }
17892
+
17541
17893
  // ../../packages/persistence/src/ids.ts
17542
17894
  import { createHash } from "crypto";
17543
17895
  function sha256Hex(input) {
@@ -17574,28 +17926,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17574
17926
  }
17575
17927
 
17576
17928
  // ../../packages/persistence/src/migrations.ts
17577
- function evidenceObjects(sql) {
17578
- const objects = [];
17579
- for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17580
- if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17581
- objects.push({ kind: "table", name: m[1] });
17582
- }
17583
- }
17584
- for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17585
- if (m[1] !== void 0 && m[2] !== void 0) {
17586
- objects.push({ kind: "column", table: m[1], name: m[2] });
17587
- }
17588
- }
17589
- return objects;
17590
- }
17591
- function evidenceExists(db, object2) {
17592
- if (object2.kind === "column") {
17593
- const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
17594
- return columns.some((c) => c.name === object2.name);
17595
- }
17596
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
17597
- return row !== void 0;
17598
- }
17599
17929
  function describeObject(object2) {
17600
17930
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
17601
17931
  }
@@ -17606,10 +17936,6 @@ function createdIndexName(statement) {
17606
17936
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17607
17937
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17608
17938
  }
17609
- function indexExists(db, name) {
17610
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
17611
- return row !== void 0;
17612
- }
17613
17939
  function applyMigrations(db) {
17614
17940
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17615
17941
  db.exec(
@@ -17628,44 +17954,39 @@ function applyMigrations(db) {
17628
17954
  const present = evidence.filter((o) => evidenceExists(db, o));
17629
17955
  if (present.length > 0 && present.length < evidence.length) {
17630
17956
  const missing = evidence.filter((o) => !present.includes(o));
17631
- 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.`;
17632
- process.stderr.write(`${message}
17633
- `);
17634
- throw new Error(message);
17957
+ 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.`;
17958
+ akaWarn(message);
17959
+ throw new Error(`[aka] ${message}`);
17635
17960
  }
17636
17961
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
17637
17962
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
17638
17963
  const statements = splitStatements(migration.sql);
17639
17964
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
17640
17965
  try {
17641
- db.exec("BEGIN IMMEDIATE");
17642
- try {
17643
- for (const statement of statements) {
17644
- const indexName = createdIndexName(statement);
17645
- if (indexName === void 0) {
17646
- if (alreadyApplied) continue;
17647
- } else if (indexExists(db, indexName)) {
17648
- continue;
17966
+ withTransaction(
17967
+ db,
17968
+ () => {
17969
+ for (const statement of statements) {
17970
+ const indexName = createdIndexName(statement);
17971
+ if (indexName === void 0) {
17972
+ if (alreadyApplied) continue;
17973
+ } else if (indexExists(db, indexName)) {
17974
+ continue;
17975
+ }
17976
+ db.exec(statement);
17649
17977
  }
17650
- db.exec(statement);
17651
- }
17652
- if (wantsFkOff && !alreadyApplied) {
17653
- const violations = db.prepare("PRAGMA foreign_key_check").all();
17654
- if (violations.length > 0) {
17655
- throw new Error(
17656
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17657
- );
17978
+ if (wantsFkOff && !alreadyApplied) {
17979
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
17980
+ if (violations.length > 0) {
17981
+ throw new Error(
17982
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17983
+ );
17984
+ }
17658
17985
  }
17659
- }
17660
- record2.run(migration.tag, Date.now());
17661
- db.exec("COMMIT");
17662
- } catch (error51) {
17663
- try {
17664
- db.exec("ROLLBACK");
17665
- } catch {
17666
- }
17667
- throw error51;
17668
- }
17986
+ record2.run(migration.tag, Date.now());
17987
+ },
17988
+ "IMMEDIATE"
17989
+ );
17669
17990
  } finally {
17670
17991
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
17671
17992
  }
@@ -17708,8 +18029,7 @@ var TOKEN_USAGE_COLUMNS = [
17708
18029
  }
17709
18030
  ];
17710
18031
  function ensureTokenUsageColumns(db) {
17711
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
17712
- const existing = new Set(columns.map((c) => c.name));
18032
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17713
18033
  for (const column of TOKEN_USAGE_COLUMNS) {
17714
18034
  if (!existing.has(column.name)) {
17715
18035
  db.exec(column.ddl);
@@ -17747,47 +18067,39 @@ function reconcileSourceProjectIds(db) {
17747
18067
  repoint: db.prepare(`UPDATE ${table2} SET project_id = ? WHERE project_id = ?`)
17748
18068
  }));
17749
18069
  const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
17750
- db.exec("BEGIN IMMEDIATE");
17751
- try {
17752
- for (const { row, canonicalId } of legacy) {
17753
- foldProject.run(
17754
- canonicalId,
17755
- row.url,
17756
- row.name,
17757
- row.attributes,
17758
- row.firstSeen,
17759
- row.lastSeen
17760
- );
17761
- repointAudit.run(canonicalId, row.id);
17762
- for (const { dropCollisions, repoint } of pathTables) {
17763
- dropCollisions.run(row.id, canonicalId);
17764
- repoint.run(canonicalId, row.id);
18070
+ withTransaction(
18071
+ db,
18072
+ () => {
18073
+ for (const { row, canonicalId } of legacy) {
18074
+ foldProject.run(
18075
+ canonicalId,
18076
+ row.url,
18077
+ row.name,
18078
+ row.attributes,
18079
+ row.firstSeen,
18080
+ row.lastSeen
18081
+ );
18082
+ repointAudit.run(canonicalId, row.id);
18083
+ for (const { dropCollisions, repoint } of pathTables) {
18084
+ dropCollisions.run(row.id, canonicalId);
18085
+ repoint.run(canonicalId, row.id);
18086
+ }
18087
+ repointCallSite.run(canonicalId, row.id);
18088
+ deleteLegacy.run(row.id);
17765
18089
  }
17766
- repointCallSite.run(canonicalId, row.id);
17767
- deleteLegacy.run(row.id);
17768
- }
17769
- db.exec("COMMIT");
17770
- } catch (error51) {
17771
- try {
17772
- db.exec("ROLLBACK");
17773
- } catch {
17774
- }
17775
- throw error51;
17776
- }
18090
+ },
18091
+ "IMMEDIATE"
18092
+ );
17777
18093
  } catch (error51) {
17778
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
17779
- `);
18094
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
17780
18095
  }
17781
18096
  }
17782
18097
  function isForeignSqliteLineage(db) {
17783
- const tenantsTable = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tenants' LIMIT 1").get();
17784
- if (tenantsTable !== void 0) return true;
17785
- const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
17786
- return eventsColumns.some((c) => c.name === "tenant_id");
18098
+ if (schemaObjectExists(db, "table", "tenants")) return true;
18099
+ return columnNames(db, "events").includes("tenant_id");
17787
18100
  }
17788
18101
  function ensureSyncedAtColumn(db, table2) {
17789
- const columns = db.prepare(`PRAGMA table_info(${table2})`).all();
17790
- if (!columns.some((c) => c.name === "synced_at")) {
18102
+ if (!columnNames(db, table2).includes("synced_at")) {
17791
18103
  db.exec(`ALTER TABLE ${table2} ADD COLUMN synced_at integer`);
17792
18104
  }
17793
18105
  }
@@ -17838,8 +18150,11 @@ function ensureDataDirSync(dir) {
17838
18150
  } catch {
17839
18151
  }
17840
18152
  }
18153
+ function walSidecars(file2) {
18154
+ return [`${file2}-wal`, `${file2}-shm`];
18155
+ }
17841
18156
  function tightenPerms(file2) {
17842
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
18157
+ for (const path of [file2, ...walSidecars(file2)]) {
17843
18158
  try {
17844
18159
  chmodSync(path, DATA_FILE_MODE);
17845
18160
  } catch {
@@ -17847,12 +18162,68 @@ function tightenPerms(file2) {
17847
18162
  }
17848
18163
  }
17849
18164
 
17850
- // ../../packages/persistence/src/repositories/sql-utils.ts
17851
- function escapeLikePattern(s) {
17852
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
18165
+ // ../../packages/persistence/src/internal/json.ts
18166
+ function safeJson(s, fallback) {
18167
+ if (s == null) return fallback;
18168
+ try {
18169
+ return JSON.parse(s);
18170
+ } catch {
18171
+ return fallback;
18172
+ }
17853
18173
  }
17854
- function placeholders(n) {
17855
- return Array.from({ length: n }, () => "?").join(", ");
18174
+ function parseJsonObject(s) {
18175
+ if (s == null) return void 0;
18176
+ try {
18177
+ const parsed = JSON.parse(s);
18178
+ if (typeof parsed === "object" && parsed !== null) return parsed;
18179
+ } catch {
18180
+ }
18181
+ return void 0;
18182
+ }
18183
+
18184
+ // ../../packages/persistence/src/internal/rows.ts
18185
+ function allRows(stmt, params) {
18186
+ if (params === void 0) return stmt.all();
18187
+ if (Array.isArray(params)) return stmt.all(...params);
18188
+ return stmt.all(params);
18189
+ }
18190
+ function getRow(stmt, params) {
18191
+ if (params === void 0) return stmt.get();
18192
+ if (Array.isArray(params)) return stmt.get(...params);
18193
+ return stmt.get(params);
18194
+ }
18195
+ function intToBool(raw) {
18196
+ return raw === 1 || raw === true;
18197
+ }
18198
+ function boolToInt(b) {
18199
+ return b ? 1 : 0;
18200
+ }
18201
+ function bindParams(row) {
18202
+ const out = {};
18203
+ for (const [key, value] of Object.entries(row)) {
18204
+ out[key] = value === void 0 ? null : value;
18205
+ }
18206
+ return out;
18207
+ }
18208
+ function countScalar(db, sql, params) {
18209
+ return getRow(db.prepare(sql), params)?.n ?? 0;
18210
+ }
18211
+ function countBy(db, sql, params) {
18212
+ const map2 = /* @__PURE__ */ new Map();
18213
+ for (const row of allRows(db.prepare(sql), params)) {
18214
+ map2.set(row.k, row.n);
18215
+ }
18216
+ return map2;
18217
+ }
18218
+ function mapRowsTolerant(rows, map2) {
18219
+ const out = [];
18220
+ for (const row of rows) {
18221
+ try {
18222
+ out.push(map2(row));
18223
+ } catch {
18224
+ }
18225
+ }
18226
+ return out;
17856
18227
  }
17857
18228
 
17858
18229
  // ../../packages/persistence/src/repositories/activity.ts
@@ -17904,15 +18275,11 @@ function encodeCursor(payload) {
17904
18275
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
17905
18276
  }
17906
18277
  function decodeCursor(cursor) {
17907
- try {
17908
- const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
17909
- if (parsed !== null && typeof parsed === "object" && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
17910
- return parsed;
17911
- }
17912
- return null;
17913
- } catch {
17914
- return null;
18278
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18279
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18280
+ return parsed;
17915
18281
  }
18282
+ return null;
17916
18283
  }
17917
18284
  var DB_EVENT_TYPE_TO_KIND = {
17918
18285
  session: "session",
@@ -17929,15 +18296,8 @@ var DB_EVENT_TYPE_TO_KIND = {
17929
18296
  };
17930
18297
  function safeParseStringArray(raw) {
17931
18298
  if (!raw) return [];
17932
- try {
17933
- const parsed = JSON.parse(raw);
17934
- return Array.isArray(parsed) ? parsed : [];
17935
- } catch {
17936
- return [];
17937
- }
17938
- }
17939
- function toBool(raw) {
17940
- return raw === 1 || raw === true;
18299
+ const parsed = safeJson(raw, null);
18300
+ return Array.isArray(parsed) ? parsed : [];
17941
18301
  }
17942
18302
  function toHarness(raw) {
17943
18303
  const parsed = Harness.safeParse(raw);
@@ -17990,8 +18350,8 @@ function buildAuditEvent(row) {
17990
18350
  severity: severityParsed?.success ? severityParsed.data : null,
17991
18351
  link: linkParsed?.success ? linkParsed.data : null,
17992
18352
  targetId: row.target_id,
17993
- internal: toBool(row.internal),
17994
- flagged: toBool(row.flagged)
18353
+ internal: intToBool(row.internal),
18354
+ flagged: intToBool(row.flagged)
17995
18355
  };
17996
18356
  }
17997
18357
  var TIMELINE_COLUMNS = `
@@ -18007,6 +18367,10 @@ var TIMELINE_COLUMNS = `
18007
18367
  json_extract(attributes, '$.internal') AS internal,
18008
18368
  json_extract(attributes, '$.flagged') AS flagged`;
18009
18369
  var SESSION_ROOT = `event_type = 'session'`;
18370
+ var HAS_ACTIVITY = `EXISTS (
18371
+ SELECT 1 FROM audit_events c
18372
+ WHERE c.root_session_id = audit_events.id
18373
+ AND c.event_type NOT IN ('hook', 'config_scan'))`;
18010
18374
  var SqliteActivityRepository = class {
18011
18375
  constructor(db, now = () => Date.now()) {
18012
18376
  this.db = db;
@@ -18017,12 +18381,16 @@ var SqliteActivityRepository = class {
18017
18381
  stats(tz) {
18018
18382
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
18019
18383
  const { startMs, endMs } = window;
18020
- const sessionsToday = this.db.prepare(
18384
+ const sessionsToday = countScalar(
18385
+ this.db,
18021
18386
  `SELECT count(*) AS n FROM audit_events
18022
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
18023
- ).get(startMs, endMs).n;
18387
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?
18388
+ AND ${HAS_ACTIVITY}`,
18389
+ [startMs, endMs]
18390
+ );
18024
18391
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
18025
- const liveNow = this.db.prepare(
18392
+ const liveNow = countScalar(
18393
+ this.db,
18026
18394
  `SELECT count(*) AS n FROM audit_events s
18027
18395
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
18028
18396
  AND max(
@@ -18031,22 +18399,29 @@ var SqliteActivityRepository = class {
18031
18399
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
18032
18400
  s.started_at
18033
18401
  )
18034
- ) >= ?`
18035
- ).get(liveThreshold).n;
18036
- const toolCallsToday = this.db.prepare(
18402
+ ) >= ?`,
18403
+ [liveThreshold]
18404
+ );
18405
+ const toolCallsToday = countScalar(
18406
+ this.db,
18037
18407
  `SELECT count(*) AS n FROM audit_events
18038
- WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`
18039
- ).get(startMs, endMs).n;
18040
- const findingsToday = this.db.prepare(
18408
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18409
+ [startMs, endMs]
18410
+ );
18411
+ const findingsToday = countScalar(
18412
+ this.db,
18041
18413
  `SELECT count(*) AS n FROM inspection_findings f
18042
18414
  JOIN audit_events e ON e.id = f.audit_event_id
18043
- WHERE e.started_at >= ? AND e.started_at < ?`
18044
- ).get(startMs, endMs).n;
18045
- const egressToday = this.db.prepare(
18415
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18416
+ [startMs, endMs]
18417
+ );
18418
+ const egressToday = countScalar(
18419
+ this.db,
18046
18420
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
18047
18421
  FROM audit_events
18048
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
18049
- ).get(startMs, endMs).n;
18422
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18423
+ [startMs, endMs]
18424
+ );
18050
18425
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18051
18426
  }
18052
18427
  listSessions(query) {
@@ -18068,7 +18443,7 @@ var SqliteActivityRepository = class {
18068
18443
  conditions.push("started_at <= ?");
18069
18444
  params.push(toMs);
18070
18445
  if (query.q) {
18071
- const pattern = `%${escapeLikePattern(query.q)}%`;
18446
+ const pattern = containsPattern(query.q);
18072
18447
  conditions.push(
18073
18448
  `(content LIKE ? ESCAPE '\\'
18074
18449
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18082,13 +18457,21 @@ var SqliteActivityRepository = class {
18082
18457
  );
18083
18458
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
18084
18459
  }
18460
+ const emptyCount = countScalar(
18461
+ this.db,
18462
+ `SELECT count(*) AS n FROM audit_events
18463
+ WHERE ${[...conditions, `NOT ${HAS_ACTIVITY}`].join(" AND ")}`,
18464
+ params
18465
+ );
18466
+ if (query.excludeEmpty) conditions.push(HAS_ACTIVITY);
18085
18467
  if (cursor) {
18086
18468
  conditions.push("(started_at < ? OR (started_at = ? AND id < ?))");
18087
18469
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18088
18470
  }
18089
18471
  const limit = query.limit;
18090
- const rows = this.db.prepare(
18091
- `SELECT id,
18472
+ const rows = allRows(
18473
+ this.db.prepare(
18474
+ `SELECT id,
18092
18475
  json_extract(attributes, '$.harness') AS harness,
18093
18476
  content AS title,
18094
18477
  json_extract(attributes, '$.project') AS project,
@@ -18101,7 +18484,9 @@ var SqliteActivityRepository = class {
18101
18484
  WHERE ${conditions.join(" AND ")}
18102
18485
  ORDER BY started_at DESC, id DESC
18103
18486
  LIMIT ?`
18104
- ).all(...params, limit + 1);
18487
+ ),
18488
+ [...params, limit + 1]
18489
+ );
18105
18490
  const hasMore = rows.length > limit;
18106
18491
  const page = hasMore ? rows.slice(0, limit) : rows;
18107
18492
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18115,11 +18500,12 @@ var SqliteActivityRepository = class {
18115
18500
  );
18116
18501
  const last = page[page.length - 1];
18117
18502
  const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
18118
- return Promise.resolve({ items, nextCursor });
18503
+ return Promise.resolve({ items, nextCursor, emptyCount });
18119
18504
  }
18120
18505
  getSession(sessionId) {
18121
- const rootRow = this.db.prepare(
18122
- `SELECT id,
18506
+ const rootRow = getRow(
18507
+ this.db.prepare(
18508
+ `SELECT id,
18123
18509
  json_extract(attributes, '$.harness') AS harness,
18124
18510
  content AS title,
18125
18511
  json_extract(attributes, '$.project') AS project,
@@ -18136,47 +18522,66 @@ var SqliteActivityRepository = class {
18136
18522
  FROM audit_events
18137
18523
  WHERE id = ? AND event_type = 'session'
18138
18524
  LIMIT 1`
18139
- ).get(sessionId);
18525
+ ),
18526
+ [sessionId]
18527
+ );
18140
18528
  if (!rootRow) return Promise.resolve(null);
18141
- const timelineRows = this.db.prepare(
18142
- `SELECT ${TIMELINE_COLUMNS}
18529
+ const timelineRows = allRows(
18530
+ this.db.prepare(
18531
+ `SELECT ${TIMELINE_COLUMNS}
18143
18532
  FROM audit_events
18144
18533
  WHERE id = ? OR root_session_id = ?
18145
18534
  ORDER BY started_at ASC, id ASC`
18146
- ).all(sessionId, sessionId);
18535
+ ),
18536
+ [sessionId, sessionId]
18537
+ );
18147
18538
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18148
- const tokenRow = this.db.prepare(
18149
- `SELECT
18539
+ const tokenRow = getRow(
18540
+ this.db.prepare(
18541
+ `SELECT
18150
18542
  coalesce(sum(input_tokens), 0) AS input,
18151
18543
  coalesce(sum(output_tokens), 0) AS output,
18152
18544
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18153
18545
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18154
18546
  FROM audit_events
18155
18547
  WHERE root_session_id = ? AND event_type = 'llm_call'`
18156
- ).get(sessionId);
18157
- const primaryModel = this.db.prepare(
18158
- `SELECT model, provider FROM audit_events
18548
+ ),
18549
+ [sessionId]
18550
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
18551
+ const primaryModel = getRow(
18552
+ this.db.prepare(
18553
+ `SELECT model, provider FROM audit_events
18159
18554
  WHERE root_session_id = ? AND event_type = 'llm_call'
18160
18555
  ORDER BY started_at ASC, id ASC
18161
18556
  LIMIT 1`
18162
- ).get(sessionId);
18163
- const toolRows = this.db.prepare(
18164
- `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18557
+ ),
18558
+ [sessionId]
18559
+ );
18560
+ const toolRows = allRows(
18561
+ this.db.prepare(
18562
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18165
18563
  count(*) AS n
18166
18564
  FROM audit_events
18167
18565
  WHERE root_session_id = ? AND event_type = 'tool_call'
18168
18566
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
18169
- ).all(sessionId);
18170
- const modelRows = this.db.prepare(
18171
- `SELECT DISTINCT model FROM audit_events
18567
+ ),
18568
+ [sessionId]
18569
+ );
18570
+ const modelRows = allRows(
18571
+ this.db.prepare(
18572
+ `SELECT DISTINCT model FROM audit_events
18172
18573
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18173
18574
  ORDER BY model`
18174
- ).all(sessionId);
18575
+ ),
18576
+ [sessionId]
18577
+ );
18175
18578
  const derivedModels = modelRows.map((r) => r.model);
18176
- const commits = this.db.prepare(
18579
+ const commits = countScalar(
18580
+ this.db,
18177
18581
  `SELECT count(*) AS n FROM audit_events
18178
- WHERE root_session_id = ? AND event_type = 'commit'`
18179
- ).get(sessionId).n;
18582
+ WHERE root_session_id = ? AND event_type = 'commit'`,
18583
+ [sessionId]
18584
+ );
18180
18585
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18181
18586
  turns: 0,
18182
18587
  findings: 0,
@@ -18243,7 +18648,10 @@ var SqliteActivityRepository = class {
18243
18648
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18244
18649
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18245
18650
  );
18246
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
18651
+ const rows = allRows(
18652
+ stmt,
18653
+ fromMs === void 0 ? void 0 : [fromMs]
18654
+ );
18247
18655
  const seen = /* @__PURE__ */ new Set();
18248
18656
  for (const row of rows) seen.add(toHarness(row.harness));
18249
18657
  return Promise.resolve([...seen]);
@@ -18266,23 +18674,23 @@ var SqliteActivityRepository = class {
18266
18674
  conditions.push("started_at >= ?");
18267
18675
  params.push(opts.fromMs);
18268
18676
  }
18269
- const rows = this.db.prepare(
18270
- `SELECT root_session_id AS sessionId, attributes
18677
+ const rows = allRows(
18678
+ this.db.prepare(
18679
+ `SELECT root_session_id AS sessionId, attributes
18271
18680
  FROM audit_events
18272
18681
  WHERE ${conditions.join(" AND ")}`
18273
- ).all(...params);
18274
- const leaves = [];
18275
- for (const row of rows) {
18276
- if (row.sessionId === null) continue;
18277
- try {
18278
- leaves.push({
18279
- sessionId: row.sessionId,
18280
- attributes: JSON.parse(row.attributes)
18281
- });
18282
- } catch {
18283
- }
18284
- }
18285
- return leaves;
18682
+ ),
18683
+ params
18684
+ );
18685
+ return mapRowsTolerant(
18686
+ rows.filter(
18687
+ (row) => row.sessionId !== null
18688
+ ),
18689
+ (row) => ({
18690
+ sessionId: row.sessionId,
18691
+ attributes: JSON.parse(row.attributes)
18692
+ })
18693
+ );
18286
18694
  }
18287
18695
  /**
18288
18696
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -18296,57 +18704,72 @@ var SqliteActivityRepository = class {
18296
18704
  );
18297
18705
  if (sessionIds.length === 0) return result;
18298
18706
  const inClause = placeholders(sessionIds.length);
18299
- const lastActivityRows = this.db.prepare(
18300
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18707
+ const lastActivityRows = allRows(
18708
+ this.db.prepare(
18709
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18301
18710
  WHERE root_session_id IN (${inClause})
18302
18711
  GROUP BY root_session_id`
18303
- ).all(...sessionIds);
18712
+ ),
18713
+ sessionIds
18714
+ );
18304
18715
  for (const row of lastActivityRows) {
18305
18716
  if (row.id === null) continue;
18306
18717
  const entry = result.get(row.id);
18307
18718
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
18308
18719
  }
18309
- const turnsRows = this.db.prepare(
18310
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18720
+ const turnsRows = allRows(
18721
+ this.db.prepare(
18722
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18311
18723
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
18312
18724
  GROUP BY root_session_id`
18313
- ).all(...sessionIds);
18725
+ ),
18726
+ sessionIds
18727
+ );
18314
18728
  for (const row of turnsRows) {
18315
18729
  if (row.id === null) continue;
18316
18730
  const entry = result.get(row.id);
18317
18731
  if (entry) entry.turns = row.n;
18318
18732
  }
18319
- const runKeyRows = this.db.prepare(
18320
- `SELECT root_session_id AS id,
18733
+ const runKeyRows = allRows(
18734
+ this.db.prepare(
18735
+ `SELECT root_session_id AS id,
18321
18736
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
18322
18737
  FROM audit_events
18323
18738
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
18324
18739
  AND json_extract(attributes, '$.run_key') IS NOT NULL
18325
18740
  GROUP BY root_session_id`
18326
- ).all(...sessionIds);
18741
+ ),
18742
+ sessionIds
18743
+ );
18327
18744
  for (const row of runKeyRows) {
18328
18745
  if (row.id === null) continue;
18329
18746
  const entry = result.get(row.id);
18330
18747
  if (entry) entry.turns = Math.max(entry.turns, row.n);
18331
18748
  }
18332
- const findingsRows = this.db.prepare(
18333
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18749
+ const findingsRows = allRows(
18750
+ this.db.prepare(
18751
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18334
18752
  JOIN audit_events e ON e.id = f.audit_event_id
18335
18753
  WHERE e.root_session_id IN (${inClause})
18336
18754
  GROUP BY e.root_session_id`
18337
- ).all(...sessionIds);
18755
+ ),
18756
+ sessionIds
18757
+ );
18338
18758
  for (const row of findingsRows) {
18339
18759
  if (row.id === null) continue;
18340
18760
  const entry = result.get(row.id);
18341
18761
  if (entry) entry.findings = row.n;
18342
18762
  }
18343
- const sharesRows = this.db.prepare(
18344
- `SELECT root_session_id AS id,
18763
+ const sharesRows = allRows(
18764
+ this.db.prepare(
18765
+ `SELECT root_session_id AS id,
18345
18766
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
18346
18767
  FROM audit_events
18347
18768
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
18348
18769
  GROUP BY root_session_id`
18349
- ).all(...sessionIds);
18770
+ ),
18771
+ sessionIds
18772
+ );
18350
18773
  for (const row of sharesRows) {
18351
18774
  if (row.id === null) continue;
18352
18775
  const entry = result.get(row.id);
@@ -18396,33 +18819,28 @@ var SqliteAuditEventsRepository = class {
18396
18819
  // the caller fails open and drops the whole pass — recovered idempotently on the
18397
18820
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
18398
18821
  runInTransaction(fn) {
18399
- this.db.exec("BEGIN");
18400
- try {
18401
- fn();
18402
- this.db.exec("COMMIT");
18403
- } catch (err) {
18404
- this.db.exec("ROLLBACK");
18405
- throw err;
18406
- }
18822
+ withTransaction(this.db, fn);
18407
18823
  }
18408
18824
  insertAuditEvent(input) {
18409
18825
  const row = toAuditEventRow(input);
18410
- this.insertStmt.run({
18411
- id: row.id,
18412
- parentId: row.parentId ?? null,
18413
- rootSessionId: row.rootSessionId ?? null,
18414
- eventType: row.eventType,
18415
- hostId: row.hostId ?? null,
18416
- harnessId: row.harnessId ?? null,
18417
- sourceProjectId: row.sourceProjectId ?? null,
18418
- startedAt: row.startedAt,
18419
- endedAt: row.endedAt ?? null,
18420
- severity: row.severity ?? null,
18421
- priority: row.priority ?? null,
18422
- content: row.content ?? null,
18423
- contentHash: row.contentHash ?? null,
18424
- attributes: row.attributes ?? null
18425
- });
18826
+ this.insertStmt.run(
18827
+ bindParams({
18828
+ id: row.id,
18829
+ parentId: row.parentId,
18830
+ rootSessionId: row.rootSessionId,
18831
+ eventType: row.eventType,
18832
+ hostId: row.hostId,
18833
+ harnessId: row.harnessId,
18834
+ sourceProjectId: row.sourceProjectId,
18835
+ startedAt: row.startedAt,
18836
+ endedAt: row.endedAt,
18837
+ severity: row.severity,
18838
+ priority: row.priority,
18839
+ content: row.content,
18840
+ contentHash: row.contentHash,
18841
+ attributes: row.attributes
18842
+ })
18843
+ );
18426
18844
  }
18427
18845
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18428
18846
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -18436,22 +18854,24 @@ var SqliteAuditEventsRepository = class {
18436
18854
  const startedAt = isoToEpochMillis(input.startedAt);
18437
18855
  if (!Number.isFinite(startedAt)) return;
18438
18856
  const id = llmCallId(input.sessionId, input.messageId);
18439
- this.upsertLlmCallStmt.run({
18440
- id,
18441
- parentId: input.parentId,
18442
- rootSessionId: input.rootSessionId,
18443
- eventType: "llm_call",
18444
- hostId: null,
18445
- harnessId: null,
18446
- sourceProjectId: null,
18447
- startedAt,
18448
- endedAt: null,
18449
- severity: null,
18450
- priority: null,
18451
- content: null,
18452
- contentHash: null,
18453
- attributes: JSON.stringify(input.attributes)
18454
- });
18857
+ this.upsertLlmCallStmt.run(
18858
+ bindParams({
18859
+ id,
18860
+ parentId: input.parentId,
18861
+ rootSessionId: input.rootSessionId,
18862
+ eventType: "llm_call",
18863
+ hostId: null,
18864
+ harnessId: null,
18865
+ sourceProjectId: null,
18866
+ startedAt,
18867
+ endedAt: null,
18868
+ severity: null,
18869
+ priority: null,
18870
+ content: null,
18871
+ contentHash: null,
18872
+ attributes: JSON.stringify(input.attributes)
18873
+ })
18874
+ );
18455
18875
  }
18456
18876
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
18457
18877
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -18475,25 +18895,29 @@ var SqliteAuditEventsRepository = class {
18475
18895
  const startedAt = isoToEpochMillis(input.startedAt);
18476
18896
  if (!Number.isFinite(startedAt)) return;
18477
18897
  const id = toolCallId(input.sessionId, input.toolUseId);
18478
- this.insertStmt.run({
18479
- id,
18480
- parentId: input.parentId,
18481
- rootSessionId: input.rootSessionId,
18482
- eventType: "tool_call",
18483
- hostId: null,
18484
- harnessId: null,
18485
- sourceProjectId: null,
18486
- startedAt,
18487
- endedAt: null,
18488
- severity: null,
18489
- priority: null,
18490
- content: null,
18491
- contentHash: null,
18492
- attributes: JSON.stringify(input.attributes)
18493
- });
18898
+ this.insertStmt.run(
18899
+ bindParams({
18900
+ id,
18901
+ parentId: input.parentId,
18902
+ rootSessionId: input.rootSessionId,
18903
+ eventType: "tool_call",
18904
+ hostId: null,
18905
+ harnessId: null,
18906
+ sourceProjectId: null,
18907
+ startedAt,
18908
+ endedAt: null,
18909
+ severity: null,
18910
+ priority: null,
18911
+ content: null,
18912
+ contentHash: null,
18913
+ attributes: JSON.stringify(input.attributes)
18914
+ })
18915
+ );
18494
18916
  }
18495
18917
  findById(id) {
18496
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
18918
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
18919
+ id
18920
+ });
18497
18921
  }
18498
18922
  // Read the `provider` snapshotted onto a session root's attributes.
18499
18923
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -18503,14 +18927,8 @@ var SqliteAuditEventsRepository = class {
18503
18927
  sessionProvider(sessionId) {
18504
18928
  const row = this.findById(sessionId);
18505
18929
  if (!row?.attributes) return void 0;
18506
- try {
18507
- const parsed = JSON.parse(row.attributes);
18508
- if (typeof parsed === "object" && parsed !== null) {
18509
- const provider = parsed.provider;
18510
- if (typeof provider === "string") return provider;
18511
- }
18512
- } catch {
18513
- }
18930
+ const provider = parseJsonObject(row.attributes)?.provider;
18931
+ if (typeof provider === "string") return provider;
18514
18932
  return void 0;
18515
18933
  }
18516
18934
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -18519,11 +18937,13 @@ var SqliteAuditEventsRepository = class {
18519
18937
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
18520
18938
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
18521
18939
  llmCallLeaves() {
18522
- return this.db.prepare(
18523
- `SELECT root_session_id AS sessionId, attributes
18940
+ return allRows(
18941
+ this.db.prepare(
18942
+ `SELECT root_session_id AS sessionId, attributes
18524
18943
  FROM audit_events
18525
18944
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
18526
- ).all();
18945
+ )
18946
+ );
18527
18947
  }
18528
18948
  };
18529
18949
 
@@ -18542,16 +18962,29 @@ var SqliteClassifiedDataRepository = class {
18542
18962
  upsert(input) {
18543
18963
  const id = classifiedDataId(input.class);
18544
18964
  const row = toClassifiedDataRow(input, id);
18545
- this.insertStmt.run({
18546
- id: row.id,
18547
- class: row.class,
18548
- label: row.label ?? null,
18549
- attributes: row.attributes ?? null
18550
- });
18965
+ this.insertStmt.run(
18966
+ bindParams({
18967
+ id: row.id,
18968
+ class: row.class,
18969
+ label: row.label,
18970
+ attributes: row.attributes
18971
+ })
18972
+ );
18551
18973
  return id;
18552
18974
  }
18553
18975
  };
18554
18976
 
18977
+ // ../../packages/persistence/src/repositories/config-scan.ts
18978
+ function latestConfigScan(db) {
18979
+ return getRow(
18980
+ db.prepare(
18981
+ `SELECT id, started_at, attributes FROM audit_events
18982
+ WHERE event_type = 'config_scan'
18983
+ ORDER BY started_at DESC, id DESC LIMIT 1`
18984
+ )
18985
+ );
18986
+ }
18987
+
18555
18988
  // ../../packages/persistence/src/repositories/config-inventory.ts
18556
18989
  var SqliteConfigInventoryRepository = class {
18557
18990
  constructor(db) {
@@ -18559,7 +18992,7 @@ var SqliteConfigInventoryRepository = class {
18559
18992
  }
18560
18993
  db;
18561
18994
  report() {
18562
- const scan2 = this.latestScan();
18995
+ const scan2 = latestConfigScan(this.db);
18563
18996
  if (!scan2) {
18564
18997
  return {
18565
18998
  scannedAt: null,
@@ -18570,17 +19003,23 @@ var SqliteConfigInventoryRepository = class {
18570
19003
  topics: []
18571
19004
  };
18572
19005
  }
18573
- const rows = this.db.prepare(
18574
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
19006
+ const rows = allRows(
19007
+ this.db.prepare(
19008
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18575
19009
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
18576
19010
  ORDER BY object_type, title`
18577
- ).all({ startedAt: scan2.started_at });
18578
- const findings = this.db.prepare(
18579
- `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
19011
+ ),
19012
+ { startedAt: scan2.started_at }
19013
+ );
19014
+ const findings = allRows(
19015
+ this.db.prepare(
19016
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18580
19017
  FROM inspection_findings f
18581
19018
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
18582
19019
  WHERE f.audit_event_id = :scanId`
18583
- ).all({ scanId: scan2.id });
19020
+ ),
19021
+ { scanId: scan2.id }
19022
+ );
18584
19023
  const skills = [];
18585
19024
  const hooks = [];
18586
19025
  const mcpServers = [];
@@ -18609,7 +19048,9 @@ var SqliteConfigInventoryRepository = class {
18609
19048
  // schema note); an override whose asset is gone simply never matches. A row
18610
19049
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
18611
19050
  trustOverrides() {
18612
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
19051
+ const rows = allRows(
19052
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
19053
+ );
18613
19054
  const map2 = /* @__PURE__ */ new Map();
18614
19055
  for (const row of rows) {
18615
19056
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -18618,13 +19059,6 @@ var SqliteConfigInventoryRepository = class {
18618
19059
  }
18619
19060
  return map2;
18620
19061
  }
18621
- latestScan() {
18622
- return this.db.prepare(
18623
- `SELECT id, started_at, attributes FROM audit_events
18624
- WHERE event_type = 'config_scan'
18625
- ORDER BY started_at DESC, id DESC LIMIT 1`
18626
- ).get();
18627
- }
18628
19062
  };
18629
19063
  function toSkillItem(row, bag) {
18630
19064
  const item = {
@@ -18735,22 +19169,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
18735
19169
  return topics;
18736
19170
  }
18737
19171
  function countScanErrors(attributes) {
18738
- if (!attributes) return 0;
18739
- try {
18740
- const parsed = JSON.parse(attributes);
18741
- const errors = parsed?.errors;
18742
- return typeof errors === "number" ? errors : 0;
18743
- } catch {
18744
- return 0;
18745
- }
19172
+ const errors = parseJsonObject(attributes)?.errors;
19173
+ return typeof errors === "number" ? errors : 0;
18746
19174
  }
18747
19175
  function parseBag(raw) {
18748
- try {
18749
- const parsed = JSON.parse(raw);
18750
- if (typeof parsed === "object" && parsed !== null) return parsed;
18751
- } catch {
18752
- }
18753
- return void 0;
19176
+ return parseJsonObject(raw);
18754
19177
  }
18755
19178
  function str(value) {
18756
19179
  return typeof value === "string" ? value : void 0;
@@ -18759,12 +19182,7 @@ function str(value) {
18759
19182
  // ../../packages/persistence/src/repositories/detections.ts
18760
19183
  var DAY_MS2 = 864e5;
18761
19184
  function parseRules(rulesJson) {
18762
- let raw;
18763
- try {
18764
- raw = JSON.parse(rulesJson);
18765
- } catch {
18766
- return [];
18767
- }
19185
+ const raw = safeJson(rulesJson, []);
18768
19186
  if (!Array.isArray(raw)) return [];
18769
19187
  const rules = [];
18770
19188
  for (const entry of raw) {
@@ -18783,11 +19201,13 @@ var SqliteDetectionsRepository = class {
18783
19201
  db;
18784
19202
  now;
18785
19203
  listDetections(query) {
18786
- const rows = this.db.prepare(
18787
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19204
+ const rows = allRows(
19205
+ this.db.prepare(
19206
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18788
19207
  rules_json AS rulesJson
18789
19208
  FROM installed_packs`
18790
- ).all();
19209
+ )
19210
+ );
18791
19211
  const available = this.availableByPack();
18792
19212
  const summaries = rows.map((r) => {
18793
19213
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -18796,7 +19216,7 @@ var SqliteDetectionsRepository = class {
18796
19216
  packId: r.packId,
18797
19217
  version: r.version,
18798
19218
  name: r.name,
18799
- enabled: r.enabled === 1,
19219
+ enabled: intToBool(r.enabled),
18800
19220
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
18801
19221
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
18802
19222
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -18813,19 +19233,23 @@ var SqliteDetectionsRepository = class {
18813
19233
  // available_packs keyed by the "namespace/packId" slug (one read per list /
18814
19234
  // detail call; the table is a handful of rows).
18815
19235
  availableByPack() {
18816
- const rows = this.db.prepare(
18817
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
19236
+ const rows = allRows(
19237
+ this.db.prepare(
19238
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18818
19239
  FROM available_packs`
18819
- ).all();
19240
+ )
19241
+ );
18820
19242
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
18821
19243
  }
18822
19244
  getDetectionStats() {
18823
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
19245
+ const rows = allRows(
19246
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
19247
+ );
18824
19248
  let rules = 0;
18825
19249
  let active = 0;
18826
19250
  const ruleIds = /* @__PURE__ */ new Set();
18827
19251
  for (const r of rows) {
18828
- if (r.enabled === 1) active += 1;
19252
+ if (intToBool(r.enabled)) active += 1;
18829
19253
  const parsed = parseRules(r.rulesJson);
18830
19254
  rules += parsed.length;
18831
19255
  for (const rule of parsed) {
@@ -18843,12 +19267,15 @@ var SqliteDetectionsRepository = class {
18843
19267
  const parts = splitDetectionId(id);
18844
19268
  if (!parts) return Promise.resolve(null);
18845
19269
  const { namespace, packId } = parts;
18846
- const row = this.db.prepare(
18847
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19270
+ const row = getRow(
19271
+ this.db.prepare(
19272
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18848
19273
  rules_json AS rulesJson, updated_at AS updatedAt
18849
19274
  FROM installed_packs
18850
19275
  WHERE namespace = ? AND pack_id = ?`
18851
- ).get(namespace, packId);
19276
+ ),
19277
+ [namespace, packId]
19278
+ );
18852
19279
  if (!row) return Promise.resolve(null);
18853
19280
  const rules = parseRules(row.rulesJson);
18854
19281
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -18866,7 +19293,7 @@ var SqliteDetectionsRepository = class {
18866
19293
  packId: row.packId,
18867
19294
  version: row.version,
18868
19295
  name: row.name,
18869
- enabled: row.enabled === 1,
19296
+ enabled: intToBool(row.enabled),
18870
19297
  rules,
18871
19298
  updatedAt: new Date(row.updatedAt),
18872
19299
  policyId: row.policyId
@@ -18881,13 +19308,14 @@ var SqliteDetectionsRepository = class {
18881
19308
  countFindingsLast30d(ruleIds) {
18882
19309
  if (ruleIds.length === 0) return 0;
18883
19310
  const since = this.now() - 30 * DAY_MS2;
18884
- const placeholders2 = ruleIds.map(() => "?").join(", ");
18885
- const row = this.db.prepare(
18886
- `SELECT count(*) AS c
19311
+ const inClause = placeholders(ruleIds.length);
19312
+ return countScalar(
19313
+ this.db,
19314
+ `SELECT count(*) AS n
18887
19315
  FROM findings f JOIN events e ON e.id = f.event_id
18888
- WHERE e.occurred_at >= ? AND f.rule_id IN (${placeholders2})`
18889
- ).get(since, ...ruleIds);
18890
- return row.c;
19316
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19317
+ [since, ...ruleIds]
19318
+ );
18891
19319
  }
18892
19320
  };
18893
19321
 
@@ -18904,16 +19332,17 @@ var SqliteEventsRepository = class {
18904
19332
  insertStmt;
18905
19333
  insertEvent(event) {
18906
19334
  const row = toEventRow(event);
18907
- this.insertStmt.run({
18908
- id: row.id,
18909
- sourceTool: row.sourceTool,
18910
- kind: row.kind,
18911
- occurredAt: row.occurredAt,
18912
- contentHash: row.contentHash,
18913
- content: row.content,
18914
- // exactOptionalPropertyTypes: the column is nullable, never undefined.
18915
- metadata: row.metadata ?? null
18916
- });
19335
+ this.insertStmt.run(
19336
+ bindParams({
19337
+ id: row.id,
19338
+ sourceTool: row.sourceTool,
19339
+ kind: row.kind,
19340
+ occurredAt: row.occurredAt,
19341
+ contentHash: row.contentHash,
19342
+ content: row.content,
19343
+ metadata: row.metadata
19344
+ })
19345
+ );
18917
19346
  }
18918
19347
  // Every recorded event's content hash — the historical backfill loads this once
18919
19348
  // to skip transcript messages it has already stored, so re-running the scan
@@ -18921,13 +19350,23 @@ var SqliteEventsRepository = class {
18921
19350
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
18922
19351
  // async EventsReadPort contract.
18923
19352
  contentHashes() {
18924
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19353
+ const rows = allRows(
19354
+ this.db.prepare("SELECT content_hash FROM events")
19355
+ );
18925
19356
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
18926
19357
  }
18927
19358
  };
18928
19359
 
18929
19360
  // ../../packages/persistence/src/repositories/exceptions.ts
18930
19361
  import { randomUUID } from "crypto";
19362
+
19363
+ // ../../packages/persistence/src/internal/sqlite-errors.ts
19364
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19365
+ function isUniqueConstraintError(err) {
19366
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19367
+ }
19368
+
19369
+ // ../../packages/persistence/src/repositories/exceptions.ts
18931
19370
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
18932
19371
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
18933
19372
  var DuplicateActiveExceptionError = class extends Error {
@@ -18948,10 +19387,6 @@ var AmbiguousExceptionIdError = class extends Error {
18948
19387
  this.name = "AmbiguousExceptionIdError";
18949
19388
  }
18950
19389
  };
18951
- var SQLITE_CONSTRAINT_UNIQUE = 2067;
18952
- function isUniqueConstraintError(err) {
18953
- return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
18954
- }
18955
19390
  var ACTIVE_PREDICATE = `revoked_at IS NULL
18956
19391
  AND (expires_at IS NULL OR expires_at > :now)
18957
19392
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -19007,10 +19442,11 @@ var SqliteExceptionsRepository = class {
19007
19442
  this.insertExceptionRow(id, input, now);
19008
19443
  } catch (err) {
19009
19444
  if (!isUniqueConstraintError(err)) throw err;
19010
- this.db.exec("BEGIN IMMEDIATE");
19011
- try {
19012
- const superseded = this.db.prepare(
19013
- `UPDATE exceptions
19445
+ withTransaction(
19446
+ this.db,
19447
+ () => {
19448
+ const superseded = this.db.prepare(
19449
+ `UPDATE exceptions
19014
19450
  SET revoked_at = :now, revoked_by = :revokedBy,
19015
19451
  revoke_reason = 'superseded by a new grant for the same value',
19016
19452
  updated_at = :now
@@ -19018,24 +19454,27 @@ var SqliteExceptionsRepository = class {
19018
19454
  AND key_version = :keyVersion AND revoked_at IS NULL
19019
19455
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
19020
19456
  OR (max_uses IS NOT NULL AND use_count >= max_uses))`
19021
- ).run({
19022
- now,
19023
- revokedBy: input.createdBy,
19024
- ruleId: input.ruleId,
19025
- valueFingerprint: input.valueFingerprint,
19026
- keyVersion: input.keyVersion
19027
- });
19028
- if (Number(superseded.changes) !== 1) {
19029
- throw new DuplicateActiveExceptionError(input.ruleId);
19030
- }
19031
- this.insertExceptionRow(id, input, now);
19032
- this.db.exec("COMMIT");
19033
- } catch (retryErr) {
19034
- this.db.exec("ROLLBACK");
19035
- throw retryErr;
19036
- }
19457
+ ).run({
19458
+ now,
19459
+ revokedBy: input.createdBy,
19460
+ ruleId: input.ruleId,
19461
+ valueFingerprint: input.valueFingerprint,
19462
+ keyVersion: input.keyVersion
19463
+ });
19464
+ if (Number(superseded.changes) !== 1) {
19465
+ throw new DuplicateActiveExceptionError(input.ruleId);
19466
+ }
19467
+ this.insertExceptionRow(id, input, now);
19468
+ },
19469
+ "IMMEDIATE"
19470
+ );
19471
+ }
19472
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
19473
+ id
19474
+ });
19475
+ if (row === void 0) {
19476
+ throw new Error("exception row not found immediately after insert");
19037
19477
  }
19038
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
19039
19478
  return parseExceptionRow(row);
19040
19479
  }
19041
19480
  insertExceptionRow(id, input, now) {
@@ -19073,14 +19512,11 @@ var SqliteExceptionsRepository = class {
19073
19512
  */
19074
19513
  list(opts) {
19075
19514
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19076
- const rows = this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`).all(opts?.includeTerminal ? {} : { now: Date.now() });
19077
- const exceptions = [];
19078
- for (const row of rows) {
19079
- try {
19080
- exceptions.push(parseExceptionRow(row));
19081
- } catch {
19082
- }
19083
- }
19515
+ const rows = allRows(
19516
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19517
+ opts?.includeTerminal ? {} : { now: Date.now() }
19518
+ );
19519
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19084
19520
  return Promise.resolve(exceptions);
19085
19521
  }
19086
19522
  /**
@@ -19090,7 +19526,12 @@ var SqliteExceptionsRepository = class {
19090
19526
  */
19091
19527
  getByIdPrefix(prefix) {
19092
19528
  if (prefix.length === 0) return Promise.resolve(void 0);
19093
- const rows = this.db.prepare(String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`).all({ pattern: `${escapeLikePattern(prefix)}%` });
19529
+ const rows = allRows(
19530
+ this.db.prepare(
19531
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
19532
+ ),
19533
+ { pattern: `${escapeLikePattern(prefix)}%` }
19534
+ );
19094
19535
  if (rows.length > 1) {
19095
19536
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19096
19537
  }
@@ -19132,30 +19573,27 @@ var SqliteExceptionsRepository = class {
19132
19573
  * a different (rotated-away) key never match, so they are excluded at read.
19133
19574
  */
19134
19575
  activeBundleEntries(keyVersion, now = Date.now()) {
19135
- const rows = this.db.prepare(
19136
- `SELECT * FROM exceptions
19576
+ const rows = allRows(
19577
+ this.db.prepare(
19578
+ `SELECT * FROM exceptions
19137
19579
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19138
19580
  ORDER BY created_at DESC, rowid DESC`
19139
- ).all({ keyVersion, now });
19140
- const entries = [];
19141
- for (const row of rows) {
19142
- try {
19143
- const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19144
- entries.push(
19145
- ExceptionBundleEntry.parse({
19146
- id: row.id,
19147
- ruleId: row.rule_id,
19148
- valueFingerprint: row.value_fingerprint,
19149
- keyVersion: row.key_version,
19150
- expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19151
- maxUses: row.max_uses,
19152
- useCount: row.use_count,
19153
- conditions
19154
- })
19155
- );
19156
- } catch {
19157
- }
19158
- }
19581
+ ),
19582
+ { keyVersion, now }
19583
+ );
19584
+ const entries = mapRowsTolerant(rows, (row) => {
19585
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19586
+ return ExceptionBundleEntry.parse({
19587
+ id: row.id,
19588
+ ruleId: row.rule_id,
19589
+ valueFingerprint: row.value_fingerprint,
19590
+ keyVersion: row.key_version,
19591
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19592
+ maxUses: row.max_uses,
19593
+ useCount: row.use_count,
19594
+ conditions
19595
+ });
19596
+ });
19159
19597
  return Promise.resolve(entries);
19160
19598
  }
19161
19599
  /**
@@ -19182,11 +19620,14 @@ var SqliteExceptionsRepository = class {
19182
19620
  }
19183
19621
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19184
19622
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19185
- const rows = this.db.prepare(
19186
- `SELECT * FROM blocked_detections
19623
+ const rows = allRows(
19624
+ this.db.prepare(
19625
+ `SELECT * FROM blocked_detections
19187
19626
  WHERE blocked_at > :cutoff
19188
19627
  ORDER BY blocked_at DESC, rowid DESC`
19189
- ).all({ cutoff: Date.now() - windowMs });
19628
+ ),
19629
+ { cutoff: Date.now() - windowMs }
19630
+ );
19190
19631
  return Promise.resolve(
19191
19632
  rows.map((row) => ({
19192
19633
  reference: row.reference,
@@ -19266,7 +19707,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
19266
19707
  )`;
19267
19708
 
19268
19709
  // ../../packages/persistence/src/repositories/findings.ts
19269
- var GROUP_ROW_CAP = 2e3;
19710
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
19711
+ var CONCAT_SEP = ",";
19712
+ var TUPLE_SEP = "|";
19713
+ function splitConcat(value) {
19714
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
19715
+ }
19270
19716
  function deriveInstanceStatus(row) {
19271
19717
  return deriveFindingStatus({
19272
19718
  kind: row.kind,
@@ -19334,13 +19780,16 @@ var SqliteFindingsRepository = class {
19334
19780
  }
19335
19781
  recentFindings(opts) {
19336
19782
  const limit = opts?.limit ?? 50;
19337
- const rows = this.db.prepare(
19338
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19783
+ const rows = allRows(
19784
+ this.db.prepare(
19785
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19339
19786
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19340
19787
  FROM findings f JOIN events e ON e.id = f.event_id
19341
19788
  ORDER BY e.occurred_at DESC, f.rowid DESC
19342
19789
  LIMIT :limit`
19343
- ).all({ limit });
19790
+ ),
19791
+ { limit }
19792
+ );
19344
19793
  return Promise.resolve(
19345
19794
  rows.map((r) => ({
19346
19795
  id: r.id,
@@ -19357,26 +19806,94 @@ var SqliteFindingsRepository = class {
19357
19806
  }))
19358
19807
  );
19359
19808
  }
19809
+ /** Live-enforced findings recorded for one session — a bare COUNT over the
19810
+ * session-stamped events (served by idx_events_session_id), so the Activity
19811
+ * page can label its findings link without the grouped pipeline. */
19812
+ sessionFindingsCount(sessionId) {
19813
+ if (!sessionId) return Promise.resolve(0);
19814
+ return Promise.resolve(
19815
+ countScalar(
19816
+ this.db,
19817
+ `SELECT count(*) AS n FROM findings f
19818
+ JOIN events e ON e.id = f.event_id
19819
+ WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
19820
+ { sessionId }
19821
+ )
19822
+ );
19823
+ }
19824
+ /** Per-rule transcript firing tally for one session — reads the OTHER finding
19825
+ * store (inspection_findings, keyed to audit_events): every detection the
19826
+ * transcript pass recorded, counted per firing rather than per unique value.
19827
+ * Rides on session-scoped grouped responses so the findings view can
19828
+ * reconcile the Activity page's tally with the deduped groups it lists. */
19829
+ sessionFirings(sessionId) {
19830
+ return Object.fromEntries(
19831
+ countBy(
19832
+ this.db,
19833
+ `SELECT d.rule_id AS k, count(*) AS n
19834
+ FROM inspection_findings f
19835
+ JOIN audit_events e ON e.id = f.audit_event_id
19836
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19837
+ WHERE e.root_session_id = :sessionId
19838
+ GROUP BY d.rule_id`,
19839
+ { sessionId }
19840
+ )
19841
+ );
19842
+ }
19360
19843
  /**
19361
- * Grouped findings for the dashboard — joins findings⋈events (repo/file
19362
- * from event metadata), groups by ruleId, computes per-filter-excluded facets,
19844
+ * Grouped findings for the dashboard — joins findings⋈events (repo/file/
19845
+ * toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
19363
19846
  * applies the requested filters, and sorts by severity then recency. Filtering
19364
19847
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19365
19848
  * reflect the full filtered set; `items` is the requested
19366
- * page (default 100); no cursor (nextCursor is always null).
19849
+ * page (default 50); no cursor (nextCursor is always null).
19850
+ *
19851
+ * Two reads, neither of which materializes a row per finding:
19852
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
19853
+ * the group and the filters need (count, providers, actions, statuses,
19854
+ * latest, search text);
19855
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
19856
+ * populate `instances` for the table's expanded rows.
19857
+ * The aggregates carry raw DB values and are translated by the same
19858
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
19859
+ * rule is ever restated in SQL.
19367
19860
  */
19368
19861
  listGroupedFindings(query) {
19369
- const rows = this.db.prepare(
19370
- `SELECT f.id, f.rule_id, f.category, f.severity, f.masked_match,
19371
- f.action_taken, f.confidence, e.occurred_at, e.source_tool,
19372
- json_extract(e.metadata, '$.repo') AS repo,
19373
- json_extract(e.metadata, '$.filePath') AS file,
19374
- e.kind AS kind, f.finding_key AS finding_key,
19375
- ${latestResolutionStatusSql("f")} AS latest_status
19376
- FROM findings f JOIN events e ON e.id = f.event_id
19377
- ORDER BY e.occurred_at DESC, f.id DESC
19378
- LIMIT :cap`
19379
- ).all({ cap: GROUP_ROW_CAP });
19862
+ const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
19863
+ const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
19864
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
19865
+ predicate: sessionPredicate,
19866
+ params: sessionParams
19867
+ });
19868
+ const rows = allRows(
19869
+ this.db.prepare(
19870
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19871
+ occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
19872
+ FROM (
19873
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19874
+ f.severity AS severity, f.masked_match AS masked_match,
19875
+ f.action_taken AS action_taken, f.confidence AS confidence,
19876
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19877
+ json_extract(e.metadata, '$.repo') AS repo,
19878
+ json_extract(e.metadata, '$.filePath') AS file,
19879
+ json_extract(e.metadata, '$.toolName') AS tool_name,
19880
+ e.kind AS kind, f.finding_key AS finding_key,
19881
+ latest.status AS latest_status,
19882
+ ROW_NUMBER() OVER (
19883
+ PARTITION BY f.rule_id
19884
+ ORDER BY e.occurred_at DESC, f.id DESC
19885
+ ) AS rn
19886
+ FROM findings f
19887
+ JOIN events e ON e.id = f.event_id
19888
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19889
+ ON latest.finding_key = f.finding_key
19890
+ ${sessionPredicate}
19891
+ )
19892
+ WHERE rn <= :cap
19893
+ ORDER BY occurred_at DESC, id DESC`
19894
+ ),
19895
+ { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
19896
+ );
19380
19897
  const groupable = rows.map((r) => ({
19381
19898
  id: r.id,
19382
19899
  ruleId: r.rule_id,
@@ -19389,9 +19906,10 @@ var SqliteFindingsRepository = class {
19389
19906
  sourceTool: r.source_tool,
19390
19907
  repo: r.repo ?? "",
19391
19908
  file: r.file ?? "",
19909
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
19392
19910
  status: deriveInstanceStatus(r)
19393
19911
  }));
19394
- const allGroups = buildFindingGroups(groupable);
19912
+ const allGroups = buildFindingGroups(groupable, { aggregates });
19395
19913
  const filterOpts = {
19396
19914
  severity: query.severity,
19397
19915
  providers: query.provider,
@@ -19407,44 +19925,134 @@ var SqliteFindingsRepository = class {
19407
19925
  };
19408
19926
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
19409
19927
  const items = sorted.slice(0, limit);
19410
- return Promise.resolve({ totals, facets, items, nextCursor: null });
19928
+ return Promise.resolve({
19929
+ totals,
19930
+ facets,
19931
+ items,
19932
+ nextCursor: null,
19933
+ ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
19934
+ });
19935
+ }
19936
+ /**
19937
+ * One row per rule_id, folding EVERY instance of the group into the values
19938
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
19939
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
19940
+ *
19941
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
19942
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19943
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19944
+ * status keeps the classifier itself in @akasecurity/schema, where
19945
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
19946
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
19947
+ * a group's row stays small however many findings it holds.
19948
+ *
19949
+ * `withSearchText` is the exception, and the one column here that does NOT
19950
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
19951
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19952
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
19953
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
19954
+ * match an instance outside the preview, which searching the preview alone
19955
+ * would silently lose, so it is fetched only when the request actually
19956
+ * carries a `q`.
19957
+ */
19958
+ groupAggregates(withSearchText, scope) {
19959
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19960
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
19961
+ group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
19962
+ const rows = this.db.prepare(
19963
+ `SELECT f.rule_id AS rule_id,
19964
+ count(*) AS instance_count,
19965
+ max(e.occurred_at) AS latest_at,
19966
+ group_concat(DISTINCT e.source_tool) AS source_tools,
19967
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
19968
+ group_concat(DISTINCT (
19969
+ e.kind || '${TUPLE_SEP}' ||
19970
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19971
+ coalesce(latest.status, '')
19972
+ )) AS status_inputs
19973
+ ${searchTextColumns}
19974
+ FROM findings f
19975
+ JOIN events e ON e.id = f.event_id
19976
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19977
+ ON latest.finding_key = f.finding_key
19978
+ ${scope.predicate}
19979
+ GROUP BY f.rule_id`
19980
+ ).all(scope.params);
19981
+ return new Map(
19982
+ rows.map((r) => [
19983
+ r.rule_id,
19984
+ {
19985
+ instanceCount: r.instance_count,
19986
+ sourceTools: splitConcat(r.source_tools),
19987
+ actionsTaken: splitConcat(r.actions_taken),
19988
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19989
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
19990
+ return {
19991
+ // deriveFindingStatus only distinguishes null from non-null here,
19992
+ // so the marker stands in for the key itself (never rendered).
19993
+ kind,
19994
+ findingKey: keyMarker === "" ? null : keyMarker,
19995
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
19996
+ };
19997
+ }),
19998
+ latestDetectedAt: epochMillisToIso(r.latest_at),
19999
+ // Free text only — joined and substring-matched, so group_concat's
20000
+ // commas need no unpicking (a repo/path containing one still matches).
20001
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
20002
+ // tell "no q this request" from "a group with no repo/file at all"
20003
+ // and skip priming a haystack nothing will read.
20004
+ ...withSearchText ? {
20005
+ searchText: [r.repos ?? "", r.files ?? "", r.tool_names ?? ""].filter((s) => s !== "").join(" ")
20006
+ } : {}
20007
+ }
20008
+ ])
20009
+ );
19411
20010
  }
19412
20011
  healthSummary() {
19413
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
20012
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
19414
20013
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
19415
- const grouped = this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken").all();
20014
+ const grouped = allRows(
20015
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
20016
+ );
19416
20017
  for (const row of grouped) {
19417
20018
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
19418
20019
  }
19419
20020
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
19420
- const sevRows = this.db.prepare(
19421
- `SELECT f.severity AS severity, count(*) AS c
20021
+ const sevRows = allRows(
20022
+ this.db.prepare(
20023
+ `SELECT f.severity AS severity, count(*) AS c
19422
20024
  FROM findings f
19423
20025
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19424
20026
  ON latest.finding_key = f.finding_key
19425
20027
  WHERE latest.status IS NULL OR latest.status != 'resolved'
19426
20028
  GROUP BY f.severity`
19427
- ).all();
20029
+ )
20030
+ );
19428
20031
  for (const row of sevRows) {
19429
20032
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
19430
20033
  }
19431
20034
  const categories = ENFORCEABLE_CATEGORIES;
19432
- const enabledRows = this.db.prepare(
19433
- `SELECT DISTINCT json_extract(target, '$.category') AS category
20035
+ const enabledRows = allRows(
20036
+ this.db.prepare(
20037
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
19434
20038
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
19435
- ).all();
20039
+ )
20040
+ );
19436
20041
  const enabled = new Set(enabledRows.map((r) => r.category));
19437
20042
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
19438
20043
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
19439
20044
  }
19440
20045
  activityByDay(days = 7) {
19441
20046
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
19442
- const rows = this.db.prepare(
19443
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
20047
+ const rows = allRows(
20048
+ this.db.prepare(
20049
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19444
20050
  FROM findings f JOIN events e ON e.id = f.event_id
19445
20051
  WHERE e.occurred_at >= :since
19446
20052
  GROUP BY day, f.action_taken`
19447
- ).all({ since });
20053
+ ),
20054
+ { since }
20055
+ );
19448
20056
  const buckets = /* @__PURE__ */ new Map();
19449
20057
  for (let i = 0; i < days; i++) {
19450
20058
  const day = isoDay(since + i * DAY_MS3);
@@ -19516,17 +20124,19 @@ var SqliteInspectionFindingsRepository = class {
19516
20124
  insertStmt;
19517
20125
  insertFinding(input) {
19518
20126
  const row = toInspectionFindingRow(input);
19519
- this.insertStmt.run({
19520
- id: row.id,
19521
- auditEventId: row.auditEventId,
19522
- inspectionDefinitionId: row.inspectionDefinitionId,
19523
- classifiedDataId: row.classifiedDataId ?? null,
19524
- spanStart: row.spanStart,
19525
- spanEnd: row.spanEnd,
19526
- maskedMatch: row.maskedMatch,
19527
- actionTaken: row.actionTaken,
19528
- confidence: row.confidence
19529
- });
20127
+ this.insertStmt.run(
20128
+ bindParams({
20129
+ id: row.id,
20130
+ auditEventId: row.auditEventId,
20131
+ inspectionDefinitionId: row.inspectionDefinitionId,
20132
+ classifiedDataId: row.classifiedDataId,
20133
+ spanStart: row.spanStart,
20134
+ spanEnd: row.spanEnd,
20135
+ maskedMatch: row.maskedMatch,
20136
+ actionTaken: row.actionTaken,
20137
+ confidence: row.confidence
20138
+ })
20139
+ );
19530
20140
  }
19531
20141
  };
19532
20142
 
@@ -19602,12 +20212,7 @@ function isMirrorDowngrade(incoming, stored) {
19602
20212
  }
19603
20213
  function ruleIdsOf(rulesJson) {
19604
20214
  const ids = /* @__PURE__ */ new Set();
19605
- let raw;
19606
- try {
19607
- raw = JSON.parse(rulesJson);
19608
- } catch {
19609
- return ids;
19610
- }
20215
+ const raw = safeJson(rulesJson, []);
19611
20216
  if (!Array.isArray(raw)) return ids;
19612
20217
  for (const entry of raw) {
19613
20218
  if (entry && typeof entry === "object") {
@@ -19680,47 +20285,48 @@ var SqliteInstalledPacksRepository = class {
19680
20285
  }));
19681
20286
  if (this.storedSignature() === inventorySignature(rows)) return;
19682
20287
  const now = Date.now();
19683
- this.db.exec("BEGIN IMMEDIATE");
19684
- try {
19685
- const mirror = this.mirrorState();
19686
- let behind = false;
19687
- for (const row of rows) {
19688
- const params = {
19689
- id: randomUUID2(),
19690
- namespace: row.namespace,
19691
- packId: row.packId,
19692
- version: row.version,
19693
- name: row.name,
19694
- rulesJson: row.rulesJson,
19695
- now
19696
- };
19697
- const stored = mirror.get(`${row.namespace}/${row.packId}`);
19698
- if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
19699
- this.upsertAvailableStmt.run({
19700
- ...params,
20288
+ withTransaction(
20289
+ this.db,
20290
+ () => {
20291
+ const mirror = this.mirrorState();
20292
+ let behind = false;
20293
+ for (const row of rows) {
20294
+ const params = {
19701
20295
  id: randomUUID2(),
19702
- recordedBy: meta3?.recordedBy ?? null
19703
- });
19704
- } else {
19705
- behind = true;
20296
+ namespace: row.namespace,
20297
+ packId: row.packId,
20298
+ version: row.version,
20299
+ name: row.name,
20300
+ rulesJson: row.rulesJson,
20301
+ now
20302
+ };
20303
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
20304
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20305
+ this.upsertAvailableStmt.run({
20306
+ ...params,
20307
+ id: randomUUID2(),
20308
+ recordedBy: meta3?.recordedBy ?? null
20309
+ });
20310
+ } else {
20311
+ behind = true;
20312
+ }
20313
+ this.insertMissingStmt.run(params);
19706
20314
  }
19707
- this.insertMissingStmt.run(params);
19708
- }
19709
- if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
19710
- this.db.exec("COMMIT");
19711
- } catch (err) {
19712
- this.db.exec("ROLLBACK");
19713
- throw err;
19714
- }
20315
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20316
+ },
20317
+ "IMMEDIATE"
20318
+ );
19715
20319
  } catch {
19716
20320
  }
19717
20321
  }
19718
20322
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
19719
20323
  // input to the downgrade guard. Read INSIDE the write transaction.
19720
20324
  mirrorState() {
19721
- const rows = this.db.prepare(
19722
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
19723
- ).all();
20325
+ const rows = allRows(
20326
+ this.db.prepare(
20327
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20328
+ )
20329
+ );
19724
20330
  return new Map(
19725
20331
  rows.map((r) => [
19726
20332
  `${r.namespace}/${r.packId}`,
@@ -19732,7 +20338,9 @@ var SqliteInstalledPacksRepository = class {
19732
20338
  // (keys joined with '/', matching the detection id slug encoding — packId may
19733
20339
  // itself contain '/', but namespace may not, so the join is unambiguous).
19734
20340
  pruneAvailable(keep) {
19735
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20341
+ const rows = allRows(
20342
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20343
+ );
19736
20344
  const keepSet = new Set(keep);
19737
20345
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
19738
20346
  for (const r of rows) {
@@ -19759,11 +20367,13 @@ var SqliteInstalledPacksRepository = class {
19759
20367
  if (this.db.isTransaction) {
19760
20368
  throw new Error("applyUpdate must not be called inside an open transaction");
19761
20369
  }
19762
- this.db.exec("BEGIN IMMEDIATE");
19763
- try {
19764
- this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
19765
- const res = this.db.prepare(
19766
- `UPDATE installed_packs SET
20370
+ let changed = false;
20371
+ withTransaction(
20372
+ this.db,
20373
+ () => {
20374
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20375
+ const res = this.db.prepare(
20376
+ `UPDATE installed_packs SET
19767
20377
  version = (SELECT a.version FROM available_packs a
19768
20378
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
19769
20379
  name = (SELECT a.name FROM available_packs a
@@ -19774,17 +20384,13 @@ var SqliteInstalledPacksRepository = class {
19774
20384
  WHERE namespace = :namespace AND pack_id = :packId
19775
20385
  AND EXISTS (SELECT 1 FROM available_packs a
19776
20386
  WHERE a.namespace = :namespace AND a.pack_id = :packId)`
19777
- ).run({ namespace, packId, now: Date.now() });
19778
- this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
19779
- this.db.exec("COMMIT");
19780
- return Number(res.changes) > 0;
19781
- } catch (err) {
19782
- try {
19783
- this.db.exec("ROLLBACK");
19784
- } catch {
19785
- }
19786
- throw err;
19787
- }
20387
+ ).run({ namespace, packId, now: Date.now() });
20388
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20389
+ changed = Number(res.changes) > 0;
20390
+ },
20391
+ "IMMEDIATE"
20392
+ );
20393
+ return changed;
19788
20394
  }
19789
20395
  /**
19790
20396
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -19798,9 +20404,11 @@ var SqliteInstalledPacksRepository = class {
19798
20404
  * JSON-level failure therefore counts as invalid.
19799
20405
  */
19800
20406
  installedRuleset() {
19801
- const rows = this.db.prepare(
19802
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
19803
- ).all();
20407
+ const rows = allRows(
20408
+ this.db.prepare(
20409
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20410
+ )
20411
+ );
19804
20412
  const out = {
19805
20413
  installedPacks: rows.length,
19806
20414
  enabledPacks: 0,
@@ -19809,7 +20417,7 @@ var SqliteInstalledPacksRepository = class {
19809
20417
  ruleActions: /* @__PURE__ */ new Map()
19810
20418
  };
19811
20419
  for (const row of rows) {
19812
- if (row.enabled !== 1) continue;
20420
+ if (!intToBool(row.enabled)) continue;
19813
20421
  out.enabledPacks += 1;
19814
20422
  const action = policyIdToAction(row.policyId);
19815
20423
  let raw;
@@ -19844,9 +20452,11 @@ var SqliteInstalledPacksRepository = class {
19844
20452
  * running max would mask a genuinely-newer parseable stamp.
19845
20453
  */
19846
20454
  newestRecordedBinary() {
19847
- const rows = this.db.prepare(
19848
- `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
19849
- ).all();
20455
+ const rows = allRows(
20456
+ this.db.prepare(
20457
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20458
+ )
20459
+ );
19850
20460
  let newest = null;
19851
20461
  for (const row of rows) {
19852
20462
  const at = row.recordedBy.lastIndexOf("@");
@@ -19861,13 +20471,15 @@ var SqliteInstalledPacksRepository = class {
19861
20471
  return newest;
19862
20472
  }
19863
20473
  counts() {
19864
- const row = this.db.prepare(
19865
- `SELECT count(*) AS packs,
20474
+ const row = getRow(
20475
+ this.db.prepare(
20476
+ `SELECT count(*) AS packs,
19866
20477
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
19867
20478
  coalesce(sum(enabled), 0) AS enabled
19868
20479
  FROM installed_packs`
19869
- ).get();
19870
- return Promise.resolve(row);
20480
+ )
20481
+ );
20482
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
19871
20483
  }
19872
20484
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
19873
20485
  // Back the Policies page's built-in catalog: how many
@@ -19880,26 +20492,29 @@ var SqliteInstalledPacksRepository = class {
19880
20492
  * attributed to Monitor, matching the Detections views.
19881
20493
  */
19882
20494
  countsByPolicyId() {
19883
- const rows = this.db.prepare(
19884
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20495
+ return countBy(
20496
+ this.db,
20497
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
19885
20498
  FROM installed_packs
19886
- GROUP BY pid`
19887
- ).all();
19888
- return new Map(rows.map((r) => [r.pid, r.n]));
20499
+ GROUP BY k`
20500
+ );
19889
20501
  }
19890
20502
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
19891
20503
  listByPolicyId(policyId) {
19892
- const rows = this.db.prepare(
19893
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20504
+ const rows = allRows(
20505
+ this.db.prepare(
20506
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
19894
20507
  FROM installed_packs
19895
20508
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
19896
20509
  ORDER BY name ASC`
19897
- ).all(policyId);
20510
+ ),
20511
+ [policyId]
20512
+ );
19898
20513
  return rows.map((r) => ({
19899
20514
  id: `${r.namespace}/${r.packId}`,
19900
20515
  name: r.name,
19901
20516
  ruleCount: parseRules(r.rulesJson).length,
19902
- enabled: r.enabled === 1
20517
+ enabled: intToBool(r.enabled)
19903
20518
  }));
19904
20519
  }
19905
20520
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -19928,14 +20543,14 @@ var SqliteInstalledPacksRepository = class {
19928
20543
  const res = this.db.prepare(
19929
20544
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
19930
20545
  WHERE namespace = :namespace AND pack_id = :packId`
19931
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
20546
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
19932
20547
  return Number(res.changes) > 0;
19933
20548
  }
19934
20549
  // Fingerprint of the recorded available mirror — compared against the
19935
20550
  // incoming inventory's signature to skip the write entirely when the running
19936
20551
  // binary's inventory hasn't changed since the last record.
19937
20552
  storedSignature() {
19938
- const rows = this.signatureStmt.all();
20553
+ const rows = allRows(this.signatureStmt);
19939
20554
  return inventorySignature(rows);
19940
20555
  }
19941
20556
  };
@@ -19963,42 +20578,48 @@ var SqliteInventoryRepository = class {
19963
20578
  upsert(input, now = Date.now()) {
19964
20579
  const id = inventoryId(input.objectType, input.identityKey);
19965
20580
  const row = toInventoryRow(input, id, now);
19966
- this.upsertStmt.run({
19967
- id: row.id,
19968
- objectType: row.objectType,
19969
- location: row.location ?? null,
19970
- title: row.title ?? null,
19971
- hostId: row.hostId ?? null,
19972
- attributes: row.attributes,
19973
- firstSeen: row.firstSeen,
19974
- lastSeen: row.lastSeen
19975
- });
20581
+ this.upsertStmt.run(
20582
+ bindParams({
20583
+ id: row.id,
20584
+ objectType: row.objectType,
20585
+ location: row.location,
20586
+ title: row.title,
20587
+ hostId: row.hostId,
20588
+ attributes: row.attributes,
20589
+ firstSeen: row.firstSeen,
20590
+ lastSeen: row.lastSeen
20591
+ })
20592
+ );
19976
20593
  return id;
19977
20594
  }
19978
20595
  // The full row, for round-trip assertions.
19979
20596
  findById(id) {
19980
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
19981
- return row;
20597
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
19982
20598
  }
19983
20599
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
19984
20600
  // served from the object_type index, never from audit_events.
19985
20601
  distinctTitles(objectType) {
19986
- const rows = this.db.prepare(
19987
- `SELECT DISTINCT title FROM inventory
20602
+ const rows = allRows(
20603
+ this.db.prepare(
20604
+ `SELECT DISTINCT title FROM inventory
19988
20605
  WHERE object_type = :objectType AND title IS NOT NULL
19989
20606
  ORDER BY title`
19990
- ).all({ objectType });
20607
+ ),
20608
+ { objectType }
20609
+ );
19991
20610
  return rows.map((r) => r.title);
19992
20611
  }
19993
20612
  // Distinct host os_version values — a facet served from an inventory index
19994
20613
  // over the generated column, never from the audit fact (confirm via EXPLAIN
19995
20614
  // QUERY PLAN).
19996
20615
  osVersions() {
19997
- const rows = this.db.prepare(
19998
- `SELECT DISTINCT os_version AS value FROM inventory
20616
+ const rows = allRows(
20617
+ this.db.prepare(
20618
+ `SELECT DISTINCT os_version AS value FROM inventory
19999
20619
  WHERE object_type = 'host' AND os_version IS NOT NULL
20000
20620
  ORDER BY value`
20001
- ).all();
20621
+ )
20622
+ );
20002
20623
  return rows.map((r) => r.value);
20003
20624
  }
20004
20625
  };
@@ -20018,14 +20639,6 @@ var EMPTY_PROJECT_AGG = {
20018
20639
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
20019
20640
  findingsCount: 0
20020
20641
  };
20021
- function safeJson(s, fallback) {
20022
- if (s == null) return fallback;
20023
- try {
20024
- return JSON.parse(s);
20025
- } catch {
20026
- return fallback;
20027
- }
20028
- }
20029
20642
  function resolveHarnessId(attrs, row) {
20030
20643
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
20031
20644
  return attrs.provider;
@@ -20221,30 +20834,49 @@ var SqliteInventoryAssetsRepository = class {
20221
20834
  configRowsCache;
20222
20835
  // ─── stats ─────────────────────────────────────────────────────────────────
20223
20836
  getInventoryStats() {
20224
- const byType = { project: 0, skill: 0, mcp: 0, hook: 0, config: 0 };
20225
- for (const r of this.db.prepare("SELECT asset_type AS t, count(*) AS n FROM inventory_asset GROUP BY asset_type").all()) {
20226
- if (r.t in byType) byType[r.t] = r.n;
20227
- }
20228
- byType.project = this.db.prepare(`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`).get().n;
20229
- const mcpTrust = { "known-good": 0, risky: 0, unapproved: 0 };
20230
- for (const r of this.db.prepare(
20231
- `SELECT coalesce(o.trust, a.trust) AS trust, count(*) AS n
20837
+ const typeCounts = countBy(
20838
+ this.db,
20839
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
20840
+ );
20841
+ const byType = {
20842
+ project: 0,
20843
+ skill: typeCounts.get("skill") ?? 0,
20844
+ mcp: typeCounts.get("mcp") ?? 0,
20845
+ hook: typeCounts.get("hook") ?? 0,
20846
+ config: typeCounts.get("config") ?? 0
20847
+ };
20848
+ byType.project = countScalar(
20849
+ this.db,
20850
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
20851
+ );
20852
+ const mcpTrustCounts = countBy(
20853
+ this.db,
20854
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20232
20855
  FROM inventory_asset a
20233
20856
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20234
20857
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20235
20858
  GROUP BY coalesce(o.trust, a.trust)`
20236
- ).all()) {
20237
- if (r.trust in mcpTrust) mcpTrust[r.trust] = r.n;
20238
- }
20239
- const harnesses = this.db.prepare(
20859
+ );
20860
+ const mcpTrust = {
20861
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
20862
+ risky: mcpTrustCounts.get("risky") ?? 0,
20863
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
20864
+ };
20865
+ const harnesses = countScalar(
20866
+ this.db,
20240
20867
  `SELECT count(*) AS n FROM inventory
20241
20868
  WHERE object_type = 'harness'
20242
- AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20243
- ).get({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }).n;
20244
- const flaggedAssets = this.db.prepare("SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'").get().n;
20245
- const flaggedProjects = this.db.prepare(
20869
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
20870
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20871
+ );
20872
+ const flaggedAssets = countScalar(
20873
+ this.db,
20874
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
20875
+ );
20876
+ const flaggedProjects = countScalar(
20877
+ this.db,
20246
20878
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20247
- ).get().n;
20879
+ );
20248
20880
  const configRows = this.configAssetRows();
20249
20881
  for (const r of configRows) {
20250
20882
  byType[r.assetType] += 1;
@@ -20501,12 +21133,15 @@ var SqliteInventoryAssetsRepository = class {
20501
21133
  }
20502
21134
  // ─── raw fetchers ────────────────────────────────────────────────────────────
20503
21135
  fetchHarnessRows() {
20504
- return this.db.prepare(
20505
- `SELECT id, title, attributes, harness_version AS harnessVersion
21136
+ return allRows(
21137
+ this.db.prepare(
21138
+ `SELECT id, title, attributes, harness_version AS harnessVersion
20506
21139
  FROM inventory
20507
21140
  WHERE object_type = 'harness'
20508
21141
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20509
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
21142
+ ),
21143
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
21144
+ );
20510
21145
  }
20511
21146
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
20512
21147
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -20516,12 +21151,13 @@ var SqliteInventoryAssetsRepository = class {
20516
21151
  const params = [...harnessInvIds];
20517
21152
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
20518
21153
  if (q) {
20519
- const pat = `%${escapeLikePattern(q)}%`;
20520
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
21154
+ const pat = containsPattern(q);
21155
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
20521
21156
  params.push(pat, pat);
20522
21157
  }
20523
- const rows = this.db.prepare(
20524
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
21158
+ const rows = allRows(
21159
+ this.db.prepare(
21160
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20525
21161
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20526
21162
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20527
21163
  FROM harness_asset ha
@@ -20529,7 +21165,9 @@ var SqliteInventoryAssetsRepository = class {
20529
21165
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20530
21166
  WHERE ${where}
20531
21167
  ORDER BY a.name ASC`
20532
- ).all(...params);
21168
+ ),
21169
+ params
21170
+ );
20533
21171
  for (const raw of rows) {
20534
21172
  const harnessInvId = raw.harnessInvId;
20535
21173
  const [asset] = this.mapAssetRows([raw]);
@@ -20548,21 +21186,24 @@ var SqliteInventoryAssetsRepository = class {
20548
21186
  params.push(...types);
20549
21187
  }
20550
21188
  if (q) {
20551
- const pat = `%${escapeLikePattern(q)}%`;
20552
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
21189
+ const pat = containsPattern(q);
21190
+ conditions.push(likeAny(["a.name", "a.sub"]));
20553
21191
  params.push(pat, pat);
20554
21192
  }
20555
21193
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
20556
21194
  const sampleRows = this.mapAssetRows(
20557
- this.db.prepare(
20558
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21195
+ allRows(
21196
+ this.db.prepare(
21197
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20559
21198
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20560
21199
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20561
21200
  FROM inventory_asset a
20562
21201
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20563
21202
  ${where}
20564
21203
  ORDER BY a.name ASC`
20565
- ).all(...params)
21204
+ ),
21205
+ params
21206
+ )
20566
21207
  );
20567
21208
  const configRows = this.configAssetRows(q).filter(
20568
21209
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -20571,14 +21212,17 @@ var SqliteInventoryAssetsRepository = class {
20571
21212
  }
20572
21213
  fetchAssetById(assetId) {
20573
21214
  const rows = this.mapAssetRows(
20574
- this.db.prepare(
20575
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21215
+ allRows(
21216
+ this.db.prepare(
21217
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20576
21218
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20577
21219
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20578
21220
  FROM inventory_asset a
20579
21221
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20580
21222
  WHERE a.id = ?`
20581
- ).all(assetId)
21223
+ ),
21224
+ [assetId]
21225
+ )
20582
21226
  );
20583
21227
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
20584
21228
  }
@@ -20634,37 +21278,39 @@ var SqliteInventoryAssetsRepository = class {
20634
21278
  return rows;
20635
21279
  }
20636
21280
  latestConfigScanId() {
20637
- const row = this.db.prepare(
20638
- `SELECT id FROM audit_events WHERE event_type = 'config_scan'
20639
- ORDER BY started_at DESC, id DESC LIMIT 1`
20640
- ).get();
20641
- return row?.id ?? null;
21281
+ return latestConfigScan(this.db)?.id ?? null;
20642
21282
  }
20643
21283
  fetchProjects(q) {
20644
21284
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
20645
21285
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
20646
21286
  const params = [];
20647
21287
  if (q) {
20648
- const pat = `%${escapeLikePattern(q)}%`;
20649
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
21288
+ const pat = containsPattern(q);
21289
+ sql += ` AND ${likeAny(["name", "url"])}`;
20650
21290
  params.push(pat, pat);
20651
21291
  }
20652
21292
  sql += " ORDER BY name ASC";
20653
- return this.db.prepare(sql).all(...params);
21293
+ return allRows(this.db.prepare(sql), params);
20654
21294
  }
20655
21295
  fetchProjectById(projectId) {
20656
- return this.db.prepare(
20657
- "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
20658
- ).get(projectId) ?? null;
21296
+ return getRow(
21297
+ this.db.prepare(
21298
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
21299
+ ),
21300
+ [projectId]
21301
+ ) ?? null;
20659
21302
  }
20660
21303
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
20661
21304
  fetchProjectsByIds(projectIds) {
20662
21305
  const map2 = /* @__PURE__ */ new Map();
20663
21306
  if (projectIds.length === 0) return map2;
20664
- const rows = this.db.prepare(
20665
- `SELECT id, url, name, attributes, last_seen AS lastSeen
21307
+ const rows = allRows(
21308
+ this.db.prepare(
21309
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
20666
21310
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
20667
- ).all(...projectIds);
21311
+ ),
21312
+ projectIds
21313
+ );
20668
21314
  for (const r of rows) map2.set(r.id, r);
20669
21315
  return map2;
20670
21316
  }
@@ -20675,8 +21321,9 @@ var SqliteInventoryAssetsRepository = class {
20675
21321
  projectAggregates(projectIds) {
20676
21322
  const map2 = /* @__PURE__ */ new Map();
20677
21323
  if (projectIds.length === 0) return map2;
20678
- const rows = this.db.prepare(
20679
- `SELECT f.project_id AS projectId,
21324
+ const rows = allRows(
21325
+ this.db.prepare(
21326
+ `SELECT f.project_id AS projectId,
20680
21327
  coalesce(o.access, f.default_access) AS eff,
20681
21328
  count(*) AS n,
20682
21329
  coalesce(sum(f.findings_count), 0) AS findings
@@ -20684,7 +21331,9 @@ var SqliteInventoryAssetsRepository = class {
20684
21331
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
20685
21332
  WHERE f.project_id IN (${placeholders(projectIds.length)})
20686
21333
  GROUP BY f.project_id, eff`
20687
- ).all(...projectIds);
21334
+ ),
21335
+ projectIds
21336
+ );
20688
21337
  for (const r of rows) {
20689
21338
  let agg = map2.get(r.projectId);
20690
21339
  if (!agg) {
@@ -20722,37 +21371,52 @@ var SqliteInventoryAssetsRepository = class {
20722
21371
  fetchProjectFilesUnder(projectId, prefix) {
20723
21372
  if (prefix === "") {
20724
21373
  return this.mapFileRows(
20725
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21374
+ allRows(
21375
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21376
+ [projectId]
21377
+ )
20726
21378
  );
20727
21379
  }
20728
21380
  return this.mapFileRows(
20729
- this.db.prepare(
20730
- this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
20731
- ).all(projectId, `${escapeLikePattern(prefix)}/%`)
21381
+ allRows(
21382
+ this.db.prepare(
21383
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21384
+ ),
21385
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21386
+ )
20732
21387
  );
20733
21388
  }
20734
21389
  fetchProjectFilesSearch(projectId, q) {
20735
- const pat = `%${escapeLikePattern(q)}%`;
21390
+ const pat = containsPattern(q);
20736
21391
  return this.mapFileRows(
20737
- this.db.prepare(
20738
- this.fileSelect(
20739
- "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
20740
- )
20741
- ).all(projectId, pat, pat)
21392
+ allRows(
21393
+ this.db.prepare(
21394
+ this.fileSelect(
21395
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21396
+ )
21397
+ ),
21398
+ [projectId, pat, pat]
21399
+ )
20742
21400
  );
20743
21401
  }
20744
21402
  fetchProjectFilesBlocked(projectId) {
20745
21403
  return this.mapFileRows(
20746
- this.db.prepare(
20747
- this.fileSelect(
20748
- "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
20749
- )
20750
- ).all(projectId)
21404
+ allRows(
21405
+ this.db.prepare(
21406
+ this.fileSelect(
21407
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21408
+ )
21409
+ ),
21410
+ [projectId]
21411
+ )
20751
21412
  );
20752
21413
  }
20753
21414
  fetchProjectFile(projectId, path) {
20754
21415
  const rows = this.mapFileRows(
20755
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21416
+ allRows(
21417
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21418
+ [projectId, path]
21419
+ )
20756
21420
  );
20757
21421
  return rows[0] ?? null;
20758
21422
  }
@@ -20766,39 +21430,32 @@ var SqlitePoliciesRepository = class {
20766
21430
  }
20767
21431
  db;
20768
21432
  readPolicies() {
20769
- const rows = this.db.prepare("SELECT * FROM policies").all();
20770
- const policies = [];
20771
- for (const row of rows) {
20772
- try {
20773
- const target = JSON.parse(row.target);
20774
- const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
20775
- policies.push(
20776
- Policy.parse({
20777
- id: row.id,
20778
- scope: row.scope,
20779
- target,
20780
- action: row.action,
20781
- enabled: row.enabled === 1,
20782
- customKeywords
20783
- })
20784
- );
20785
- } catch {
20786
- }
20787
- }
21433
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21434
+ const policies = mapRowsTolerant(rows, (row) => {
21435
+ const target = JSON.parse(row.target);
21436
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21437
+ return Policy.parse({
21438
+ id: row.id,
21439
+ scope: row.scope,
21440
+ target,
21441
+ action: row.action,
21442
+ enabled: intToBool(row.enabled),
21443
+ customKeywords
21444
+ });
21445
+ });
20788
21446
  return Promise.resolve(policies);
20789
21447
  }
20790
21448
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
20791
21449
  // detection-type config exists from first run. Only when the table is empty,
20792
21450
  // so a user's edits are never clobbered.
20793
21451
  seedDefaults() {
20794
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21452
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
20795
21453
  if (count > 0) return;
20796
21454
  const stmt = this.db.prepare(
20797
21455
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
20798
21456
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
20799
21457
  );
20800
- this.db.exec("BEGIN");
20801
- try {
21458
+ failOpenTransaction(this.db, () => {
20802
21459
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
20803
21460
  stmt.run({
20804
21461
  id: randomUUID4(),
@@ -20807,10 +21464,41 @@ var SqlitePoliciesRepository = class {
20807
21464
  now: Date.now()
20808
21465
  });
20809
21466
  }
20810
- this.db.exec("COMMIT");
20811
- } catch {
20812
- this.db.exec("ROLLBACK");
20813
- }
21467
+ });
21468
+ }
21469
+ // Insert-or-update the single global per-category policy row, keyed on the
21470
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21471
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21472
+ // values), so the runtime's resolveAction reads rows written by either path
21473
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21474
+ // `id` and `created_at` are left exactly as they were.
21475
+ upsertCategoryAction(category, action) {
21476
+ const now = Date.now();
21477
+ this.db.prepare(
21478
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21479
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21480
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21481
+ ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
21482
+ }
21483
+ // Caps every global per-category policy currently set to block/redact down
21484
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21485
+ // Returns the number of rows changed.
21486
+ capCategoryActions() {
21487
+ const info = this.db.prepare(
21488
+ `UPDATE policies SET action='warn', updated_at=:now
21489
+ WHERE scope='global' AND action IN ('block','redact')
21490
+ AND json_extract(target,'$.category') IS NOT NULL`
21491
+ ).run({ now: Date.now() });
21492
+ return Number(info.changes);
21493
+ }
21494
+ // Read the current action for a single global per-category policy row, mirroring
21495
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21496
+ // row exists yet, so callers can distinguish an unset category from a set one.
21497
+ getCategoryAction(category) {
21498
+ const row = this.db.prepare(
21499
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21500
+ ).get({ category });
21501
+ return row?.action;
20814
21502
  }
20815
21503
  };
20816
21504
 
@@ -20886,7 +21574,7 @@ var SqliteProjectFilesRepository = class {
20886
21574
  maxStampStmt;
20887
21575
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
20888
21576
  replaceForProject(projectId, scan2, now) {
20889
- const { maxStamp } = this.maxStampStmt.get({ projectId });
21577
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
20890
21578
  const stamp = Math.max(now, maxStamp + 1);
20891
21579
  for (const file2 of scan2.files) {
20892
21580
  this.upsertStmt.run({
@@ -20969,7 +21657,7 @@ var SqliteResolutionsRepository = class {
20969
21657
  }
20970
21658
  /** The newest disposition recorded for a finding key, or undefined if none. */
20971
21659
  latestByKey(key) {
20972
- const row = this.latestStmt.get({ findingKey: key });
21660
+ const row = getRow(this.latestStmt, { findingKey: key });
20973
21661
  if (!row) return void 0;
20974
21662
  return {
20975
21663
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -20986,7 +21674,7 @@ var SqliteResolutionsRepository = class {
20986
21674
  * the CLI) surfaces for that file.
20987
21675
  */
20988
21676
  openAtRestKeysForPath(path) {
20989
- const rows = this.openAtRestStmt.all({ path });
21677
+ const rows = allRows(this.openAtRestStmt, { path });
20990
21678
  return rows.map((r) => r.finding_key);
20991
21679
  }
20992
21680
  /**
@@ -20997,7 +21685,7 @@ var SqliteResolutionsRepository = class {
20997
21685
  * resolution row (see scan.ts).
20998
21686
  */
20999
21687
  resolvedAtRestKeysForPath(path) {
21000
- const rows = this.resolvedAtRestStmt.all({ path });
21688
+ const rows = allRows(this.resolvedAtRestStmt, { path });
21001
21689
  return rows.map((r) => r.finding_key);
21002
21690
  }
21003
21691
  };
@@ -21026,38 +21714,31 @@ var SqliteScanLedgerRepository = class {
21026
21714
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
21027
21715
  // older ruleset are simply absent, which reads as "never scanned".
21028
21716
  entriesForRuleset(rulesetHash) {
21029
- const rows = this.readStmt.all({ rulesetHash });
21717
+ const rows = allRows(this.readStmt, {
21718
+ rulesetHash
21719
+ });
21030
21720
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
21031
21721
  }
21032
21722
  upsertEntries(entries) {
21033
21723
  if (entries.length === 0) return;
21034
21724
  const scannedAt = Date.now();
21035
- try {
21036
- this.db.exec("BEGIN");
21037
- try {
21038
- for (const entry of entries) {
21039
- this.upsertStmt.run({
21040
- path: entry.path,
21041
- mtime: entry.mtime,
21042
- contentHash: entry.contentHash,
21043
- rulesetHash: entry.rulesetHash,
21044
- scannedAt
21045
- });
21046
- }
21047
- this.db.exec("COMMIT");
21048
- } catch (err) {
21049
- this.db.exec("ROLLBACK");
21050
- throw err;
21725
+ failOpenTransaction(this.db, () => {
21726
+ for (const entry of entries) {
21727
+ this.upsertStmt.run({
21728
+ path: entry.path,
21729
+ mtime: entry.mtime,
21730
+ contentHash: entry.contentHash,
21731
+ rulesetHash: entry.rulesetHash,
21732
+ scannedAt
21733
+ });
21051
21734
  }
21052
- } catch {
21053
- }
21735
+ });
21054
21736
  }
21055
21737
  };
21056
21738
 
21057
21739
  // ../../packages/persistence/src/repositories/security.ts
21058
21740
  var DAY_MS4 = 864e5;
21059
21741
  var SEVERITIES = ["critical", "high", "medium", "low"];
21060
- var RANGE_DAYS = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
21061
21742
  var ACTION_TO_KIND = {
21062
21743
  block: "blocked",
21063
21744
  redact: "redacted",
@@ -21072,8 +21753,14 @@ var SCAN_COVERAGE = [
21072
21753
  { provider: "copilot", coverage: 0, supported: false },
21073
21754
  { provider: "api", coverage: 0, supported: false }
21074
21755
  ];
21756
+ var GRANULARITY = {
21757
+ "7d": "day",
21758
+ "30d": "day",
21759
+ "3m": "week",
21760
+ "6m": "week"
21761
+ };
21075
21762
  function granularityFor(range) {
21076
- return range === "7d" || range === "30d" ? "day" : "week";
21763
+ return GRANULARITY[range];
21077
21764
  }
21078
21765
  function startOfUtcDay2(ms) {
21079
21766
  return Math.floor(ms / DAY_MS4) * DAY_MS4;
@@ -21127,8 +21814,9 @@ var SqliteSecurityRepository = class {
21127
21814
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21128
21815
  // double-counting a key that accumulated several append-only rows.
21129
21816
  severitySummary() {
21130
- const rows = this.db.prepare(
21131
- `SELECT f.severity AS severity,
21817
+ const rows = allRows(
21818
+ this.db.prepare(
21819
+ `SELECT f.severity AS severity,
21132
21820
  COUNT(*) AS count,
21133
21821
  SUM(CASE
21134
21822
  WHEN e.kind != 'code_change' THEN 1
@@ -21147,7 +21835,8 @@ var SqliteSecurityRepository = class {
21147
21835
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21148
21836
  ON latest.finding_key = f.finding_key
21149
21837
  GROUP BY f.severity`
21150
- ).all();
21838
+ )
21839
+ );
21151
21840
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21152
21841
  const bySeverity = SEVERITIES.map((severity) => ({
21153
21842
  severity,
@@ -21231,14 +21920,15 @@ var SqliteSecurityRepository = class {
21231
21920
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21232
21921
  const now = this.now();
21233
21922
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
21234
- const rows = this.db.prepare(
21235
- // first_detected_at is the PRESERVED first-detection time (set once on a
21236
- // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21237
- // measures from first sighting — not the latest re-scan's event, whose
21238
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21239
- // the parent event's occurred_at defends against any legacy/edge row the
21240
- // backfill left null.
21241
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21923
+ const rows = allRows(
21924
+ this.db.prepare(
21925
+ // first_detected_at is the PRESERVED first-detection time (set once on a
21926
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21927
+ // measures from first sighting — not the latest re-scan's event, whose
21928
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21929
+ // the parent event's occurred_at defends against any legacy/edge row the
21930
+ // backfill left null.
21931
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21242
21932
  (
21243
21933
  SELECT fr.status FROM finding_resolution fr
21244
21934
  WHERE fr.finding_key = f.finding_key
@@ -21264,14 +21954,16 @@ var SqliteSecurityRepository = class {
21264
21954
  WHERE fr.finding_key = f.finding_key
21265
21955
  AND fr.resolved_at >= :windowStart
21266
21956
  )`
21267
- // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21268
- // any resolution activity at/after the window start — a row this method
21269
- // ultimately counts has its LATEST resolution inside the window, which
21270
- // implies such a row exists, so nothing wanted is dropped. The exact
21271
- // latest-wins + status/method + window gate stays in JS below,
21272
- // dialect-agnostic. Without this, a
21273
- // 7d request evaluated the store's entire trackable-findings history.
21274
- ).all({ windowStart });
21957
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21958
+ // any resolution activity at/after the window start — a row this method
21959
+ // ultimately counts has its LATEST resolution inside the window, which
21960
+ // implies such a row exists, so nothing wanted is dropped. The exact
21961
+ // latest-wins + status/method + window gate stays in JS below,
21962
+ // dialect-agnostic. Without this, a
21963
+ // 7d request evaluated the store's entire trackable-findings history.
21964
+ ),
21965
+ { windowStart }
21966
+ );
21275
21967
  const sums = /* @__PURE__ */ new Map();
21276
21968
  const counts = /* @__PURE__ */ new Map();
21277
21969
  for (const r of rows) {
@@ -21301,8 +21993,9 @@ var SqliteSecurityRepository = class {
21301
21993
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
21302
21994
  const now = this.now();
21303
21995
  const from = now - RANGE_DAYS[range] * DAY_MS4;
21304
- const rows = this.db.prepare(
21305
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21996
+ const rows = allRows(
21997
+ this.db.prepare(
21998
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21306
21999
  FROM findings f JOIN events e ON e.id = f.event_id
21307
22000
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21308
22001
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -21310,7 +22003,9 @@ var SqliteSecurityRepository = class {
21310
22003
  GROUP BY repo
21311
22004
  ORDER BY c DESC, repo
21312
22005
  LIMIT :limit`
21313
- ).all({ from, to: now, limit });
22006
+ ),
22007
+ { from, to: now, limit }
22008
+ );
21314
22009
  const items = rows.map((r) => ({
21315
22010
  id: `repo_${r.repo}`,
21316
22011
  name: r.repo,
@@ -21332,8 +22027,9 @@ var SqliteSecurityRepository = class {
21332
22027
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21333
22028
  // capped at `limit`.
21334
22029
  recentlyResolved(limit = 20) {
21335
- const rows = this.db.prepare(
21336
- `SELECT f.finding_key AS finding_key,
22030
+ const rows = allRows(
22031
+ this.db.prepare(
22032
+ `SELECT f.finding_key AS finding_key,
21337
22033
  f.rule_id AS rule_id,
21338
22034
  f.severity AS severity,
21339
22035
  json_extract(e.metadata, '$.filePath') AS path,
@@ -21367,7 +22063,9 @@ var SqliteSecurityRepository = class {
21367
22063
  ) IS NOT NULL
21368
22064
  ORDER BY latest_resolved_at DESC
21369
22065
  LIMIT :limit`
21370
- ).all({ limit });
22066
+ ),
22067
+ { limit }
22068
+ );
21371
22069
  const items = rows.map((r) => ({
21372
22070
  findingKey: r.finding_key,
21373
22071
  ruleId: r.rule_id,
@@ -21384,12 +22082,15 @@ var SqliteSecurityRepository = class {
21384
22082
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
21385
22083
  // numeric and the JS aggregations bucket/split on ms directly.
21386
22084
  findingsInRange(fromMs, toMs) {
21387
- const rows = this.db.prepare(
21388
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
22085
+ const rows = allRows(
22086
+ this.db.prepare(
22087
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21389
22088
  FROM findings f JOIN events e ON e.id = f.event_id
21390
22089
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21391
22090
  ORDER BY e.occurred_at`
21392
- ).all({ from: fromMs, to: toMs });
22091
+ ),
22092
+ { from: fromMs, to: toMs }
22093
+ );
21393
22094
  return rows.map((r) => ({
21394
22095
  occurredAt: r.occurred_at,
21395
22096
  severity: r.severity,
@@ -21403,12 +22104,7 @@ import { randomUUID as randomUUID7 } from "crypto";
21403
22104
  var KIND_ORDER = ["provider", "internal", "ip"];
21404
22105
  var CALL_SITE_EMBED_CAP = 200;
21405
22106
  function parseNetwork(networkJson) {
21406
- if (!networkJson) return null;
21407
- try {
21408
- return JSON.parse(networkJson);
21409
- } catch {
21410
- return null;
21411
- }
22107
+ return safeJson(networkJson, null);
21412
22108
  }
21413
22109
  function toEndpointSummary(row) {
21414
22110
  return {
@@ -21495,27 +22191,39 @@ var SqliteSharesRepository = class {
21495
22191
  }
21496
22192
  db;
21497
22193
  stats() {
21498
- const scalar = (sql) => this.db.prepare(sql).get()?.n ?? 0;
21499
- const destinations = scalar("SELECT count(*) AS n FROM share_destination");
21500
- const endpoints = scalar("SELECT count(*) AS n FROM share_endpoint");
21501
- const callSites = scalar("SELECT count(*) AS n FROM share_call_site");
21502
- const insecure = scalar(
22194
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
22195
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
22196
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22197
+ const insecure = countScalar(
22198
+ this.db,
21503
22199
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
21504
22200
  );
21505
- const needsReview = scalar(
22201
+ const needsReview = countScalar(
22202
+ this.db,
21506
22203
  `SELECT count(DISTINCT d.id) AS n
21507
22204
  FROM share_destination d
21508
22205
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
21509
22206
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
21510
22207
  );
21511
- const byKind = { provider: 0, internal: 0, ip: 0 };
21512
- for (const row of this.db.prepare("SELECT kind, count(*) AS n FROM share_destination GROUP BY kind").all()) {
21513
- byKind[row.kind] = row.n;
21514
- }
21515
- const byTrust = { recognized: 0, internal: 0, unverified: 0, ip: 0 };
21516
- for (const row of this.db.prepare("SELECT trust, count(*) AS n FROM share_destination GROUP BY trust").all()) {
21517
- byTrust[row.trust] = row.n;
21518
- }
22208
+ const kindCounts = countBy(
22209
+ this.db,
22210
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
22211
+ );
22212
+ const byKind = {
22213
+ provider: kindCounts.get("provider") ?? 0,
22214
+ internal: kindCounts.get("internal") ?? 0,
22215
+ ip: kindCounts.get("ip") ?? 0
22216
+ };
22217
+ const trustCounts = countBy(
22218
+ this.db,
22219
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
22220
+ );
22221
+ const byTrust = {
22222
+ recognized: trustCounts.get("recognized") ?? 0,
22223
+ internal: trustCounts.get("internal") ?? 0,
22224
+ unverified: trustCounts.get("unverified") ?? 0,
22225
+ ip: trustCounts.get("ip") ?? 0
22226
+ };
21519
22227
  return Promise.resolve({
21520
22228
  destinations,
21521
22229
  endpoints,
@@ -21631,7 +22339,7 @@ var SqliteSharesRepository = class {
21631
22339
  }
21632
22340
  let sql;
21633
22341
  if (q) {
21634
- const pattern = `%${escapeLikePattern(q)}%`;
22342
+ const pattern = containsPattern(q);
21635
22343
  conditions.push(
21636
22344
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
21637
22345
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -21651,24 +22359,31 @@ var SqliteSharesRepository = class {
21651
22359
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
21652
22360
  ORDER BY d.created_at ASC, d.id ASC`;
21653
22361
  }
21654
- const rows = this.db.prepare(sql).all(...params);
22362
+ const rows = allRows(
22363
+ this.db.prepare(sql),
22364
+ params
22365
+ );
21655
22366
  return rows.map((r) => this.mapDestRow(r));
21656
22367
  }
21657
22368
  fetchDestinationById(destinationId) {
21658
- const row = this.db.prepare(
21659
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22369
+ const row = getRow(
22370
+ this.db.prepare(
22371
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
21660
22372
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
21661
22373
  o.decision AS overrideDecision
21662
22374
  FROM share_destination d
21663
22375
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
21664
22376
  WHERE d.id = ?`
21665
- ).get(destinationId);
22377
+ ),
22378
+ [destinationId]
22379
+ );
21666
22380
  return row ? this.mapDestRow(row) : null;
21667
22381
  }
21668
22382
  fetchEndpoints(destinationIds) {
21669
22383
  if (destinationIds.length === 0) return [];
21670
- const rows = this.db.prepare(
21671
- `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
22384
+ const rows = allRows(
22385
+ this.db.prepare(
22386
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
21672
22387
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
21673
22388
  count(c.id) AS callSiteCount
21674
22389
  FROM share_endpoint e
@@ -21676,7 +22391,9 @@ var SqliteSharesRepository = class {
21676
22391
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
21677
22392
  GROUP BY e.id
21678
22393
  ORDER BY e.created_at ASC, e.id ASC`
21679
- ).all(...destinationIds);
22394
+ ),
22395
+ destinationIds
22396
+ );
21680
22397
  return rows.map((r) => ({
21681
22398
  id: r.id,
21682
22399
  destinationId: r.destinationId,
@@ -21701,13 +22418,16 @@ var SqliteSharesRepository = class {
21701
22418
  }
21702
22419
  fetchCallSites(endpointIds) {
21703
22420
  if (endpointIds.length === 0) return [];
21704
- const rows = this.db.prepare(
21705
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22421
+ const rows = allRows(
22422
+ this.db.prepare(
22423
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
21706
22424
  project_id AS projectId
21707
22425
  FROM share_call_site
21708
22426
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
21709
22427
  ORDER BY created_at ASC, id ASC`
21710
- ).all(...endpointIds);
22428
+ ),
22429
+ endpointIds
22430
+ );
21711
22431
  return rows.map((r) => ({
21712
22432
  id: r.id,
21713
22433
  endpointId: r.endpointId,
@@ -21743,27 +22463,36 @@ var SqliteSourceProjectRepository = class {
21743
22463
  upsert(input, now = Date.now()) {
21744
22464
  const id = sourceProjectId(input.url);
21745
22465
  const row = toSourceProjectRow(input, id, now);
21746
- this.upsertStmt.run({
21747
- id: row.id,
21748
- url: row.url ?? null,
21749
- name: row.name ?? null,
21750
- attributes: row.attributes,
21751
- firstSeen: row.firstSeen,
21752
- lastSeen: row.lastSeen
21753
- });
22466
+ this.upsertStmt.run(
22467
+ bindParams({
22468
+ id: row.id,
22469
+ url: row.url,
22470
+ name: row.name,
22471
+ attributes: row.attributes,
22472
+ firstSeen: row.firstSeen,
22473
+ lastSeen: row.lastSeen
22474
+ })
22475
+ );
21754
22476
  return id;
21755
22477
  }
21756
22478
  findById(id) {
21757
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22479
+ return getRow(
22480
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22481
+ {
22482
+ id
22483
+ }
22484
+ );
21758
22485
  }
21759
22486
  // Distinct project names — a filter facet, served from the source_project
21760
22487
  // table, never from the audit fact table.
21761
22488
  distinctNames() {
21762
- const rows = this.db.prepare(
21763
- `SELECT DISTINCT name FROM source_project
22489
+ const rows = allRows(
22490
+ this.db.prepare(
22491
+ `SELECT DISTINCT name FROM source_project
21764
22492
  WHERE name IS NOT NULL
21765
22493
  ORDER BY name`
21766
- ).all();
22494
+ )
22495
+ );
21767
22496
  return rows.map((r) => r.name);
21768
22497
  }
21769
22498
  };
@@ -21782,8 +22511,7 @@ function hasLegacySampleRows(db) {
21782
22511
  function purgeSampleData(db) {
21783
22512
  try {
21784
22513
  if (!hasLegacySampleRows(db)) return;
21785
- db.exec("BEGIN");
21786
- try {
22514
+ withTransaction(db, () => {
21787
22515
  db.exec(
21788
22516
  `DELETE FROM share_call_site WHERE endpoint_id IN (
21789
22517
  SELECT e.id FROM share_endpoint e
@@ -21828,11 +22556,7 @@ function purgeSampleData(db) {
21828
22556
  value TEXT NOT NULL
21829
22557
  )`);
21830
22558
  db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
21831
- db.exec("COMMIT");
21832
- } catch (err) {
21833
- db.exec("ROLLBACK");
21834
- throw err;
21835
- }
22559
+ });
21836
22560
  } catch {
21837
22561
  }
21838
22562
  }
@@ -21851,7 +22575,7 @@ function openWithPragmas(file2) {
21851
22575
  function backupLegacyStore(file2) {
21852
22576
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
21853
22577
  renameSync(file2, backup);
21854
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
22578
+ for (const sidecar of walSidecars(file2)) {
21855
22579
  if (existsSync(sidecar)) rmSync(sidecar);
21856
22580
  }
21857
22581
  return backup;
@@ -21864,9 +22588,8 @@ function openLocalDatabase(dir) {
21864
22588
  db.close();
21865
22589
  const backup = backupLegacyStore(file2);
21866
22590
  db = openWithPragmas(file2);
21867
- process.stderr.write(
21868
- `[aka] Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.
21869
- `
22591
+ akaWarn(
22592
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
21870
22593
  );
21871
22594
  }
21872
22595
  applyMigrations(db);
@@ -21894,96 +22617,77 @@ function openLocalDatabase(dir) {
21894
22617
  const configInventory = new SqliteConfigInventoryRepository(db);
21895
22618
  policies.seedDefaults();
21896
22619
  function recordCapture(event, detected) {
21897
- try {
21898
- db.exec("BEGIN");
21899
- try {
21900
- events.insertEvent(event);
21901
- const sessionId = event.metadata?.sessionId;
21902
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
21903
- db.exec("COMMIT");
21904
- } catch (err) {
21905
- db.exec("ROLLBACK");
21906
- throw err;
21907
- }
21908
- } catch {
21909
- }
22620
+ failOpenTransaction(db, () => {
22621
+ events.insertEvent(event);
22622
+ const sessionId = event.metadata?.sessionId;
22623
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
22624
+ });
21910
22625
  }
21911
22626
  function ensureInventory(ctx) {
21912
22627
  const resolved = {};
21913
- try {
21914
- db.exec("BEGIN");
21915
- try {
21916
- const now = Date.now();
21917
- if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
21918
- if (ctx.harness) {
21919
- resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
21920
- }
21921
- resolved.accountId = inventory.upsert(
21922
- linkHost(
21923
- {
21924
- objectType: "user",
21925
- identityKey: "local",
21926
- attributes: { source: "local" }
21927
- },
21928
- resolved.hostId
21929
- ),
21930
- now
21931
- );
21932
- if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
21933
- db.exec("COMMIT");
21934
- } catch (err) {
21935
- db.exec("ROLLBACK");
21936
- throw err;
21937
- }
21938
- } catch {
21939
- return {};
21940
- }
21941
- return resolved;
22628
+ const committed = failOpenTransaction(db, () => {
22629
+ const now = Date.now();
22630
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22631
+ if (ctx.harness) {
22632
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
22633
+ }
22634
+ resolved.accountId = inventory.upsert(
22635
+ linkHost(
22636
+ {
22637
+ objectType: "user",
22638
+ identityKey: "local",
22639
+ attributes: { source: "local" }
22640
+ },
22641
+ resolved.hostId
22642
+ ),
22643
+ now
22644
+ );
22645
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22646
+ });
22647
+ return committed ? resolved : {};
21942
22648
  }
21943
22649
  function recordConfigScan(record2) {
21944
- try {
21945
- db.exec("BEGIN");
21946
- try {
21947
- const now = isoToEpochMillis(record2.scanEvent.startedAt);
21948
- for (const item of record2.items) inventory.upsert(item, now);
21949
- auditEvents.insertAuditEvent(record2.scanEvent);
21950
- const definitionIds = /* @__PURE__ */ new Map();
21951
- for (const def of record2.definitions ?? []) {
21952
- definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
21953
- }
21954
- for (const finding of record2.findings ?? []) {
21955
- const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
21956
- if (!definitionId) continue;
21957
- inspectionFindings.insertFinding({
21958
- id: randomUUID8(),
21959
- auditEventId: record2.scanEvent.id,
21960
- inspectionDefinitionId: definitionId,
21961
- span: finding.span,
21962
- maskedMatch: finding.maskedMatch,
21963
- actionTaken: finding.actionTaken,
21964
- confidence: finding.confidence
21965
- });
21966
- }
21967
- db.exec("COMMIT");
21968
- } catch (err) {
21969
- db.exec("ROLLBACK");
21970
- throw err;
22650
+ failOpenTransaction(db, () => {
22651
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
22652
+ for (const item of record2.items) inventory.upsert(item, now);
22653
+ auditEvents.insertAuditEvent(record2.scanEvent);
22654
+ const definitionIds = /* @__PURE__ */ new Map();
22655
+ for (const def of record2.definitions ?? []) {
22656
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22657
+ }
22658
+ for (const finding of record2.findings ?? []) {
22659
+ const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
22660
+ if (!definitionId) continue;
22661
+ inspectionFindings.insertFinding({
22662
+ id: randomUUID8(),
22663
+ auditEventId: record2.scanEvent.id,
22664
+ inspectionDefinitionId: definitionId,
22665
+ span: finding.span,
22666
+ maskedMatch: finding.maskedMatch,
22667
+ actionTaken: finding.actionTaken,
22668
+ confidence: finding.confidence
22669
+ });
21971
22670
  }
21972
- } catch {
21973
- }
22671
+ });
21974
22672
  }
21975
22673
  function recordProjectFiles(projectId, scan2) {
21976
22674
  if (scan2.files.length === 0) return;
22675
+ failOpenTransaction(db, () => {
22676
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
22677
+ });
22678
+ }
22679
+ async function transaction(fn) {
22680
+ db.exec("BEGIN");
21977
22681
  try {
21978
- db.exec("BEGIN");
22682
+ const result = await fn();
22683
+ db.exec("COMMIT");
22684
+ return result;
22685
+ } catch (err) {
21979
22686
  try {
21980
- projectFiles.replaceForProject(projectId, scan2, Date.now());
21981
- db.exec("COMMIT");
21982
- } catch (err) {
21983
22687
  db.exec("ROLLBACK");
21984
- throw err;
22688
+ } catch {
21985
22689
  }
21986
- } catch {
22690
+ throw err;
21987
22691
  }
21988
22692
  }
21989
22693
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -22002,8 +22706,7 @@ function openLocalDatabase(dir) {
22002
22706
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
22003
22707
  });
22004
22708
  if (stale.length === 0) return;
22005
- db.exec("BEGIN");
22006
- try {
22709
+ withTransaction(db, () => {
22007
22710
  for (const { id } of stale) {
22008
22711
  db.prepare(
22009
22712
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -22015,11 +22718,7 @@ function openLocalDatabase(dir) {
22015
22718
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
22016
22719
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
22017
22720
  }
22018
- db.exec("COMMIT");
22019
- } catch (err) {
22020
- db.exec("ROLLBACK");
22021
- throw err;
22022
- }
22721
+ });
22023
22722
  } catch {
22024
22723
  }
22025
22724
  }
@@ -22061,6 +22760,7 @@ function openLocalDatabase(dir) {
22061
22760
  purgeSampleData: () => {
22062
22761
  purgeSampleData(db);
22063
22762
  },
22763
+ transaction,
22064
22764
  close: () => {
22065
22765
  db.close();
22066
22766
  }
@@ -22153,12 +22853,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
22153
22853
  }
22154
22854
  }
22155
22855
  function readJson(file2) {
22856
+ let text;
22156
22857
  try {
22157
- const parsed = JSON.parse(readFileSync2(file2, "utf8"));
22158
- return typeof parsed === "object" && parsed !== null ? parsed : null;
22858
+ text = readFileSync2(file2, "utf8");
22159
22859
  } catch {
22160
22860
  return null;
22161
22861
  }
22862
+ return parseJsonObject(text) ?? null;
22863
+ }
22864
+
22865
+ // ../../packages/persistence/src/warn-era-cap.ts
22866
+ import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
22867
+ import { join as join5 } from "path";
22868
+ var MARKER = "warn-era-capped";
22869
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22870
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
22871
+ const marker = join5(dataDir2, MARKER);
22872
+ if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22873
+ const capped = db.policies.capCategoryActions();
22874
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
22875
+ `, { mode: DATA_FILE_MODE });
22876
+ return { capped };
22162
22877
  }
22163
22878
 
22164
22879
  // ../../packages/plugin-sdk/src/provider-env.ts
@@ -22233,23 +22948,30 @@ function resolveProviderSafe() {
22233
22948
  // ../../packages/plugin-sdk/src/config-inventory.ts
22234
22949
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22235
22950
  import { homedir as homedir2 } from "os";
22236
- import { basename as basename2, join as join6 } from "path";
22951
+ import { basename as basename2, join as join7 } from "path";
22952
+
22953
+ // ../../packages/detections/src/escape-regexp.ts
22954
+ function escapeRegExp(value) {
22955
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22956
+ }
22957
+
22958
+ // ../../packages/detections/src/matchers/limits.ts
22959
+ var MAX_MATCHES_PER_RULE = 1e4;
22237
22960
 
22238
22961
  // ../../packages/detections/src/matchers/keyword.ts
22239
22962
  var KeywordMatcher2 = class {
22240
22963
  match(text, rule) {
22241
22964
  if (rule.matcher.type !== "keyword") return [];
22242
22965
  const { keywords, caseSensitive } = rule.matcher;
22243
- const haystack = caseSensitive ? text : text.toLowerCase();
22244
22966
  const spans = [];
22245
22967
  for (const kw of keywords) {
22246
- const needle = caseSensitive ? kw : kw.toLowerCase();
22247
- let idx = 0;
22248
- while (idx < haystack.length) {
22249
- const pos = haystack.indexOf(needle, idx);
22250
- if (pos === -1) break;
22251
- spans.push({ start: pos, end: pos + kw.length });
22252
- idx = pos + 1;
22968
+ if (kw.length === 0) continue;
22969
+ if (spans.length >= MAX_MATCHES_PER_RULE) break;
22970
+ const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
22971
+ let m;
22972
+ while ((m = re.exec(text)) !== null) {
22973
+ spans.push({ start: m.index, end: m.index + m[0].length });
22974
+ if (spans.length >= MAX_MATCHES_PER_RULE) break;
22253
22975
  }
22254
22976
  }
22255
22977
  return spans;
@@ -22257,7 +22979,6 @@ var KeywordMatcher2 = class {
22257
22979
  };
22258
22980
 
22259
22981
  // ../../packages/detections/src/matchers/regex.ts
22260
- var MAX_MATCHES_PER_RULE = 1e4;
22261
22982
  var RegexMatcher2 = class {
22262
22983
  match(text, rule) {
22263
22984
  if (rule.matcher.type !== "regex") return [];
@@ -22930,7 +23651,6 @@ var db_table_name_default = {
22930
23651
  "SELECT * FROM ",
22931
23652
  "SELECT COUNT(*) FROM ",
22932
23653
  "INSERT INTO ",
22933
- "UPDATE ",
22934
23654
  "DELETE FROM ",
22935
23655
  "CREATE TABLE ",
22936
23656
  "ALTER TABLE ",
@@ -24367,8 +25087,8 @@ function bundledDetections() {
24367
25087
  }
24368
25088
 
24369
25089
  // ../../packages/plugin-sdk/src/repo.ts
24370
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
24371
- import { basename, dirname, isAbsolute, join as join5, sep as sep2 } from "path";
25090
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25091
+ import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
24372
25092
 
24373
25093
  // ../../packages/plugin-sdk/src/events.ts
24374
25094
  import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
@@ -24380,20 +25100,23 @@ import { createHash as createHash4 } from "crypto";
24380
25100
  import { arch, hostname as hostname3, platform, release } from "os";
24381
25101
 
24382
25102
  // ../../packages/plugin-sdk/src/nudge.ts
24383
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
24384
- import { join as join7 } from "path";
25103
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25104
+ import { join as join8 } from "path";
24385
25105
 
24386
25106
  // ../../packages/plugin-sdk/src/project-files.ts
24387
25107
  var import_ignore = __toESM(require_ignore(), 1);
24388
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
24389
- import { basename as basename3, join as join8, relative, sep as sep3 } from "path";
25108
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25109
+ import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
24390
25110
 
24391
25111
  // ../../packages/plugin-sdk/src/runtime.ts
24392
25112
  import { randomUUID as randomUUID10 } from "crypto";
24393
25113
 
25114
+ // ../../packages/plugin-sdk/src/suppressions.ts
25115
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
25116
+
24394
25117
  // ../../packages/plugin-sdk/src/throttle.ts
24395
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
24396
- import { join as join9 } from "path";
25118
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
25119
+ import { join as join10 } from "path";
24397
25120
 
24398
25121
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
24399
25122
  import { randomUUID as randomUUID11 } from "crypto";
@@ -24586,6 +25309,14 @@ var StandaloneDataGateway = class {
24586
25309
  sweepTerminalExceptions(retentionMs) {
24587
25310
  return this.db.exceptions.sweepTerminal(retentionMs);
24588
25311
  }
25312
+ // The warn-era enforcement cap, standalone-only store maintenance invoked
25313
+ // from SessionStart, not part of the DataGateway port. Returns the number
25314
+ // of block/redact rows capped to warn (0 for a redact-policy store or an
25315
+ // already-capped one).
25316
+ capWarnEraEnforcement(policyMode) {
25317
+ const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
25318
+ return { capped };
25319
+ }
24589
25320
  // One project-file scan → the local project_file tree (one transaction inside
24590
25321
  // the LocalDatabase, fail-open there). Like the sweep above, this is
24591
25322
  // NOT part of the DataGateway port: the file tree is a local-store read model.
@@ -24798,6 +25529,11 @@ function fenced(body) {
24798
25529
  return [fence, body, fence].join("\n");
24799
25530
  }
24800
25531
 
25532
+ // src/command-registry.ts
25533
+ import { readdirSync as readdirSync3 } from "fs";
25534
+ import { fileURLToPath } from "url";
25535
+ var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
25536
+
24801
25537
  // src/render.ts
24802
25538
  var SEVERITY_WEIGHT = { critical: 4, high: 3, medium: 2, low: 1 };
24803
25539
  var SEVERITY_GLYPH = {
@@ -24825,6 +25561,7 @@ function shortTime(iso) {
24825
25561
  function empty(message) {
24826
25562
  return message;
24827
25563
  }
25564
+ var CATEGORY_ORDER2 = DetectionCategory.options;
24828
25565
  function healthScore(summary) {
24829
25566
  const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
24830
25567
  const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
@@ -24841,7 +25578,7 @@ function renderFindings(findings, status, severity) {
24841
25578
  `${severityGlyph(f.severity)} ${f.severity}`,
24842
25579
  f.category,
24843
25580
  f.ruleId,
24844
- f.actionTaken,
25581
+ toApiAction(f.actionTaken),
24845
25582
  f.maskedMatch
24846
25583
  ]);
24847
25584
  const heading = severity !== void 0 ? `\u25CF Recent ${severity} findings (${String(findings.length)})` : `\u25CF Recent findings (${String(findings.length)})`;
@@ -25047,7 +25784,7 @@ function renderAudit(findings) {
25047
25784
  }
25048
25785
  const rows = findings.map((f) => [
25049
25786
  shortTime(f.occurredAt),
25050
- f.actionTaken,
25787
+ toApiAction(f.actionTaken),
25051
25788
  f.ruleId,
25052
25789
  f.category,
25053
25790
  // Join only the parts that are present so a finding missing sourceTool/kind