@akasecurity/ai-tc-claude-code 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -494,9 +494,13 @@ var require_ignore = __commonJS({
494
494
  // src/hooks/session-start.ts
495
495
  import { readFileSync as readFileSync8 } from "fs";
496
496
 
497
+ // ../../packages/plugin-sdk/src/config.ts
498
+ import { existsSync as existsSync3 } from "fs";
499
+ import { join as join6 } from "path";
500
+
497
501
  // ../../packages/persistence/src/database.ts
498
502
  import { randomUUID as randomUUID8 } from "crypto";
499
- import { existsSync, renameSync, rmSync } from "fs";
503
+ import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
500
504
  import { join, sep } from "path";
501
505
  import { DatabaseSync } from "node:sqlite";
502
506
 
@@ -545,6 +549,22 @@ var SQLITE_MIGRATIONS = [
545
549
  {
546
550
  tag: "0010_events_session_expression_index",
547
551
  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"
552
+ },
553
+ {
554
+ tag: "0011_egress_writer",
555
+ sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
556
+ },
557
+ {
558
+ tag: "0012_handy_the_captain",
559
+ sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
560
+ },
561
+ {
562
+ tag: "0013_legacy_history_backfill_support",
563
+ sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
564
+ },
565
+ {
566
+ tag: "0014_drop_legacy_events_findings",
567
+ sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
548
568
  }
549
569
  ];
550
570
 
@@ -15375,7 +15395,12 @@ var FindingFacets = external_exports.object({
15375
15395
  severity: external_exports.array(FindingFacetItem),
15376
15396
  subtype: external_exports.array(FindingFacetItem),
15377
15397
  provider: external_exports.array(FindingFacetItem),
15378
- action: external_exports.array(FindingFacetItem)
15398
+ action: external_exports.array(FindingFacetItem),
15399
+ // Counts by the group's derived status. The SQLite store derives a status
15400
+ // for every instance, so every group lands in a bucket; a status-less
15401
+ // group (possible only for callers whose rows carry no statuses) is
15402
+ // counted under no value.
15403
+ status: external_exports.array(FindingFacetItem)
15379
15404
  }).meta({ id: "FindingFacets" });
15380
15405
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
15381
15406
  var ListGroupedFindingsQuery = external_exports.object({
@@ -15385,6 +15410,10 @@ var ListGroupedFindingsQuery = external_exports.object({
15385
15410
  subtype: external_exports.array(external_exports.string()).optional(),
15386
15411
  provider: external_exports.array(FindingProvider).optional(),
15387
15412
  action: external_exports.array(FindingAction).optional(),
15413
+ // Matches a group's DERIVED status (see FindingGroup.status), not its
15414
+ // individual instances' — so a filtered group's Status column always reads
15415
+ // one of the requested values.
15416
+ status: external_exports.array(FindingStatus).optional(),
15388
15417
  q: external_exports.string().optional(),
15389
15418
  // Scope to findings whose event carries this session id (the Activity page's
15390
15419
  // session → findings drilldown). Findings without a session never match.
@@ -15575,6 +15604,33 @@ var ToolCallAttributes = external_exports.object({
15575
15604
  parent_uuid: external_exports.string().optional(),
15576
15605
  run_key: external_exports.string().optional()
15577
15606
  }).catchall(external_exports.unknown());
15607
+ var CaptureAttributes = external_exports.object({
15608
+ // The harness/tool that produced the capture (`claude-code`, `cli`, …). A
15609
+ // column on the legacy `events` table; here it rides the bag because a
15610
+ // capture-typed audit row has no equivalent column of its own.
15611
+ source_tool: external_exports.string().optional(),
15612
+ file_path: external_exports.string().optional(),
15613
+ repo: external_exports.string().optional(),
15614
+ // The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
15615
+ // gives a non-file capture a display location ("via Bash") when file_path
15616
+ // is absent. The tool NAME only, never its arguments/output.
15617
+ tool_name: external_exports.string().optional(),
15618
+ // Presence-only provenance flag: set when the file is excluded by the
15619
+ // repo's .gitignore. Omitted (not false) for tracked files.
15620
+ gitignored: external_exports.boolean().optional(),
15621
+ // Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
15622
+ // reading from disk), never a partial fragment (a hook-captured edit).
15623
+ whole_file: external_exports.boolean().optional(),
15624
+ // Distributed-tracing correlation: `correlation_id` ties the capture back to
15625
+ // the request that produced it; `trace_id` is the originating span's W3C
15626
+ // trace id when telemetry is enabled.
15627
+ correlation_id: external_exports.uuid().optional(),
15628
+ trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
15629
+ // Ids of the detection exceptions that downgraded findings in this capture
15630
+ // to 'allow' — the enforcement audit trail's link back to the grant that
15631
+ // authorized the bypass.
15632
+ exception_ids: external_exports.array(external_exports.guid()).optional()
15633
+ }).catchall(external_exports.unknown());
15578
15634
  var ToolCallInspection = external_exports.object({
15579
15635
  ruleId: external_exports.string().min(1),
15580
15636
  ruleName: external_exports.string(),
@@ -15661,7 +15717,18 @@ var InspectionFindingInput = external_exports.object({
15661
15717
  span: Span,
15662
15718
  maskedMatch: external_exports.string(),
15663
15719
  actionTaken: ActionTaken,
15664
- confidence: external_exports.number().min(0).max(1)
15720
+ confidence: external_exports.number().min(0).max(1),
15721
+ // Stable, content-addressed key correlating this finding across re-detections
15722
+ // — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
15723
+ // its unique index). Optional: only an at-rest/re-scannable finding carries
15724
+ // one; an in-flight capture (prompt/response) has nothing to re-detect
15725
+ // against and leaves it unset, so every insert is a fresh row.
15726
+ findingKey: external_exports.string().optional(),
15727
+ // The ORIGINAL detection time, preserved across a later re-detection of the
15728
+ // same findingKey — mirrors the legacy `findings.first_detected_at`.
15729
+ // Optional: when omitted, the writer derives it from the referenced audit
15730
+ // event's startedAt on first insert (see SqliteInspectionFindingsRepository).
15731
+ firstDetectedAt: external_exports.iso.datetime().optional()
15665
15732
  });
15666
15733
  var InventoryContext = external_exports.object({
15667
15734
  host: InventoryInput.optional(),
@@ -15863,6 +15930,7 @@ var ActivityOverviewResponse = external_exports.object({
15863
15930
 
15864
15931
  // ../../packages/schema/src/zod/event.ts
15865
15932
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15933
+ var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
15866
15934
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15867
15935
  var EventMetadata = external_exports.object({
15868
15936
  sessionId: external_exports.string().optional(),
@@ -16203,6 +16271,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16203
16271
 
16204
16272
  // ../../packages/schema/src/zod/rule.ts
16205
16273
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16274
+ var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
16206
16275
  var KeywordMatcher = external_exports.object({
16207
16276
  type: external_exports.literal("keyword"),
16208
16277
  // An empty keyword matches at every position, yielding one zero-length span
@@ -16227,9 +16296,10 @@ function matchesEmptyString(pattern, flags) {
16227
16296
  return false;
16228
16297
  }
16229
16298
  }
16299
+ var MAX_PATTERN_LENGTH = 2e3;
16230
16300
  var RegexMatcher = external_exports.object({
16231
16301
  type: external_exports.literal("regex"),
16232
- pattern: external_exports.string(),
16302
+ pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
16233
16303
  flags: external_exports.string().default("gi"),
16234
16304
  captureGroup: external_exports.number().int().nonnegative().optional()
16235
16305
  }).refine((v) => isValidRegex(v.pattern, v.flags), {
@@ -16350,6 +16420,12 @@ var PolicyBundle = external_exports.object({
16350
16420
  // on-disk caches — that omit the field still parse; consumers read
16351
16421
  // `bundle.exceptions ?? []`.
16352
16422
  exceptions: external_exports.array(ExceptionBundleEntry).optional(),
16423
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
16424
+ // from a versioned installed pack. Optional so older backends — and older
16425
+ // on-disk caches — that omit the field still parse; consumers fall back to
16426
+ // the rule's own spec version. NOT the bundle version above — see
16427
+ // installedRuleset's ruleVersions for the source of truth.
16428
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
16353
16429
  customKeywords: external_exports.array(external_exports.string()),
16354
16430
  fetchedAt: external_exports.iso.datetime()
16355
16431
  }).meta({ id: "PolicyBundle" });
@@ -16910,6 +16986,212 @@ function buildDetectionsList(summaries, query) {
16910
16986
  return { counts, items: filtered.map(summaryToDetectionListItem) };
16911
16987
  }
16912
16988
 
16989
+ // ../../packages/schema/src/zod/shares.ts
16990
+ var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
16991
+ var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
16992
+ var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
16993
+ var DATA_CLASS_ORDER = DataClass.options;
16994
+ var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
16995
+ var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
16996
+ var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
16997
+ var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
16998
+ var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
16999
+ var ReviewInfo = external_exports.object({
17000
+ needsReview: external_exports.boolean(),
17001
+ reasons: external_exports.array(ReviewReason)
17002
+ }).meta({ id: "ReviewInfo" });
17003
+ var DestinationNetwork = external_exports.object({
17004
+ port: external_exports.number().int().nullable(),
17005
+ geo: external_exports.string().nullable(),
17006
+ ptr: external_exports.string().nullable()
17007
+ }).meta({ id: "DestinationNetwork" });
17008
+ var EndpointSummary = external_exports.object({
17009
+ id: external_exports.string(),
17010
+ method: HttpMethod,
17011
+ transport: Transport,
17012
+ url: external_exports.string(),
17013
+ template: external_exports.boolean(),
17014
+ dataClass: DataClass,
17015
+ lastSeen: external_exports.iso.datetime(),
17016
+ callSiteCount: external_exports.number().int().nonnegative()
17017
+ }).meta({ id: "EndpointSummary" });
17018
+ var CallSite = external_exports.object({
17019
+ id: external_exports.string(),
17020
+ project: external_exports.string(),
17021
+ file: external_exports.string(),
17022
+ line: external_exports.number().int().nonnegative(),
17023
+ snippet: external_exports.string(),
17024
+ dynamic: external_exports.boolean(),
17025
+ vendored: external_exports.boolean(),
17026
+ /** Deep-link to the Inventory project, when the repo is governed there. */
17027
+ projectId: external_exports.string().nullable()
17028
+ }).meta({ id: "CallSite" });
17029
+ var EndpointWithSites = EndpointSummary.extend({
17030
+ sites: external_exports.array(CallSite)
17031
+ }).meta({ id: "EndpointWithSites" });
17032
+ var ShareDestinationSummary = external_exports.object({
17033
+ id: external_exports.string(),
17034
+ kind: DestinationKind,
17035
+ name: external_exports.string(),
17036
+ host: external_exports.string(),
17037
+ category: external_exports.string(),
17038
+ trust: ShareTrustLevel,
17039
+ /** Effective state (decision applied over the trust default). */
17040
+ status: EgressStatus,
17041
+ /** True when an egress decision override differs from the trust default. */
17042
+ isCustom: external_exports.boolean(),
17043
+ lastSeen: external_exports.iso.datetime(),
17044
+ endpointCount: external_exports.number().int().nonnegative(),
17045
+ callSiteCount: external_exports.number().int().nonnegative(),
17046
+ transports: external_exports.array(Transport),
17047
+ /** Most-sensitive first. */
17048
+ dataClasses: external_exports.array(DataClass),
17049
+ review: ReviewInfo,
17050
+ /** Non-provider hosts only; null for providers. */
17051
+ network: DestinationNetwork.nullable(),
17052
+ /** Embedded for inline expansion — no call sites here. */
17053
+ endpoints: external_exports.array(EndpointSummary)
17054
+ }).meta({ id: "ShareDestinationSummary" });
17055
+ var ShareDestinationDetail = ShareDestinationSummary.omit({
17056
+ endpointCount: true,
17057
+ callSiteCount: true,
17058
+ endpoints: true
17059
+ }).extend({
17060
+ /** Ownership/geo rationale; null for providers. */
17061
+ note: external_exports.string().nullable(),
17062
+ endpoints: external_exports.array(EndpointWithSites)
17063
+ }).meta({ id: "ShareDestinationDetail" });
17064
+ var ReviewDestination = external_exports.object({
17065
+ id: external_exports.string(),
17066
+ kind: DestinationKind,
17067
+ name: external_exports.string(),
17068
+ /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
17069
+ host: external_exports.string(),
17070
+ trust: ShareTrustLevel,
17071
+ status: EgressStatus,
17072
+ review: ReviewInfo,
17073
+ topDataClass: DataClass,
17074
+ callSiteCount: external_exports.number().int().nonnegative(),
17075
+ lastSeen: external_exports.iso.datetime()
17076
+ }).meta({ id: "ReviewDestination" });
17077
+ var ShareDestinationGroup = external_exports.object({
17078
+ kind: DestinationKind,
17079
+ total: external_exports.number().int().nonnegative(),
17080
+ items: external_exports.array(ShareDestinationSummary)
17081
+ }).meta({ id: "ShareDestinationGroup" });
17082
+ var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
17083
+ var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
17084
+ var SharesStats = external_exports.object({
17085
+ destinations: external_exports.number().int().nonnegative(),
17086
+ endpoints: external_exports.number().int().nonnegative(),
17087
+ callSites: external_exports.number().int().nonnegative(),
17088
+ needsReview: external_exports.number().int().nonnegative(),
17089
+ insecure: external_exports.number().int().nonnegative(),
17090
+ byKind: external_exports.object({
17091
+ provider: external_exports.number().int().nonnegative(),
17092
+ internal: external_exports.number().int().nonnegative(),
17093
+ external: external_exports.number().int().nonnegative(),
17094
+ ip: external_exports.number().int().nonnegative()
17095
+ }),
17096
+ byTrust: external_exports.object({
17097
+ recognized: external_exports.number().int().nonnegative(),
17098
+ internal: external_exports.number().int().nonnegative(),
17099
+ unverified: external_exports.number().int().nonnegative(),
17100
+ ip: external_exports.number().int().nonnegative()
17101
+ })
17102
+ }).meta({ id: "SharesStats" });
17103
+ var SetEgressDecisionBody = external_exports.object({
17104
+ /** `null` clears the override — reverts to the trust default, isCustom false. */
17105
+ decision: EgressDecision.nullable()
17106
+ }).meta({ id: "SetEgressDecisionBody" });
17107
+ var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
17108
+ var ListShareDestinationsQuery = external_exports.object({
17109
+ /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
17110
+ q: external_exports.string().optional(),
17111
+ /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
17112
+ kind: external_exports.array(DestinationKind).optional(),
17113
+ /** Reserved for future grouping modes; only 'destination' is supported today. */
17114
+ groupBy: external_exports.enum(["destination"]).default("destination"),
17115
+ /**
17116
+ * When true, return a flat severity-ordered `items[]` instead of `groups`.
17117
+ * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17118
+ * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17119
+ * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17120
+ */
17121
+ review: external_exports.stringbool().default(false)
17122
+ });
17123
+ var ExportSharesQuery = external_exports.object({
17124
+ format: external_exports.enum(["csv", "json"]).default("csv"),
17125
+ q: external_exports.string().optional(),
17126
+ kind: external_exports.array(DestinationKind).optional()
17127
+ });
17128
+
17129
+ // ../../packages/schema/src/zod/egress-extraction.ts
17130
+ var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
17131
+ var ProviderRegistryEntry = external_exports.object({
17132
+ id: external_exports.string(),
17133
+ name: external_exports.string(),
17134
+ category: external_exports.string(),
17135
+ /** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
17136
+ hostSuffixes: external_exports.array(external_exports.string()).min(1),
17137
+ /** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
17138
+ apiBase: external_exports.string(),
17139
+ /** Most-sensitive first; index 0 becomes the endpoint dataClass. */
17140
+ defaultDataClasses: external_exports.array(DataClass).min(1),
17141
+ /** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
17142
+ sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
17143
+ }).meta({ id: "ProviderRegistryEntry" });
17144
+ var EgressCallSiteHit = external_exports.object({
17145
+ file: external_exports.string(),
17146
+ line: external_exports.number().int().positive(),
17147
+ snippet: external_exports.string(),
17148
+ dynamic: external_exports.boolean(),
17149
+ vendored: external_exports.boolean()
17150
+ }).meta({ id: "EgressCallSiteHit" });
17151
+ var ResolvedEgressHit = external_exports.object({
17152
+ host: external_exports.string(),
17153
+ kind: DestinationKind,
17154
+ name: external_exports.string(),
17155
+ category: external_exports.string(),
17156
+ trust: ShareTrustLevel,
17157
+ network: DestinationNetwork.nullable(),
17158
+ method: HttpMethod,
17159
+ transport: Transport,
17160
+ url: external_exports.string(),
17161
+ template: external_exports.boolean(),
17162
+ dataClass: DataClass,
17163
+ site: EgressCallSiteHit
17164
+ }).meta({ id: "ResolvedEgressHit" });
17165
+ var EgressReconcile = external_exports.discriminatedUnion("mode", [
17166
+ external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
17167
+ external_exports.object({
17168
+ mode: external_exports.literal("ledger"),
17169
+ scannedFiles: external_exports.array(external_exports.string()),
17170
+ deletedFiles: external_exports.array(external_exports.string())
17171
+ })
17172
+ ]).meta({ id: "EgressReconcile" });
17173
+ var RecordProjectEgressInput = external_exports.object({
17174
+ /** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
17175
+ projectKey: external_exports.string().min(1),
17176
+ /** Display name only — never keys reconciliation. */
17177
+ project: external_exports.string(),
17178
+ projectId: external_exports.string().nullable(),
17179
+ reconcile: EgressReconcile,
17180
+ hits: external_exports.array(ResolvedEgressHit)
17181
+ }).meta({ id: "RecordProjectEgressInput" });
17182
+ var EgressWriteSummary = external_exports.object({
17183
+ destinations: external_exports.number().int().nonnegative(),
17184
+ endpoints: external_exports.number().int().nonnegative(),
17185
+ callSites: external_exports.number().int().nonnegative(),
17186
+ truncated: external_exports.boolean(),
17187
+ /**
17188
+ * Files the cap dropped whole. Their stored rows were left untouched, so a
17189
+ * ledger-keeping caller must withhold their ledger entries and read them
17190
+ * again next scan.
17191
+ */
17192
+ droppedFiles: external_exports.array(external_exports.string()).default([])
17193
+ }).meta({ id: "EgressWriteSummary" });
17194
+
16913
17195
  // ../../packages/schema/src/zod/findings-group-build.ts
16914
17196
  function toApiAction(dbVal) {
16915
17197
  const map2 = {
@@ -17057,6 +17339,15 @@ function groupActions(g) {
17057
17339
  actionsCache.set(g, actions);
17058
17340
  return actions;
17059
17341
  }
17342
+ function countInstancesByStatus(statusInputs, statuses) {
17343
+ const statusSet = new Set(statuses);
17344
+ let sum = 0;
17345
+ for (const input of statusInputs) {
17346
+ if (input.count === void 0) return null;
17347
+ if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
17348
+ }
17349
+ return sum;
17350
+ }
17060
17351
  function applyFindingFilters(groups, opts) {
17061
17352
  let filtered = groups;
17062
17353
  if (opts.severity && opts.severity.length > 0) {
@@ -17075,6 +17366,10 @@ function applyFindingFilters(groups, opts) {
17075
17366
  const subtypeSet = new Set(opts.subtype);
17076
17367
  filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
17077
17368
  }
17369
+ if (opts.statuses && opts.statuses.length > 0) {
17370
+ const statusSet = new Set(opts.statuses);
17371
+ filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
17372
+ }
17078
17373
  if (opts.q) {
17079
17374
  const q = opts.q.toLowerCase();
17080
17375
  filtered = filtered.filter((g) => groupHaystack(g).includes(q));
@@ -17096,6 +17391,7 @@ function computeFindingFacets(allGroups, opts) {
17096
17391
  const forSeverity = applyFindingFilters(allGroups, {
17097
17392
  providers: opts.providers,
17098
17393
  actions: opts.actions,
17394
+ statuses: opts.statuses,
17099
17395
  q: opts.q,
17100
17396
  subtype: opts.subtype
17101
17397
  });
@@ -17105,6 +17401,7 @@ function computeFindingFacets(allGroups, opts) {
17105
17401
  }
17106
17402
  const forProvider = applyFindingFilters(allGroups, {
17107
17403
  actions: opts.actions,
17404
+ statuses: opts.statuses,
17108
17405
  q: opts.q,
17109
17406
  subtype: opts.subtype,
17110
17407
  severity: opts.severity
@@ -17115,6 +17412,7 @@ function computeFindingFacets(allGroups, opts) {
17115
17412
  }
17116
17413
  const forAction = applyFindingFilters(allGroups, {
17117
17414
  providers: opts.providers,
17415
+ statuses: opts.statuses,
17118
17416
  q: opts.q,
17119
17417
  subtype: opts.subtype,
17120
17418
  severity: opts.severity
@@ -17126,17 +17424,30 @@ function computeFindingFacets(allGroups, opts) {
17126
17424
  const forSubtype = applyFindingFilters(allGroups, {
17127
17425
  providers: opts.providers,
17128
17426
  actions: opts.actions,
17427
+ statuses: opts.statuses,
17129
17428
  q: opts.q,
17130
17429
  severity: opts.severity
17131
17430
  });
17132
17431
  const subtypeMap = /* @__PURE__ */ new Map();
17133
17432
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
17433
+ const forStatus = applyFindingFilters(allGroups, {
17434
+ providers: opts.providers,
17435
+ actions: opts.actions,
17436
+ q: opts.q,
17437
+ subtype: opts.subtype,
17438
+ severity: opts.severity
17439
+ });
17440
+ const statusMap = /* @__PURE__ */ new Map();
17441
+ for (const g of forStatus) {
17442
+ if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
17443
+ }
17134
17444
  const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17135
17445
  return {
17136
17446
  severity: toItems(severityMap),
17137
17447
  provider: toItems(providerMap),
17138
17448
  action: toItems(actionMap),
17139
- subtype: toItems(subtypeMap)
17449
+ subtype: toItems(subtypeMap),
17450
+ status: toItems(statusMap)
17140
17451
  };
17141
17452
  }
17142
17453
 
@@ -17171,10 +17482,14 @@ var PatchInstalledPackRequest = external_exports.object({
17171
17482
  }).meta({ id: "PatchInstalledPackRequest" });
17172
17483
 
17173
17484
  // ../../packages/schema/src/zod/local.ts
17174
- var WORKSPACE_SETTINGS_SPEC_VERSION = 2;
17485
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17175
17486
  var RunMode = external_exports.enum(["standalone"]);
17176
17487
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17177
17488
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
17489
+ var ModelJudgeConsent = external_exports.object({
17490
+ acknowledgedAt: external_exports.iso.datetime(),
17491
+ payloadVersion: external_exports.number().int().positive()
17492
+ });
17178
17493
  var WorkspaceSettings = external_exports.object({
17179
17494
  specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
17180
17495
  // Settings files written by earlier releases may carry the retired 'attached'
@@ -17186,38 +17501,20 @@ var WorkspaceSettings = external_exports.object({
17186
17501
  policy: SimpleDetectionPolicy.default("redact"),
17187
17502
  // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
17188
17503
  historicalAccess: HistoricalAccess.default("session-only"),
17504
+ // In-place egress extraction on the scan paths; disable to stop all Data
17505
+ // Shares writes.
17506
+ dataSharesInPlace: external_exports.boolean().default(true),
17189
17507
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17190
- onboardedAt: external_exports.iso.datetime().optional()
17508
+ onboardedAt: external_exports.iso.datetime().optional(),
17509
+ // Records that the user consented to sending findings to the model API for
17510
+ // the /aka:setup judge, along with the payload-shape version they agreed to.
17511
+ // Absent until granted; a stale payloadVersion means the consent no longer
17512
+ // covers the current payload and must be re-granted.
17513
+ modelJudgeConsent: ModelJudgeConsent.optional()
17191
17514
  });
17192
17515
  function defaultWorkspaceSettings() {
17193
17516
  return WorkspaceSettings.parse({});
17194
17517
  }
17195
- function toEventRow(event) {
17196
- return {
17197
- id: event.id,
17198
- sourceTool: event.sourceTool,
17199
- kind: event.kind,
17200
- occurredAt: isoToEpochMillis(event.occurredAt),
17201
- contentHash: event.contentHash,
17202
- content: event.content,
17203
- metadata: event.metadata ? JSON.stringify(event.metadata) : null
17204
- };
17205
- }
17206
- function toFindingRow(finding2) {
17207
- return {
17208
- id: finding2.id,
17209
- eventId: finding2.eventId,
17210
- ruleId: finding2.ruleId,
17211
- category: finding2.category,
17212
- severity: finding2.severity,
17213
- spanStart: finding2.span.start,
17214
- spanEnd: finding2.span.end,
17215
- maskedMatch: finding2.maskedMatch,
17216
- actionTaken: finding2.actionTaken,
17217
- confidence: finding2.confidence,
17218
- findingKey: finding2.findingKey ?? null
17219
- };
17220
- }
17221
17518
  function toInventoryRow(input, id, now) {
17222
17519
  return {
17223
17520
  id,
@@ -17287,7 +17584,42 @@ function toInspectionFindingRow(input) {
17287
17584
  spanEnd: input.span.end,
17288
17585
  maskedMatch: input.maskedMatch,
17289
17586
  actionTaken: input.actionTaken,
17290
- confidence: input.confidence
17587
+ confidence: input.confidence,
17588
+ findingKey: input.findingKey ?? null,
17589
+ firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
17590
+ };
17591
+ }
17592
+ function toCaptureAttributes(event) {
17593
+ const metadata = event.metadata;
17594
+ return {
17595
+ source_tool: event.sourceTool,
17596
+ ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
17597
+ ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
17598
+ ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
17599
+ ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
17600
+ ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
17601
+ ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
17602
+ ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
17603
+ ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
17604
+ // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
17605
+ // has ever populated either), but every legacy metadata key still rides
17606
+ // the bag rather than being silently dropped — CaptureAttributes'
17607
+ // `.catchall(z.unknown())` carries the long tail.
17608
+ ...metadata?.model !== void 0 ? { model: metadata.model } : {},
17609
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
17610
+ };
17611
+ }
17612
+ function captureDefinitionVersion(finding2) {
17613
+ return `capture/${finding2.category}/${finding2.severity}`;
17614
+ }
17615
+ function toCaptureDefinitionInput(finding2) {
17616
+ return {
17617
+ ruleId: finding2.ruleId,
17618
+ version: captureDefinitionVersion(finding2),
17619
+ name: finding2.ruleId,
17620
+ category: finding2.category,
17621
+ severity: finding2.severity,
17622
+ definition: JSON.stringify({ ruleId: finding2.ruleId })
17291
17623
  };
17292
17624
  }
17293
17625
 
@@ -17669,145 +18001,6 @@ var SetupHandoffOffer = external_exports.object({
17669
18001
  path: ["liveKeys"]
17670
18002
  });
17671
18003
 
17672
- // ../../packages/schema/src/zod/shares.ts
17673
- var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17674
- var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
17675
- var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
17676
- var DATA_CLASS_ORDER = DataClass.options;
17677
- var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
17678
- var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
17679
- var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
17680
- var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
17681
- var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
17682
- var ReviewInfo = external_exports.object({
17683
- needsReview: external_exports.boolean(),
17684
- reasons: external_exports.array(ReviewReason)
17685
- }).meta({ id: "ReviewInfo" });
17686
- var DestinationNetwork = external_exports.object({
17687
- port: external_exports.number().int().nullable(),
17688
- geo: external_exports.string().nullable(),
17689
- ptr: external_exports.string().nullable()
17690
- }).meta({ id: "DestinationNetwork" });
17691
- var EndpointSummary = external_exports.object({
17692
- id: external_exports.string(),
17693
- method: HttpMethod,
17694
- transport: Transport,
17695
- url: external_exports.string(),
17696
- template: external_exports.boolean(),
17697
- dataClass: DataClass,
17698
- lastSeen: external_exports.iso.datetime(),
17699
- callSiteCount: external_exports.number().int().nonnegative()
17700
- }).meta({ id: "EndpointSummary" });
17701
- var CallSite = external_exports.object({
17702
- id: external_exports.string(),
17703
- project: external_exports.string(),
17704
- file: external_exports.string(),
17705
- line: external_exports.number().int().nonnegative(),
17706
- snippet: external_exports.string(),
17707
- dynamic: external_exports.boolean(),
17708
- vendored: external_exports.boolean(),
17709
- /** Deep-link to the Inventory project, when the repo is governed there. */
17710
- projectId: external_exports.string().nullable()
17711
- }).meta({ id: "CallSite" });
17712
- var EndpointWithSites = EndpointSummary.extend({
17713
- sites: external_exports.array(CallSite)
17714
- }).meta({ id: "EndpointWithSites" });
17715
- var ShareDestinationSummary = external_exports.object({
17716
- id: external_exports.string(),
17717
- kind: DestinationKind,
17718
- name: external_exports.string(),
17719
- host: external_exports.string(),
17720
- category: external_exports.string(),
17721
- trust: ShareTrustLevel,
17722
- /** Effective state (decision applied over the trust default). */
17723
- status: EgressStatus,
17724
- /** True when an egress decision override differs from the trust default. */
17725
- isCustom: external_exports.boolean(),
17726
- lastSeen: external_exports.iso.datetime(),
17727
- endpointCount: external_exports.number().int().nonnegative(),
17728
- callSiteCount: external_exports.number().int().nonnegative(),
17729
- transports: external_exports.array(Transport),
17730
- /** Most-sensitive first. */
17731
- dataClasses: external_exports.array(DataClass),
17732
- review: ReviewInfo,
17733
- /** Non-provider hosts only; null for providers. */
17734
- network: DestinationNetwork.nullable(),
17735
- /** Embedded for inline expansion — no call sites here. */
17736
- endpoints: external_exports.array(EndpointSummary)
17737
- }).meta({ id: "ShareDestinationSummary" });
17738
- var ShareDestinationDetail = ShareDestinationSummary.omit({
17739
- endpointCount: true,
17740
- callSiteCount: true,
17741
- endpoints: true
17742
- }).extend({
17743
- /** Ownership/geo rationale; null for providers. */
17744
- note: external_exports.string().nullable(),
17745
- endpoints: external_exports.array(EndpointWithSites)
17746
- }).meta({ id: "ShareDestinationDetail" });
17747
- var ReviewDestination = external_exports.object({
17748
- id: external_exports.string(),
17749
- kind: DestinationKind,
17750
- name: external_exports.string(),
17751
- /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
17752
- host: external_exports.string(),
17753
- trust: ShareTrustLevel,
17754
- status: EgressStatus,
17755
- review: ReviewInfo,
17756
- topDataClass: DataClass,
17757
- callSiteCount: external_exports.number().int().nonnegative(),
17758
- lastSeen: external_exports.iso.datetime()
17759
- }).meta({ id: "ReviewDestination" });
17760
- var ShareDestinationGroup = external_exports.object({
17761
- kind: DestinationKind,
17762
- total: external_exports.number().int().nonnegative(),
17763
- items: external_exports.array(ShareDestinationSummary)
17764
- }).meta({ id: "ShareDestinationGroup" });
17765
- var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
17766
- var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
17767
- var SharesStats = external_exports.object({
17768
- destinations: external_exports.number().int().nonnegative(),
17769
- endpoints: external_exports.number().int().nonnegative(),
17770
- callSites: external_exports.number().int().nonnegative(),
17771
- needsReview: external_exports.number().int().nonnegative(),
17772
- insecure: external_exports.number().int().nonnegative(),
17773
- byKind: external_exports.object({
17774
- provider: external_exports.number().int().nonnegative(),
17775
- internal: external_exports.number().int().nonnegative(),
17776
- ip: external_exports.number().int().nonnegative()
17777
- }),
17778
- byTrust: external_exports.object({
17779
- recognized: external_exports.number().int().nonnegative(),
17780
- internal: external_exports.number().int().nonnegative(),
17781
- unverified: external_exports.number().int().nonnegative(),
17782
- ip: external_exports.number().int().nonnegative()
17783
- })
17784
- }).meta({ id: "SharesStats" });
17785
- var SetEgressDecisionBody = external_exports.object({
17786
- /** `null` clears the override — reverts to the trust default, isCustom false. */
17787
- decision: EgressDecision.nullable()
17788
- }).meta({ id: "SetEgressDecisionBody" });
17789
- var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
17790
- var ListShareDestinationsQuery = external_exports.object({
17791
- /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
17792
- q: external_exports.string().optional(),
17793
- /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
17794
- kind: external_exports.array(DestinationKind).optional(),
17795
- /** Reserved for future grouping modes; only 'destination' is supported today. */
17796
- groupBy: external_exports.enum(["destination"]).default("destination"),
17797
- /**
17798
- * When true, return a flat severity-ordered `items[]` instead of `groups`.
17799
- * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17800
- * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17801
- * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17802
- */
17803
- review: external_exports.stringbool().default(false)
17804
- });
17805
- var ExportSharesQuery = external_exports.object({
17806
- format: external_exports.enum(["csv", "json"]).default("csv"),
17807
- q: external_exports.string().optional(),
17808
- kind: external_exports.array(DestinationKind).optional()
17809
- });
17810
-
17811
18004
  // ../../packages/schema/src/zod/shares-access.ts
17812
18005
  var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
17813
18006
  function trustDefaultStatus(trust) {
@@ -17827,7 +18020,7 @@ function deriveReviewReasons(trust, transports) {
17827
18020
  const reasons = [];
17828
18021
  if (trust === "ip") reasons.push("raw_ip");
17829
18022
  if (trust === "unverified") reasons.push("unverified_domain");
17830
- if (transports.includes("http")) reasons.push("plaintext_transport");
18023
+ if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
17831
18024
  return reasons;
17832
18025
  }
17833
18026
  function buildReviewInfo(trust, transports) {
@@ -17854,6 +18047,48 @@ function reviewSeverityRank(reasons) {
17854
18047
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17855
18048
  }
17856
18049
 
18050
+ // ../../packages/persistence/src/ids.ts
18051
+ import { createHash } from "crypto";
18052
+ function sha256Hex(input) {
18053
+ return createHash("sha256").update(input).digest("hex");
18054
+ }
18055
+ function inventoryId(objectType, identityKey) {
18056
+ return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
18057
+ }
18058
+ function sourceProjectId(url2) {
18059
+ return sha256Hex(canonicalIdentity(["source_project", url2]));
18060
+ }
18061
+ function classifiedDataId(cls) {
18062
+ return sha256Hex(canonicalIdentity(["classified_data", cls]));
18063
+ }
18064
+ function inspectionDefinitionId(ruleId, version2) {
18065
+ return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
18066
+ }
18067
+ function llmCallId(sessionId, messageId) {
18068
+ return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
18069
+ }
18070
+ function toolCallId(sessionId, toolUseId) {
18071
+ return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
18072
+ }
18073
+ function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
18074
+ return sha256Hex(
18075
+ canonicalIdentity([
18076
+ "inspection_finding",
18077
+ auditEventId,
18078
+ ruleId,
18079
+ String(spanStart),
18080
+ String(spanEnd)
18081
+ ])
18082
+ );
18083
+ }
18084
+ var NO_SESSION = "no_session";
18085
+ var NO_PATH = "no_path";
18086
+ function captureId(sessionId, contentHash, filePath = null) {
18087
+ return sha256Hex(
18088
+ canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
18089
+ );
18090
+ }
18091
+
17857
18092
  // ../../packages/persistence/src/internal/sql-text.ts
17858
18093
  function escapeLikePattern(s) {
17859
18094
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -17950,39 +18185,81 @@ function evidenceExists(db, object2) {
17950
18185
  return schemaObjectExists(db, "table", object2.name);
17951
18186
  }
17952
18187
 
17953
- // ../../packages/persistence/src/ids.ts
17954
- import { createHash } from "crypto";
17955
- function sha256Hex(input) {
17956
- return createHash("sha256").update(input).digest("hex");
18188
+ // ../../packages/persistence/src/internal/rows.ts
18189
+ function allRows(stmt, params) {
18190
+ if (params === void 0) return stmt.all();
18191
+ if (Array.isArray(params)) return stmt.all(...params);
18192
+ return stmt.all(params);
17957
18193
  }
17958
- function inventoryId(objectType, identityKey) {
17959
- return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
18194
+ function getRow(stmt, params) {
18195
+ if (params === void 0) return stmt.get();
18196
+ if (Array.isArray(params)) return stmt.get(...params);
18197
+ return stmt.get(params);
17960
18198
  }
17961
- function sourceProjectId(url2) {
17962
- return sha256Hex(canonicalIdentity(["source_project", url2]));
18199
+ function intToBool(raw) {
18200
+ return raw === 1 || raw === true;
17963
18201
  }
17964
- function classifiedDataId(cls) {
17965
- return sha256Hex(canonicalIdentity(["classified_data", cls]));
18202
+ function boolToInt(b) {
18203
+ return b ? 1 : 0;
17966
18204
  }
17967
- function inspectionDefinitionId(ruleId, version2) {
17968
- return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
18205
+ function bindParams(row) {
18206
+ const out = {};
18207
+ for (const [key, value] of Object.entries(row)) {
18208
+ out[key] = value === void 0 ? null : value;
18209
+ }
18210
+ return out;
17969
18211
  }
17970
- function llmCallId(sessionId, messageId) {
17971
- return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
18212
+ function countScalar(db, sql, params) {
18213
+ return getRow(db.prepare(sql), params)?.n ?? 0;
17972
18214
  }
17973
- function toolCallId(sessionId, toolUseId) {
17974
- return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
18215
+ function countBy(db, sql, params) {
18216
+ const map2 = /* @__PURE__ */ new Map();
18217
+ for (const row of allRows(db.prepare(sql), params)) {
18218
+ map2.set(row.k, row.n);
18219
+ }
18220
+ return map2;
17975
18221
  }
17976
- function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17977
- return sha256Hex(
17978
- canonicalIdentity([
17979
- "inspection_finding",
17980
- auditEventId,
17981
- definitionId,
17982
- String(spanStart),
17983
- String(spanEnd)
17984
- ])
17985
- );
18222
+ function mapRowsTolerant(rows, map2) {
18223
+ const out = [];
18224
+ for (const row of rows) {
18225
+ try {
18226
+ out.push(map2(row));
18227
+ } catch {
18228
+ }
18229
+ }
18230
+ return out;
18231
+ }
18232
+
18233
+ // ../../packages/persistence/src/paths.ts
18234
+ import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18235
+ var DATA_DIR_MODE = 448;
18236
+ var DATA_FILE_MODE = 384;
18237
+ var DB_FILENAME = "aka.db";
18238
+ function chmodBestEffort(path, mode) {
18239
+ try {
18240
+ chmodSync(path, mode);
18241
+ } catch {
18242
+ }
18243
+ }
18244
+ function tightenDir(dir) {
18245
+ chmodBestEffort(dir, DATA_DIR_MODE);
18246
+ }
18247
+ function ensureDataDirSync(dir) {
18248
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18249
+ tightenDir(dir);
18250
+ }
18251
+ function dbSidecars(file2) {
18252
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18253
+ }
18254
+ function tightenFile(file2) {
18255
+ try {
18256
+ if (lstatSync(file2).isSymbolicLink()) return;
18257
+ } catch {
18258
+ }
18259
+ chmodBestEffort(file2, DATA_FILE_MODE);
18260
+ }
18261
+ function tightenPerms(file2) {
18262
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
17986
18263
  }
17987
18264
 
17988
18265
  // ../../packages/persistence/src/migrations.ts
@@ -17996,7 +18273,8 @@ function createdIndexName(statement) {
17996
18273
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17997
18274
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17998
18275
  }
17999
- function applyMigrations(db) {
18276
+ var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
18277
+ function applyMigrations(db, file2) {
18000
18278
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
18001
18279
  db.exec(
18002
18280
  "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
@@ -18010,6 +18288,7 @@ function applyMigrations(db) {
18010
18288
  );
18011
18289
  for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
18012
18290
  if (applied.has(migration.tag)) continue;
18291
+ if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
18013
18292
  const evidence = evidenceObjects(migration.sql);
18014
18293
  const present = evidence.filter((o) => evidenceExists(db, o));
18015
18294
  if (present.length > 0 && present.length < evidence.length) {
@@ -18054,13 +18333,54 @@ function applyMigrations(db) {
18054
18333
  if (legacyCount < SQLITE_MIGRATIONS.length) {
18055
18334
  db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
18056
18335
  }
18057
- ensureSyncedAtColumn(db, "events");
18058
18336
  ensureSyncedAtColumn(db, "audit_events");
18059
18337
  ensureScanLedgerTable(db);
18060
18338
  ensureBlockedDetectionsTable(db);
18339
+ ensureRuleProbeCacheTable(db);
18061
18340
  ensureWriteGateTrigger(db);
18062
18341
  ensureTokenUsageColumns(db);
18063
18342
  reconcileSourceProjectIds(db);
18343
+ if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
18344
+ const drained = runLegacyHistoryBackfill(db);
18345
+ if (drained) applyLegacyDropMigration(db, file2);
18346
+ }
18347
+ }
18348
+ function applyLegacyDropMigration(db, file2) {
18349
+ const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
18350
+ if (!migration) return;
18351
+ if (file2) {
18352
+ try {
18353
+ backupBeforeLegacyDrop(db, file2);
18354
+ } catch (error51) {
18355
+ akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
18356
+ return;
18357
+ }
18358
+ }
18359
+ try {
18360
+ withTransaction(
18361
+ db,
18362
+ () => {
18363
+ const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
18364
+ if (alreadyDropped) return;
18365
+ for (const statement of splitStatements(migration.sql)) {
18366
+ db.exec(statement);
18367
+ }
18368
+ db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
18369
+ migration.tag,
18370
+ Date.now()
18371
+ );
18372
+ },
18373
+ "IMMEDIATE"
18374
+ );
18375
+ } catch (error51) {
18376
+ akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
18377
+ }
18378
+ }
18379
+ function backupBeforeLegacyDrop(db, file2) {
18380
+ const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18381
+ db.prepare("VACUUM INTO ?").run(backup);
18382
+ tightenFile(backup);
18383
+ return backup;
18064
18384
  }
18065
18385
  var TOKEN_USAGE_COLUMNS = [
18066
18386
  {
@@ -18089,6 +18409,7 @@ var TOKEN_USAGE_COLUMNS = [
18089
18409
  }
18090
18410
  ];
18091
18411
  function ensureTokenUsageColumns(db) {
18412
+ if (!schemaObjectExists(db, "table", "audit_events")) return;
18092
18413
  const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
18093
18414
  for (const column of TOKEN_USAGE_COLUMNS) {
18094
18415
  if (!existing.has(column.name)) {
@@ -18154,11 +18475,187 @@ function reconcileSourceProjectIds(db) {
18154
18475
  akaWarn(`source_project id reconcile failed: ${String(error51)}`);
18155
18476
  }
18156
18477
  }
18478
+ var LEGACY_BACKFILL_BATCH_SIZE = 200;
18479
+ var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
18480
+ function getLegacyCopyWatermark(db, source) {
18481
+ const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
18482
+ return row?.lastRowid ?? 0;
18483
+ }
18484
+ function setLegacyCopyWatermark(db, source, lastRowid) {
18485
+ db.prepare(
18486
+ `INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
18487
+ ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
18488
+ ).run(source, lastRowid);
18489
+ }
18490
+ function drainLegacyTable(db, source, selectStmt, handleRows) {
18491
+ let watermark = getLegacyCopyWatermark(db, source);
18492
+ let processed = 0;
18493
+ while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
18494
+ const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
18495
+ if (rows.length === 0) return true;
18496
+ withTransaction(
18497
+ db,
18498
+ () => {
18499
+ handleRows(rows);
18500
+ watermark = rows[rows.length - 1]?.rowid ?? watermark;
18501
+ setLegacyCopyWatermark(db, source, watermark);
18502
+ },
18503
+ "IMMEDIATE"
18504
+ );
18505
+ processed += rows.length;
18506
+ if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
18507
+ }
18508
+ return false;
18509
+ }
18510
+ function parseLegacyEventMetadata(raw) {
18511
+ if (raw === null) return void 0;
18512
+ try {
18513
+ return JSON.parse(raw);
18514
+ } catch {
18515
+ return void 0;
18516
+ }
18517
+ }
18518
+ function toLegacyAuditAttributesJson(row) {
18519
+ return JSON.stringify(
18520
+ toCaptureAttributes({
18521
+ id: row.id,
18522
+ sourceTool: row.sourceTool,
18523
+ kind: row.kind,
18524
+ occurredAt: new Date(row.occurredAt).toISOString(),
18525
+ contentHash: row.contentHash,
18526
+ content: row.content,
18527
+ metadata: row.metadata
18528
+ })
18529
+ );
18530
+ }
18531
+ function copyLegacyEvents(db) {
18532
+ const selectStmt = db.prepare(
18533
+ `SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
18534
+ content_hash AS contentHash, content, metadata
18535
+ FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
18536
+ );
18537
+ const insertStmt = db.prepare(
18538
+ `INSERT OR IGNORE INTO audit_events
18539
+ (id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
18540
+ VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
18541
+ );
18542
+ const stubRootStmt = db.prepare(
18543
+ `INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
18544
+ );
18545
+ return drainLegacyTable(
18546
+ db,
18547
+ "events",
18548
+ selectStmt,
18549
+ (rows) => {
18550
+ for (const row of rows) {
18551
+ const metadata = parseLegacyEventMetadata(row.metadata);
18552
+ const sessionId = metadata?.sessionId ?? null;
18553
+ if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
18554
+ insertStmt.run(
18555
+ bindParams({
18556
+ id: row.id,
18557
+ parentId: sessionId,
18558
+ rootSessionId: sessionId,
18559
+ eventType: row.kind,
18560
+ startedAt: row.occurredAt,
18561
+ content: row.content,
18562
+ contentHash: row.contentHash,
18563
+ attributes: toLegacyAuditAttributesJson({ ...row, metadata })
18564
+ })
18565
+ );
18566
+ }
18567
+ }
18568
+ );
18569
+ }
18570
+ function copyLegacyFindings(db) {
18571
+ const selectStmt = db.prepare(
18572
+ `SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
18573
+ span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
18574
+ action_taken AS actionTaken, confidence, finding_key AS findingKey,
18575
+ first_detected_at AS firstDetectedAt
18576
+ FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
18577
+ );
18578
+ const definitionStmt = db.prepare(
18579
+ `INSERT OR IGNORE INTO inspection_definitions
18580
+ (id, rule_id, name, category, severity, definition, version)
18581
+ VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
18582
+ );
18583
+ const findingStmt = db.prepare(
18584
+ `INSERT INTO inspection_findings
18585
+ (id, audit_event_id, inspection_definition_id, classified_data_id,
18586
+ span_start, span_end, masked_match, action_taken, confidence,
18587
+ finding_key, first_detected_at)
18588
+ VALUES
18589
+ (:id, :auditEventId, :inspectionDefinitionId, NULL,
18590
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
18591
+ :findingKey, :firstDetectedAt)
18592
+ ON CONFLICT(id) DO NOTHING
18593
+ ON CONFLICT (finding_key) DO UPDATE SET
18594
+ first_detected_at = CASE
18595
+ WHEN first_detected_at IS NULL THEN excluded.first_detected_at
18596
+ WHEN excluded.first_detected_at IS NULL THEN first_detected_at
18597
+ ELSE min(first_detected_at, excluded.first_detected_at)
18598
+ END`
18599
+ );
18600
+ return drainLegacyTable(
18601
+ db,
18602
+ "findings",
18603
+ selectStmt,
18604
+ (rows) => {
18605
+ const definitionIds = /* @__PURE__ */ new Map();
18606
+ for (const row of rows) {
18607
+ const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
18608
+ let definitionId = definitionIds.get(tupleKey);
18609
+ if (definitionId === void 0) {
18610
+ const version2 = `unmigrated/${row.category}/${row.severity}`;
18611
+ definitionId = inspectionDefinitionId(row.ruleId, version2);
18612
+ definitionStmt.run(
18613
+ bindParams({
18614
+ id: definitionId,
18615
+ ruleId: row.ruleId,
18616
+ name: row.ruleId,
18617
+ category: row.category,
18618
+ severity: row.severity,
18619
+ definition: "",
18620
+ version: version2
18621
+ })
18622
+ );
18623
+ definitionIds.set(tupleKey, definitionId);
18624
+ }
18625
+ findingStmt.run(
18626
+ bindParams({
18627
+ id: row.id,
18628
+ auditEventId: row.eventId,
18629
+ inspectionDefinitionId: definitionId,
18630
+ spanStart: row.spanStart,
18631
+ spanEnd: row.spanEnd,
18632
+ maskedMatch: row.maskedMatch,
18633
+ actionTaken: row.actionTaken,
18634
+ confidence: row.confidence,
18635
+ findingKey: row.findingKey,
18636
+ firstDetectedAt: row.firstDetectedAt
18637
+ })
18638
+ );
18639
+ }
18640
+ }
18641
+ );
18642
+ }
18643
+ function runLegacyHistoryBackfill(db) {
18644
+ try {
18645
+ const eventsCaughtUp = copyLegacyEvents(db);
18646
+ if (!eventsCaughtUp) return false;
18647
+ return copyLegacyFindings(db);
18648
+ } catch (error51) {
18649
+ akaWarn(`legacy history backfill failed: ${String(error51)}`);
18650
+ return false;
18651
+ }
18652
+ }
18157
18653
  function isForeignSqliteLineage(db) {
18158
18654
  if (schemaObjectExists(db, "table", "tenants")) return true;
18159
18655
  return columnNames(db, "events").includes("tenant_id");
18160
18656
  }
18161
18657
  function ensureSyncedAtColumn(db, table) {
18658
+ if (!schemaObjectExists(db, "table", table)) return;
18162
18659
  if (!columnNames(db, table).includes("synced_at")) {
18163
18660
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
18164
18661
  }
@@ -18179,6 +18676,7 @@ function ensureWriteGateTrigger(db) {
18179
18676
  CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
18180
18677
  )`);
18181
18678
  db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
18679
+ if (!schemaObjectExists(db, "table", "installed_packs")) return;
18182
18680
  db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
18183
18681
  BEFORE UPDATE OF version, name, rules_json ON installed_packs
18184
18682
  WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
@@ -18197,29 +18695,13 @@ function ensureBlockedDetectionsTable(db) {
18197
18695
  blocked_at INTEGER NOT NULL
18198
18696
  )`);
18199
18697
  }
18200
-
18201
- // ../../packages/persistence/src/paths.ts
18202
- import { chmodSync, mkdirSync } from "fs";
18203
- var DATA_DIR_MODE = 448;
18204
- var DATA_FILE_MODE = 384;
18205
- var DB_FILENAME = "aka.db";
18206
- function ensureDataDirSync(dir) {
18207
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18208
- try {
18209
- chmodSync(dir, DATA_DIR_MODE);
18210
- } catch {
18211
- }
18212
- }
18213
- function walSidecars(file2) {
18214
- return [`${file2}-wal`, `${file2}-shm`];
18215
- }
18216
- function tightenPerms(file2) {
18217
- for (const path of [file2, ...walSidecars(file2)]) {
18218
- try {
18219
- chmodSync(path, DATA_FILE_MODE);
18220
- } catch {
18221
- }
18222
- }
18698
+ function ensureRuleProbeCacheTable(db) {
18699
+ db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
18700
+ rule_key TEXT PRIMARY KEY,
18701
+ verdict TEXT NOT NULL,
18702
+ worst_probe_ms REAL NOT NULL,
18703
+ checked_at INTEGER NOT NULL
18704
+ )`);
18223
18705
  }
18224
18706
 
18225
18707
  // ../../packages/persistence/src/internal/json.ts
@@ -18241,51 +18723,6 @@ function parseJsonObject(s) {
18241
18723
  return void 0;
18242
18724
  }
18243
18725
 
18244
- // ../../packages/persistence/src/internal/rows.ts
18245
- function allRows(stmt, params) {
18246
- if (params === void 0) return stmt.all();
18247
- if (Array.isArray(params)) return stmt.all(...params);
18248
- return stmt.all(params);
18249
- }
18250
- function getRow(stmt, params) {
18251
- if (params === void 0) return stmt.get();
18252
- if (Array.isArray(params)) return stmt.get(...params);
18253
- return stmt.get(params);
18254
- }
18255
- function intToBool(raw) {
18256
- return raw === 1 || raw === true;
18257
- }
18258
- function boolToInt(b) {
18259
- return b ? 1 : 0;
18260
- }
18261
- function bindParams(row) {
18262
- const out = {};
18263
- for (const [key, value] of Object.entries(row)) {
18264
- out[key] = value === void 0 ? null : value;
18265
- }
18266
- return out;
18267
- }
18268
- function countScalar(db, sql, params) {
18269
- return getRow(db.prepare(sql), params)?.n ?? 0;
18270
- }
18271
- function countBy(db, sql, params) {
18272
- const map2 = /* @__PURE__ */ new Map();
18273
- for (const row of allRows(db.prepare(sql), params)) {
18274
- map2.set(row.k, row.n);
18275
- }
18276
- return map2;
18277
- }
18278
- function mapRowsTolerant(rows, map2) {
18279
- const out = [];
18280
- for (const row of rows) {
18281
- try {
18282
- out.push(map2(row));
18283
- } catch {
18284
- }
18285
- }
18286
- return out;
18287
- }
18288
-
18289
18726
  // ../../packages/persistence/src/repositories/activity.ts
18290
18727
  var DAY_MS = 864e5;
18291
18728
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18902,6 +19339,21 @@ var SqliteAuditEventsRepository = class {
18902
19339
  })
18903
19340
  );
18904
19341
  }
19342
+ // Idempotent stub of a session's structural root. Session-scoped leaves
19343
+ // (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
19344
+ // INSERT OR IGNORE does NOT suppress a foreign-key violation (only
19345
+ // UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
19346
+ // raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
19347
+ // dropping the write under failOpenTransaction. SessionStart's own root write
19348
+ // is itself fail-open and marks "attempted", not "succeeded", so a session
19349
+ // with no root row yet is a real, permanent condition, not a transient race.
19350
+ // The stub carries no dimensions/attributes; an authoritative root
19351
+ // (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
19352
+ // on the id PK, so the stub never shadows real data. This is the single named
19353
+ // home for that FK invariant — call it before writing any session-scoped row.
19354
+ ensureSessionRoot(sessionId, startedAt) {
19355
+ this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
19356
+ }
18905
19357
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18906
19358
  // (which takes a caller-supplied random id), the id here is MINTED internally
18907
19359
  // from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
@@ -19363,8 +19815,14 @@ var SqliteDetectionsRepository = class {
19363
19815
  )
19364
19816
  );
19365
19817
  }
19366
- // Findings whose parent event occurred in the last 30 days and whose rule_id is
19367
- // in the given set. Mirrors the security repo's findings⋈events window join.
19818
+ // Findings whose parent audit event occurred in the last 30 days, is one of
19819
+ // the four capture kinds, and whose definition's rule_id is in the given set.
19820
+ // Mirrors the security repo's inspection_findings⋈audit_events window join.
19821
+ // rule_id lives on inspection_definitions, not the finding row, so the join
19822
+ // chains through it. audit_events also holds structural rows (session, run,
19823
+ // tool_call, llm_call, source_lookup, config_scan) that never had a legacy
19824
+ // events counterpart, so the event_type predicate keeps this count identical
19825
+ // to the old findings⋈events one.
19368
19826
  countFindingsLast30d(ruleIds) {
19369
19827
  if (ruleIds.length === 0) return 0;
19370
19828
  const since = this.now() - 30 * DAY_MS2;
@@ -19372,8 +19830,12 @@ var SqliteDetectionsRepository = class {
19372
19830
  return countScalar(
19373
19831
  this.db,
19374
19832
  `SELECT count(*) AS n
19375
- FROM findings f JOIN events e ON e.id = f.event_id
19376
- WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19833
+ FROM inspection_findings f
19834
+ JOIN audit_events e ON e.id = f.audit_event_id
19835
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19836
+ WHERE e.started_at >= ?
19837
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
19838
+ AND d.rule_id IN (${inClause})`,
19377
19839
  [since, ...ruleIds]
19378
19840
  );
19379
19841
  }
@@ -19383,35 +19845,24 @@ var SqliteDetectionsRepository = class {
19383
19845
  var SqliteEventsRepository = class {
19384
19846
  constructor(db) {
19385
19847
  this.db = db;
19386
- this.insertStmt = db.prepare(
19387
- `INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
19388
- VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
19389
- );
19390
19848
  }
19391
19849
  db;
19392
- insertStmt;
19393
- insertEvent(event) {
19394
- const row = toEventRow(event);
19395
- this.insertStmt.run(
19396
- bindParams({
19397
- id: row.id,
19398
- sourceTool: row.sourceTool,
19399
- kind: row.kind,
19400
- occurredAt: row.occurredAt,
19401
- contentHash: row.contentHash,
19402
- content: row.content,
19403
- metadata: row.metadata
19404
- })
19405
- );
19406
- }
19407
- // Every recorded event's content hash — the historical backfill loads this once
19408
- // to skip transcript messages it has already stored, so re-running the scan
19409
- // never duplicates findings.
19850
+ // Every recorded capture's content hash — the historical backfill loads this
19851
+ // once to skip transcript messages it has already stored, so re-running the
19852
+ // scan never duplicates findings.
19410
19853
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
19411
19854
  // async EventsReadPort contract.
19855
+ //
19856
+ // audit_events also holds structural rows (session, run, tool_call, llm_call,
19857
+ // source_lookup, config_scan) with a NULL content_hash, so the capture-kind
19858
+ // predicate isn't load-bearing here — it documents intent and keeps the scan
19859
+ // index-friendly rather than walking rows that can never match.
19412
19860
  contentHashes() {
19413
19861
  const rows = allRows(
19414
- this.db.prepare("SELECT content_hash FROM events")
19862
+ this.db.prepare(
19863
+ `SELECT content_hash FROM audit_events
19864
+ WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
19865
+ )
19415
19866
  );
19416
19867
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
19417
19868
  }
@@ -19747,17 +20198,20 @@ function parseExceptionRow(row) {
19747
20198
  }
19748
20199
 
19749
20200
  // ../../packages/persistence/src/repositories/resolution-sql.ts
19750
- function latestResolutionStatusSql(findingsAlias) {
20201
+ function latestResolutionColumnSql(column, findingsAlias) {
19751
20202
  return `(
19752
- SELECT fr.status FROM finding_resolution fr
20203
+ SELECT fr.${column} FROM finding_resolution fr
19753
20204
  WHERE fr.finding_key = ${findingsAlias}.finding_key
19754
20205
  ORDER BY fr.created_at DESC, fr.rowid DESC
19755
20206
  LIMIT 1
19756
20207
  )`;
19757
20208
  }
20209
+ function latestResolutionStatusSql(findingsAlias) {
20210
+ return latestResolutionColumnSql("status", findingsAlias);
20211
+ }
19758
20212
  var LATEST_RESOLUTION_BY_KEY_SQL = `(
19759
- SELECT finding_key, status FROM (
19760
- SELECT fr.finding_key, fr.status,
20213
+ SELECT finding_key, status, method, resolved_at FROM (
20214
+ SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
19761
20215
  ROW_NUMBER() OVER (
19762
20216
  PARTITION BY fr.finding_key
19763
20217
  ORDER BY fr.created_at DESC, fr.rowid DESC
@@ -19784,68 +20238,21 @@ var DAY_MS3 = 864e5;
19784
20238
  var SqliteFindingsRepository = class {
19785
20239
  constructor(db) {
19786
20240
  this.db = db;
19787
- this.insertStmt = db.prepare(
19788
- `INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
19789
- VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
19790
- (SELECT occurred_at FROM events WHERE id = :eventId))
19791
- ON CONFLICT (finding_key) DO UPDATE SET
19792
- event_id = excluded.event_id,
19793
- category = excluded.category,
19794
- severity = excluded.severity,
19795
- span_start = excluded.span_start,
19796
- span_end = excluded.span_end,
19797
- masked_match = excluded.masked_match,
19798
- action_taken = excluded.action_taken,
19799
- confidence = excluded.confidence`
19800
- );
19801
- this.sessionDupStmt = db.prepare(
19802
- `SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
19803
- WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
19804
- AND json_extract(e.metadata, '$.sessionId') = :sessionId
19805
- LIMIT 1`
19806
- );
19807
20241
  }
19808
20242
  db;
19809
- insertStmt;
19810
- sessionDupStmt;
19811
- insertFindings(findings, scope = {}) {
19812
- for (const finding2 of findings) {
19813
- if (scope.sessionId && this.isSessionDuplicate(finding2, scope.sessionId)) continue;
19814
- const row = toFindingRow(finding2);
19815
- this.insertStmt.run({
19816
- id: row.id,
19817
- eventId: row.eventId,
19818
- ruleId: row.ruleId,
19819
- category: row.category,
19820
- severity: row.severity,
19821
- spanStart: row.spanStart,
19822
- spanEnd: row.spanEnd,
19823
- maskedMatch: row.maskedMatch,
19824
- actionTaken: row.actionTaken,
19825
- confidence: row.confidence,
19826
- findingKey: row.findingKey ?? null
19827
- });
19828
- }
19829
- }
19830
- // True when an earlier event in the same session already recorded a finding
19831
- // with the same rule and masked value. The current event is inserted before
19832
- // its findings, but carries no findings yet, so this never self-matches.
19833
- isSessionDuplicate(finding2, sessionId) {
19834
- const hit = this.sessionDupStmt.get({
19835
- ruleId: finding2.ruleId,
19836
- maskedMatch: finding2.maskedMatch,
19837
- sessionId
19838
- });
19839
- return hit !== void 0;
19840
- }
19841
20243
  recentFindings(opts) {
19842
20244
  const limit = opts?.limit ?? 50;
19843
20245
  const rows = allRows(
19844
20246
  this.db.prepare(
19845
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19846
- f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19847
- FROM findings f JOIN events e ON e.id = f.event_id
19848
- ORDER BY e.occurred_at DESC, f.rowid DESC
20247
+ `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
20248
+ f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
20249
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
20250
+ e.event_type AS kind
20251
+ FROM inspection_findings f
20252
+ JOIN audit_events e ON e.id = f.audit_event_id
20253
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20254
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20255
+ ORDER BY e.started_at DESC, f.rowid DESC
19849
20256
  LIMIT :limit`
19850
20257
  ),
19851
20258
  { limit }
@@ -19867,25 +20274,34 @@ var SqliteFindingsRepository = class {
19867
20274
  );
19868
20275
  }
19869
20276
  /** Live-enforced findings recorded for one session — a bare COUNT over the
19870
- * session-stamped events (served by idx_events_session_id), so the Activity
20277
+ * session-stamped audit_events (served by idx_audit_session), so the Activity
19871
20278
  * page can label its findings link without the grouped pipeline. */
19872
20279
  sessionFindingsCount(sessionId) {
19873
20280
  if (!sessionId) return Promise.resolve(0);
19874
20281
  return Promise.resolve(
19875
20282
  countScalar(
19876
20283
  this.db,
19877
- `SELECT count(*) AS n FROM findings f
19878
- JOIN events e ON e.id = f.event_id
19879
- WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
20284
+ `SELECT count(*) AS n FROM inspection_findings f
20285
+ JOIN audit_events e ON e.id = f.audit_event_id
20286
+ WHERE e.root_session_id = :sessionId
20287
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
19880
20288
  { sessionId }
19881
20289
  )
19882
20290
  );
19883
20291
  }
19884
- /** Per-rule transcript firing tally for one session — reads the OTHER finding
19885
- * store (inspection_findings, keyed to audit_events): every detection the
19886
- * transcript pass recorded, counted per firing rather than per unique value.
19887
- * Rides on session-scoped grouped responses so the findings view can
19888
- * reconcile the Activity page's tally with the deduped groups it lists. */
20292
+ /** Per-rule transcript firing tally for one session — every detection the
20293
+ * transcript-reconciler pass recorded against the session's `tool_call` rows,
20294
+ * counted per firing rather than per unique value. Rides on session-scoped
20295
+ * grouped responses so the findings view can reconcile the Activity page's
20296
+ * tally with the deduped groups it lists.
20297
+ *
20298
+ * `inspection_findings`/`audit_events` are now the SAME physical tables the
20299
+ * rest of this class reads for the live-capture list above (they used to be
20300
+ * a separate store), so this excludes the four capture kinds those rows
20301
+ * already carry — without that exclusion, every live-capture finding in the
20302
+ * session would be tallied here too, double-counting against the grouped
20303
+ * list this response rides alongside. The reconciler attaches its findings
20304
+ * only to `tool_call` rows, which the exclusion leaves untouched. */
19889
20305
  sessionFirings(sessionId) {
19890
20306
  return Object.fromEntries(
19891
20307
  countBy(
@@ -19895,18 +20311,25 @@ var SqliteFindingsRepository = class {
19895
20311
  JOIN audit_events e ON e.id = f.audit_event_id
19896
20312
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19897
20313
  WHERE e.root_session_id = :sessionId
20314
+ AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
19898
20315
  GROUP BY d.rule_id`,
19899
20316
  { sessionId }
19900
20317
  )
19901
20318
  );
19902
20319
  }
19903
20320
  /**
19904
- * Grouped findings for the dashboard — joins findingsevents (repo/file/
19905
- * toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
19906
- * applies the requested filters, and sorts by severity then recency. Filtering
20321
+ * Grouped findings for the dashboard — joins inspection_findingsaudit_events
20322
+ * ⋈inspection_definitions (repo/file/toolName from the audit event's
20323
+ * attributes bag, rule_id/category/severity from the definition), scoped to
20324
+ * the four capture kinds (audit_events also holds structural/reconciler/scan
20325
+ * rows this list must never surface), groups by ruleId, computes
20326
+ * per-filter-excluded facets, applies the requested filters, and sorts by
20327
+ * severity then recency. Filtering
19907
20328
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19908
20329
  * reflect the full filtered set; `items` is the requested
19909
- * page (default 50); no cursor (nextCursor is always null).
20330
+ * page (default 50); no cursor (nextCursor is always null). Under a `status`
20331
+ * filter, `totals.findings` counts only instances whose derived status was
20332
+ * requested, and each item's instance preview is narrowed the same way.
19910
20333
  *
19911
20334
  * Two reads, neither of which materializes a row per finding:
19912
20335
  * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
@@ -19919,10 +20342,11 @@ var SqliteFindingsRepository = class {
19919
20342
  * rule is ever restated in SQL.
19920
20343
  */
19921
20344
  listGroupedFindings(query) {
19922
- const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
20345
+ const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20346
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
19923
20347
  const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
19924
20348
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
19925
- predicate: sessionPredicate,
20349
+ predicate,
19926
20350
  params: sessionParams
19927
20351
  });
19928
20352
  const rows = allRows(
@@ -19930,24 +20354,26 @@ var SqliteFindingsRepository = class {
19930
20354
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19931
20355
  occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
19932
20356
  FROM (
19933
- SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19934
- f.severity AS severity, f.masked_match AS masked_match,
20357
+ SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20358
+ d.severity AS severity, f.masked_match AS masked_match,
19935
20359
  f.action_taken AS action_taken, f.confidence AS confidence,
19936
- e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19937
- json_extract(e.metadata, '$.repo') AS repo,
19938
- json_extract(e.metadata, '$.filePath') AS file,
19939
- json_extract(e.metadata, '$.toolName') AS tool_name,
19940
- e.kind AS kind, f.finding_key AS finding_key,
20360
+ e.started_at AS occurred_at,
20361
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
20362
+ json_extract(e.attributes, '$.repo') AS repo,
20363
+ json_extract(e.attributes, '$.file_path') AS file,
20364
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
20365
+ e.event_type AS kind, f.finding_key AS finding_key,
19941
20366
  latest.status AS latest_status,
19942
20367
  ROW_NUMBER() OVER (
19943
- PARTITION BY f.rule_id
19944
- ORDER BY e.occurred_at DESC, f.id DESC
20368
+ PARTITION BY d.rule_id
20369
+ ORDER BY e.started_at DESC, f.id DESC
19945
20370
  ) AS rn
19946
- FROM findings f
19947
- JOIN events e ON e.id = f.event_id
20371
+ FROM inspection_findings f
20372
+ JOIN audit_events e ON e.id = f.audit_event_id
20373
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19948
20374
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19949
20375
  ON latest.finding_key = f.finding_key
19950
- ${sessionPredicate}
20376
+ ${predicate}
19951
20377
  )
19952
20378
  WHERE rn <= :cap
19953
20379
  ORDER BY occurred_at DESC, id DESC`
@@ -19974,17 +20400,29 @@ var SqliteFindingsRepository = class {
19974
20400
  severity: query.severity,
19975
20401
  providers: query.provider,
19976
20402
  actions: query.action,
20403
+ statuses: query.status,
19977
20404
  subtype: query.subtype,
19978
20405
  q: query.q
19979
20406
  };
19980
20407
  const facets = computeFindingFacets(allGroups, filterOpts);
19981
20408
  const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
20409
+ const statusFilter = query.status ?? [];
19982
20410
  const totals = {
19983
- findings: sorted.reduce((acc, g) => acc + g.instanceCount, 0),
20411
+ findings: sorted.reduce((acc, g) => {
20412
+ if (statusFilter.length === 0) return acc + g.instanceCount;
20413
+ const agg = aggregates.get(g.id);
20414
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
20415
+ }, 0),
19984
20416
  groups: sorted.length
19985
20417
  };
19986
20418
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
19987
- const items = sorted.slice(0, limit);
20419
+ const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20420
+ const items = sorted.slice(0, limit).map(
20421
+ (g) => statusSet ? {
20422
+ ...g,
20423
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20424
+ } : g
20425
+ );
19988
20426
  return Promise.resolve({
19989
20427
  totals,
19990
20428
  facets,
@@ -19998,45 +20436,62 @@ var SqliteFindingsRepository = class {
19998
20436
  * buildFindingGroups cannot recover from a preview. Bounded by the number of
19999
20437
  * distinct rule_ids (the installed packs' rules), not by the store's size.
20000
20438
  *
20001
- * The per-instance sets ride back as group_concat lists of RAW DB values —
20002
- * source_tool, action_taken, and the (kind, has-key, latest-status) triples
20003
- * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
20004
- * status keeps the classifier itself in @akasecurity/schema, where
20005
- * severitySummary's SQL and this query can't drift apart on what 'resolved'
20006
- * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
20439
+ * A single scan, folded in two levels: the inner SELECT groups by
20440
+ * (rule_id, status tuple) so each (kind, has-key, latest-status) combination
20441
+ * carries its instance count countInstancesByStatus needs those counts for
20442
+ * status-scoped totals — and the outer SELECT folds the tuples back to one
20443
+ * row per rule. The per-instance sets ride back as group_concat lists of RAW
20444
+ * DB values source_tool, action_taken, and the tuples deriveFindingStatus
20445
+ * consumes. Aggregating the status INPUTS rather than a status keeps the
20446
+ * classifier itself in @akasecurity/schema, where severitySummary's SQL and
20447
+ * this query can't drift apart on what 'resolved' means (see
20448
+ * resolution-sql.ts). The concat-of-concats can repeat a value across
20449
+ * tuples; the schema mappers dedupe, and each set is bounded by an enum, so
20007
20450
  * a group's row stays small however many findings it holds.
20008
20451
  *
20009
20452
  * `withSearchText` is the exception, and the one column here that does NOT
20010
- * stay small: the group's distinct repos/filePaths, whose size tracks how many
20011
- * distinct paths a rule fired across — for a rule hitting mostly-unique paths
20012
- * that is a string proportional to the store (~8MB over 200k distinct paths,
20013
- * and buildHaystack lowercases a second copy). It buys `q` the ability to
20014
- * match an instance outside the preview, which searching the preview alone
20015
- * would silently lose, so it is fetched only when the request actually
20016
- * carries a `q`.
20453
+ * stay small: the group's per-tuple-distinct repos/filePaths, whose size
20454
+ * tracks how many distinct paths a rule fired across — for a rule hitting
20455
+ * mostly-unique paths that is a string proportional to the store (~8MB over
20456
+ * 200k distinct paths, and buildHaystack lowercases a second copy). It buys
20457
+ * `q` the ability to match an instance outside the preview, which searching
20458
+ * the preview alone would silently lose, so it is fetched only when the
20459
+ * request actually carries a `q`. (Substring matching is unaffected by a
20460
+ * path repeating across tuples.)
20017
20461
  */
20018
20462
  groupAggregates(withSearchText, scope) {
20019
- const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
20020
- group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
20021
- group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
20463
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20464
+ group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
20465
+ group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
20022
20466
  const rows = this.db.prepare(
20023
- `SELECT f.rule_id AS rule_id,
20024
- count(*) AS instance_count,
20025
- max(e.occurred_at) AS latest_at,
20026
- group_concat(DISTINCT e.source_tool) AS source_tools,
20027
- group_concat(DISTINCT f.action_taken) AS actions_taken,
20028
- group_concat(DISTINCT (
20029
- e.kind || '${TUPLE_SEP}' ||
20030
- (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
20031
- coalesce(latest.status, '')
20032
- )) AS status_inputs
20033
- ${searchTextColumns}
20034
- FROM findings f
20035
- JOIN events e ON e.id = f.event_id
20036
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20037
- ON latest.finding_key = f.finding_key
20038
- ${scope.predicate}
20039
- GROUP BY f.rule_id`
20467
+ `SELECT rule_id,
20468
+ sum(tuple_count) AS instance_count,
20469
+ max(latest_at) AS latest_at,
20470
+ group_concat(source_tools) AS source_tools,
20471
+ group_concat(actions_taken) AS actions_taken,
20472
+ group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
20473
+ group_concat(repos) AS repos,
20474
+ group_concat(files) AS files,
20475
+ group_concat(tool_names) AS tool_names
20476
+ FROM (
20477
+ SELECT d.rule_id AS rule_id,
20478
+ e.event_type || '${TUPLE_SEP}' ||
20479
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
20480
+ coalesce(latest.status, '') AS status_tuple,
20481
+ count(*) AS tuple_count,
20482
+ max(e.started_at) AS latest_at,
20483
+ group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
20484
+ group_concat(DISTINCT f.action_taken) AS actions_taken
20485
+ ${innerSearchColumns}
20486
+ FROM inspection_findings f
20487
+ JOIN audit_events e ON e.id = f.audit_event_id
20488
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20489
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20490
+ ON latest.finding_key = f.finding_key
20491
+ ${scope.predicate}
20492
+ GROUP BY d.rule_id, status_tuple
20493
+ )
20494
+ GROUP BY rule_id`
20040
20495
  ).all(scope.params);
20041
20496
  return new Map(
20042
20497
  rows.map((r) => [
@@ -20046,13 +20501,14 @@ var SqliteFindingsRepository = class {
20046
20501
  sourceTools: splitConcat(r.source_tools),
20047
20502
  actionsTaken: splitConcat(r.actions_taken),
20048
20503
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
20049
- const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
20504
+ const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
20050
20505
  return {
20051
20506
  // deriveFindingStatus only distinguishes null from non-null here,
20052
20507
  // so the marker stands in for the key itself (never rendered).
20053
20508
  kind,
20054
20509
  findingKey: keyMarker === "" ? null : keyMarker,
20055
- latestResolutionStatus: latestStatus === "" ? null : latestStatus
20510
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus,
20511
+ count: Number(count)
20056
20512
  };
20057
20513
  }),
20058
20514
  latestDetectedAt: epochMillisToIso(r.latest_at),
@@ -20069,10 +20525,21 @@ var SqliteFindingsRepository = class {
20069
20525
  );
20070
20526
  }
20071
20527
  healthSummary() {
20072
- const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
20528
+ const total = countScalar(
20529
+ this.db,
20530
+ `SELECT count(*) AS n FROM inspection_findings f
20531
+ JOIN audit_events e ON e.id = f.audit_event_id
20532
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
20533
+ );
20073
20534
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
20074
20535
  const grouped = allRows(
20075
- this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
20536
+ this.db.prepare(
20537
+ `SELECT f.action_taken AS action_taken, count(*) AS c
20538
+ FROM inspection_findings f
20539
+ JOIN audit_events e ON e.id = f.audit_event_id
20540
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20541
+ GROUP BY f.action_taken`
20542
+ )
20076
20543
  );
20077
20544
  for (const row of grouped) {
20078
20545
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
@@ -20080,12 +20547,15 @@ var SqliteFindingsRepository = class {
20080
20547
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
20081
20548
  const sevRows = allRows(
20082
20549
  this.db.prepare(
20083
- `SELECT f.severity AS severity, count(*) AS c
20084
- FROM findings f
20550
+ `SELECT d.severity AS severity, count(*) AS c
20551
+ FROM inspection_findings f
20552
+ JOIN audit_events e ON e.id = f.audit_event_id
20553
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20085
20554
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20086
20555
  ON latest.finding_key = f.finding_key
20087
- WHERE latest.status IS NULL OR latest.status != 'resolved'
20088
- GROUP BY f.severity`
20556
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20557
+ AND (latest.status IS NULL OR latest.status != 'resolved')
20558
+ GROUP BY d.severity`
20089
20559
  )
20090
20560
  );
20091
20561
  for (const row of sevRows) {
@@ -20106,9 +20576,11 @@ var SqliteFindingsRepository = class {
20106
20576
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
20107
20577
  const rows = allRows(
20108
20578
  this.db.prepare(
20109
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
20110
- FROM findings f JOIN events e ON e.id = f.event_id
20111
- WHERE e.occurred_at >= :since
20579
+ `SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
20580
+ FROM inspection_findings f
20581
+ JOIN audit_events e ON e.id = f.audit_event_id
20582
+ WHERE e.started_at >= :since
20583
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20112
20584
  GROUP BY day, f.action_taken`
20113
20585
  ),
20114
20586
  { since }
@@ -20173,15 +20645,59 @@ var SqliteInspectionFindingsRepository = class {
20173
20645
  this.insertStmt = db.prepare(
20174
20646
  `INSERT INTO inspection_findings
20175
20647
  (id, audit_event_id, inspection_definition_id, classified_data_id,
20176
- span_start, span_end, masked_match, action_taken, confidence)
20648
+ span_start, span_end, masked_match, action_taken, confidence,
20649
+ finding_key, first_detected_at)
20177
20650
  VALUES
20178
20651
  (:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
20179
- :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence)
20180
- ON CONFLICT(id) DO NOTHING`
20652
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
20653
+ :findingKey,
20654
+ COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
20655
+ ON CONFLICT(id) DO UPDATE SET
20656
+ inspection_definition_id = excluded.inspection_definition_id
20657
+ ON CONFLICT (finding_key) DO UPDATE SET
20658
+ audit_event_id = excluded.audit_event_id,
20659
+ inspection_definition_id = excluded.inspection_definition_id,
20660
+ classified_data_id = excluded.classified_data_id,
20661
+ span_start = excluded.span_start,
20662
+ span_end = excluded.span_end,
20663
+ masked_match = excluded.masked_match,
20664
+ action_taken = excluded.action_taken,
20665
+ confidence = excluded.confidence`
20666
+ );
20667
+ this.sessionDupStmt = db.prepare(
20668
+ `SELECT 1 FROM inspection_findings f
20669
+ JOIN audit_events e ON e.id = f.audit_event_id
20670
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20671
+ WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
20672
+ AND e.root_session_id = :sessionId
20673
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20674
+ LIMIT 1`
20675
+ );
20676
+ this.eventDupStmt = db.prepare(
20677
+ `SELECT 1 FROM inspection_findings f
20678
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20679
+ WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
20680
+ AND f.masked_match = :maskedMatch
20681
+ AND f.span_start = :spanStart AND f.span_end = :spanEnd
20682
+ LIMIT 1`
20181
20683
  );
20182
20684
  }
20183
20685
  db;
20184
20686
  insertStmt;
20687
+ sessionDupStmt;
20688
+ eventDupStmt;
20689
+ // True when an earlier event in the same session already recorded a finding
20690
+ // with the same rule and masked value. The current event's own findings are
20691
+ // inserted one at a time in caller order, so an earlier finding in the SAME
20692
+ // recordCapture call is visible to a later duplicate check within it too.
20693
+ isSessionDuplicate(ruleId, maskedMatch, sessionId) {
20694
+ return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
20695
+ }
20696
+ // True when this exact detection (rule + masked value + span) is already
20697
+ // recorded against the given audit event.
20698
+ isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
20699
+ return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
20700
+ }
20185
20701
  insertFinding(input) {
20186
20702
  const row = toInspectionFindingRow(input);
20187
20703
  this.insertStmt.run(
@@ -20194,7 +20710,9 @@ var SqliteInspectionFindingsRepository = class {
20194
20710
  spanEnd: row.spanEnd,
20195
20711
  maskedMatch: row.maskedMatch,
20196
20712
  actionTaken: row.actionTaken,
20197
- confidence: row.confidence
20713
+ confidence: row.confidence,
20714
+ findingKey: row.findingKey,
20715
+ firstDetectedAt: row.firstDetectedAt
20198
20716
  })
20199
20717
  );
20200
20718
  }
@@ -20466,7 +20984,7 @@ var SqliteInstalledPacksRepository = class {
20466
20984
  installedRuleset() {
20467
20985
  const rows = allRows(
20468
20986
  this.db.prepare(
20469
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20987
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
20470
20988
  )
20471
20989
  );
20472
20990
  const out = {
@@ -20474,7 +20992,8 @@ var SqliteInstalledPacksRepository = class {
20474
20992
  enabledPacks: 0,
20475
20993
  rules: [],
20476
20994
  invalidRules: 0,
20477
- ruleActions: /* @__PURE__ */ new Map()
20995
+ ruleActions: /* @__PURE__ */ new Map(),
20996
+ ruleVersions: /* @__PURE__ */ new Map()
20478
20997
  };
20479
20998
  for (const row of rows) {
20480
20999
  if (!intToBool(row.enabled)) continue;
@@ -20496,6 +21015,7 @@ var SqliteInstalledPacksRepository = class {
20496
21015
  if (parsed.success) {
20497
21016
  out.rules.push(parsed.data);
20498
21017
  out.ruleActions.set(parsed.data.id, action);
21018
+ out.ruleVersions.set(parsed.data.id, row.version);
20499
21019
  } else out.invalidRules += 1;
20500
21020
  }
20501
21021
  }
@@ -21670,19 +22190,19 @@ var SqliteResolutionsRepository = class {
21670
22190
  );
21671
22191
  this.openAtRestStmt = db.prepare(
21672
22192
  `SELECT DISTINCT f.finding_key AS finding_key
21673
- FROM findings f
21674
- JOIN events e ON e.id = f.event_id
21675
- WHERE e.kind = 'code_change'
21676
- AND json_extract(e.metadata, '$.filePath') = :path
22193
+ FROM inspection_findings f
22194
+ JOIN audit_events e ON e.id = f.audit_event_id
22195
+ WHERE e.event_type = 'code_change'
22196
+ AND json_extract(e.attributes, '$.file_path') = :path
21677
22197
  AND f.finding_key IS NOT NULL
21678
22198
  AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
21679
22199
  );
21680
22200
  this.resolvedAtRestStmt = db.prepare(
21681
22201
  `SELECT DISTINCT f.finding_key AS finding_key
21682
- FROM findings f
21683
- JOIN events e ON e.id = f.event_id
21684
- WHERE e.kind = 'code_change'
21685
- AND json_extract(e.metadata, '$.filePath') = :path
22202
+ FROM inspection_findings f
22203
+ JOIN audit_events e ON e.id = f.audit_event_id
22204
+ WHERE e.event_type = 'code_change'
22205
+ AND json_extract(e.attributes, '$.file_path') = :path
21686
22206
  AND f.finding_key IS NOT NULL
21687
22207
  AND ${latestResolutionStatusSql("f")} = 'resolved'`
21688
22208
  );
@@ -21750,6 +22270,35 @@ var SqliteResolutionsRepository = class {
21750
22270
  }
21751
22271
  };
21752
22272
 
22273
+ // ../../packages/persistence/src/repositories/rule-probe-cache.ts
22274
+ var SqliteRuleProbeCacheRepository = class {
22275
+ constructor(db) {
22276
+ this.db = db;
22277
+ this.upsertStmt = db.prepare(
22278
+ `INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
22279
+ VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
22280
+ ON CONFLICT (rule_key) DO UPDATE SET
22281
+ verdict = excluded.verdict,
22282
+ worst_probe_ms = excluded.worst_probe_ms,
22283
+ checked_at = excluded.checked_at`
22284
+ );
22285
+ this.readStmt = db.prepare(
22286
+ `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
22287
+ );
22288
+ }
22289
+ db;
22290
+ upsertStmt;
22291
+ readStmt;
22292
+ getVerdict(ruleKey) {
22293
+ return getRow(this.readStmt, { ruleKey });
22294
+ }
22295
+ setVerdict(ruleKey, verdict, worstProbeMs) {
22296
+ failOpenTransaction(this.db, () => {
22297
+ this.upsertStmt.run({ ruleKey, verdict, worstProbeMs, checkedAt: Date.now() });
22298
+ });
22299
+ }
22300
+ };
22301
+
21753
22302
  // ../../packages/persistence/src/repositories/scan-ledger.ts
21754
22303
  var SqliteScanLedgerRepository = class {
21755
22304
  constructor(db) {
@@ -21876,25 +22425,27 @@ var SqliteSecurityRepository = class {
21876
22425
  severitySummary() {
21877
22426
  const rows = allRows(
21878
22427
  this.db.prepare(
21879
- `SELECT f.severity AS severity,
22428
+ `SELECT d.severity AS severity,
21880
22429
  COUNT(*) AS count,
21881
22430
  SUM(CASE
21882
- WHEN e.kind != 'code_change' THEN 1
22431
+ WHEN e.event_type != 'code_change' THEN 1
21883
22432
  WHEN f.finding_key IS NULL THEN 0
21884
22433
  WHEN latest.status = 'resolved' THEN 1
21885
22434
  ELSE 0
21886
22435
  END) AS caught,
21887
22436
  SUM(CASE
21888
- WHEN e.kind = 'code_change'
22437
+ WHEN e.event_type = 'code_change'
21889
22438
  AND f.finding_key IS NOT NULL
21890
22439
  AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
21891
22440
  ELSE 0
21892
22441
  END) AS open_at_rest
21893
- FROM findings f
21894
- JOIN events e ON e.id = f.event_id
22442
+ FROM inspection_findings f
22443
+ JOIN audit_events e ON e.id = f.audit_event_id
22444
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
21895
22445
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21896
22446
  ON latest.finding_key = f.finding_key
21897
- GROUP BY f.severity`
22447
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22448
+ GROUP BY d.severity`
21898
22449
  )
21899
22450
  );
21900
22451
  const byRow = new Map(rows.map((r) => [r.severity, r]));
@@ -21960,7 +22511,7 @@ var SqliteSecurityRepository = class {
21960
22511
  // Mean time-to-remediate per bucket, split by severity — a sibling of
21961
22512
  // findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
21962
22513
  // on a different timestamp: findingsTimeseries buckets by first-detection
21963
- // (events.occurred_at), this buckets by resolution time (the latest
22514
+ // (audit_events.started_at), this buckets by resolution time (the latest
21964
22515
  // finding_resolution row's resolved_at) — it's a "resolved in this bucket"
21965
22516
  // trend, not a "detected in this bucket" one. Only findings whose LATEST
21966
22517
  // resolution row (latest-resolution-wins, same correlated subquery as
@@ -21985,30 +22536,20 @@ var SqliteSecurityRepository = class {
21985
22536
  // first_detected_at is the PRESERVED first-detection time (set once on a
21986
22537
  // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21987
22538
  // measures from first sighting — not the latest re-scan's event, whose
21988
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21989
- // the parent event's occurred_at defends against any legacy/edge row the
21990
- // backfill left null.
21991
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21992
- (
21993
- SELECT fr.status FROM finding_resolution fr
21994
- WHERE fr.finding_key = f.finding_key
21995
- ORDER BY fr.created_at DESC, fr.rowid DESC
21996
- LIMIT 1
21997
- ) AS latest_status,
21998
- (
21999
- SELECT fr.method FROM finding_resolution fr
22000
- WHERE fr.finding_key = f.finding_key
22001
- ORDER BY fr.created_at DESC, fr.rowid DESC
22002
- LIMIT 1
22003
- ) AS latest_method,
22004
- (
22005
- SELECT fr.resolved_at FROM finding_resolution fr
22006
- WHERE fr.finding_key = f.finding_key
22007
- ORDER BY fr.created_at DESC, fr.rowid DESC
22008
- LIMIT 1
22009
- ) AS latest_resolved_at
22010
- FROM findings f JOIN events e ON e.id = f.event_id
22539
+ // started_at the upsert overwrites onto inspection_findings.audit_event_id.
22540
+ // COALESCE onto the parent event's started_at defends against any
22541
+ // legacy/edge row the backfill left null.
22542
+ `SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
22543
+ latest.status AS latest_status,
22544
+ latest.method AS latest_method,
22545
+ latest.resolved_at AS latest_resolved_at
22546
+ FROM inspection_findings f
22547
+ JOIN audit_events e ON e.id = f.audit_event_id
22548
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22549
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22550
+ ON latest.finding_key = f.finding_key
22011
22551
  WHERE f.finding_key IS NOT NULL
22552
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22012
22553
  AND EXISTS (
22013
22554
  SELECT 1 FROM finding_resolution fr
22014
22555
  WHERE fr.finding_key = f.finding_key
@@ -22055,11 +22596,13 @@ var SqliteSecurityRepository = class {
22055
22596
  const from = now - RANGE_DAYS[range] * DAY_MS4;
22056
22597
  const rows = allRows(
22057
22598
  this.db.prepare(
22058
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
22059
- FROM findings f JOIN events e ON e.id = f.event_id
22060
- WHERE e.occurred_at >= :from AND e.occurred_at < :to
22061
- AND json_extract(e.metadata, '$.repo') IS NOT NULL
22062
- AND json_extract(e.metadata, '$.repo') != ''
22599
+ `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
22600
+ FROM inspection_findings f
22601
+ JOIN audit_events e ON e.id = f.audit_event_id
22602
+ WHERE e.started_at >= :from AND e.started_at < :to
22603
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22604
+ AND json_extract(e.attributes, '$.repo') IS NOT NULL
22605
+ AND json_extract(e.attributes, '$.repo') != ''
22063
22606
  GROUP BY repo
22064
22607
  ORDER BY c DESC, repo
22065
22608
  LIMIT :limit`
@@ -22083,44 +22626,28 @@ var SqliteSecurityRepository = class {
22083
22626
  // secret came back) is excluded — it is not currently resolved. Legacy
22084
22627
  // at-rest findings with finding_key IS NULL are excluded outright (the
22085
22628
  // resolution lifecycle can never attach to them). Path comes from the
22086
- // finding's parent event (kind 'code_change', metadata.filePath) — mirrors
22087
- // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
22088
- // capped at `limit`.
22629
+ // finding's parent event (event_type 'code_change', attributes.file_path) —
22630
+ // mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
22631
+ // DESC, capped at `limit`.
22089
22632
  recentlyResolved(limit = 20) {
22090
22633
  const rows = allRows(
22091
22634
  this.db.prepare(
22092
22635
  `SELECT f.finding_key AS finding_key,
22093
- f.rule_id AS rule_id,
22094
- f.severity AS severity,
22095
- json_extract(e.metadata, '$.filePath') AS path,
22096
- COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at,
22097
- (
22098
- SELECT fr.resolved_at FROM finding_resolution fr
22099
- WHERE fr.finding_key = f.finding_key
22100
- ORDER BY fr.created_at DESC, fr.rowid DESC
22101
- LIMIT 1
22102
- ) AS latest_resolved_at
22103
- FROM findings f JOIN events e ON e.id = f.event_id
22104
- WHERE e.kind = 'code_change'
22105
- AND f.finding_key IS NOT NULL
22106
- AND (
22107
- SELECT fr.status FROM finding_resolution fr
22108
- WHERE fr.finding_key = f.finding_key
22109
- ORDER BY fr.created_at DESC, fr.rowid DESC
22110
- LIMIT 1
22111
- ) = 'resolved'
22112
- AND (
22113
- SELECT fr.method FROM finding_resolution fr
22114
- WHERE fr.finding_key = f.finding_key
22115
- ORDER BY fr.created_at DESC, fr.rowid DESC
22116
- LIMIT 1
22117
- ) = 'fixed-at-source'
22118
- AND (
22119
- SELECT fr.resolved_at FROM finding_resolution fr
22120
- WHERE fr.finding_key = f.finding_key
22121
- ORDER BY fr.created_at DESC, fr.rowid DESC
22122
- LIMIT 1
22123
- ) IS NOT NULL
22636
+ d.rule_id AS rule_id,
22637
+ d.severity AS severity,
22638
+ json_extract(e.attributes, '$.file_path') AS path,
22639
+ COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
22640
+ latest.resolved_at AS latest_resolved_at
22641
+ FROM inspection_findings f
22642
+ JOIN audit_events e ON e.id = f.audit_event_id
22643
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22644
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22645
+ ON latest.finding_key = f.finding_key
22646
+ WHERE e.event_type = 'code_change'
22647
+ AND f.finding_key IS NOT NULL
22648
+ AND latest.status = 'resolved'
22649
+ AND latest.method = 'fixed-at-source'
22650
+ AND latest.resolved_at IS NOT NULL
22124
22651
  ORDER BY latest_resolved_at DESC
22125
22652
  LIMIT :limit`
22126
22653
  ),
@@ -22139,15 +22666,18 @@ var SqliteSecurityRepository = class {
22139
22666
  return Promise.resolve({ items });
22140
22667
  }
22141
22668
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
22142
- // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
22669
+ // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
22143
22670
  // numeric and the JS aggregations bucket/split on ms directly.
22144
22671
  findingsInRange(fromMs, toMs) {
22145
22672
  const rows = allRows(
22146
22673
  this.db.prepare(
22147
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
22148
- FROM findings f JOIN events e ON e.id = f.event_id
22149
- WHERE e.occurred_at >= :from AND e.occurred_at < :to
22150
- ORDER BY e.occurred_at`
22674
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
22675
+ FROM inspection_findings f
22676
+ JOIN audit_events e ON e.id = f.audit_event_id
22677
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22678
+ WHERE e.started_at >= :from AND e.started_at < :to
22679
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22680
+ ORDER BY e.started_at`
22151
22681
  ),
22152
22682
  { from: fromMs, to: toMs }
22153
22683
  );
@@ -22161,11 +22691,50 @@ var SqliteSecurityRepository = class {
22161
22691
 
22162
22692
  // ../../packages/persistence/src/repositories/shares.ts
22163
22693
  import { randomUUID as randomUUID7 } from "crypto";
22164
- var KIND_ORDER = ["provider", "internal", "ip"];
22694
+ var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22695
+ var IN_CHUNK = 500;
22696
+ var KIND_ORDER = ["provider", "internal", "external", "ip"];
22697
+ var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
22698
+ var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
22699
+ LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
22165
22700
  var CALL_SITE_EMBED_CAP = 200;
22166
22701
  function parseNetwork(networkJson) {
22167
22702
  return safeJson(networkJson, null);
22168
22703
  }
22704
+ function capHits(all, mode) {
22705
+ if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
22706
+ return { hits: [...all], droppedFiles: [], truncated: false };
22707
+ }
22708
+ if (mode === "walk") {
22709
+ return {
22710
+ hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
22711
+ droppedFiles: [],
22712
+ truncated: true
22713
+ };
22714
+ }
22715
+ const byFile = /* @__PURE__ */ new Map();
22716
+ for (const hit of all) {
22717
+ const bucket = byFile.get(hit.site.file);
22718
+ if (bucket === void 0) byFile.set(hit.site.file, [hit]);
22719
+ else bucket.push(hit);
22720
+ }
22721
+ const hits = [];
22722
+ const droppedFiles = [];
22723
+ for (const [file2, bucket] of byFile) {
22724
+ if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
22725
+ else hits.push(...bucket);
22726
+ }
22727
+ return { hits, droppedFiles, truncated: true };
22728
+ }
22729
+ function withoutDroppedFiles(reconcile, droppedFiles) {
22730
+ if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
22731
+ const dropped = new Set(droppedFiles);
22732
+ return {
22733
+ mode: "ledger",
22734
+ scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
22735
+ deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
22736
+ };
22737
+ }
22169
22738
  function toEndpointSummary(row) {
22170
22739
  return {
22171
22740
  id: row.id,
@@ -22256,13 +22825,15 @@ var SqliteSharesRepository = class {
22256
22825
  const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22257
22826
  const insecure = countScalar(
22258
22827
  this.db,
22259
- "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
22828
+ `SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
22829
+ WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
22260
22830
  );
22261
22831
  const needsReview = countScalar(
22262
22832
  this.db,
22263
22833
  `SELECT count(DISTINCT d.id) AS n
22264
22834
  FROM share_destination d
22265
- LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
22835
+ LEFT JOIN share_endpoint e ON e.destination_id = d.id
22836
+ AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
22266
22837
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
22267
22838
  );
22268
22839
  const kindCounts = countBy(
@@ -22272,6 +22843,7 @@ var SqliteSharesRepository = class {
22272
22843
  const byKind = {
22273
22844
  provider: kindCounts.get("provider") ?? 0,
22274
22845
  internal: kindCounts.get("internal") ?? 0,
22846
+ external: kindCounts.get("external") ?? 0,
22275
22847
  ip: kindCounts.get("ip") ?? 0
22276
22848
  };
22277
22849
  const trustCounts = countBy(
@@ -22347,23 +22919,316 @@ var SqliteSharesRepository = class {
22347
22919
  // real edit from a no-such-destination.
22348
22920
  /**
22349
22921
  * Set (decision) or clear (null) the egress decision override for a destination.
22350
- * `null` deletes the override row → reverts to the trust default.
22922
+ * `null` deletes the override rows → reverts to the trust default.
22923
+ *
22924
+ * The written row carries both the destination id and its host, so the
22925
+ * decision re-attaches by host after the destination is pruned and
22926
+ * re-detected under a fresh id. Rows written before the host column existed
22927
+ * (host NULL, matched by destination id) are replaced rather than left to
22928
+ * shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
22929
+ * would otherwise race a concurrent prune.
22351
22930
  */
22352
22931
  setEgressDecision(destinationId, decision) {
22353
- const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
22354
- if (exists === void 0) return false;
22355
- if (decision === null) {
22356
- this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
22357
- return true;
22932
+ let existed = false;
22933
+ withTransaction(
22934
+ this.db,
22935
+ () => {
22936
+ const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
22937
+ if (dest === void 0) return;
22938
+ existed = true;
22939
+ this.db.prepare(
22940
+ `DELETE FROM egress_decision_override
22941
+ WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
22942
+ ).run({ host: dest.host, destinationId });
22943
+ if (decision === null) return;
22944
+ this.db.prepare(
22945
+ `INSERT INTO egress_decision_override
22946
+ (id, destination_id, host, decision, created_at, updated_at)
22947
+ VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22948
+ ).run({
22949
+ id: randomUUID7(),
22950
+ destinationId,
22951
+ host: dest.host,
22952
+ decision,
22953
+ now: Date.now()
22954
+ });
22955
+ },
22956
+ "IMMEDIATE"
22957
+ );
22958
+ return existed;
22959
+ }
22960
+ /**
22961
+ * Record one project's statically-extracted egress: reconcile the previously
22962
+ * stored call sites against this scan, upsert destination → endpoint → call
22963
+ * site for every hit, confirm `last_seen` on everything the project still
22964
+ * references, and drop what no longer has evidence.
22965
+ *
22966
+ * Reconciliation keys on `projectKey` alone; `project` and `projectId` are
22967
+ * display payload and never scope a delete. The whole write is one
22968
+ * transaction: a failure leaves the project's previous inventory exactly as
22969
+ * it was, and THROWS rather than reporting a partial write — callers decide
22970
+ * their own fail-open behavior, and the scanner additionally withholds its
22971
+ * ledger commit so the next scan retries.
22972
+ *
22973
+ * Over-cap input is truncated at a FILE boundary, and the files that lost
22974
+ * their hits are both excluded from the reconcile delete and named in
22975
+ * `droppedFiles`. That pairing is what keeps truncation non-destructive on
22976
+ * the ledger path: a dropped file keeps whatever rows it already had, and its
22977
+ * caller withholds the ledger entry so the next scan reads it again.
22978
+ */
22979
+ recordProjectEgress(input) {
22980
+ const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
22981
+ const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
22982
+ const now = Date.now();
22983
+ let summary = {
22984
+ destinations: 0,
22985
+ endpoints: 0,
22986
+ callSites: 0,
22987
+ truncated,
22988
+ droppedFiles
22989
+ };
22990
+ withTransaction(
22991
+ this.db,
22992
+ () => {
22993
+ const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
22994
+ this.reconcileCallSites(input.projectKey, reconcile);
22995
+ this.upsertHits(input, hits, projectId, now);
22996
+ this.confirmLastSeen(input.projectKey, now);
22997
+ this.pruneOrphans();
22998
+ summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
22999
+ },
23000
+ "IMMEDIATE"
23001
+ );
23002
+ return summary;
23003
+ }
23004
+ // ─── Egress write internals ──────────────────────────────────────────────────
23005
+ /**
23006
+ * Clear the stored call sites this scan is responsible for re-creating.
23007
+ *
23008
+ * Each pipeline may only delete rows its own walker could have produced. The
23009
+ * fs walk behind 'walk' mode never descends into dot-directories, so its
23010
+ * delete excludes dot-path files — those rows are the plugin scanner's to
23011
+ * reconcile, and deleting them here would make the two pipelines erase each
23012
+ * other's rows on every alternating scan. 'ledger' mode names its files
23013
+ * outright and never mass-deletes, so rows the fs walk contributed for files
23014
+ * the scanner skips (vendored, oversize) survive it.
23015
+ */
23016
+ reconcileCallSites(projectKey, reconcile) {
23017
+ if (reconcile.mode === "walk") {
23018
+ const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
23019
+ this.db.prepare(
23020
+ `DELETE FROM share_call_site
23021
+ WHERE project_key = :key
23022
+ AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
23023
+ AND file NOT LIKE '.%'
23024
+ AND file NOT LIKE '%/.%'`
23025
+ ).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
23026
+ return;
23027
+ }
23028
+ const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
23029
+ for (let i = 0; i < files.length; i += IN_CHUNK) {
23030
+ const chunk = files.slice(i, i + IN_CHUNK);
23031
+ this.db.prepare(
23032
+ `DELETE FROM share_call_site
23033
+ WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
23034
+ ).run(projectKey, ...chunk);
23035
+ }
23036
+ }
23037
+ /**
23038
+ * Upsert every hit as destination → endpoint → call site. Destinations key on
23039
+ * `host` and endpoints on `(destination_id, method, url)`, both shared across
23040
+ * projects; only the call site carries `project_key`. A destination's `note`
23041
+ * is user-owned and never overwritten. The id caches keep one upsert per
23042
+ * distinct host and endpoint, so the first hit for a host supplies its
23043
+ * classification for this batch.
23044
+ */
23045
+ upsertHits(input, hits, projectId, now) {
23046
+ if (hits.length === 0) return;
23047
+ const destStmt = this.db.prepare(
23048
+ `INSERT INTO share_destination
23049
+ (id, kind, name, host, category, trust, network_json, last_seen, provenance,
23050
+ created_at, updated_at)
23051
+ VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
23052
+ ON CONFLICT (host) DO UPDATE SET
23053
+ kind = excluded.kind,
23054
+ name = excluded.name,
23055
+ category = excluded.category,
23056
+ trust = excluded.trust,
23057
+ network_json = excluded.network_json,
23058
+ last_seen = excluded.last_seen,
23059
+ updated_at = excluded.updated_at`
23060
+ );
23061
+ const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
23062
+ const endpointStmt = this.db.prepare(
23063
+ `INSERT INTO share_endpoint
23064
+ (id, destination_id, method, transport, url, template, data_class, last_seen,
23065
+ created_at, updated_at)
23066
+ VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
23067
+ :now, :now)
23068
+ ON CONFLICT (destination_id, method, url) DO UPDATE SET
23069
+ transport = excluded.transport,
23070
+ template = excluded.template,
23071
+ data_class = excluded.data_class,
23072
+ last_seen = excluded.last_seen,
23073
+ updated_at = excluded.updated_at`
23074
+ );
23075
+ const endpointIdStmt = this.db.prepare(
23076
+ "SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
23077
+ );
23078
+ const siteStmt = this.db.prepare(
23079
+ `INSERT INTO share_call_site
23080
+ (id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
23081
+ project_id, created_at, updated_at)
23082
+ VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
23083
+ :vendored, :projectId, :now, :now)
23084
+ ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
23085
+ snippet = excluded.snippet,
23086
+ dynamic = excluded.dynamic,
23087
+ vendored = excluded.vendored,
23088
+ project = excluded.project,
23089
+ project_id = COALESCE(excluded.project_id, share_call_site.project_id),
23090
+ updated_at = excluded.updated_at`
23091
+ );
23092
+ const destIds = /* @__PURE__ */ new Map();
23093
+ const endpointIds = /* @__PURE__ */ new Map();
23094
+ for (const hit of hits) {
23095
+ let destinationId = destIds.get(hit.host);
23096
+ if (destinationId === void 0) {
23097
+ destStmt.run({
23098
+ id: randomUUID7(),
23099
+ kind: hit.kind,
23100
+ name: hit.name,
23101
+ host: hit.host,
23102
+ category: hit.category,
23103
+ trust: hit.trust,
23104
+ networkJson: hit.network === null ? null : JSON.stringify(hit.network),
23105
+ now
23106
+ });
23107
+ destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
23108
+ destIds.set(hit.host, destinationId);
23109
+ }
23110
+ const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
23111
+ let endpointId = endpointIds.get(endpointKey);
23112
+ if (endpointId === void 0) {
23113
+ endpointStmt.run({
23114
+ id: randomUUID7(),
23115
+ destinationId,
23116
+ method: hit.method,
23117
+ transport: hit.transport,
23118
+ url: hit.url,
23119
+ template: boolToInt(hit.template),
23120
+ dataClass: hit.dataClass,
23121
+ now
23122
+ });
23123
+ endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
23124
+ endpointIds.set(endpointKey, endpointId);
23125
+ }
23126
+ siteStmt.run({
23127
+ id: randomUUID7(),
23128
+ endpointId,
23129
+ project: input.project,
23130
+ projectKey: input.projectKey,
23131
+ file: hit.site.file,
23132
+ line: hit.site.line,
23133
+ snippet: hit.site.snippet,
23134
+ dynamic: boolToInt(hit.site.dynamic),
23135
+ vendored: boolToInt(hit.site.vendored),
23136
+ projectId,
23137
+ now
23138
+ });
22358
23139
  }
23140
+ }
23141
+ /**
23142
+ * The source-project id this project's stored call sites already carry, if
23143
+ * any. Only the pipeline that resolves a source project supplies one; the
23144
+ * other passes null and inherits this, so the link stops flapping between a
23145
+ * real id and NULL depending on which pipeline ran last. The value is a
23146
+ * per-project attribute stored redundantly on each row, so any row's is
23147
+ * representative.
23148
+ */
23149
+ knownProjectId(projectKey) {
23150
+ return getRow(
23151
+ this.db.prepare(
23152
+ `SELECT project_id AS projectId FROM share_call_site
23153
+ WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
23154
+ ),
23155
+ [projectKey]
23156
+ )?.projectId ?? null;
23157
+ }
23158
+ /**
23159
+ * Stamp `last_seen` on every endpoint and destination this project still
23160
+ * references — including rows the scan preserved rather than re-wrote, so a
23161
+ * ledger-skipped file's references don't decay into "stale" on the page.
23162
+ */
23163
+ confirmLastSeen(projectKey, now) {
22359
23164
  this.db.prepare(
22360
- `INSERT INTO egress_decision_override (id, destination_id, decision, created_at, updated_at)
22361
- VALUES (:id, :destinationId, :decision, :now, :now)
22362
- ON CONFLICT (destination_id) DO UPDATE SET
22363
- decision = excluded.decision,
22364
- updated_at = excluded.updated_at`
22365
- ).run({ id: randomUUID7(), destinationId, decision, now: Date.now() });
22366
- return true;
23165
+ `UPDATE share_endpoint SET last_seen = :now, updated_at = :now
23166
+ WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
23167
+ ).run({ now, key: projectKey });
23168
+ this.db.prepare(
23169
+ `UPDATE share_destination SET last_seen = :now, updated_at = :now
23170
+ WHERE id IN (SELECT DISTINCT e.destination_id
23171
+ FROM share_endpoint e
23172
+ JOIN share_call_site c ON c.endpoint_id = e.id
23173
+ WHERE c.project_key = :key)`
23174
+ ).run({ now, key: projectKey });
23175
+ }
23176
+ /**
23177
+ * Drop rows left without evidence: endpoints with no call site, then
23178
+ * destinations with no endpoint. Call sites are the only evidence either one
23179
+ * has, so a row that lost its last one belongs to no project any more.
23180
+ *
23181
+ * Overrides are deleted between the two steps, and only the ones written
23182
+ * before the host column existed. Those match a destination by id alone;
23183
+ * because the id link is released on delete rather than cascading, leaving
23184
+ * them would accumulate rows that match neither join arm and that nothing can
23185
+ * reach again. Host-bearing rows deliberately survive — the host is what
23186
+ * re-attaches a user's decision when the destination comes back.
23187
+ */
23188
+ pruneOrphans() {
23189
+ this.db.exec(
23190
+ `DELETE FROM share_endpoint
23191
+ WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
23192
+ );
23193
+ this.db.exec(
23194
+ `DELETE FROM egress_decision_override
23195
+ WHERE host IS NULL
23196
+ AND destination_id IN (
23197
+ SELECT d.id FROM share_destination d
23198
+ WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
23199
+ );
23200
+ this.db.exec(
23201
+ `DELETE FROM share_destination
23202
+ WHERE NOT EXISTS (
23203
+ SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
23204
+ );
23205
+ }
23206
+ /**
23207
+ * Live totals for one project. Destinations and endpoints are shared across
23208
+ * projects and carry no project column, so both are counted through the call
23209
+ * sites that reference them.
23210
+ */
23211
+ projectTotals(projectKey) {
23212
+ return {
23213
+ destinations: countScalar(
23214
+ this.db,
23215
+ `SELECT count(DISTINCT e.destination_id) AS n
23216
+ FROM share_endpoint e
23217
+ JOIN share_call_site c ON c.endpoint_id = e.id
23218
+ WHERE c.project_key = ?`,
23219
+ [projectKey]
23220
+ ),
23221
+ endpoints: countScalar(
23222
+ this.db,
23223
+ "SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
23224
+ [projectKey]
23225
+ ),
23226
+ callSites: countScalar(
23227
+ this.db,
23228
+ "SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
23229
+ [projectKey]
23230
+ )
23231
+ };
22367
23232
  }
22368
23233
  // ─── Raw fetchers ────────────────────────────────────────────────────────────
22369
23234
  mapDestRow(r) {
@@ -22383,7 +23248,8 @@ var SqliteSharesRepository = class {
22383
23248
  fetchDestinations(q, kinds, reviewOnly = false) {
22384
23249
  const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22385
23250
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22386
- d.created_at AS createdAt, o.decision AS overrideDecision`;
23251
+ d.created_at AS createdAt,
23252
+ COALESCE(oh.decision, ol.decision) AS overrideDecision`;
22387
23253
  const conditions = [];
22388
23254
  const params = [];
22389
23255
  if (kinds && kinds.length > 0) {
@@ -22394,7 +23260,8 @@ var SqliteSharesRepository = class {
22394
23260
  conditions.push(
22395
23261
  `(d.trust IN ('unverified', 'ip')
22396
23262
  OR EXISTS (SELECT 1 FROM share_endpoint re
22397
- WHERE re.destination_id = d.id AND re.transport = 'http'))`
23263
+ WHERE re.destination_id = d.id
23264
+ AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
22398
23265
  );
22399
23266
  }
22400
23267
  let sql;
@@ -22407,7 +23274,7 @@ var SqliteSharesRepository = class {
22407
23274
  params.push(pattern, pattern, pattern, pattern, pattern);
22408
23275
  sql = `SELECT DISTINCT ${cols}
22409
23276
  FROM share_destination d
22410
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
23277
+ ${OVERRIDE_JOIN}
22411
23278
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
22412
23279
  LEFT JOIN share_call_site c ON c.endpoint_id = e.id
22413
23280
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
@@ -22415,7 +23282,7 @@ var SqliteSharesRepository = class {
22415
23282
  } else {
22416
23283
  sql = `SELECT ${cols}
22417
23284
  FROM share_destination d
22418
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
23285
+ ${OVERRIDE_JOIN}
22419
23286
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
22420
23287
  ORDER BY d.created_at ASC, d.id ASC`;
22421
23288
  }
@@ -22430,9 +23297,9 @@ var SqliteSharesRepository = class {
22430
23297
  this.db.prepare(
22431
23298
  `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22432
23299
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22433
- o.decision AS overrideDecision
23300
+ COALESCE(oh.decision, ol.decision) AS overrideDecision
22434
23301
  FROM share_destination d
22435
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
23302
+ ${OVERRIDE_JOIN}
22436
23303
  WHERE d.id = ?`
22437
23304
  ),
22438
23305
  [destinationId]
@@ -22634,9 +23501,10 @@ function openWithPragmas(file2) {
22634
23501
  }
22635
23502
  function backupLegacyStore(file2) {
22636
23503
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
22637
- renameSync(file2, backup);
22638
- for (const sidecar of walSidecars(file2)) {
22639
- if (existsSync(sidecar)) rmSync(sidecar);
23504
+ renameSync2(file2, backup);
23505
+ tightenFile(backup);
23506
+ for (const sidecar of dbSidecars(file2)) {
23507
+ if (existsSync(sidecar)) rmSync2(sidecar);
22640
23508
  }
22641
23509
  return backup;
22642
23510
  }
@@ -22652,7 +23520,7 @@ function openLocalDatabase(dir) {
22652
23520
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
22653
23521
  );
22654
23522
  }
22655
- applyMigrations(db);
23523
+ applyMigrations(db, file2);
22656
23524
  tightenPerms(file2);
22657
23525
  const events = new SqliteEventsRepository(db);
22658
23526
  const findings = new SqliteFindingsRepository(db);
@@ -22661,6 +23529,7 @@ function openLocalDatabase(dir) {
22661
23529
  const scanLedger = new SqliteScanLedgerRepository(db);
22662
23530
  const exceptions = new SqliteExceptionsRepository(db);
22663
23531
  const resolutions = new SqliteResolutionsRepository(db);
23532
+ const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
22664
23533
  const security = new SqliteSecurityRepository(db);
22665
23534
  const detections = new SqliteDetectionsRepository(db);
22666
23535
  const shares = new SqliteSharesRepository(db);
@@ -22678,9 +23547,56 @@ function openLocalDatabase(dir) {
22678
23547
  policies.seedDefaults();
22679
23548
  function recordCapture(event, detected) {
22680
23549
  failOpenTransaction(db, () => {
22681
- events.insertEvent(event);
22682
23550
  const sessionId = event.metadata?.sessionId;
22683
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
23551
+ if (sessionId) {
23552
+ auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
23553
+ }
23554
+ const auditEventId = captureId(
23555
+ sessionId ?? null,
23556
+ event.contentHash,
23557
+ event.metadata?.filePath ?? null
23558
+ );
23559
+ auditEvents.insertAuditEvent({
23560
+ id: auditEventId,
23561
+ eventType: event.kind,
23562
+ startedAt: event.occurredAt,
23563
+ parentId: sessionId,
23564
+ rootSessionId: sessionId,
23565
+ content: event.content,
23566
+ contentHash: event.contentHash,
23567
+ attributes: toCaptureAttributes(event)
23568
+ });
23569
+ const definitionIds = /* @__PURE__ */ new Map();
23570
+ for (const finding2 of detected) {
23571
+ if (sessionId && inspectionFindings.isSessionDuplicate(finding2.ruleId, finding2.maskedMatch, sessionId)) {
23572
+ continue;
23573
+ }
23574
+ if (inspectionFindings.isEventDuplicate(
23575
+ auditEventId,
23576
+ finding2.ruleId,
23577
+ finding2.maskedMatch,
23578
+ finding2.span.start,
23579
+ finding2.span.end
23580
+ )) {
23581
+ continue;
23582
+ }
23583
+ const key = `${finding2.ruleId}@${captureDefinitionVersion(finding2)}`;
23584
+ let definitionId = definitionIds.get(key);
23585
+ if (!definitionId) {
23586
+ definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding2));
23587
+ definitionIds.set(key, definitionId);
23588
+ }
23589
+ inspectionFindings.insertFinding({
23590
+ id: finding2.id,
23591
+ auditEventId,
23592
+ inspectionDefinitionId: definitionId,
23593
+ span: finding2.span,
23594
+ maskedMatch: finding2.maskedMatch,
23595
+ actionTaken: finding2.actionTaken,
23596
+ confidence: finding2.confidence,
23597
+ findingKey: finding2.findingKey ?? void 0
23598
+ });
23599
+ }
22684
23600
  });
22685
23601
  }
22686
23602
  function ensureInventory(ctx) {
@@ -22798,6 +23714,7 @@ function openLocalDatabase(dir) {
22798
23714
  scanLedger,
22799
23715
  exceptions,
22800
23716
  resolutions,
23717
+ ruleProbeCache,
22801
23718
  security,
22802
23719
  detections,
22803
23720
  shares,
@@ -22827,9 +23744,12 @@ function openLocalDatabase(dir) {
22827
23744
  };
22828
23745
  }
22829
23746
 
23747
+ // ../../packages/persistence/src/finding-key.ts
23748
+ import { createHash as createHash3 } from "crypto";
23749
+
22830
23750
  // ../../packages/persistence/src/fingerprint.ts
22831
23751
  import { createHmac, randomBytes } from "crypto";
22832
- import { chmodSync as chmodSync2, readFileSync, renameSync as renameSync2, writeFileSync } from "fs";
23752
+ import { readFileSync } from "fs";
22833
23753
  import { join as join2 } from "path";
22834
23754
  var KEY_FILENAME = "exception.key";
22835
23755
  var KEY_MATERIAL_BYTES = 32;
@@ -22866,8 +23786,8 @@ function readFingerprintKey(dataDir2) {
22866
23786
  }
22867
23787
 
22868
23788
  // ../../packages/persistence/src/local-layout.ts
22869
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync2, renameSync as renameSync3 } from "fs";
22870
- import { chmod, mkdir } from "fs/promises";
23789
+ import { renameSync as renameSync3 } from "fs";
23790
+ import { mkdir } from "fs/promises";
22871
23791
  import { homedir } from "os";
22872
23792
  import { join as join3 } from "path";
22873
23793
  function defaultDataDir() {
@@ -22882,6 +23802,9 @@ function dataDir(base = defaultDataDir()) {
22882
23802
  function dbPath(base = defaultDataDir()) {
22883
23803
  return join3(dataDir(base), "aka.db");
22884
23804
  }
23805
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
23806
+ ensureDataDirSync(dir);
23807
+ }
22885
23808
  function migrateLegacyLayout(base = defaultDataDir()) {
22886
23809
  const moves = [
22887
23810
  { name: "config.json", dest: settingsDir(base) },
@@ -22889,19 +23812,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
22889
23812
  ];
22890
23813
  for (const { name, dest } of moves) {
22891
23814
  try {
22892
- mkdirSync2(dest, { recursive: true, mode: DATA_DIR_MODE });
22893
- try {
22894
- chmodSync3(dest, DATA_DIR_MODE);
22895
- } catch {
22896
- }
22897
- renameSync3(join3(base, name), join3(dest, name));
23815
+ ensureDataDirSync(dest);
23816
+ const moved = join3(dest, name);
23817
+ renameSync3(join3(base, name), moved);
23818
+ tightenFile(moved);
22898
23819
  } catch {
22899
23820
  }
22900
23821
  }
22901
23822
  }
22902
23823
 
22903
23824
  // ../../packages/persistence/src/settings.ts
22904
- import { readFileSync as readFileSync2, renameSync as renameSync4, writeFileSync as writeFileSync2 } from "fs";
23825
+ import { readFileSync as readFileSync2 } from "fs";
22905
23826
  import { join as join4 } from "path";
22906
23827
  function readWorkspaceSettings(base = defaultDataDir()) {
22907
23828
  const record2 = readJson(join4(settingsDir(base), "settings.json"));
@@ -22923,7 +23844,7 @@ function readJson(file2) {
22923
23844
  }
22924
23845
 
22925
23846
  // ../../packages/persistence/src/warn-era-cap.ts
22926
- import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
23847
+ import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
22927
23848
  import { join as join5 } from "path";
22928
23849
  var MARKER = "warn-era-capped";
22929
23850
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
@@ -22931,7 +23852,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22931
23852
  const marker = join5(dataDir2, MARKER);
22932
23853
  if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22933
23854
  const capped = db.policies.capCategoryActions();
22934
- writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
23855
+ writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
22935
23856
  `, { mode: DATA_FILE_MODE });
22936
23857
  return { capped };
22937
23858
  }
@@ -22986,6 +23907,12 @@ function resolveProvider() {
22986
23907
 
22987
23908
  // ../../packages/plugin-sdk/src/config.ts
22988
23909
  function loadConfig(base = defaultDataDir()) {
23910
+ try {
23911
+ ensureLayoutDirSync(base);
23912
+ const settingsFile = join6(settingsDir(base), "settings.json");
23913
+ if (existsSync3(settingsFile)) tightenFile(settingsFile);
23914
+ } catch {
23915
+ }
22989
23916
  migrateLegacyLayout(base);
22990
23917
  const settings = readWorkspaceSettings(base);
22991
23918
  return {
@@ -23008,15 +23935,583 @@ function resolveProviderSafe() {
23008
23935
  // ../../packages/plugin-sdk/src/config-inventory.ts
23009
23936
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
23010
23937
  import { homedir as homedir2 } from "os";
23011
- import { basename as basename3, join as join7 } from "path";
23938
+ import { basename as basename3, join as join8 } from "path";
23939
+
23940
+ // ../../packages/detections/src/egress/registry.ts
23941
+ var EXTRACTOR_VERSION = "1";
23942
+ var PROVIDER_REGISTRY = [
23943
+ {
23944
+ id: "stripe",
23945
+ name: "Stripe",
23946
+ category: "Payments",
23947
+ hostSuffixes: ["stripe.com"],
23948
+ apiBase: "https://api.stripe.com",
23949
+ defaultDataClasses: ["pii", "customer"],
23950
+ sdks: {
23951
+ npm: ["stripe"],
23952
+ pypi: ["stripe"],
23953
+ go: ["github.com/stripe/stripe-go"],
23954
+ maven: ["com.stripe"],
23955
+ rubygems: ["stripe"],
23956
+ composer: ["stripe/stripe-php"],
23957
+ nuget: ["Stripe.net"]
23958
+ }
23959
+ },
23960
+ {
23961
+ id: "datadog",
23962
+ name: "Datadog",
23963
+ category: "Observability",
23964
+ hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
23965
+ apiBase: "https://api.datadoghq.com",
23966
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23967
+ sdks: {
23968
+ npm: ["dd-trace", "@datadog/browser-logs"],
23969
+ pypi: ["datadog", "ddtrace"],
23970
+ go: ["github.com/DataDog/dd-trace-go"],
23971
+ maven: ["com.datadoghq"],
23972
+ rubygems: ["ddtrace", "dogapi"],
23973
+ nuget: ["Datadog.Trace"]
23974
+ }
23975
+ },
23976
+ {
23977
+ id: "newrelic",
23978
+ name: "New Relic",
23979
+ category: "Observability",
23980
+ hostSuffixes: ["newrelic.com", "nr-data.net"],
23981
+ apiBase: "https://api.newrelic.com",
23982
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23983
+ sdks: {
23984
+ npm: ["newrelic"],
23985
+ pypi: ["newrelic"],
23986
+ go: ["github.com/newrelic/go-agent"],
23987
+ maven: ["com.newrelic.agent.java"],
23988
+ rubygems: ["newrelic_rpm"],
23989
+ nuget: ["NewRelic.Agent"]
23990
+ }
23991
+ },
23992
+ {
23993
+ id: "sentry",
23994
+ name: "Sentry",
23995
+ category: "Error tracking",
23996
+ hostSuffixes: ["sentry.io"],
23997
+ apiBase: "https://sentry.io",
23998
+ defaultDataClasses: ["source", "telemetry"],
23999
+ sdks: {
24000
+ npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
24001
+ pypi: ["sentry-sdk"],
24002
+ go: ["github.com/getsentry/sentry-go"],
24003
+ maven: ["io.sentry"],
24004
+ rubygems: ["sentry-ruby"],
24005
+ cargo: ["sentry"],
24006
+ composer: ["sentry/sentry"],
24007
+ nuget: ["Sentry"]
24008
+ }
24009
+ },
24010
+ {
24011
+ id: "openai",
24012
+ name: "OpenAI",
24013
+ category: "LLM provider",
24014
+ hostSuffixes: ["openai.com"],
24015
+ apiBase: "https://api.openai.com",
24016
+ defaultDataClasses: ["pii", "source"],
24017
+ sdks: {
24018
+ npm: ["openai"],
24019
+ pypi: ["openai"],
24020
+ go: ["github.com/sashabaranov/go-openai"],
24021
+ maven: ["com.openai"],
24022
+ rubygems: ["ruby-openai"],
24023
+ cargo: ["async-openai"],
24024
+ composer: ["openai-php/client"],
24025
+ nuget: ["OpenAI"]
24026
+ }
24027
+ },
24028
+ {
24029
+ id: "anthropic",
24030
+ name: "Anthropic",
24031
+ category: "LLM provider",
24032
+ hostSuffixes: ["anthropic.com"],
24033
+ apiBase: "https://api.anthropic.com",
24034
+ defaultDataClasses: ["pii", "source"],
24035
+ sdks: {
24036
+ npm: ["@anthropic-ai/sdk"],
24037
+ pypi: ["anthropic"],
24038
+ go: ["github.com/anthropics/anthropic-sdk-go"],
24039
+ nuget: ["Anthropic.SDK"]
24040
+ }
24041
+ },
24042
+ {
24043
+ id: "aws",
24044
+ name: "Amazon Web Services",
24045
+ category: "Cloud platform",
24046
+ hostSuffixes: ["amazonaws.com"],
24047
+ apiBase: "https://s3.amazonaws.com",
24048
+ defaultDataClasses: ["secrets", "customer"],
24049
+ sdks: {
24050
+ npm: ["@aws-sdk/client-s3", "aws-sdk"],
24051
+ pypi: ["boto3"],
24052
+ go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
24053
+ maven: ["com.amazonaws", "software.amazon.awssdk"],
24054
+ rubygems: ["aws-sdk-s3"],
24055
+ cargo: ["aws-sdk-s3"],
24056
+ nuget: ["AWSSDK.S3"]
24057
+ }
24058
+ },
24059
+ {
24060
+ id: "gcp",
24061
+ name: "Google Cloud",
24062
+ category: "Cloud platform",
24063
+ hostSuffixes: ["googleapis.com"],
24064
+ apiBase: "https://storage.googleapis.com",
24065
+ defaultDataClasses: ["customer", "logs"],
24066
+ sdks: {
24067
+ npm: ["@google-cloud/storage"],
24068
+ pypi: ["google-cloud-storage"],
24069
+ go: ["cloud.google.com/go"],
24070
+ maven: ["com.google.cloud"],
24071
+ rubygems: ["google-cloud-storage"],
24072
+ nuget: ["Google.Cloud.Storage.V1"]
24073
+ }
24074
+ },
24075
+ {
24076
+ id: "azure",
24077
+ name: "Microsoft Azure",
24078
+ category: "Cloud platform",
24079
+ hostSuffixes: ["azure.com", "windows.net"],
24080
+ apiBase: "https://management.azure.com",
24081
+ defaultDataClasses: ["customer", "logs"],
24082
+ sdks: {
24083
+ npm: ["@azure/storage-blob"],
24084
+ pypi: ["azure-storage-blob"],
24085
+ go: ["github.com/Azure/azure-sdk-for-go"],
24086
+ maven: ["com.azure"],
24087
+ rubygems: ["azure-storage-blob"],
24088
+ nuget: ["Azure.Storage.Blobs"]
24089
+ }
24090
+ },
24091
+ {
24092
+ id: "slack",
24093
+ name: "Slack",
24094
+ category: "Notifications",
24095
+ hostSuffixes: ["slack.com"],
24096
+ apiBase: "https://slack.com/api",
24097
+ defaultDataClasses: ["logs"],
24098
+ sdks: {
24099
+ npm: ["@slack/web-api"],
24100
+ pypi: ["slack-sdk"],
24101
+ go: ["github.com/slack-go/slack"],
24102
+ maven: ["com.slack.api"],
24103
+ rubygems: ["slack-ruby-client"],
24104
+ composer: ["slack-php/slack-api"],
24105
+ nuget: ["SlackNet"]
24106
+ }
24107
+ },
24108
+ {
24109
+ id: "segment",
24110
+ name: "Segment",
24111
+ category: "Analytics",
24112
+ hostSuffixes: ["segment.io", "segment.com"],
24113
+ apiBase: "https://api.segment.io",
24114
+ defaultDataClasses: ["customer"],
24115
+ sdks: {
24116
+ npm: ["@segment/analytics-node", "analytics-node"],
24117
+ pypi: ["segment-analytics-python"],
24118
+ go: ["github.com/segmentio/analytics-go"],
24119
+ maven: ["com.segment.analytics.java"],
24120
+ rubygems: ["analytics-ruby"],
24121
+ nuget: ["Analytics"]
24122
+ }
24123
+ },
24124
+ {
24125
+ id: "twilio",
24126
+ name: "Twilio",
24127
+ category: "Communications",
24128
+ hostSuffixes: ["twilio.com"],
24129
+ apiBase: "https://api.twilio.com",
24130
+ defaultDataClasses: ["pii", "customer"],
24131
+ sdks: {
24132
+ npm: ["twilio"],
24133
+ pypi: ["twilio"],
24134
+ go: ["github.com/twilio/twilio-go"],
24135
+ maven: ["com.twilio.sdk"],
24136
+ rubygems: ["twilio-ruby"],
24137
+ composer: ["twilio/sdk"],
24138
+ nuget: ["Twilio"]
24139
+ }
24140
+ },
24141
+ {
24142
+ id: "sendgrid",
24143
+ name: "SendGrid",
24144
+ category: "Email",
24145
+ hostSuffixes: ["sendgrid.com"],
24146
+ apiBase: "https://api.sendgrid.com",
24147
+ defaultDataClasses: ["pii"],
24148
+ sdks: {
24149
+ npm: ["@sendgrid/mail"],
24150
+ pypi: ["sendgrid"],
24151
+ go: ["github.com/sendgrid/sendgrid-go"],
24152
+ maven: ["com.sendgrid"],
24153
+ rubygems: ["sendgrid-ruby"],
24154
+ composer: ["sendgrid/sendgrid"],
24155
+ nuget: ["SendGrid"]
24156
+ }
24157
+ },
24158
+ {
24159
+ id: "mailgun",
24160
+ name: "Mailgun",
24161
+ category: "Email",
24162
+ hostSuffixes: ["mailgun.net"],
24163
+ apiBase: "https://api.mailgun.net",
24164
+ defaultDataClasses: ["pii"],
24165
+ sdks: {
24166
+ npm: ["mailgun.js"],
24167
+ pypi: ["mailgun"],
24168
+ rubygems: ["mailgun-ruby"],
24169
+ composer: ["mailgun/mailgun-php"],
24170
+ nuget: ["Mailgun"]
24171
+ }
24172
+ },
24173
+ {
24174
+ id: "mixpanel",
24175
+ name: "Mixpanel",
24176
+ category: "Analytics",
24177
+ hostSuffixes: ["mixpanel.com"],
24178
+ apiBase: "https://api.mixpanel.com",
24179
+ defaultDataClasses: ["customer", "telemetry"],
24180
+ sdks: {
24181
+ npm: ["mixpanel"],
24182
+ pypi: ["mixpanel"],
24183
+ rubygems: ["mixpanel-ruby"],
24184
+ nuget: ["Mixpanel"]
24185
+ }
24186
+ },
24187
+ {
24188
+ id: "amplitude",
24189
+ name: "Amplitude",
24190
+ category: "Analytics",
24191
+ hostSuffixes: ["amplitude.com"],
24192
+ apiBase: "https://api2.amplitude.com",
24193
+ defaultDataClasses: ["customer", "telemetry"],
24194
+ sdks: {
24195
+ npm: ["@amplitude/analytics-node"],
24196
+ pypi: ["amplitude-analytics"],
24197
+ nuget: ["Amplitude"]
24198
+ }
24199
+ },
24200
+ {
24201
+ id: "posthog",
24202
+ name: "PostHog",
24203
+ category: "Analytics",
24204
+ hostSuffixes: ["posthog.com"],
24205
+ apiBase: "https://us.i.posthog.com",
24206
+ defaultDataClasses: ["customer", "telemetry"],
24207
+ sdks: {
24208
+ npm: ["posthog-node", "posthog-js"],
24209
+ pypi: ["posthog"],
24210
+ go: ["github.com/posthog/posthog-go"],
24211
+ rubygems: ["posthog-ruby"],
24212
+ composer: ["posthog/posthog-php"],
24213
+ nuget: ["PostHog"]
24214
+ }
24215
+ },
24216
+ {
24217
+ id: "honeycomb",
24218
+ name: "Honeycomb",
24219
+ category: "Observability",
24220
+ hostSuffixes: ["honeycomb.io"],
24221
+ apiBase: "https://api.honeycomb.io",
24222
+ defaultDataClasses: ["telemetry", "metrics"],
24223
+ sdks: {
24224
+ npm: ["libhoney"],
24225
+ pypi: ["libhoney"],
24226
+ go: ["github.com/honeycombio/libhoney-go"],
24227
+ rubygems: ["libhoney"]
24228
+ }
24229
+ },
24230
+ {
24231
+ id: "grafana",
24232
+ name: "Grafana Cloud",
24233
+ category: "Observability",
24234
+ hostSuffixes: ["grafana.net"],
24235
+ apiBase: "https://grafana.net",
24236
+ defaultDataClasses: ["logs", "metrics"],
24237
+ sdks: {
24238
+ npm: ["@grafana/faro-web-sdk"]
24239
+ }
24240
+ },
24241
+ {
24242
+ id: "splunk",
24243
+ name: "Splunk",
24244
+ category: "Observability",
24245
+ hostSuffixes: ["splunkcloud.com", "splunk.com"],
24246
+ apiBase: "https://http-inputs.splunkcloud.com",
24247
+ defaultDataClasses: ["logs"],
24248
+ sdks: {
24249
+ npm: ["splunk-logging"],
24250
+ pypi: ["splunk-sdk"],
24251
+ maven: ["com.splunk"],
24252
+ nuget: ["Splunk.Logging.Common"]
24253
+ }
24254
+ },
24255
+ {
24256
+ id: "pagerduty",
24257
+ name: "PagerDuty",
24258
+ category: "Incident response",
24259
+ hostSuffixes: ["pagerduty.com"],
24260
+ apiBase: "https://api.pagerduty.com",
24261
+ defaultDataClasses: ["logs"],
24262
+ sdks: {
24263
+ npm: ["@pagerduty/pdjs"],
24264
+ pypi: ["pdpyras"],
24265
+ go: ["github.com/PagerDuty/go-pagerduty"],
24266
+ rubygems: ["pagerduty"]
24267
+ }
24268
+ },
24269
+ {
24270
+ id: "github",
24271
+ name: "GitHub",
24272
+ category: "Developer platform",
24273
+ hostSuffixes: ["github.com", "githubusercontent.com"],
24274
+ apiBase: "https://api.github.com",
24275
+ defaultDataClasses: ["source"],
24276
+ sdks: {
24277
+ npm: ["@octokit/rest", "octokit"],
24278
+ pypi: ["pygithub"],
24279
+ go: ["github.com/google/go-github"],
24280
+ maven: ["org.kohsuke.github-api"],
24281
+ rubygems: ["octokit"],
24282
+ cargo: ["octocrab"],
24283
+ composer: ["knplabs/github-api"],
24284
+ nuget: ["Octokit"]
24285
+ }
24286
+ },
24287
+ {
24288
+ id: "gitlab",
24289
+ name: "GitLab",
24290
+ category: "Developer platform",
24291
+ hostSuffixes: ["gitlab.com"],
24292
+ apiBase: "https://gitlab.com/api",
24293
+ defaultDataClasses: ["source"],
24294
+ sdks: {
24295
+ npm: ["@gitbeaker/rest"],
24296
+ pypi: ["python-gitlab"],
24297
+ go: ["gitlab.com/gitlab-org/api/client-go"],
24298
+ rubygems: ["gitlab"],
24299
+ nuget: ["GitLabApiClient"]
24300
+ }
24301
+ },
24302
+ {
24303
+ id: "auth0",
24304
+ name: "Auth0",
24305
+ category: "Identity",
24306
+ hostSuffixes: ["auth0.com"],
24307
+ apiBase: "https://login.auth0.com",
24308
+ defaultDataClasses: ["pii"],
24309
+ sdks: {
24310
+ npm: ["auth0"],
24311
+ pypi: ["auth0-python"],
24312
+ go: ["github.com/auth0/go-auth0"],
24313
+ maven: ["com.auth0"],
24314
+ rubygems: ["auth0"],
24315
+ composer: ["auth0/auth0-php"],
24316
+ nuget: ["Auth0.ManagementApi"]
24317
+ }
24318
+ },
24319
+ {
24320
+ id: "okta",
24321
+ name: "Okta",
24322
+ category: "Identity",
24323
+ hostSuffixes: ["okta.com", "oktapreview.com"],
24324
+ apiBase: "https://login.okta.com",
24325
+ defaultDataClasses: ["pii"],
24326
+ sdks: {
24327
+ npm: ["@okta/okta-sdk-nodejs"],
24328
+ pypi: ["okta"],
24329
+ go: ["github.com/okta/okta-sdk-golang"],
24330
+ maven: ["com.okta.sdk"],
24331
+ nuget: ["Okta.Sdk"]
24332
+ }
24333
+ },
24334
+ {
24335
+ id: "clerk",
24336
+ name: "Clerk",
24337
+ category: "Identity",
24338
+ hostSuffixes: ["clerk.com", "clerk.dev"],
24339
+ apiBase: "https://api.clerk.com",
24340
+ defaultDataClasses: ["pii"],
24341
+ sdks: {
24342
+ npm: ["@clerk/backend", "@clerk/nextjs"],
24343
+ pypi: ["clerk-backend-api"],
24344
+ go: ["github.com/clerk/clerk-sdk-go"]
24345
+ }
24346
+ },
24347
+ {
24348
+ id: "supabase",
24349
+ name: "Supabase",
24350
+ category: "Backend platform",
24351
+ hostSuffixes: ["supabase.co", "supabase.com"],
24352
+ apiBase: "https://api.supabase.com",
24353
+ defaultDataClasses: ["pii", "customer"],
24354
+ sdks: {
24355
+ npm: ["@supabase/supabase-js"],
24356
+ pypi: ["supabase"],
24357
+ cargo: ["postgrest"]
24358
+ }
24359
+ },
24360
+ {
24361
+ id: "firebase",
24362
+ name: "Firebase",
24363
+ category: "Backend platform",
24364
+ hostSuffixes: ["firebaseio.com", "firebase.google.com"],
24365
+ apiBase: "https://firebaseio.com",
24366
+ defaultDataClasses: ["customer"],
24367
+ sdks: {
24368
+ npm: ["firebase", "firebase-admin"],
24369
+ pypi: ["firebase-admin"],
24370
+ go: ["firebase.google.com/go"],
24371
+ maven: ["com.google.firebase"]
24372
+ }
24373
+ },
24374
+ {
24375
+ id: "mongodb-atlas",
24376
+ name: "MongoDB Atlas",
24377
+ category: "Database SaaS",
24378
+ hostSuffixes: ["mongodb.net", "mongodb.com"],
24379
+ apiBase: "https://cloud.mongodb.com",
24380
+ defaultDataClasses: ["customer"],
24381
+ sdks: {
24382
+ npm: ["mongodb"],
24383
+ pypi: ["pymongo"],
24384
+ go: ["go.mongodb.org/mongo-driver"],
24385
+ maven: ["org.mongodb"],
24386
+ rubygems: ["mongo"],
24387
+ cargo: ["mongodb"],
24388
+ nuget: ["MongoDB.Driver"]
24389
+ }
24390
+ },
24391
+ {
24392
+ id: "planetscale",
24393
+ name: "PlanetScale",
24394
+ category: "Database SaaS",
24395
+ hostSuffixes: ["psdb.cloud", "planetscale.com"],
24396
+ apiBase: "https://api.planetscale.com",
24397
+ defaultDataClasses: ["customer"],
24398
+ sdks: {
24399
+ npm: ["@planetscale/database"],
24400
+ go: ["github.com/planetscale/planetscale-go"]
24401
+ }
24402
+ },
24403
+ {
24404
+ id: "algolia",
24405
+ name: "Algolia",
24406
+ category: "Search SaaS",
24407
+ hostSuffixes: ["algolia.net", "algolianet.com"],
24408
+ apiBase: "https://algolia.net",
24409
+ defaultDataClasses: ["customer"],
24410
+ sdks: {
24411
+ npm: ["algoliasearch"],
24412
+ pypi: ["algoliasearch"],
24413
+ go: ["github.com/algolia/algoliasearch-client-go"],
24414
+ maven: ["com.algolia"],
24415
+ rubygems: ["algolia"],
24416
+ composer: ["algolia/algoliasearch-client-php"],
24417
+ nuget: ["Algolia.Search"]
24418
+ }
24419
+ },
24420
+ {
24421
+ id: "cloudflare",
24422
+ name: "Cloudflare",
24423
+ category: "CDN / edge",
24424
+ hostSuffixes: ["cloudflare.com", "workers.dev"],
24425
+ apiBase: "https://api.cloudflare.com",
24426
+ defaultDataClasses: ["logs"],
24427
+ sdks: {
24428
+ npm: ["cloudflare"],
24429
+ pypi: ["cloudflare"],
24430
+ go: ["github.com/cloudflare/cloudflare-go"],
24431
+ nuget: ["CloudFlare.Client"]
24432
+ }
24433
+ },
24434
+ {
24435
+ id: "huggingface",
24436
+ name: "Hugging Face",
24437
+ category: "LLM provider",
24438
+ hostSuffixes: ["huggingface.co"],
24439
+ apiBase: "https://api-inference.huggingface.co",
24440
+ defaultDataClasses: ["source"],
24441
+ sdks: {
24442
+ npm: ["@huggingface/inference"],
24443
+ pypi: ["huggingface-hub", "transformers"],
24444
+ rubygems: ["hugging-face"]
24445
+ }
24446
+ },
24447
+ {
24448
+ id: "cohere",
24449
+ name: "Cohere",
24450
+ category: "LLM provider",
24451
+ hostSuffixes: ["cohere.com", "cohere.ai"],
24452
+ apiBase: "https://api.cohere.com",
24453
+ defaultDataClasses: ["pii", "source"],
24454
+ sdks: {
24455
+ npm: ["cohere-ai"],
24456
+ pypi: ["cohere"],
24457
+ go: ["github.com/cohere-ai/cohere-go"]
24458
+ }
24459
+ },
24460
+ {
24461
+ id: "mistral",
24462
+ name: "Mistral AI",
24463
+ category: "LLM provider",
24464
+ hostSuffixes: ["mistral.ai"],
24465
+ apiBase: "https://api.mistral.ai",
24466
+ defaultDataClasses: ["pii", "source"],
24467
+ sdks: {
24468
+ npm: ["@mistralai/mistralai"],
24469
+ pypi: ["mistralai"],
24470
+ go: ["github.com/gage-technologies/mistral-go"]
24471
+ }
24472
+ }
24473
+ ];
24474
+ var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
24475
+ ${JSON.stringify(PROVIDER_REGISTRY)}`;
24476
+
24477
+ // ../../packages/detections/src/egress/extract.ts
24478
+ var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
24479
+ var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
24480
+ var SECRET_VALUE = new RegExp(
24481
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
24482
+ "gi"
24483
+ );
24484
+ var AUTH_SCHEME_VALUE = new RegExp(
24485
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
24486
+ "gi"
24487
+ );
24488
+ var WEBHOOK_SECRET_PATHS = [
24489
+ { hosts: ["hooks.slack.com"], prefix: "/services/" },
24490
+ {
24491
+ hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
24492
+ prefix: "/api/webhooks/"
24493
+ },
24494
+ { hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
24495
+ { hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
24496
+ ];
24497
+ function escapeRegExp(literal2) {
24498
+ return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
24499
+ }
24500
+ var WEBHOOK_URL = new RegExp(
24501
+ `(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
24502
+ (entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
24503
+ ).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
24504
+ "gi"
24505
+ );
23012
24506
 
23013
24507
  // ../../packages/detections/src/escape-regexp.ts
23014
- function escapeRegExp(value) {
24508
+ function escapeRegExp2(value) {
23015
24509
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23016
24510
  }
23017
24511
 
23018
24512
  // ../../packages/detections/src/matchers/limits.ts
23019
24513
  var MAX_MATCHES_PER_RULE = 1e4;
24514
+ var MAX_REGEX_INPUT_LENGTH = 2e5;
23020
24515
 
23021
24516
  // ../../packages/detections/src/matchers/keyword.ts
23022
24517
  var KeywordMatcher2 = class {
@@ -23027,7 +24522,7 @@ var KeywordMatcher2 = class {
23027
24522
  for (const kw of keywords) {
23028
24523
  if (kw.length === 0) continue;
23029
24524
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
23030
- const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
24525
+ const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
23031
24526
  let m;
23032
24527
  while ((m = re.exec(text)) !== null) {
23033
24528
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -23044,9 +24539,13 @@ var RegexMatcher2 = class {
23044
24539
  if (rule.matcher.type !== "regex") return [];
23045
24540
  const { pattern, flags, captureGroup } = rule.matcher;
23046
24541
  const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
24542
+ const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
23047
24543
  const spans = [];
23048
24544
  let m;
23049
- while ((m = re.exec(text)) !== null) {
24545
+ const maxIterations = scanText2.length + 1;
24546
+ let iterations = 0;
24547
+ while ((m = re.exec(scanText2)) !== null) {
24548
+ if (++iterations > maxIterations) break;
23050
24549
  const group = captureGroup != null ? m[captureGroup] : m[0];
23051
24550
  if (m[0].length === 0) re.lastIndex++;
23052
24551
  if (group && spans.length < MAX_MATCHES_PER_RULE) {
@@ -23150,7 +24649,7 @@ function isCorroborated(candidate, candidates, text) {
23150
24649
  for (const label of labels) {
23151
24650
  const trimmed = label.trim();
23152
24651
  if (trimmed.length === 0) continue;
23153
- const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(trimmed)}(?![A-Za-z0-9])`, "i");
24652
+ const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
23154
24653
  if (re.test(haystack)) return true;
23155
24654
  }
23156
24655
  }
@@ -23397,6 +24896,31 @@ function whole(command) {
23397
24896
  return { start: 0, end: command.length };
23398
24897
  }
23399
24898
 
24899
+ // ../../packages/detections/src/security/redos-probe.ts
24900
+ var EXPONENTIAL_UNITS = [
24901
+ "a",
24902
+ "0",
24903
+ " ",
24904
+ "x",
24905
+ "ab",
24906
+ "a.",
24907
+ "a-",
24908
+ "a_",
24909
+ "a@",
24910
+ "a/",
24911
+ "a:",
24912
+ "a=",
24913
+ "a;",
24914
+ "aA0",
24915
+ " "
24916
+ ];
24917
+ var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
24918
+ (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
24919
+ );
24920
+ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
24921
+ (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
24922
+ );
24923
+
23400
24924
  // ../../rules/code-flaws/auth-jwt-no-verify.json
23401
24925
  var auth_jwt_no_verify_default = {
23402
24926
  specVersion: 1,
@@ -25444,7 +26968,7 @@ function ensureBundledPacks() {
25444
26968
  return false;
25445
26969
  }
25446
26970
  }
25447
- function scanText(text) {
26971
+ function scanText(text, ruleVersions) {
25448
26972
  if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
25449
26973
  try {
25450
26974
  const rules = getLoadedRules();
@@ -25456,7 +26980,7 @@ function scanText(text) {
25456
26980
  return {
25457
26981
  ruleId: m.ruleId,
25458
26982
  ruleName: rule?.name ?? m.ruleId,
25459
- ruleVersion: String(rule?.specVersion ?? 1),
26983
+ ruleVersion: ruleVersions?.[m.ruleId] ?? String(rule?.specVersion ?? 1),
25460
26984
  category: m.category,
25461
26985
  severity: m.severity,
25462
26986
  span: m.span,
@@ -25474,8 +26998,8 @@ function maskText(text) {
25474
26998
  }
25475
26999
 
25476
27000
  // ../../packages/plugin-sdk/src/repo.ts
25477
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25478
- import { basename as basename2, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
27001
+ import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
27002
+ import { basename as basename2, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
25479
27003
  function resolveRepoIdentity(cwd) {
25480
27004
  try {
25481
27005
  const root = findGitRoot(cwd);
@@ -25525,7 +27049,7 @@ function resolveGitBranch(cwd) {
25525
27049
  try {
25526
27050
  const root = findGitRoot(cwd);
25527
27051
  if (!root) return void 0;
25528
- const dotGit = join6(root, ".git");
27052
+ const dotGit = join7(root, ".git");
25529
27053
  let gitdir;
25530
27054
  try {
25531
27055
  gitdir = statSync(dotGit).isDirectory() ? dotGit : resolveWorktreeGitdir(root, dotGit);
@@ -25533,7 +27057,7 @@ function resolveGitBranch(cwd) {
25533
27057
  return void 0;
25534
27058
  }
25535
27059
  if (gitdir === void 0) return void 0;
25536
- const head = safeRead(join6(gitdir, "HEAD"));
27060
+ const head = safeRead(join7(gitdir, "HEAD"));
25537
27061
  if (!head) return void 0;
25538
27062
  return /^ref:\s*refs\/heads\/(.+?)\s*$/m.exec(head)?.[1];
25539
27063
  } catch {
@@ -25543,37 +27067,37 @@ function resolveGitBranch(cwd) {
25543
27067
  function resolveWorktreeGitdir(root, dotGitFile) {
25544
27068
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGitFile) ?? "")?.[1];
25545
27069
  if (!target) return void 0;
25546
- return isAbsolute(target) ? target : join6(root, target);
27070
+ return isAbsolute(target) ? target : join7(root, target);
25547
27071
  }
25548
27072
  function findGitRoot(start) {
25549
27073
  let dir = start;
25550
27074
  for (; ; ) {
25551
- if (existsSync3(join6(dir, ".git"))) return dir;
27075
+ if (existsSync4(join7(dir, ".git"))) return dir;
25552
27076
  const parent = dirname(dir);
25553
27077
  if (parent === dir) return void 0;
25554
27078
  dir = parent;
25555
27079
  }
25556
27080
  }
25557
27081
  function resolveGitContext(root) {
25558
- const dotGit = join6(root, ".git");
27082
+ const dotGit = join7(root, ".git");
25559
27083
  try {
25560
27084
  if (statSync(dotGit).isDirectory()) {
25561
- return { configPath: join6(dotGit, "config"), headRoot: root };
27085
+ return { configPath: join7(dotGit, "config"), headRoot: root };
25562
27086
  }
25563
27087
  } catch {
25564
27088
  return void 0;
25565
27089
  }
25566
27090
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
25567
27091
  if (!target) return void 0;
25568
- const gitdir = isAbsolute(target) ? target : join6(root, target);
25569
- if (existsSync3(join6(gitdir, "config"))) {
25570
- return { configPath: join6(gitdir, "config"), headRoot: root };
27092
+ const gitdir = isAbsolute(target) ? target : join7(root, target);
27093
+ if (existsSync4(join7(gitdir, "config"))) {
27094
+ return { configPath: join7(gitdir, "config"), headRoot: root };
25571
27095
  }
25572
- const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
27096
+ const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
25573
27097
  if (!commonRaw) return void 0;
25574
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
27098
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
25575
27099
  const headRoot = basename2(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
25576
- return { configPath: join6(commonGitDir, "config"), headRoot };
27100
+ return { configPath: join7(commonGitDir, "config"), headRoot };
25577
27101
  }
25578
27102
  function safeRead(path) {
25579
27103
  try {
@@ -25639,31 +27163,31 @@ function resolveConfigInventory(input) {
25639
27163
  };
25640
27164
  try {
25641
27165
  const home = input.homeDir ?? homedir2();
25642
- const claudeDir = join7(home, ".claude");
27166
+ const claudeDir = join8(home, ".claude");
25643
27167
  const repo = resolveRepoIdentity(input.cwd);
25644
27168
  const repoIdentity = repo?.url ?? input.cwd;
25645
27169
  const projectSource = `project:${repoIdentity}`;
25646
- collectSettingsHooks(scan2, join7(claudeDir, "settings.json"), "user");
25647
- collectSettingsHooks(scan2, join7(input.cwd, ".claude", "settings.json"), "project");
25648
- collectSettingsHooks(scan2, join7(input.cwd, ".claude", "settings.local.json"), "local");
27170
+ collectSettingsHooks(scan2, join8(claudeDir, "settings.json"), "user");
27171
+ collectSettingsHooks(scan2, join8(input.cwd, ".claude", "settings.json"), "project");
27172
+ collectSettingsHooks(scan2, join8(input.cwd, ".claude", "settings.local.json"), "local");
25649
27173
  const projectOrigin = { scope: "project", project: repoIdentity };
25650
- collectMcpFile(scan2, join7(input.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
25651
- collectUserClaudeJson(scan2, join7(home, ".claude.json"), input.cwd, repoIdentity);
25652
- collectMcpFile(scan2, join7(claudeDir, "settings.json"), { scope: "user" });
25653
- collectMcpFile(scan2, join7(input.cwd, ".claude", "settings.json"), projectOrigin);
25654
- collectMcpFile(scan2, join7(input.cwd, ".claude", "settings.local.json"), {
27174
+ collectMcpFile(scan2, join8(input.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
27175
+ collectUserClaudeJson(scan2, join8(home, ".claude.json"), input.cwd, repoIdentity);
27176
+ collectMcpFile(scan2, join8(claudeDir, "settings.json"), { scope: "user" });
27177
+ collectMcpFile(scan2, join8(input.cwd, ".claude", "settings.json"), projectOrigin);
27178
+ collectMcpFile(scan2, join8(input.cwd, ".claude", "settings.local.json"), {
25655
27179
  scope: "local",
25656
27180
  project: repoIdentity
25657
27181
  });
25658
27182
  collectConfigFiles(scan2, claudeDir, input.cwd);
25659
- collectSkillsDir(scan2, join7(claudeDir, "skills"), { source: "local", scope: "user" });
25660
- collectSkillsDir(scan2, join7(input.cwd, ".claude", "skills"), {
27183
+ collectSkillsDir(scan2, join8(claudeDir, "skills"), { source: "local", scope: "user" });
27184
+ collectSkillsDir(scan2, join8(input.cwd, ".claude", "skills"), {
25661
27185
  source: projectSource,
25662
27186
  scope: "project"
25663
27187
  });
25664
27188
  collectInstalledPlugins(scan2, claudeDir);
25665
27189
  collectMarketplaceSkills(scan2, claudeDir);
25666
- collectSkillsDir(scan2, join7(input.cwd, "skills"), { source: projectSource, scope: "project" });
27190
+ collectSkillsDir(scan2, join8(input.cwd, "skills"), { source: projectSource, scope: "project" });
25667
27191
  scan2.skills = dedupeSkills(scan2.skills);
25668
27192
  scan2.mcpServers = dedupeMcpServers(scan2.mcpServers);
25669
27193
  } catch (err) {
@@ -25792,7 +27316,7 @@ function projectEntryFor(projects, cwd) {
25792
27316
  return void 0;
25793
27317
  }
25794
27318
  function collectPluginManifestMcp(scan2, installPath, origin) {
25795
- const manifestPath = join7(installPath, ".claude-plugin", "plugin.json");
27319
+ const manifestPath = join8(installPath, ".claude-plugin", "plugin.json");
25796
27320
  const raw = readOptional(manifestPath);
25797
27321
  if (raw === void 0) return;
25798
27322
  try {
@@ -25800,7 +27324,7 @@ function collectPluginManifestMcp(scan2, installPath, origin) {
25800
27324
  if (typeof parsed !== "object" || parsed === null) return;
25801
27325
  const declared = parsed.mcpServers;
25802
27326
  if (typeof declared === "string" && declared.length > 0) {
25803
- collectMcpFile(scan2, join7(installPath, declared), origin, { recordErrors: true });
27327
+ collectMcpFile(scan2, join8(installPath, declared), origin, { recordErrors: true });
25804
27328
  } else {
25805
27329
  collectMcpObject(scan2, declared, manifestPath, origin);
25806
27330
  }
@@ -25817,14 +27341,14 @@ var SETTINGS_KEY_LABELS = [
25817
27341
  ["statusLine", "status line"]
25818
27342
  ];
25819
27343
  function collectConfigFiles(scan2, claudeDir, cwd) {
25820
- settingsConfigFile(scan2, join7(claudeDir, "settings.json"), "user", "User settings");
25821
- settingsConfigFile(scan2, join7(cwd, ".claude", "settings.json"), "project", "Project settings");
25822
- settingsConfigFile(scan2, join7(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
25823
- memoryConfigFile(scan2, join7(claudeDir, "CLAUDE.md"), "user", "User memory");
25824
- memoryConfigFile(scan2, join7(cwd, "CLAUDE.md"), "project", "Project memory");
25825
- mcpJsonConfigFile(scan2, join7(cwd, ".mcp.json"));
25826
- dirConfigFile(scan2, join7(cwd, ".claude", "commands"), "Slash commands", "command");
25827
- dirConfigFile(scan2, join7(cwd, ".claude", "agents"), "Subagents", "subagent");
27344
+ settingsConfigFile(scan2, join8(claudeDir, "settings.json"), "user", "User settings");
27345
+ settingsConfigFile(scan2, join8(cwd, ".claude", "settings.json"), "project", "Project settings");
27346
+ settingsConfigFile(scan2, join8(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
27347
+ memoryConfigFile(scan2, join8(claudeDir, "CLAUDE.md"), "user", "User memory");
27348
+ memoryConfigFile(scan2, join8(cwd, "CLAUDE.md"), "project", "Project memory");
27349
+ mcpJsonConfigFile(scan2, join8(cwd, ".mcp.json"));
27350
+ dirConfigFile(scan2, join8(cwd, ".claude", "commands"), "Slash commands", "command");
27351
+ dirConfigFile(scan2, join8(cwd, ".claude", "agents"), "Subagents", "subagent");
25828
27352
  }
25829
27353
  function configFileEntry(path, scope, kind) {
25830
27354
  try {
@@ -25899,7 +27423,7 @@ function countMarkdownFiles(dir, depth) {
25899
27423
  let count = 0;
25900
27424
  for (const dirent of readdirSync(dir, { withFileTypes: true })) {
25901
27425
  if (dirent.name.startsWith(".")) continue;
25902
- if (dirent.isDirectory()) count += countMarkdownFiles(join7(dir, dirent.name), depth + 1);
27426
+ if (dirent.isDirectory()) count += countMarkdownFiles(join8(dir, dirent.name), depth + 1);
25903
27427
  else if (dirent.name.endsWith(".md")) count += 1;
25904
27428
  }
25905
27429
  return count;
@@ -25912,7 +27436,7 @@ function collectSkillsDir(scan2, dir, origin) {
25912
27436
  return;
25913
27437
  }
25914
27438
  for (const name of names) {
25915
- const skillFile = join7(dir, name, "SKILL.md");
27439
+ const skillFile = join8(dir, name, "SKILL.md");
25916
27440
  try {
25917
27441
  const raw = readOptional(skillFile);
25918
27442
  if (raw === void 0) continue;
@@ -25921,7 +27445,7 @@ function collectSkillsDir(scan2, dir, origin) {
25921
27445
  name: front.name ?? name,
25922
27446
  source: origin.source,
25923
27447
  scope: origin.scope,
25924
- location: join7(dir, name),
27448
+ location: join8(dir, name),
25925
27449
  updatedAt: statSync2(skillFile).mtime.toISOString()
25926
27450
  };
25927
27451
  const version2 = front.version ?? origin.defaultVersion;
@@ -25940,10 +27464,10 @@ function parseFrontmatter(raw) {
25940
27464
  if (lines[0]?.trim() !== "---") return out;
25941
27465
  for (const line of lines.slice(1)) {
25942
27466
  if (line.trim() === "---") break;
25943
- const sep4 = line.indexOf(":");
25944
- if (sep4 === -1) continue;
25945
- const key = line.slice(0, sep4).trim();
25946
- const value = line.slice(sep4 + 1).trim().replace(/^['"]|['"]$/g, "");
27467
+ const sep5 = line.indexOf(":");
27468
+ if (sep5 === -1) continue;
27469
+ const key = line.slice(0, sep5).trim();
27470
+ const value = line.slice(sep5 + 1).trim().replace(/^['"]|['"]$/g, "");
25947
27471
  if (value === "") continue;
25948
27472
  if (key === "name") out.name = value;
25949
27473
  else if (key === "description") out.description = value;
@@ -25952,7 +27476,7 @@ function parseFrontmatter(raw) {
25952
27476
  return out;
25953
27477
  }
25954
27478
  function collectInstalledPlugins(scan2, claudeDir) {
25955
- const manifestPath = join7(claudeDir, "plugins", "installed_plugins.json");
27479
+ const manifestPath = join8(claudeDir, "plugins", "installed_plugins.json");
25956
27480
  const raw = readOptional(manifestPath);
25957
27481
  if (raw === void 0) return;
25958
27482
  let plugins;
@@ -25977,7 +27501,7 @@ function collectInstalledPlugins(scan2, claudeDir) {
25977
27501
  if (typeof installPath !== "string" || seen.has(installPath)) continue;
25978
27502
  seen.add(installPath);
25979
27503
  const version2 = install.version;
25980
- const hooksPath = join7(installPath, "hooks", "hooks.json");
27504
+ const hooksPath = join8(installPath, "hooks", "hooks.json");
25981
27505
  const hooksRaw = readOptional(hooksPath);
25982
27506
  if (hooksRaw !== void 0) {
25983
27507
  try {
@@ -25997,22 +27521,22 @@ function collectInstalledPlugins(scan2, claudeDir) {
25997
27521
  }
25998
27522
  const origin = { source: marketplace, scope: "plugin", pluginName };
25999
27523
  if (typeof version2 === "string") origin.defaultVersion = version2;
26000
- collectSkillsDir(scan2, join7(installPath, "skills"), origin);
27524
+ collectSkillsDir(scan2, join8(installPath, "skills"), origin);
26001
27525
  const mcpOrigin = { scope: "plugin", pluginName, marketplace };
26002
- collectMcpFile(scan2, join7(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
27526
+ collectMcpFile(scan2, join8(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
26003
27527
  collectPluginManifestMcp(scan2, installPath, mcpOrigin);
26004
27528
  }
26005
27529
  }
26006
27530
  }
26007
27531
  function collectMarketplaceSkills(scan2, claudeDir) {
26008
- for (const mp of readMarketplaces(join7(claudeDir, "plugins", "known_marketplaces.json"))) {
27532
+ for (const mp of readMarketplaces(join8(claudeDir, "plugins", "known_marketplaces.json"))) {
26009
27533
  if (isClaudeOfficialMarketplace(mp.name, mp.repo)) continue;
26010
- collectSkillsDir(scan2, join7(mp.installLocation, "skills"), {
27534
+ collectSkillsDir(scan2, join8(mp.installLocation, "skills"), {
26011
27535
  source: mp.name,
26012
27536
  scope: "plugin"
26013
27537
  });
26014
- collectPluginSkillDirs(scan2, join7(mp.installLocation, "plugins"), mp.name);
26015
- collectPluginSkillDirs(scan2, join7(mp.installLocation, "external_plugins"), mp.name);
27538
+ collectPluginSkillDirs(scan2, join8(mp.installLocation, "plugins"), mp.name);
27539
+ collectPluginSkillDirs(scan2, join8(mp.installLocation, "external_plugins"), mp.name);
26016
27540
  }
26017
27541
  }
26018
27542
  function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
@@ -26023,7 +27547,7 @@ function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
26023
27547
  return;
26024
27548
  }
26025
27549
  for (const plugin of plugins) {
26026
- collectSkillsDir(scan2, join7(pluginsDir, plugin, "skills"), {
27550
+ collectSkillsDir(scan2, join8(pluginsDir, plugin, "skills"), {
26027
27551
  source: marketplace,
26028
27552
  scope: "plugin",
26029
27553
  pluginName: plugin
@@ -26097,10 +27621,7 @@ function str2(value) {
26097
27621
  }
26098
27622
 
26099
27623
  // ../../packages/plugin-sdk/src/events.ts
26100
- import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
26101
-
26102
- // ../../packages/plugin-sdk/src/finding-key.ts
26103
- import { createHash as createHash4 } from "crypto";
27624
+ import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
26104
27625
 
26105
27626
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
26106
27627
  import { arch, hostname as hostname3, platform, release } from "os";
@@ -26132,31 +27653,35 @@ function resolveInventoryContext(input) {
26132
27653
  }
26133
27654
 
26134
27655
  // ../../packages/plugin-sdk/src/nudge.ts
26135
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
26136
- import { join as join8 } from "path";
27656
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
27657
+ import { join as join9 } from "path";
26137
27658
  var SESSION_START_MARKER = "session-start-last";
26138
27659
  function claimSessionStart(dataDir2, sessionId) {
26139
27660
  return claimOncePerSession(dataDir2, SESSION_START_MARKER, sessionId);
26140
27661
  }
26141
27662
  function claimOncePerSession(dataDir2, marker, sessionId) {
26142
27663
  if (!sessionId) return true;
26143
- const path = join8(dataDir2, marker);
27664
+ const path = join9(dataDir2, marker);
26144
27665
  try {
26145
27666
  if (readFileSync5(path, "utf8") === sessionId) return false;
26146
27667
  } catch {
26147
27668
  }
26148
27669
  try {
26149
- mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
26150
- writeFileSync4(path, sessionId, { mode: DATA_FILE_MODE });
27670
+ mkdirSync2(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
27671
+ writeFileSync3(path, sessionId, { mode: DATA_FILE_MODE });
26151
27672
  } catch {
26152
27673
  }
26153
27674
  return true;
26154
27675
  }
26155
27676
 
27677
+ // ../../packages/plugin-sdk/src/paths.ts
27678
+ import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
27679
+ import { basename as basename4, dirname as dirname2, sep as sep3 } from "path";
27680
+
26156
27681
  // ../../packages/plugin-sdk/src/project-files.ts
26157
27682
  var import_ignore = __toESM(require_ignore(), 1);
26158
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
26159
- import { basename as basename4, join as join9, relative, sep as sep3 } from "path";
27683
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
27684
+ import { basename as basename5, join as join10, relative, sep as sep4 } from "path";
26160
27685
  var SKIP_DIRS = /* @__PURE__ */ new Set([
26161
27686
  ".git",
26162
27687
  "node_modules",
@@ -26176,7 +27701,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
26176
27701
  var MAX_FILES = 2e4;
26177
27702
  function readIgnoreLayer(dir) {
26178
27703
  try {
26179
- const content = readFileSync6(join9(dir, ".gitignore"), "utf8");
27704
+ const content = readFileSync6(join10(dir, ".gitignore"), "utf8");
26180
27705
  return { base: dir, matcher: (0, import_ignore.default)().add(content) };
26181
27706
  } catch {
26182
27707
  return void 0;
@@ -26185,7 +27710,7 @@ function readIgnoreLayer(dir) {
26185
27710
  function isIgnored(layers, absPath, isDir) {
26186
27711
  let ignored = false;
26187
27712
  for (const layer of layers) {
26188
- const rel = relative(layer.base, absPath).split(sep3).join("/") + (isDir ? "/" : "");
27713
+ const rel = relative(layer.base, absPath).split(sep4).join("/") + (isDir ? "/" : "");
26189
27714
  const verdict = layer.matcher.test(rel);
26190
27715
  if (verdict.ignored) ignored = true;
26191
27716
  else if (verdict.unignored) ignored = false;
@@ -26246,7 +27771,7 @@ function resolveProjectFiles(cwd) {
26246
27771
  let visit2 = function(dir, layers) {
26247
27772
  let dirents;
26248
27773
  try {
26249
- dirents = readdirSync2(dir, { withFileTypes: true, encoding: "utf8" });
27774
+ dirents = readdirSync3(dir, { withFileTypes: true, encoding: "utf8" });
26250
27775
  } catch {
26251
27776
  walk.lostSubtree = true;
26252
27777
  return false;
@@ -26254,10 +27779,10 @@ function resolveProjectFiles(cwd) {
26254
27779
  const layer = readIgnoreLayer(dir);
26255
27780
  const dirLayers = layer ? [...layers, layer] : layers;
26256
27781
  for (const entry of dirents) {
26257
- const fullPath = join9(dir, entry.name);
27782
+ const fullPath = join10(dir, entry.name);
26258
27783
  if (entry.isDirectory()) {
26259
27784
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
26260
- if (existsSync4(join9(fullPath, ".git"))) continue;
27785
+ if (existsSync5(join10(fullPath, ".git"))) continue;
26261
27786
  if (visit2(fullPath, dirLayers)) return true;
26262
27787
  continue;
26263
27788
  }
@@ -26265,10 +27790,10 @@ function resolveProjectFiles(cwd) {
26265
27790
  if (entry.name === ".git") continue;
26266
27791
  if (isIgnored(dirLayers, fullPath, false)) continue;
26267
27792
  if (files.length >= MAX_FILES) return true;
26268
- const relPath = relative(root, fullPath).split(sep3).join("/");
27793
+ const relPath = relative(root, fullPath).split(sep4).join("/");
26269
27794
  files.push({
26270
27795
  path: relPath,
26271
- name: basename4(entry.name),
27796
+ name: basename5(entry.name),
26272
27797
  origin: classifyOrigin(relPath, entry.name),
26273
27798
  defaultAccess: "approved"
26274
27799
  });
@@ -26298,17 +27823,17 @@ import { randomUUID as randomUUID10 } from "crypto";
26298
27823
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
26299
27824
 
26300
27825
  // ../../packages/plugin-sdk/src/throttle.ts
26301
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
26302
- import { join as join10 } from "path";
27826
+ import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
27827
+ import { join as join11 } from "path";
26303
27828
  function throttled(dataDir2, markerName, windowMs) {
26304
- const marker = join10(dataDir2, markerName);
27829
+ const marker = join11(dataDir2, markerName);
26305
27830
  try {
26306
27831
  if (Date.now() - statSync3(marker).mtimeMs < windowMs) return true;
26307
27832
  } catch {
26308
27833
  }
26309
27834
  try {
26310
- mkdirSync4(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
26311
- writeFileSync5(marker, String(Date.now()), { mode: DATA_FILE_MODE });
27835
+ mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
27836
+ writeFileSync4(marker, String(Date.now()), { mode: DATA_FILE_MODE });
26312
27837
  } catch {
26313
27838
  }
26314
27839
  return false;
@@ -26346,7 +27871,8 @@ var StandaloneDataGateway = class {
26346
27871
  }
26347
27872
  // The id is minted inside the repository from the natural key — the plugin can't
26348
27873
  // import @akasecurity/persistence to compute it, so the gateway is the boundary that
26349
- // hands the natural key across. INSERT OR IGNORE → idempotent re-reads.
27874
+ // hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
27875
+ // converge a streaming partial/final split (see insertLlmCall).
26350
27876
  recordLlmCall(input) {
26351
27877
  this.db.auditEvents.insertLlmCall(input);
26352
27878
  return Promise.resolve();
@@ -26388,7 +27914,9 @@ var StandaloneDataGateway = class {
26388
27914
  // caller's transaction (Layer 2b). The audit-event id the findings FK into is the
26389
27915
  // SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
26390
27916
  // idempotently. Definitions/classified-data are idempotent upserts; findings are
26391
- // content-addressed INSERT OR IGNORE.
27917
+ // content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
27918
+ // so a re-detection under a bumped rule version repoints the definition FK rather
27919
+ // than no-opping.
26392
27920
  writeToolCall(input) {
26393
27921
  this.db.auditEvents.insertToolCall(input);
26394
27922
  if (input.inspections.length === 0) return;
@@ -26408,7 +27936,7 @@ var StandaloneDataGateway = class {
26408
27936
  });
26409
27937
  const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
26410
27938
  this.db.inspectionFindings.insertFinding({
26411
- id: inspectionFindingId(auditEventId, definitionId, insp.span.start, insp.span.end),
27939
+ id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
26412
27940
  auditEventId,
26413
27941
  inspectionDefinitionId: definitionId,
26414
27942
  classifiedDataId: classifiedDataId2,
@@ -26457,10 +27985,17 @@ var StandaloneDataGateway = class {
26457
27985
  try {
26458
27986
  const snapshot = this.db.installedPacks.installedRuleset();
26459
27987
  if (snapshot.installedPacks === 0) return void 0;
26460
- if (snapshot.enabledPacks === 0) return { rules: [], ruleActions: /* @__PURE__ */ new Map(), complete: true };
27988
+ if (snapshot.enabledPacks === 0) {
27989
+ return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
27990
+ }
26461
27991
  if (snapshot.invalidRules > 0) return void 0;
26462
27992
  if (snapshot.rules.length === 0) return void 0;
26463
- return { rules: snapshot.rules, ruleActions: snapshot.ruleActions, complete: true };
27993
+ return {
27994
+ rules: snapshot.rules,
27995
+ ruleActions: snapshot.ruleActions,
27996
+ ruleVersions: snapshot.ruleVersions,
27997
+ complete: true
27998
+ };
26464
27999
  } catch {
26465
28000
  return void 0;
26466
28001
  }
@@ -26488,6 +28023,7 @@ var StandaloneDataGateway = class {
26488
28023
  policies: [...policies, ...rulePolicies],
26489
28024
  rules: installed ? installed.rules : [],
26490
28025
  ...installed ? { rulesComplete: true } : {},
28026
+ ...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
26491
28027
  ...exceptions !== void 0 ? { exceptions } : {},
26492
28028
  customKeywords,
26493
28029
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -26586,6 +28122,13 @@ var StandaloneDataGateway = class {
26586
28122
  this.db.scanLedger.upsertEntries(entries);
26587
28123
  return Promise.resolve();
26588
28124
  }
28125
+ getRuleProbeVerdict(ruleKey) {
28126
+ return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
28127
+ }
28128
+ setRuleProbeVerdict(ruleKey, verdict, worstProbeMs) {
28129
+ this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs);
28130
+ return Promise.resolve();
28131
+ }
26589
28132
  openAtRestKeysForPath(path) {
26590
28133
  return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
26591
28134
  }
@@ -26596,6 +28139,12 @@ var StandaloneDataGateway = class {
26596
28139
  this.db.resolutions.insertResolution(input);
26597
28140
  return Promise.resolve();
26598
28141
  }
28142
+ // Bare forward — no toggle read here. The plugin-path kill-switch is
28143
+ // enforced by the caller, which already holds the parsed workspace
28144
+ // settings; this class only ever sees `dataDir`, not the settings base.
28145
+ recordProjectEgress(input) {
28146
+ return Promise.resolve(this.db.shares.recordProjectEgress(input));
28147
+ }
26599
28148
  close() {
26600
28149
  this.db.close();
26601
28150
  return Promise.resolve();
@@ -26740,7 +28289,7 @@ function buildSessionRoot(sessionId, input, ctx, resolved, provider, branch) {
26740
28289
 
26741
28290
  // src/history/reconcile-trigger.ts
26742
28291
  import { spawn } from "child_process";
26743
- import { dirname as dirname2, join as join12 } from "path";
28292
+ import { dirname as dirname3, join as join13 } from "path";
26744
28293
  import { fileURLToPath } from "url";
26745
28294
 
26746
28295
  // src/history/tail.ts
@@ -26748,13 +28297,13 @@ import { createHash as createHash5 } from "crypto";
26748
28297
  import {
26749
28298
  closeSync,
26750
28299
  fstatSync,
26751
- mkdirSync as mkdirSync5,
28300
+ mkdirSync as mkdirSync4,
26752
28301
  openSync,
26753
28302
  readFileSync as readFileSync7,
26754
28303
  readSync,
26755
- writeFileSync as writeFileSync6
28304
+ writeFileSync as writeFileSync5
26756
28305
  } from "fs";
26757
- import { join as join11 } from "path";
28306
+ import { join as join12 } from "path";
26758
28307
  var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
26759
28308
  function safeSessionId(sessionId) {
26760
28309
  if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
@@ -26770,8 +28319,8 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
26770
28319
  try {
26771
28320
  const marker = `${RECONCILE_MARKER_PREFIX}-${safeSessionId(sessionId)}`;
26772
28321
  if (throttled(dataDir2, marker, RECONCILE_THROTTLE_MS)) return;
26773
- const here = dirname2(fileURLToPath(import.meta.url));
26774
- const child = spawn(process.execPath, [join12(here, "reconcile.js"), sessionId, transcriptPath], {
28322
+ const here = dirname3(fileURLToPath(import.meta.url));
28323
+ const child = spawn(process.execPath, [join13(here, "reconcile.js"), sessionId, transcriptPath], {
26775
28324
  detached: true,
26776
28325
  stdio: "ignore"
26777
28326
  });
@@ -26784,13 +28333,23 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
26784
28333
  async function readStdin() {
26785
28334
  return new Promise((resolve) => {
26786
28335
  let data = "";
26787
- process.stdin.setEncoding("utf8");
26788
- process.stdin.on("data", (chunk) => {
26789
- data += chunk;
26790
- });
26791
- process.stdin.on("end", () => {
28336
+ let settled = false;
28337
+ const finish = () => {
28338
+ if (settled) return;
28339
+ settled = true;
28340
+ clearTimeout(timer);
28341
+ process.stdin.removeListener("data", onData);
28342
+ process.stdin.removeListener("end", finish);
26792
28343
  resolve(data);
26793
- });
28344
+ };
28345
+ const onData = (chunk) => {
28346
+ data += chunk;
28347
+ };
28348
+ const timer = setTimeout(finish, 5e3);
28349
+ process.stdin.setEncoding("utf8");
28350
+ process.stdin.on("data", onData);
28351
+ process.stdin.on("end", finish);
28352
+ process.stdin.on("error", finish);
26794
28353
  });
26795
28354
  }
26796
28355
  function parseJson(raw) {