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

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.
@@ -546,6 +546,10 @@ var SQLITE_MIGRATIONS = [
546
546
  {
547
547
  tag: "0010_events_session_expression_index",
548
548
  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"
549
+ },
550
+ {
551
+ tag: "0011_egress_writer",
552
+ 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'
549
553
  }
550
554
  ];
551
555
 
@@ -16201,6 +16205,7 @@ var ExceptionBundleEntry = DetectionException.pick({
16201
16205
 
16202
16206
  // ../../packages/schema/src/zod/rule.ts
16203
16207
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16208
+ var RuleProbeVerdict = external_exports.enum(["safe", "quarantined"]).meta({ id: "RuleProbeVerdict" });
16204
16209
  var KeywordMatcher = external_exports.object({
16205
16210
  type: external_exports.literal("keyword"),
16206
16211
  // An empty keyword matches at every position, yielding one zero-length span
@@ -16225,9 +16230,10 @@ function matchesEmptyString(pattern, flags) {
16225
16230
  return false;
16226
16231
  }
16227
16232
  }
16233
+ var MAX_PATTERN_LENGTH = 2e3;
16228
16234
  var RegexMatcher = external_exports.object({
16229
16235
  type: external_exports.literal("regex"),
16230
- pattern: external_exports.string(),
16236
+ pattern: external_exports.string().min(1).max(MAX_PATTERN_LENGTH),
16231
16237
  flags: external_exports.string().default("gi"),
16232
16238
  captureGroup: external_exports.number().int().nonnegative().optional()
16233
16239
  }).refine((v) => isValidRegex(v.pattern, v.flags), {
@@ -16794,6 +16800,212 @@ function buildDetectionsList(summaries, query) {
16794
16800
  return { counts, items: filtered.map(summaryToDetectionListItem) };
16795
16801
  }
16796
16802
 
16803
+ // ../../packages/schema/src/zod/shares.ts
16804
+ var DestinationKind = external_exports.enum(["provider", "internal", "external", "ip"]).meta({ id: "DestinationKind" });
16805
+ var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp", "ws", "wss"]).meta({ id: "Transport" });
16806
+ var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
16807
+ var DATA_CLASS_ORDER = DataClass.options;
16808
+ var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
16809
+ var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
16810
+ var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
16811
+ var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
16812
+ var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE", "SDK", "REF"]).meta({ id: "HttpMethod" });
16813
+ var ReviewInfo = external_exports.object({
16814
+ needsReview: external_exports.boolean(),
16815
+ reasons: external_exports.array(ReviewReason)
16816
+ }).meta({ id: "ReviewInfo" });
16817
+ var DestinationNetwork = external_exports.object({
16818
+ port: external_exports.number().int().nullable(),
16819
+ geo: external_exports.string().nullable(),
16820
+ ptr: external_exports.string().nullable()
16821
+ }).meta({ id: "DestinationNetwork" });
16822
+ var EndpointSummary = external_exports.object({
16823
+ id: external_exports.string(),
16824
+ method: HttpMethod,
16825
+ transport: Transport,
16826
+ url: external_exports.string(),
16827
+ template: external_exports.boolean(),
16828
+ dataClass: DataClass,
16829
+ lastSeen: external_exports.iso.datetime(),
16830
+ callSiteCount: external_exports.number().int().nonnegative()
16831
+ }).meta({ id: "EndpointSummary" });
16832
+ var CallSite = external_exports.object({
16833
+ id: external_exports.string(),
16834
+ project: external_exports.string(),
16835
+ file: external_exports.string(),
16836
+ line: external_exports.number().int().nonnegative(),
16837
+ snippet: external_exports.string(),
16838
+ dynamic: external_exports.boolean(),
16839
+ vendored: external_exports.boolean(),
16840
+ /** Deep-link to the Inventory project, when the repo is governed there. */
16841
+ projectId: external_exports.string().nullable()
16842
+ }).meta({ id: "CallSite" });
16843
+ var EndpointWithSites = EndpointSummary.extend({
16844
+ sites: external_exports.array(CallSite)
16845
+ }).meta({ id: "EndpointWithSites" });
16846
+ var ShareDestinationSummary = external_exports.object({
16847
+ id: external_exports.string(),
16848
+ kind: DestinationKind,
16849
+ name: external_exports.string(),
16850
+ host: external_exports.string(),
16851
+ category: external_exports.string(),
16852
+ trust: ShareTrustLevel,
16853
+ /** Effective state (decision applied over the trust default). */
16854
+ status: EgressStatus,
16855
+ /** True when an egress decision override differs from the trust default. */
16856
+ isCustom: external_exports.boolean(),
16857
+ lastSeen: external_exports.iso.datetime(),
16858
+ endpointCount: external_exports.number().int().nonnegative(),
16859
+ callSiteCount: external_exports.number().int().nonnegative(),
16860
+ transports: external_exports.array(Transport),
16861
+ /** Most-sensitive first. */
16862
+ dataClasses: external_exports.array(DataClass),
16863
+ review: ReviewInfo,
16864
+ /** Non-provider hosts only; null for providers. */
16865
+ network: DestinationNetwork.nullable(),
16866
+ /** Embedded for inline expansion — no call sites here. */
16867
+ endpoints: external_exports.array(EndpointSummary)
16868
+ }).meta({ id: "ShareDestinationSummary" });
16869
+ var ShareDestinationDetail = ShareDestinationSummary.omit({
16870
+ endpointCount: true,
16871
+ callSiteCount: true,
16872
+ endpoints: true
16873
+ }).extend({
16874
+ /** Ownership/geo rationale; null for providers. */
16875
+ note: external_exports.string().nullable(),
16876
+ endpoints: external_exports.array(EndpointWithSites)
16877
+ }).meta({ id: "ShareDestinationDetail" });
16878
+ var ReviewDestination = external_exports.object({
16879
+ id: external_exports.string(),
16880
+ kind: DestinationKind,
16881
+ name: external_exports.string(),
16882
+ /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
16883
+ host: external_exports.string(),
16884
+ trust: ShareTrustLevel,
16885
+ status: EgressStatus,
16886
+ review: ReviewInfo,
16887
+ topDataClass: DataClass,
16888
+ callSiteCount: external_exports.number().int().nonnegative(),
16889
+ lastSeen: external_exports.iso.datetime()
16890
+ }).meta({ id: "ReviewDestination" });
16891
+ var ShareDestinationGroup = external_exports.object({
16892
+ kind: DestinationKind,
16893
+ total: external_exports.number().int().nonnegative(),
16894
+ items: external_exports.array(ShareDestinationSummary)
16895
+ }).meta({ id: "ShareDestinationGroup" });
16896
+ var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
16897
+ var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
16898
+ var SharesStats = external_exports.object({
16899
+ destinations: external_exports.number().int().nonnegative(),
16900
+ endpoints: external_exports.number().int().nonnegative(),
16901
+ callSites: external_exports.number().int().nonnegative(),
16902
+ needsReview: external_exports.number().int().nonnegative(),
16903
+ insecure: external_exports.number().int().nonnegative(),
16904
+ byKind: external_exports.object({
16905
+ provider: external_exports.number().int().nonnegative(),
16906
+ internal: external_exports.number().int().nonnegative(),
16907
+ external: external_exports.number().int().nonnegative(),
16908
+ ip: external_exports.number().int().nonnegative()
16909
+ }),
16910
+ byTrust: external_exports.object({
16911
+ recognized: external_exports.number().int().nonnegative(),
16912
+ internal: external_exports.number().int().nonnegative(),
16913
+ unverified: external_exports.number().int().nonnegative(),
16914
+ ip: external_exports.number().int().nonnegative()
16915
+ })
16916
+ }).meta({ id: "SharesStats" });
16917
+ var SetEgressDecisionBody = external_exports.object({
16918
+ /** `null` clears the override — reverts to the trust default, isCustom false. */
16919
+ decision: EgressDecision.nullable()
16920
+ }).meta({ id: "SetEgressDecisionBody" });
16921
+ var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
16922
+ var ListShareDestinationsQuery = external_exports.object({
16923
+ /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
16924
+ q: external_exports.string().optional(),
16925
+ /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
16926
+ kind: external_exports.array(DestinationKind).optional(),
16927
+ /** Reserved for future grouping modes; only 'destination' is supported today. */
16928
+ groupBy: external_exports.enum(["destination"]).default("destination"),
16929
+ /**
16930
+ * When true, return a flat severity-ordered `items[]` instead of `groups`.
16931
+ * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
16932
+ * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
16933
+ * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
16934
+ */
16935
+ review: external_exports.stringbool().default(false)
16936
+ });
16937
+ var ExportSharesQuery = external_exports.object({
16938
+ format: external_exports.enum(["csv", "json"]).default("csv"),
16939
+ q: external_exports.string().optional(),
16940
+ kind: external_exports.array(DestinationKind).optional()
16941
+ });
16942
+
16943
+ // ../../packages/schema/src/zod/egress-extraction.ts
16944
+ var EgressEcosystem = external_exports.enum(["npm", "pypi", "go", "maven", "rubygems", "cargo", "composer", "nuget"]).meta({ id: "EgressEcosystem" });
16945
+ var ProviderRegistryEntry = external_exports.object({
16946
+ id: external_exports.string(),
16947
+ name: external_exports.string(),
16948
+ category: external_exports.string(),
16949
+ /** Suffix-matched: 'stripe.com' matches api.stripe.com, never evilstripe.com. */
16950
+ hostSuffixes: external_exports.array(external_exports.string()).min(1),
16951
+ /** Canonical API base URL recorded for manifest-derived (method 'SDK') endpoints. */
16952
+ apiBase: external_exports.string(),
16953
+ /** Most-sensitive first; index 0 becomes the endpoint dataClass. */
16954
+ defaultDataClasses: external_exports.array(DataClass).min(1),
16955
+ /** SDK identifiers per ecosystem ('go' prefix-matched by path, 'maven' by group-id prefix). */
16956
+ sdks: external_exports.partialRecord(EgressEcosystem, external_exports.array(external_exports.string()))
16957
+ }).meta({ id: "ProviderRegistryEntry" });
16958
+ var EgressCallSiteHit = external_exports.object({
16959
+ file: external_exports.string(),
16960
+ line: external_exports.number().int().positive(),
16961
+ snippet: external_exports.string(),
16962
+ dynamic: external_exports.boolean(),
16963
+ vendored: external_exports.boolean()
16964
+ }).meta({ id: "EgressCallSiteHit" });
16965
+ var ResolvedEgressHit = external_exports.object({
16966
+ host: external_exports.string(),
16967
+ kind: DestinationKind,
16968
+ name: external_exports.string(),
16969
+ category: external_exports.string(),
16970
+ trust: ShareTrustLevel,
16971
+ network: DestinationNetwork.nullable(),
16972
+ method: HttpMethod,
16973
+ transport: Transport,
16974
+ url: external_exports.string(),
16975
+ template: external_exports.boolean(),
16976
+ dataClass: DataClass,
16977
+ site: EgressCallSiteHit
16978
+ }).meta({ id: "ResolvedEgressHit" });
16979
+ var EgressReconcile = external_exports.discriminatedUnion("mode", [
16980
+ external_exports.object({ mode: external_exports.literal("walk"), walkedPrefix: external_exports.string() }),
16981
+ external_exports.object({
16982
+ mode: external_exports.literal("ledger"),
16983
+ scannedFiles: external_exports.array(external_exports.string()),
16984
+ deletedFiles: external_exports.array(external_exports.string())
16985
+ })
16986
+ ]).meta({ id: "EgressReconcile" });
16987
+ var RecordProjectEgressInput = external_exports.object({
16988
+ /** Stable reconcile key: 'git:<repo identity>' or 'path:<abs root>' (non-git). */
16989
+ projectKey: external_exports.string().min(1),
16990
+ /** Display name only — never keys reconciliation. */
16991
+ project: external_exports.string(),
16992
+ projectId: external_exports.string().nullable(),
16993
+ reconcile: EgressReconcile,
16994
+ hits: external_exports.array(ResolvedEgressHit)
16995
+ }).meta({ id: "RecordProjectEgressInput" });
16996
+ var EgressWriteSummary = external_exports.object({
16997
+ destinations: external_exports.number().int().nonnegative(),
16998
+ endpoints: external_exports.number().int().nonnegative(),
16999
+ callSites: external_exports.number().int().nonnegative(),
17000
+ truncated: external_exports.boolean(),
17001
+ /**
17002
+ * Files the cap dropped whole. Their stored rows were left untouched, so a
17003
+ * ledger-keeping caller must withhold their ledger entries and read them
17004
+ * again next scan.
17005
+ */
17006
+ droppedFiles: external_exports.array(external_exports.string()).default([])
17007
+ }).meta({ id: "EgressWriteSummary" });
17008
+
16797
17009
  // ../../packages/schema/src/zod/findings-group-build.ts
16798
17010
  function toApiAction(dbVal) {
16799
17011
  const map2 = {
@@ -17055,7 +17267,7 @@ var PatchInstalledPackRequest = external_exports.object({
17055
17267
  }).meta({ id: "PatchInstalledPackRequest" });
17056
17268
 
17057
17269
  // ../../packages/schema/src/zod/local.ts
17058
- var WORKSPACE_SETTINGS_SPEC_VERSION = 2;
17270
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 3;
17059
17271
  var RunMode = external_exports.enum(["standalone"]);
17060
17272
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17061
17273
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
@@ -17070,6 +17282,9 @@ var WorkspaceSettings = external_exports.object({
17070
17282
  policy: SimpleDetectionPolicy.default("redact"),
17071
17283
  // Consent for scanning pre-install surfaces; opt-in (see HistoricalAccess).
17072
17284
  historicalAccess: HistoricalAccess.default("session-only"),
17285
+ // In-place egress extraction on the scan paths; disable to stop all Data
17286
+ // Shares writes.
17287
+ dataSharesInPlace: external_exports.boolean().default(true),
17073
17288
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17074
17289
  onboardedAt: external_exports.iso.datetime().optional()
17075
17290
  });
@@ -17553,145 +17768,6 @@ var SetupHandoffOffer = external_exports.object({
17553
17768
  path: ["liveKeys"]
17554
17769
  });
17555
17770
 
17556
- // ../../packages/schema/src/zod/shares.ts
17557
- var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17558
- var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
17559
- var DataClass = external_exports.enum(["secrets", "pii", "customer", "source", "telemetry", "logs", "metrics", "none"]).meta({ id: "DataClass" });
17560
- var DATA_CLASS_ORDER = DataClass.options;
17561
- var ShareTrustLevel = external_exports.enum(["recognized", "internal", "unverified", "ip"]).meta({ id: "ShareTrustLevel" });
17562
- var EgressDecision = external_exports.enum(["allow", "block"]).meta({ id: "EgressDecision" });
17563
- var EgressStatus = external_exports.enum(["allowed", "blocked", "review"]).meta({ id: "EgressStatus" });
17564
- var ReviewReason = external_exports.enum(["raw_ip", "unverified_domain", "plaintext_transport"]).meta({ id: "ReviewReason" });
17565
- var HttpMethod = external_exports.enum(["GET", "POST", "PUT", "DELETE"]).meta({ id: "HttpMethod" });
17566
- var ReviewInfo = external_exports.object({
17567
- needsReview: external_exports.boolean(),
17568
- reasons: external_exports.array(ReviewReason)
17569
- }).meta({ id: "ReviewInfo" });
17570
- var DestinationNetwork = external_exports.object({
17571
- port: external_exports.number().int().nullable(),
17572
- geo: external_exports.string().nullable(),
17573
- ptr: external_exports.string().nullable()
17574
- }).meta({ id: "DestinationNetwork" });
17575
- var EndpointSummary = external_exports.object({
17576
- id: external_exports.string(),
17577
- method: HttpMethod,
17578
- transport: Transport,
17579
- url: external_exports.string(),
17580
- template: external_exports.boolean(),
17581
- dataClass: DataClass,
17582
- lastSeen: external_exports.iso.datetime(),
17583
- callSiteCount: external_exports.number().int().nonnegative()
17584
- }).meta({ id: "EndpointSummary" });
17585
- var CallSite = external_exports.object({
17586
- id: external_exports.string(),
17587
- project: external_exports.string(),
17588
- file: external_exports.string(),
17589
- line: external_exports.number().int().nonnegative(),
17590
- snippet: external_exports.string(),
17591
- dynamic: external_exports.boolean(),
17592
- vendored: external_exports.boolean(),
17593
- /** Deep-link to the Inventory project, when the repo is governed there. */
17594
- projectId: external_exports.string().nullable()
17595
- }).meta({ id: "CallSite" });
17596
- var EndpointWithSites = EndpointSummary.extend({
17597
- sites: external_exports.array(CallSite)
17598
- }).meta({ id: "EndpointWithSites" });
17599
- var ShareDestinationSummary = external_exports.object({
17600
- id: external_exports.string(),
17601
- kind: DestinationKind,
17602
- name: external_exports.string(),
17603
- host: external_exports.string(),
17604
- category: external_exports.string(),
17605
- trust: ShareTrustLevel,
17606
- /** Effective state (decision applied over the trust default). */
17607
- status: EgressStatus,
17608
- /** True when an egress decision override differs from the trust default. */
17609
- isCustom: external_exports.boolean(),
17610
- lastSeen: external_exports.iso.datetime(),
17611
- endpointCount: external_exports.number().int().nonnegative(),
17612
- callSiteCount: external_exports.number().int().nonnegative(),
17613
- transports: external_exports.array(Transport),
17614
- /** Most-sensitive first. */
17615
- dataClasses: external_exports.array(DataClass),
17616
- review: ReviewInfo,
17617
- /** Non-provider hosts only; null for providers. */
17618
- network: DestinationNetwork.nullable(),
17619
- /** Embedded for inline expansion — no call sites here. */
17620
- endpoints: external_exports.array(EndpointSummary)
17621
- }).meta({ id: "ShareDestinationSummary" });
17622
- var ShareDestinationDetail = ShareDestinationSummary.omit({
17623
- endpointCount: true,
17624
- callSiteCount: true,
17625
- endpoints: true
17626
- }).extend({
17627
- /** Ownership/geo rationale; null for providers. */
17628
- note: external_exports.string().nullable(),
17629
- endpoints: external_exports.array(EndpointWithSites)
17630
- }).meta({ id: "ShareDestinationDetail" });
17631
- var ReviewDestination = external_exports.object({
17632
- id: external_exports.string(),
17633
- kind: DestinationKind,
17634
- name: external_exports.string(),
17635
- /** Registrable host — lets the strip derive the provider lettermark, as the register does. */
17636
- host: external_exports.string(),
17637
- trust: ShareTrustLevel,
17638
- status: EgressStatus,
17639
- review: ReviewInfo,
17640
- topDataClass: DataClass,
17641
- callSiteCount: external_exports.number().int().nonnegative(),
17642
- lastSeen: external_exports.iso.datetime()
17643
- }).meta({ id: "ReviewDestination" });
17644
- var ShareDestinationGroup = external_exports.object({
17645
- kind: DestinationKind,
17646
- total: external_exports.number().int().nonnegative(),
17647
- items: external_exports.array(ShareDestinationSummary)
17648
- }).meta({ id: "ShareDestinationGroup" });
17649
- var ListShareDestinationsResponse = external_exports.object({ groups: external_exports.array(ShareDestinationGroup) }).meta({ id: "ListShareDestinationsResponse" });
17650
- var NeedsReviewResponse = external_exports.object({ items: external_exports.array(ReviewDestination) }).meta({ id: "NeedsReviewResponse" });
17651
- var SharesStats = external_exports.object({
17652
- destinations: external_exports.number().int().nonnegative(),
17653
- endpoints: external_exports.number().int().nonnegative(),
17654
- callSites: external_exports.number().int().nonnegative(),
17655
- needsReview: external_exports.number().int().nonnegative(),
17656
- insecure: external_exports.number().int().nonnegative(),
17657
- byKind: external_exports.object({
17658
- provider: external_exports.number().int().nonnegative(),
17659
- internal: external_exports.number().int().nonnegative(),
17660
- ip: external_exports.number().int().nonnegative()
17661
- }),
17662
- byTrust: external_exports.object({
17663
- recognized: external_exports.number().int().nonnegative(),
17664
- internal: external_exports.number().int().nonnegative(),
17665
- unverified: external_exports.number().int().nonnegative(),
17666
- ip: external_exports.number().int().nonnegative()
17667
- })
17668
- }).meta({ id: "SharesStats" });
17669
- var SetEgressDecisionBody = external_exports.object({
17670
- /** `null` clears the override — reverts to the trust default, isCustom false. */
17671
- decision: EgressDecision.nullable()
17672
- }).meta({ id: "SetEgressDecisionBody" });
17673
- var SetEgressDecisionResponse = external_exports.object({ destination: ShareDestinationSummary }).meta({ id: "SetEgressDecisionResponse" });
17674
- var ListShareDestinationsQuery = external_exports.object({
17675
- /** Case-insensitive match over destination name/category, endpoint url, call-site project/file. */
17676
- q: external_exports.string().optional(),
17677
- /** Repeatable. Restrict to these DestinationKind values; absent means all kinds. */
17678
- kind: external_exports.array(DestinationKind).optional(),
17679
- /** Reserved for future grouping modes; only 'destination' is supported today. */
17680
- groupBy: external_exports.enum(["destination"]).default("destination"),
17681
- /**
17682
- * When true, return a flat severity-ordered `items[]` instead of `groups`.
17683
- * Uses `z.stringbool()` (NOT `z.coerce.boolean()` — `Boolean(str)` is true for
17684
- * any non-empty string, so `?review=false`/`?review=0` would wrongly coerce
17685
- * to `true`). `z.stringbool()` parses true/1/yes vs false/0/no correctly.
17686
- */
17687
- review: external_exports.stringbool().default(false)
17688
- });
17689
- var ExportSharesQuery = external_exports.object({
17690
- format: external_exports.enum(["csv", "json"]).default("csv"),
17691
- q: external_exports.string().optional(),
17692
- kind: external_exports.array(DestinationKind).optional()
17693
- });
17694
-
17695
17771
  // ../../packages/schema/src/zod/shares-access.ts
17696
17772
  var ALLOWED_BY_DEFAULT_TRUST = /* @__PURE__ */ new Set(["recognized", "internal"]);
17697
17773
  function trustDefaultStatus(trust) {
@@ -17711,7 +17787,7 @@ function deriveReviewReasons(trust, transports) {
17711
17787
  const reasons = [];
17712
17788
  if (trust === "ip") reasons.push("raw_ip");
17713
17789
  if (trust === "unverified") reasons.push("unverified_domain");
17714
- if (transports.includes("http")) reasons.push("plaintext_transport");
17790
+ if (transports.includes("http") || transports.includes("ws")) reasons.push("plaintext_transport");
17715
17791
  return reasons;
17716
17792
  }
17717
17793
  function buildReviewInfo(trust, transports) {
@@ -17942,6 +18018,7 @@ function applyMigrations(db) {
17942
18018
  ensureSyncedAtColumn(db, "audit_events");
17943
18019
  ensureScanLedgerTable(db);
17944
18020
  ensureBlockedDetectionsTable(db);
18021
+ ensureRuleProbeCacheTable(db);
17945
18022
  ensureWriteGateTrigger(db);
17946
18023
  ensureTokenUsageColumns(db);
17947
18024
  reconcileSourceProjectIds(db);
@@ -18081,6 +18158,14 @@ function ensureBlockedDetectionsTable(db) {
18081
18158
  blocked_at INTEGER NOT NULL
18082
18159
  )`);
18083
18160
  }
18161
+ function ensureRuleProbeCacheTable(db) {
18162
+ db.exec(`CREATE TABLE IF NOT EXISTS rule_probe_cache (
18163
+ rule_key TEXT PRIMARY KEY,
18164
+ verdict TEXT NOT NULL,
18165
+ worst_probe_ms REAL NOT NULL,
18166
+ checked_at INTEGER NOT NULL
18167
+ )`);
18168
+ }
18084
18169
 
18085
18170
  // ../../packages/persistence/src/paths.ts
18086
18171
  import { chmodSync, mkdirSync } from "fs";
@@ -21634,6 +21719,35 @@ var SqliteResolutionsRepository = class {
21634
21719
  }
21635
21720
  };
21636
21721
 
21722
+ // ../../packages/persistence/src/repositories/rule-probe-cache.ts
21723
+ var SqliteRuleProbeCacheRepository = class {
21724
+ constructor(db) {
21725
+ this.db = db;
21726
+ this.upsertStmt = db.prepare(
21727
+ `INSERT INTO rule_probe_cache (rule_key, verdict, worst_probe_ms, checked_at)
21728
+ VALUES (:ruleKey, :verdict, :worstProbeMs, :checkedAt)
21729
+ ON CONFLICT (rule_key) DO UPDATE SET
21730
+ verdict = excluded.verdict,
21731
+ worst_probe_ms = excluded.worst_probe_ms,
21732
+ checked_at = excluded.checked_at`
21733
+ );
21734
+ this.readStmt = db.prepare(
21735
+ `SELECT verdict, worst_probe_ms AS worstProbeMs FROM rule_probe_cache WHERE rule_key = :ruleKey`
21736
+ );
21737
+ }
21738
+ db;
21739
+ upsertStmt;
21740
+ readStmt;
21741
+ getVerdict(ruleKey) {
21742
+ return getRow(this.readStmt, { ruleKey });
21743
+ }
21744
+ setVerdict(ruleKey, verdict, worstProbeMs2) {
21745
+ failOpenTransaction(this.db, () => {
21746
+ this.upsertStmt.run({ ruleKey, verdict, worstProbeMs: worstProbeMs2, checkedAt: Date.now() });
21747
+ });
21748
+ }
21749
+ };
21750
+
21637
21751
  // ../../packages/persistence/src/repositories/scan-ledger.ts
21638
21752
  var SqliteScanLedgerRepository = class {
21639
21753
  constructor(db) {
@@ -22045,11 +22159,50 @@ var SqliteSecurityRepository = class {
22045
22159
 
22046
22160
  // ../../packages/persistence/src/repositories/shares.ts
22047
22161
  import { randomUUID as randomUUID7 } from "crypto";
22048
- var KIND_ORDER = ["provider", "internal", "ip"];
22162
+ var MAX_EGRESS_CALL_SITES_PER_PROJECT = 5e3;
22163
+ var IN_CHUNK = 500;
22164
+ var KIND_ORDER = ["provider", "internal", "external", "ip"];
22165
+ var PLAINTEXT_TRANSPORT_SQL = "('http', 'ws')";
22166
+ var OVERRIDE_JOIN = `LEFT JOIN egress_decision_override oh ON oh.host = d.host
22167
+ LEFT JOIN egress_decision_override ol ON ol.destination_id = d.id AND ol.host IS NULL`;
22049
22168
  var CALL_SITE_EMBED_CAP = 200;
22050
22169
  function parseNetwork(networkJson) {
22051
22170
  return safeJson(networkJson, null);
22052
22171
  }
22172
+ function capHits(all, mode) {
22173
+ if (all.length <= MAX_EGRESS_CALL_SITES_PER_PROJECT) {
22174
+ return { hits: [...all], droppedFiles: [], truncated: false };
22175
+ }
22176
+ if (mode === "walk") {
22177
+ return {
22178
+ hits: all.slice(0, MAX_EGRESS_CALL_SITES_PER_PROJECT),
22179
+ droppedFiles: [],
22180
+ truncated: true
22181
+ };
22182
+ }
22183
+ const byFile = /* @__PURE__ */ new Map();
22184
+ for (const hit of all) {
22185
+ const bucket = byFile.get(hit.site.file);
22186
+ if (bucket === void 0) byFile.set(hit.site.file, [hit]);
22187
+ else bucket.push(hit);
22188
+ }
22189
+ const hits = [];
22190
+ const droppedFiles = [];
22191
+ for (const [file2, bucket] of byFile) {
22192
+ if (hits.length + bucket.length > MAX_EGRESS_CALL_SITES_PER_PROJECT) droppedFiles.push(file2);
22193
+ else hits.push(...bucket);
22194
+ }
22195
+ return { hits, droppedFiles, truncated: true };
22196
+ }
22197
+ function withoutDroppedFiles(reconcile, droppedFiles) {
22198
+ if (reconcile.mode === "walk" || droppedFiles.length === 0) return reconcile;
22199
+ const dropped = new Set(droppedFiles);
22200
+ return {
22201
+ mode: "ledger",
22202
+ scannedFiles: reconcile.scannedFiles.filter((file2) => !dropped.has(file2)),
22203
+ deletedFiles: reconcile.deletedFiles.filter((file2) => !dropped.has(file2))
22204
+ };
22205
+ }
22053
22206
  function toEndpointSummary(row) {
22054
22207
  return {
22055
22208
  id: row.id,
@@ -22140,13 +22293,15 @@ var SqliteSharesRepository = class {
22140
22293
  const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22141
22294
  const insecure = countScalar(
22142
22295
  this.db,
22143
- "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
22296
+ `SELECT count(DISTINCT destination_id) AS n FROM share_endpoint
22297
+ WHERE transport IN ${PLAINTEXT_TRANSPORT_SQL}`
22144
22298
  );
22145
22299
  const needsReview = countScalar(
22146
22300
  this.db,
22147
22301
  `SELECT count(DISTINCT d.id) AS n
22148
22302
  FROM share_destination d
22149
- LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
22303
+ LEFT JOIN share_endpoint e ON e.destination_id = d.id
22304
+ AND e.transport IN ${PLAINTEXT_TRANSPORT_SQL}
22150
22305
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
22151
22306
  );
22152
22307
  const kindCounts = countBy(
@@ -22156,6 +22311,7 @@ var SqliteSharesRepository = class {
22156
22311
  const byKind = {
22157
22312
  provider: kindCounts.get("provider") ?? 0,
22158
22313
  internal: kindCounts.get("internal") ?? 0,
22314
+ external: kindCounts.get("external") ?? 0,
22159
22315
  ip: kindCounts.get("ip") ?? 0
22160
22316
  };
22161
22317
  const trustCounts = countBy(
@@ -22231,23 +22387,316 @@ var SqliteSharesRepository = class {
22231
22387
  // real edit from a no-such-destination.
22232
22388
  /**
22233
22389
  * Set (decision) or clear (null) the egress decision override for a destination.
22234
- * `null` deletes the override row → reverts to the trust default.
22390
+ * `null` deletes the override rows → reverts to the trust default.
22391
+ *
22392
+ * The written row carries both the destination id and its host, so the
22393
+ * decision re-attaches by host after the destination is pruned and
22394
+ * re-detected under a fresh id. Rows written before the host column existed
22395
+ * (host NULL, matched by destination id) are replaced rather than left to
22396
+ * shadow the new one. Runs IMMEDIATE: the host lookup is read-then-write and
22397
+ * would otherwise race a concurrent prune.
22235
22398
  */
22236
22399
  setEgressDecision(destinationId, decision) {
22237
- const exists = this.db.prepare("SELECT 1 FROM share_destination WHERE id = ?").get(destinationId);
22238
- if (exists === void 0) return false;
22239
- if (decision === null) {
22240
- this.db.prepare("DELETE FROM egress_decision_override WHERE destination_id = ?").run(destinationId);
22241
- return true;
22400
+ let existed = false;
22401
+ withTransaction(
22402
+ this.db,
22403
+ () => {
22404
+ const dest = this.db.prepare("SELECT host FROM share_destination WHERE id = ?").get(destinationId);
22405
+ if (dest === void 0) return;
22406
+ existed = true;
22407
+ this.db.prepare(
22408
+ `DELETE FROM egress_decision_override
22409
+ WHERE host = :host OR (destination_id = :destinationId AND host IS NULL)`
22410
+ ).run({ host: dest.host, destinationId });
22411
+ if (decision === null) return;
22412
+ this.db.prepare(
22413
+ `INSERT INTO egress_decision_override
22414
+ (id, destination_id, host, decision, created_at, updated_at)
22415
+ VALUES (:id, :destinationId, :host, :decision, :now, :now)`
22416
+ ).run({
22417
+ id: randomUUID7(),
22418
+ destinationId,
22419
+ host: dest.host,
22420
+ decision,
22421
+ now: Date.now()
22422
+ });
22423
+ },
22424
+ "IMMEDIATE"
22425
+ );
22426
+ return existed;
22427
+ }
22428
+ /**
22429
+ * Record one project's statically-extracted egress: reconcile the previously
22430
+ * stored call sites against this scan, upsert destination → endpoint → call
22431
+ * site for every hit, confirm `last_seen` on everything the project still
22432
+ * references, and drop what no longer has evidence.
22433
+ *
22434
+ * Reconciliation keys on `projectKey` alone; `project` and `projectId` are
22435
+ * display payload and never scope a delete. The whole write is one
22436
+ * transaction: a failure leaves the project's previous inventory exactly as
22437
+ * it was, and THROWS rather than reporting a partial write — callers decide
22438
+ * their own fail-open behavior, and the scanner additionally withholds its
22439
+ * ledger commit so the next scan retries.
22440
+ *
22441
+ * Over-cap input is truncated at a FILE boundary, and the files that lost
22442
+ * their hits are both excluded from the reconcile delete and named in
22443
+ * `droppedFiles`. That pairing is what keeps truncation non-destructive on
22444
+ * the ledger path: a dropped file keeps whatever rows it already had, and its
22445
+ * caller withholds the ledger entry so the next scan reads it again.
22446
+ */
22447
+ recordProjectEgress(input) {
22448
+ const { hits, droppedFiles, truncated } = capHits(input.hits, input.reconcile.mode);
22449
+ const reconcile = withoutDroppedFiles(input.reconcile, droppedFiles);
22450
+ const now = Date.now();
22451
+ let summary = {
22452
+ destinations: 0,
22453
+ endpoints: 0,
22454
+ callSites: 0,
22455
+ truncated,
22456
+ droppedFiles
22457
+ };
22458
+ withTransaction(
22459
+ this.db,
22460
+ () => {
22461
+ const projectId = input.projectId ?? this.knownProjectId(input.projectKey);
22462
+ this.reconcileCallSites(input.projectKey, reconcile);
22463
+ this.upsertHits(input, hits, projectId, now);
22464
+ this.confirmLastSeen(input.projectKey, now);
22465
+ this.pruneOrphans();
22466
+ summary = { ...this.projectTotals(input.projectKey), truncated, droppedFiles };
22467
+ },
22468
+ "IMMEDIATE"
22469
+ );
22470
+ return summary;
22471
+ }
22472
+ // ─── Egress write internals ──────────────────────────────────────────────────
22473
+ /**
22474
+ * Clear the stored call sites this scan is responsible for re-creating.
22475
+ *
22476
+ * Each pipeline may only delete rows its own walker could have produced. The
22477
+ * fs walk behind 'walk' mode never descends into dot-directories, so its
22478
+ * delete excludes dot-path files — those rows are the plugin scanner's to
22479
+ * reconcile, and deleting them here would make the two pipelines erase each
22480
+ * other's rows on every alternating scan. 'ledger' mode names its files
22481
+ * outright and never mass-deletes, so rows the fs walk contributed for files
22482
+ * the scanner skips (vendored, oversize) survive it.
22483
+ */
22484
+ reconcileCallSites(projectKey, reconcile) {
22485
+ if (reconcile.mode === "walk") {
22486
+ const prefix = reconcile.walkedPrefix.replace(/\/+$/, "");
22487
+ this.db.prepare(
22488
+ `DELETE FROM share_call_site
22489
+ WHERE project_key = :key
22490
+ AND (:prefix = '' OR file = :prefix OR file LIKE :subtree ESCAPE '\\')
22491
+ AND file NOT LIKE '.%'
22492
+ AND file NOT LIKE '%/.%'`
22493
+ ).run({ key: projectKey, prefix, subtree: `${escapeLikePattern(prefix)}/%` });
22494
+ return;
22495
+ }
22496
+ const files = [.../* @__PURE__ */ new Set([...reconcile.scannedFiles, ...reconcile.deletedFiles])];
22497
+ for (let i = 0; i < files.length; i += IN_CHUNK) {
22498
+ const chunk = files.slice(i, i + IN_CHUNK);
22499
+ this.db.prepare(
22500
+ `DELETE FROM share_call_site
22501
+ WHERE project_key = ? AND file IN (${placeholders(chunk.length)})`
22502
+ ).run(projectKey, ...chunk);
22503
+ }
22504
+ }
22505
+ /**
22506
+ * Upsert every hit as destination → endpoint → call site. Destinations key on
22507
+ * `host` and endpoints on `(destination_id, method, url)`, both shared across
22508
+ * projects; only the call site carries `project_key`. A destination's `note`
22509
+ * is user-owned and never overwritten. The id caches keep one upsert per
22510
+ * distinct host and endpoint, so the first hit for a host supplies its
22511
+ * classification for this batch.
22512
+ */
22513
+ upsertHits(input, hits, projectId, now) {
22514
+ if (hits.length === 0) return;
22515
+ const destStmt = this.db.prepare(
22516
+ `INSERT INTO share_destination
22517
+ (id, kind, name, host, category, trust, network_json, last_seen, provenance,
22518
+ created_at, updated_at)
22519
+ VALUES (:id, :kind, :name, :host, :category, :trust, :networkJson, :now, 'scan', :now, :now)
22520
+ ON CONFLICT (host) DO UPDATE SET
22521
+ kind = excluded.kind,
22522
+ name = excluded.name,
22523
+ category = excluded.category,
22524
+ trust = excluded.trust,
22525
+ network_json = excluded.network_json,
22526
+ last_seen = excluded.last_seen,
22527
+ updated_at = excluded.updated_at`
22528
+ );
22529
+ const destIdStmt = this.db.prepare("SELECT id FROM share_destination WHERE host = ?");
22530
+ const endpointStmt = this.db.prepare(
22531
+ `INSERT INTO share_endpoint
22532
+ (id, destination_id, method, transport, url, template, data_class, last_seen,
22533
+ created_at, updated_at)
22534
+ VALUES (:id, :destinationId, :method, :transport, :url, :template, :dataClass, :now,
22535
+ :now, :now)
22536
+ ON CONFLICT (destination_id, method, url) DO UPDATE SET
22537
+ transport = excluded.transport,
22538
+ template = excluded.template,
22539
+ data_class = excluded.data_class,
22540
+ last_seen = excluded.last_seen,
22541
+ updated_at = excluded.updated_at`
22542
+ );
22543
+ const endpointIdStmt = this.db.prepare(
22544
+ "SELECT id FROM share_endpoint WHERE destination_id = ? AND method = ? AND url = ?"
22545
+ );
22546
+ const siteStmt = this.db.prepare(
22547
+ `INSERT INTO share_call_site
22548
+ (id, endpoint_id, project, project_key, file, line, snippet, dynamic, vendored,
22549
+ project_id, created_at, updated_at)
22550
+ VALUES (:id, :endpointId, :project, :projectKey, :file, :line, :snippet, :dynamic,
22551
+ :vendored, :projectId, :now, :now)
22552
+ ON CONFLICT (endpoint_id, project_key, file, line) DO UPDATE SET
22553
+ snippet = excluded.snippet,
22554
+ dynamic = excluded.dynamic,
22555
+ vendored = excluded.vendored,
22556
+ project = excluded.project,
22557
+ project_id = COALESCE(excluded.project_id, share_call_site.project_id),
22558
+ updated_at = excluded.updated_at`
22559
+ );
22560
+ const destIds = /* @__PURE__ */ new Map();
22561
+ const endpointIds = /* @__PURE__ */ new Map();
22562
+ for (const hit of hits) {
22563
+ let destinationId = destIds.get(hit.host);
22564
+ if (destinationId === void 0) {
22565
+ destStmt.run({
22566
+ id: randomUUID7(),
22567
+ kind: hit.kind,
22568
+ name: hit.name,
22569
+ host: hit.host,
22570
+ category: hit.category,
22571
+ trust: hit.trust,
22572
+ networkJson: hit.network === null ? null : JSON.stringify(hit.network),
22573
+ now
22574
+ });
22575
+ destinationId = getRow(destIdStmt, [hit.host])?.id ?? "";
22576
+ destIds.set(hit.host, destinationId);
22577
+ }
22578
+ const endpointKey = `${destinationId}\0${hit.method}\0${hit.url}`;
22579
+ let endpointId = endpointIds.get(endpointKey);
22580
+ if (endpointId === void 0) {
22581
+ endpointStmt.run({
22582
+ id: randomUUID7(),
22583
+ destinationId,
22584
+ method: hit.method,
22585
+ transport: hit.transport,
22586
+ url: hit.url,
22587
+ template: boolToInt(hit.template),
22588
+ dataClass: hit.dataClass,
22589
+ now
22590
+ });
22591
+ endpointId = getRow(endpointIdStmt, [destinationId, hit.method, hit.url])?.id ?? "";
22592
+ endpointIds.set(endpointKey, endpointId);
22593
+ }
22594
+ siteStmt.run({
22595
+ id: randomUUID7(),
22596
+ endpointId,
22597
+ project: input.project,
22598
+ projectKey: input.projectKey,
22599
+ file: hit.site.file,
22600
+ line: hit.site.line,
22601
+ snippet: hit.site.snippet,
22602
+ dynamic: boolToInt(hit.site.dynamic),
22603
+ vendored: boolToInt(hit.site.vendored),
22604
+ projectId,
22605
+ now
22606
+ });
22242
22607
  }
22608
+ }
22609
+ /**
22610
+ * The source-project id this project's stored call sites already carry, if
22611
+ * any. Only the pipeline that resolves a source project supplies one; the
22612
+ * other passes null and inherits this, so the link stops flapping between a
22613
+ * real id and NULL depending on which pipeline ran last. The value is a
22614
+ * per-project attribute stored redundantly on each row, so any row's is
22615
+ * representative.
22616
+ */
22617
+ knownProjectId(projectKey) {
22618
+ return getRow(
22619
+ this.db.prepare(
22620
+ `SELECT project_id AS projectId FROM share_call_site
22621
+ WHERE project_key = ? AND project_id IS NOT NULL LIMIT 1`
22622
+ ),
22623
+ [projectKey]
22624
+ )?.projectId ?? null;
22625
+ }
22626
+ /**
22627
+ * Stamp `last_seen` on every endpoint and destination this project still
22628
+ * references — including rows the scan preserved rather than re-wrote, so a
22629
+ * ledger-skipped file's references don't decay into "stale" on the page.
22630
+ */
22631
+ confirmLastSeen(projectKey, now) {
22243
22632
  this.db.prepare(
22244
- `INSERT INTO egress_decision_override (id, destination_id, decision, created_at, updated_at)
22245
- VALUES (:id, :destinationId, :decision, :now, :now)
22246
- ON CONFLICT (destination_id) DO UPDATE SET
22247
- decision = excluded.decision,
22248
- updated_at = excluded.updated_at`
22249
- ).run({ id: randomUUID7(), destinationId, decision, now: Date.now() });
22250
- return true;
22633
+ `UPDATE share_endpoint SET last_seen = :now, updated_at = :now
22634
+ WHERE id IN (SELECT DISTINCT endpoint_id FROM share_call_site WHERE project_key = :key)`
22635
+ ).run({ now, key: projectKey });
22636
+ this.db.prepare(
22637
+ `UPDATE share_destination SET last_seen = :now, updated_at = :now
22638
+ WHERE id IN (SELECT DISTINCT e.destination_id
22639
+ FROM share_endpoint e
22640
+ JOIN share_call_site c ON c.endpoint_id = e.id
22641
+ WHERE c.project_key = :key)`
22642
+ ).run({ now, key: projectKey });
22643
+ }
22644
+ /**
22645
+ * Drop rows left without evidence: endpoints with no call site, then
22646
+ * destinations with no endpoint. Call sites are the only evidence either one
22647
+ * has, so a row that lost its last one belongs to no project any more.
22648
+ *
22649
+ * Overrides are deleted between the two steps, and only the ones written
22650
+ * before the host column existed. Those match a destination by id alone;
22651
+ * because the id link is released on delete rather than cascading, leaving
22652
+ * them would accumulate rows that match neither join arm and that nothing can
22653
+ * reach again. Host-bearing rows deliberately survive — the host is what
22654
+ * re-attaches a user's decision when the destination comes back.
22655
+ */
22656
+ pruneOrphans() {
22657
+ this.db.exec(
22658
+ `DELETE FROM share_endpoint
22659
+ WHERE NOT EXISTS (SELECT 1 FROM share_call_site c WHERE c.endpoint_id = share_endpoint.id)`
22660
+ );
22661
+ this.db.exec(
22662
+ `DELETE FROM egress_decision_override
22663
+ WHERE host IS NULL
22664
+ AND destination_id IN (
22665
+ SELECT d.id FROM share_destination d
22666
+ WHERE NOT EXISTS (SELECT 1 FROM share_endpoint e WHERE e.destination_id = d.id))`
22667
+ );
22668
+ this.db.exec(
22669
+ `DELETE FROM share_destination
22670
+ WHERE NOT EXISTS (
22671
+ SELECT 1 FROM share_endpoint e WHERE e.destination_id = share_destination.id)`
22672
+ );
22673
+ }
22674
+ /**
22675
+ * Live totals for one project. Destinations and endpoints are shared across
22676
+ * projects and carry no project column, so both are counted through the call
22677
+ * sites that reference them.
22678
+ */
22679
+ projectTotals(projectKey) {
22680
+ return {
22681
+ destinations: countScalar(
22682
+ this.db,
22683
+ `SELECT count(DISTINCT e.destination_id) AS n
22684
+ FROM share_endpoint e
22685
+ JOIN share_call_site c ON c.endpoint_id = e.id
22686
+ WHERE c.project_key = ?`,
22687
+ [projectKey]
22688
+ ),
22689
+ endpoints: countScalar(
22690
+ this.db,
22691
+ "SELECT count(DISTINCT endpoint_id) AS n FROM share_call_site WHERE project_key = ?",
22692
+ [projectKey]
22693
+ ),
22694
+ callSites: countScalar(
22695
+ this.db,
22696
+ "SELECT count(*) AS n FROM share_call_site WHERE project_key = ?",
22697
+ [projectKey]
22698
+ )
22699
+ };
22251
22700
  }
22252
22701
  // ─── Raw fetchers ────────────────────────────────────────────────────────────
22253
22702
  mapDestRow(r) {
@@ -22267,7 +22716,8 @@ var SqliteSharesRepository = class {
22267
22716
  fetchDestinations(q, kinds, reviewOnly = false) {
22268
22717
  const cols = `d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22269
22718
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22270
- d.created_at AS createdAt, o.decision AS overrideDecision`;
22719
+ d.created_at AS createdAt,
22720
+ COALESCE(oh.decision, ol.decision) AS overrideDecision`;
22271
22721
  const conditions = [];
22272
22722
  const params = [];
22273
22723
  if (kinds && kinds.length > 0) {
@@ -22278,7 +22728,8 @@ var SqliteSharesRepository = class {
22278
22728
  conditions.push(
22279
22729
  `(d.trust IN ('unverified', 'ip')
22280
22730
  OR EXISTS (SELECT 1 FROM share_endpoint re
22281
- WHERE re.destination_id = d.id AND re.transport = 'http'))`
22731
+ WHERE re.destination_id = d.id
22732
+ AND re.transport IN ${PLAINTEXT_TRANSPORT_SQL}))`
22282
22733
  );
22283
22734
  }
22284
22735
  let sql;
@@ -22291,7 +22742,7 @@ var SqliteSharesRepository = class {
22291
22742
  params.push(pattern, pattern, pattern, pattern, pattern);
22292
22743
  sql = `SELECT DISTINCT ${cols}
22293
22744
  FROM share_destination d
22294
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22745
+ ${OVERRIDE_JOIN}
22295
22746
  LEFT JOIN share_endpoint e ON e.destination_id = d.id
22296
22747
  LEFT JOIN share_call_site c ON c.endpoint_id = e.id
22297
22748
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
@@ -22299,7 +22750,7 @@ var SqliteSharesRepository = class {
22299
22750
  } else {
22300
22751
  sql = `SELECT ${cols}
22301
22752
  FROM share_destination d
22302
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22753
+ ${OVERRIDE_JOIN}
22303
22754
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
22304
22755
  ORDER BY d.created_at ASC, d.id ASC`;
22305
22756
  }
@@ -22314,9 +22765,9 @@ var SqliteSharesRepository = class {
22314
22765
  this.db.prepare(
22315
22766
  `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22316
22767
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
22317
- o.decision AS overrideDecision
22768
+ COALESCE(oh.decision, ol.decision) AS overrideDecision
22318
22769
  FROM share_destination d
22319
- LEFT JOIN egress_decision_override o ON o.destination_id = d.id
22770
+ ${OVERRIDE_JOIN}
22320
22771
  WHERE d.id = ?`
22321
22772
  ),
22322
22773
  [destinationId]
@@ -22545,6 +22996,7 @@ function openLocalDatabase(dir) {
22545
22996
  const scanLedger = new SqliteScanLedgerRepository(db);
22546
22997
  const exceptions = new SqliteExceptionsRepository(db);
22547
22998
  const resolutions = new SqliteResolutionsRepository(db);
22999
+ const ruleProbeCache = new SqliteRuleProbeCacheRepository(db);
22548
23000
  const security = new SqliteSecurityRepository(db);
22549
23001
  const detections = new SqliteDetectionsRepository(db);
22550
23002
  const shares = new SqliteSharesRepository(db);
@@ -22682,6 +23134,7 @@ function openLocalDatabase(dir) {
22682
23134
  scanLedger,
22683
23135
  exceptions,
22684
23136
  resolutions,
23137
+ ruleProbeCache,
22685
23138
  security,
22686
23139
  detections,
22687
23140
  shares,
@@ -22922,13 +23375,581 @@ import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as s
22922
23375
  import { homedir as homedir2 } from "os";
22923
23376
  import { basename as basename2, join as join7 } from "path";
22924
23377
 
23378
+ // ../../packages/detections/src/egress/registry.ts
23379
+ var EXTRACTOR_VERSION = "1";
23380
+ var PROVIDER_REGISTRY = [
23381
+ {
23382
+ id: "stripe",
23383
+ name: "Stripe",
23384
+ category: "Payments",
23385
+ hostSuffixes: ["stripe.com"],
23386
+ apiBase: "https://api.stripe.com",
23387
+ defaultDataClasses: ["pii", "customer"],
23388
+ sdks: {
23389
+ npm: ["stripe"],
23390
+ pypi: ["stripe"],
23391
+ go: ["github.com/stripe/stripe-go"],
23392
+ maven: ["com.stripe"],
23393
+ rubygems: ["stripe"],
23394
+ composer: ["stripe/stripe-php"],
23395
+ nuget: ["Stripe.net"]
23396
+ }
23397
+ },
23398
+ {
23399
+ id: "datadog",
23400
+ name: "Datadog",
23401
+ category: "Observability",
23402
+ hostSuffixes: ["datadoghq.com", "datadoghq.eu"],
23403
+ apiBase: "https://api.datadoghq.com",
23404
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23405
+ sdks: {
23406
+ npm: ["dd-trace", "@datadog/browser-logs"],
23407
+ pypi: ["datadog", "ddtrace"],
23408
+ go: ["github.com/DataDog/dd-trace-go"],
23409
+ maven: ["com.datadoghq"],
23410
+ rubygems: ["ddtrace", "dogapi"],
23411
+ nuget: ["Datadog.Trace"]
23412
+ }
23413
+ },
23414
+ {
23415
+ id: "newrelic",
23416
+ name: "New Relic",
23417
+ category: "Observability",
23418
+ hostSuffixes: ["newrelic.com", "nr-data.net"],
23419
+ apiBase: "https://api.newrelic.com",
23420
+ defaultDataClasses: ["telemetry", "logs", "metrics"],
23421
+ sdks: {
23422
+ npm: ["newrelic"],
23423
+ pypi: ["newrelic"],
23424
+ go: ["github.com/newrelic/go-agent"],
23425
+ maven: ["com.newrelic.agent.java"],
23426
+ rubygems: ["newrelic_rpm"],
23427
+ nuget: ["NewRelic.Agent"]
23428
+ }
23429
+ },
23430
+ {
23431
+ id: "sentry",
23432
+ name: "Sentry",
23433
+ category: "Error tracking",
23434
+ hostSuffixes: ["sentry.io"],
23435
+ apiBase: "https://sentry.io",
23436
+ defaultDataClasses: ["source", "telemetry"],
23437
+ sdks: {
23438
+ npm: ["@sentry/node", "@sentry/react", "@sentry/nextjs"],
23439
+ pypi: ["sentry-sdk"],
23440
+ go: ["github.com/getsentry/sentry-go"],
23441
+ maven: ["io.sentry"],
23442
+ rubygems: ["sentry-ruby"],
23443
+ cargo: ["sentry"],
23444
+ composer: ["sentry/sentry"],
23445
+ nuget: ["Sentry"]
23446
+ }
23447
+ },
23448
+ {
23449
+ id: "openai",
23450
+ name: "OpenAI",
23451
+ category: "LLM provider",
23452
+ hostSuffixes: ["openai.com"],
23453
+ apiBase: "https://api.openai.com",
23454
+ defaultDataClasses: ["pii", "source"],
23455
+ sdks: {
23456
+ npm: ["openai"],
23457
+ pypi: ["openai"],
23458
+ go: ["github.com/sashabaranov/go-openai"],
23459
+ maven: ["com.openai"],
23460
+ rubygems: ["ruby-openai"],
23461
+ cargo: ["async-openai"],
23462
+ composer: ["openai-php/client"],
23463
+ nuget: ["OpenAI"]
23464
+ }
23465
+ },
23466
+ {
23467
+ id: "anthropic",
23468
+ name: "Anthropic",
23469
+ category: "LLM provider",
23470
+ hostSuffixes: ["anthropic.com"],
23471
+ apiBase: "https://api.anthropic.com",
23472
+ defaultDataClasses: ["pii", "source"],
23473
+ sdks: {
23474
+ npm: ["@anthropic-ai/sdk"],
23475
+ pypi: ["anthropic"],
23476
+ go: ["github.com/anthropics/anthropic-sdk-go"],
23477
+ nuget: ["Anthropic.SDK"]
23478
+ }
23479
+ },
23480
+ {
23481
+ id: "aws",
23482
+ name: "Amazon Web Services",
23483
+ category: "Cloud platform",
23484
+ hostSuffixes: ["amazonaws.com"],
23485
+ apiBase: "https://s3.amazonaws.com",
23486
+ defaultDataClasses: ["secrets", "customer"],
23487
+ sdks: {
23488
+ npm: ["@aws-sdk/client-s3", "aws-sdk"],
23489
+ pypi: ["boto3"],
23490
+ go: ["github.com/aws/aws-sdk-go", "github.com/aws/aws-sdk-go-v2"],
23491
+ maven: ["com.amazonaws", "software.amazon.awssdk"],
23492
+ rubygems: ["aws-sdk-s3"],
23493
+ cargo: ["aws-sdk-s3"],
23494
+ nuget: ["AWSSDK.S3"]
23495
+ }
23496
+ },
23497
+ {
23498
+ id: "gcp",
23499
+ name: "Google Cloud",
23500
+ category: "Cloud platform",
23501
+ hostSuffixes: ["googleapis.com"],
23502
+ apiBase: "https://storage.googleapis.com",
23503
+ defaultDataClasses: ["customer", "logs"],
23504
+ sdks: {
23505
+ npm: ["@google-cloud/storage"],
23506
+ pypi: ["google-cloud-storage"],
23507
+ go: ["cloud.google.com/go"],
23508
+ maven: ["com.google.cloud"],
23509
+ rubygems: ["google-cloud-storage"],
23510
+ nuget: ["Google.Cloud.Storage.V1"]
23511
+ }
23512
+ },
23513
+ {
23514
+ id: "azure",
23515
+ name: "Microsoft Azure",
23516
+ category: "Cloud platform",
23517
+ hostSuffixes: ["azure.com", "windows.net"],
23518
+ apiBase: "https://management.azure.com",
23519
+ defaultDataClasses: ["customer", "logs"],
23520
+ sdks: {
23521
+ npm: ["@azure/storage-blob"],
23522
+ pypi: ["azure-storage-blob"],
23523
+ go: ["github.com/Azure/azure-sdk-for-go"],
23524
+ maven: ["com.azure"],
23525
+ rubygems: ["azure-storage-blob"],
23526
+ nuget: ["Azure.Storage.Blobs"]
23527
+ }
23528
+ },
23529
+ {
23530
+ id: "slack",
23531
+ name: "Slack",
23532
+ category: "Notifications",
23533
+ hostSuffixes: ["slack.com"],
23534
+ apiBase: "https://slack.com/api",
23535
+ defaultDataClasses: ["logs"],
23536
+ sdks: {
23537
+ npm: ["@slack/web-api"],
23538
+ pypi: ["slack-sdk"],
23539
+ go: ["github.com/slack-go/slack"],
23540
+ maven: ["com.slack.api"],
23541
+ rubygems: ["slack-ruby-client"],
23542
+ composer: ["slack-php/slack-api"],
23543
+ nuget: ["SlackNet"]
23544
+ }
23545
+ },
23546
+ {
23547
+ id: "segment",
23548
+ name: "Segment",
23549
+ category: "Analytics",
23550
+ hostSuffixes: ["segment.io", "segment.com"],
23551
+ apiBase: "https://api.segment.io",
23552
+ defaultDataClasses: ["customer"],
23553
+ sdks: {
23554
+ npm: ["@segment/analytics-node", "analytics-node"],
23555
+ pypi: ["segment-analytics-python"],
23556
+ go: ["github.com/segmentio/analytics-go"],
23557
+ maven: ["com.segment.analytics.java"],
23558
+ rubygems: ["analytics-ruby"],
23559
+ nuget: ["Analytics"]
23560
+ }
23561
+ },
23562
+ {
23563
+ id: "twilio",
23564
+ name: "Twilio",
23565
+ category: "Communications",
23566
+ hostSuffixes: ["twilio.com"],
23567
+ apiBase: "https://api.twilio.com",
23568
+ defaultDataClasses: ["pii", "customer"],
23569
+ sdks: {
23570
+ npm: ["twilio"],
23571
+ pypi: ["twilio"],
23572
+ go: ["github.com/twilio/twilio-go"],
23573
+ maven: ["com.twilio.sdk"],
23574
+ rubygems: ["twilio-ruby"],
23575
+ composer: ["twilio/sdk"],
23576
+ nuget: ["Twilio"]
23577
+ }
23578
+ },
23579
+ {
23580
+ id: "sendgrid",
23581
+ name: "SendGrid",
23582
+ category: "Email",
23583
+ hostSuffixes: ["sendgrid.com"],
23584
+ apiBase: "https://api.sendgrid.com",
23585
+ defaultDataClasses: ["pii"],
23586
+ sdks: {
23587
+ npm: ["@sendgrid/mail"],
23588
+ pypi: ["sendgrid"],
23589
+ go: ["github.com/sendgrid/sendgrid-go"],
23590
+ maven: ["com.sendgrid"],
23591
+ rubygems: ["sendgrid-ruby"],
23592
+ composer: ["sendgrid/sendgrid"],
23593
+ nuget: ["SendGrid"]
23594
+ }
23595
+ },
23596
+ {
23597
+ id: "mailgun",
23598
+ name: "Mailgun",
23599
+ category: "Email",
23600
+ hostSuffixes: ["mailgun.net"],
23601
+ apiBase: "https://api.mailgun.net",
23602
+ defaultDataClasses: ["pii"],
23603
+ sdks: {
23604
+ npm: ["mailgun.js"],
23605
+ pypi: ["mailgun"],
23606
+ rubygems: ["mailgun-ruby"],
23607
+ composer: ["mailgun/mailgun-php"],
23608
+ nuget: ["Mailgun"]
23609
+ }
23610
+ },
23611
+ {
23612
+ id: "mixpanel",
23613
+ name: "Mixpanel",
23614
+ category: "Analytics",
23615
+ hostSuffixes: ["mixpanel.com"],
23616
+ apiBase: "https://api.mixpanel.com",
23617
+ defaultDataClasses: ["customer", "telemetry"],
23618
+ sdks: {
23619
+ npm: ["mixpanel"],
23620
+ pypi: ["mixpanel"],
23621
+ rubygems: ["mixpanel-ruby"],
23622
+ nuget: ["Mixpanel"]
23623
+ }
23624
+ },
23625
+ {
23626
+ id: "amplitude",
23627
+ name: "Amplitude",
23628
+ category: "Analytics",
23629
+ hostSuffixes: ["amplitude.com"],
23630
+ apiBase: "https://api2.amplitude.com",
23631
+ defaultDataClasses: ["customer", "telemetry"],
23632
+ sdks: {
23633
+ npm: ["@amplitude/analytics-node"],
23634
+ pypi: ["amplitude-analytics"],
23635
+ nuget: ["Amplitude"]
23636
+ }
23637
+ },
23638
+ {
23639
+ id: "posthog",
23640
+ name: "PostHog",
23641
+ category: "Analytics",
23642
+ hostSuffixes: ["posthog.com"],
23643
+ apiBase: "https://us.i.posthog.com",
23644
+ defaultDataClasses: ["customer", "telemetry"],
23645
+ sdks: {
23646
+ npm: ["posthog-node", "posthog-js"],
23647
+ pypi: ["posthog"],
23648
+ go: ["github.com/posthog/posthog-go"],
23649
+ rubygems: ["posthog-ruby"],
23650
+ composer: ["posthog/posthog-php"],
23651
+ nuget: ["PostHog"]
23652
+ }
23653
+ },
23654
+ {
23655
+ id: "honeycomb",
23656
+ name: "Honeycomb",
23657
+ category: "Observability",
23658
+ hostSuffixes: ["honeycomb.io"],
23659
+ apiBase: "https://api.honeycomb.io",
23660
+ defaultDataClasses: ["telemetry", "metrics"],
23661
+ sdks: {
23662
+ npm: ["libhoney"],
23663
+ pypi: ["libhoney"],
23664
+ go: ["github.com/honeycombio/libhoney-go"],
23665
+ rubygems: ["libhoney"]
23666
+ }
23667
+ },
23668
+ {
23669
+ id: "grafana",
23670
+ name: "Grafana Cloud",
23671
+ category: "Observability",
23672
+ hostSuffixes: ["grafana.net"],
23673
+ apiBase: "https://grafana.net",
23674
+ defaultDataClasses: ["logs", "metrics"],
23675
+ sdks: {
23676
+ npm: ["@grafana/faro-web-sdk"]
23677
+ }
23678
+ },
23679
+ {
23680
+ id: "splunk",
23681
+ name: "Splunk",
23682
+ category: "Observability",
23683
+ hostSuffixes: ["splunkcloud.com", "splunk.com"],
23684
+ apiBase: "https://http-inputs.splunkcloud.com",
23685
+ defaultDataClasses: ["logs"],
23686
+ sdks: {
23687
+ npm: ["splunk-logging"],
23688
+ pypi: ["splunk-sdk"],
23689
+ maven: ["com.splunk"],
23690
+ nuget: ["Splunk.Logging.Common"]
23691
+ }
23692
+ },
23693
+ {
23694
+ id: "pagerduty",
23695
+ name: "PagerDuty",
23696
+ category: "Incident response",
23697
+ hostSuffixes: ["pagerduty.com"],
23698
+ apiBase: "https://api.pagerduty.com",
23699
+ defaultDataClasses: ["logs"],
23700
+ sdks: {
23701
+ npm: ["@pagerduty/pdjs"],
23702
+ pypi: ["pdpyras"],
23703
+ go: ["github.com/PagerDuty/go-pagerduty"],
23704
+ rubygems: ["pagerduty"]
23705
+ }
23706
+ },
23707
+ {
23708
+ id: "github",
23709
+ name: "GitHub",
23710
+ category: "Developer platform",
23711
+ hostSuffixes: ["github.com", "githubusercontent.com"],
23712
+ apiBase: "https://api.github.com",
23713
+ defaultDataClasses: ["source"],
23714
+ sdks: {
23715
+ npm: ["@octokit/rest", "octokit"],
23716
+ pypi: ["pygithub"],
23717
+ go: ["github.com/google/go-github"],
23718
+ maven: ["org.kohsuke.github-api"],
23719
+ rubygems: ["octokit"],
23720
+ cargo: ["octocrab"],
23721
+ composer: ["knplabs/github-api"],
23722
+ nuget: ["Octokit"]
23723
+ }
23724
+ },
23725
+ {
23726
+ id: "gitlab",
23727
+ name: "GitLab",
23728
+ category: "Developer platform",
23729
+ hostSuffixes: ["gitlab.com"],
23730
+ apiBase: "https://gitlab.com/api",
23731
+ defaultDataClasses: ["source"],
23732
+ sdks: {
23733
+ npm: ["@gitbeaker/rest"],
23734
+ pypi: ["python-gitlab"],
23735
+ go: ["gitlab.com/gitlab-org/api/client-go"],
23736
+ rubygems: ["gitlab"],
23737
+ nuget: ["GitLabApiClient"]
23738
+ }
23739
+ },
23740
+ {
23741
+ id: "auth0",
23742
+ name: "Auth0",
23743
+ category: "Identity",
23744
+ hostSuffixes: ["auth0.com"],
23745
+ apiBase: "https://login.auth0.com",
23746
+ defaultDataClasses: ["pii"],
23747
+ sdks: {
23748
+ npm: ["auth0"],
23749
+ pypi: ["auth0-python"],
23750
+ go: ["github.com/auth0/go-auth0"],
23751
+ maven: ["com.auth0"],
23752
+ rubygems: ["auth0"],
23753
+ composer: ["auth0/auth0-php"],
23754
+ nuget: ["Auth0.ManagementApi"]
23755
+ }
23756
+ },
23757
+ {
23758
+ id: "okta",
23759
+ name: "Okta",
23760
+ category: "Identity",
23761
+ hostSuffixes: ["okta.com", "oktapreview.com"],
23762
+ apiBase: "https://login.okta.com",
23763
+ defaultDataClasses: ["pii"],
23764
+ sdks: {
23765
+ npm: ["@okta/okta-sdk-nodejs"],
23766
+ pypi: ["okta"],
23767
+ go: ["github.com/okta/okta-sdk-golang"],
23768
+ maven: ["com.okta.sdk"],
23769
+ nuget: ["Okta.Sdk"]
23770
+ }
23771
+ },
23772
+ {
23773
+ id: "clerk",
23774
+ name: "Clerk",
23775
+ category: "Identity",
23776
+ hostSuffixes: ["clerk.com", "clerk.dev"],
23777
+ apiBase: "https://api.clerk.com",
23778
+ defaultDataClasses: ["pii"],
23779
+ sdks: {
23780
+ npm: ["@clerk/backend", "@clerk/nextjs"],
23781
+ pypi: ["clerk-backend-api"],
23782
+ go: ["github.com/clerk/clerk-sdk-go"]
23783
+ }
23784
+ },
23785
+ {
23786
+ id: "supabase",
23787
+ name: "Supabase",
23788
+ category: "Backend platform",
23789
+ hostSuffixes: ["supabase.co", "supabase.com"],
23790
+ apiBase: "https://api.supabase.com",
23791
+ defaultDataClasses: ["pii", "customer"],
23792
+ sdks: {
23793
+ npm: ["@supabase/supabase-js"],
23794
+ pypi: ["supabase"],
23795
+ cargo: ["postgrest"]
23796
+ }
23797
+ },
23798
+ {
23799
+ id: "firebase",
23800
+ name: "Firebase",
23801
+ category: "Backend platform",
23802
+ hostSuffixes: ["firebaseio.com", "firebase.google.com"],
23803
+ apiBase: "https://firebaseio.com",
23804
+ defaultDataClasses: ["customer"],
23805
+ sdks: {
23806
+ npm: ["firebase", "firebase-admin"],
23807
+ pypi: ["firebase-admin"],
23808
+ go: ["firebase.google.com/go"],
23809
+ maven: ["com.google.firebase"]
23810
+ }
23811
+ },
23812
+ {
23813
+ id: "mongodb-atlas",
23814
+ name: "MongoDB Atlas",
23815
+ category: "Database SaaS",
23816
+ hostSuffixes: ["mongodb.net", "mongodb.com"],
23817
+ apiBase: "https://cloud.mongodb.com",
23818
+ defaultDataClasses: ["customer"],
23819
+ sdks: {
23820
+ npm: ["mongodb"],
23821
+ pypi: ["pymongo"],
23822
+ go: ["go.mongodb.org/mongo-driver"],
23823
+ maven: ["org.mongodb"],
23824
+ rubygems: ["mongo"],
23825
+ cargo: ["mongodb"],
23826
+ nuget: ["MongoDB.Driver"]
23827
+ }
23828
+ },
23829
+ {
23830
+ id: "planetscale",
23831
+ name: "PlanetScale",
23832
+ category: "Database SaaS",
23833
+ hostSuffixes: ["psdb.cloud", "planetscale.com"],
23834
+ apiBase: "https://api.planetscale.com",
23835
+ defaultDataClasses: ["customer"],
23836
+ sdks: {
23837
+ npm: ["@planetscale/database"],
23838
+ go: ["github.com/planetscale/planetscale-go"]
23839
+ }
23840
+ },
23841
+ {
23842
+ id: "algolia",
23843
+ name: "Algolia",
23844
+ category: "Search SaaS",
23845
+ hostSuffixes: ["algolia.net", "algolianet.com"],
23846
+ apiBase: "https://algolia.net",
23847
+ defaultDataClasses: ["customer"],
23848
+ sdks: {
23849
+ npm: ["algoliasearch"],
23850
+ pypi: ["algoliasearch"],
23851
+ go: ["github.com/algolia/algoliasearch-client-go"],
23852
+ maven: ["com.algolia"],
23853
+ rubygems: ["algolia"],
23854
+ composer: ["algolia/algoliasearch-client-php"],
23855
+ nuget: ["Algolia.Search"]
23856
+ }
23857
+ },
23858
+ {
23859
+ id: "cloudflare",
23860
+ name: "Cloudflare",
23861
+ category: "CDN / edge",
23862
+ hostSuffixes: ["cloudflare.com", "workers.dev"],
23863
+ apiBase: "https://api.cloudflare.com",
23864
+ defaultDataClasses: ["logs"],
23865
+ sdks: {
23866
+ npm: ["cloudflare"],
23867
+ pypi: ["cloudflare"],
23868
+ go: ["github.com/cloudflare/cloudflare-go"],
23869
+ nuget: ["CloudFlare.Client"]
23870
+ }
23871
+ },
23872
+ {
23873
+ id: "huggingface",
23874
+ name: "Hugging Face",
23875
+ category: "LLM provider",
23876
+ hostSuffixes: ["huggingface.co"],
23877
+ apiBase: "https://api-inference.huggingface.co",
23878
+ defaultDataClasses: ["source"],
23879
+ sdks: {
23880
+ npm: ["@huggingface/inference"],
23881
+ pypi: ["huggingface-hub", "transformers"],
23882
+ rubygems: ["hugging-face"]
23883
+ }
23884
+ },
23885
+ {
23886
+ id: "cohere",
23887
+ name: "Cohere",
23888
+ category: "LLM provider",
23889
+ hostSuffixes: ["cohere.com", "cohere.ai"],
23890
+ apiBase: "https://api.cohere.com",
23891
+ defaultDataClasses: ["pii", "source"],
23892
+ sdks: {
23893
+ npm: ["cohere-ai"],
23894
+ pypi: ["cohere"],
23895
+ go: ["github.com/cohere-ai/cohere-go"]
23896
+ }
23897
+ },
23898
+ {
23899
+ id: "mistral",
23900
+ name: "Mistral AI",
23901
+ category: "LLM provider",
23902
+ hostSuffixes: ["mistral.ai"],
23903
+ apiBase: "https://api.mistral.ai",
23904
+ defaultDataClasses: ["pii", "source"],
23905
+ sdks: {
23906
+ npm: ["@mistralai/mistralai"],
23907
+ pypi: ["mistralai"],
23908
+ go: ["github.com/gage-technologies/mistral-go"]
23909
+ }
23910
+ }
23911
+ ];
23912
+ var EGRESS_VERSION_MATERIAL = `${EXTRACTOR_VERSION}
23913
+ ${JSON.stringify(PROVIDER_REGISTRY)}`;
23914
+
23915
+ // ../../packages/detections/src/egress/extract.ts
23916
+ var SECRET_KEY_NAMES = "api[_-]?key|apikey|private[_-]?key|access[_-]?key|access[_-]?token|token|secret|credentials?|password|passwd|pwd|authorization|sig|signature|sas|assertion";
23917
+ var AUTH_SCHEMES = "Bearer|Basic|Token|Digest|ApiKey|SSWS|AWS4-HMAC-SHA256";
23918
+ var SECRET_VALUE = new RegExp(
23919
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(?!(?:${AUTH_SCHEMES})[\\s'"\`])[^\\s'"\`&]+`,
23920
+ "gi"
23921
+ );
23922
+ var AUTH_SCHEME_VALUE = new RegExp(
23923
+ `((?:${SECRET_KEY_NAMES})['"\`]?\\s*[:=]\\s*['"\`]?)(${AUTH_SCHEMES})\\s+[^\\s'"\`]+`,
23924
+ "gi"
23925
+ );
23926
+ var WEBHOOK_SECRET_PATHS = [
23927
+ { hosts: ["hooks.slack.com"], prefix: "/services/" },
23928
+ {
23929
+ hosts: ["discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com"],
23930
+ prefix: "/api/webhooks/"
23931
+ },
23932
+ { hosts: ["hooks.zapier.com"], prefix: "/hooks/" },
23933
+ { hosts: ["outlook.office.com", "outlook.office365.com"], prefix: "/webhook/" }
23934
+ ];
23935
+ function escapeRegExp(literal2) {
23936
+ return literal2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23937
+ }
23938
+ var WEBHOOK_URL = new RegExp(
23939
+ `(https?://(?:${WEBHOOK_SECRET_PATHS.flatMap(
23940
+ (entry) => entry.hosts.map((host) => `${escapeRegExp(host)}${escapeRegExp(entry.prefix)}`)
23941
+ ).join("|")}))[^\\s'"\`<>()[\\]{},;]+`,
23942
+ "gi"
23943
+ );
23944
+
22925
23945
  // ../../packages/detections/src/escape-regexp.ts
22926
- function escapeRegExp(value) {
23946
+ function escapeRegExp2(value) {
22927
23947
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22928
23948
  }
22929
23949
 
22930
23950
  // ../../packages/detections/src/matchers/limits.ts
22931
23951
  var MAX_MATCHES_PER_RULE = 1e4;
23952
+ var MAX_REGEX_INPUT_LENGTH = 2e5;
22932
23953
 
22933
23954
  // ../../packages/detections/src/matchers/keyword.ts
22934
23955
  var KeywordMatcher2 = class {
@@ -22939,7 +23960,7 @@ var KeywordMatcher2 = class {
22939
23960
  for (const kw of keywords) {
22940
23961
  if (kw.length === 0) continue;
22941
23962
  if (spans.length >= MAX_MATCHES_PER_RULE) break;
22942
- const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
23963
+ const re = new RegExp(escapeRegExp2(kw), caseSensitive ? "gu" : "giu");
22943
23964
  let m;
22944
23965
  while ((m = re.exec(text)) !== null) {
22945
23966
  spans.push({ start: m.index, end: m.index + m[0].length });
@@ -22956,9 +23977,13 @@ var RegexMatcher2 = class {
22956
23977
  if (rule.matcher.type !== "regex") return [];
22957
23978
  const { pattern, flags, captureGroup } = rule.matcher;
22958
23979
  const re = new RegExp(pattern, flags.includes("d") ? flags : `${flags}d`);
23980
+ const scanText2 = text.length > MAX_REGEX_INPUT_LENGTH ? text.slice(0, MAX_REGEX_INPUT_LENGTH) : text;
22959
23981
  const spans = [];
22960
23982
  let m;
22961
- while ((m = re.exec(text)) !== null) {
23983
+ const maxIterations = scanText2.length + 1;
23984
+ let iterations = 0;
23985
+ while ((m = re.exec(scanText2)) !== null) {
23986
+ if (++iterations > maxIterations) break;
22962
23987
  const group = captureGroup != null ? m[captureGroup] : m[0];
22963
23988
  if (m[0].length === 0) re.lastIndex++;
22964
23989
  if (group && spans.length < MAX_MATCHES_PER_RULE) {
@@ -23062,7 +24087,7 @@ function isCorroborated(candidate, candidates, text) {
23062
24087
  for (const label of labels) {
23063
24088
  const trimmed = label.trim();
23064
24089
  if (trimmed.length === 0) continue;
23065
- const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(trimmed)}(?![A-Za-z0-9])`, "i");
24090
+ const re = new RegExp(`(?<![A-Za-z0-9])${escapeRegExp2(trimmed)}(?![A-Za-z0-9])`, "i");
23066
24091
  if (re.test(haystack)) return true;
23067
24092
  }
23068
24093
  }
@@ -23227,6 +24252,112 @@ var CONFIG_POSTURE_RULES = [
23227
24252
  }
23228
24253
  ];
23229
24254
 
24255
+ // ../../packages/detections/src/security/redos-probe.ts
24256
+ var BUDGET_MS = 100;
24257
+ var EXPONENTIAL_UNITS = [
24258
+ "a",
24259
+ "0",
24260
+ " ",
24261
+ "x",
24262
+ "ab",
24263
+ "a.",
24264
+ "a-",
24265
+ "a_",
24266
+ "a@",
24267
+ "a/",
24268
+ "a:",
24269
+ "a=",
24270
+ "a;",
24271
+ "aA0",
24272
+ " "
24273
+ ];
24274
+ var EXPONENTIAL_PROBES = EXPONENTIAL_UNITS.flatMap(
24275
+ (unit) => [23, 25].map((len) => unit.repeat(Math.ceil(len / unit.length)).slice(0, len) + "!")
24276
+ );
24277
+ var POLYNOMIAL_PROBES = ["abc-", "a.", "a ", "a=", "x", "0", "a@", "a/", "ab"].map(
24278
+ (unit) => unit.repeat(1e4).slice(0, 4e4) + "!"
24279
+ );
24280
+ function literalPrefix(pattern) {
24281
+ let prefix = "";
24282
+ let i = 0;
24283
+ if (pattern[i] === "^") i++;
24284
+ while (i < pattern.length) {
24285
+ const c = pattern[i];
24286
+ if (c === void 0) break;
24287
+ if (c === "\\") {
24288
+ const next = pattern[i + 1];
24289
+ if (next === "b" || next === "B") {
24290
+ i += 2;
24291
+ continue;
24292
+ }
24293
+ if (next === void 0 || /[dDwWsSnrtfv.]/.test(next)) break;
24294
+ prefix += next;
24295
+ i += 2;
24296
+ continue;
24297
+ }
24298
+ if ("([{.*+?|)]}^$".includes(c)) break;
24299
+ prefix += c;
24300
+ i++;
24301
+ }
24302
+ return prefix;
24303
+ }
24304
+ function fuelChars(pattern) {
24305
+ const fuel = /* @__PURE__ */ new Set();
24306
+ for (const m of pattern.matchAll(/\[\^?([^\]]+)\]/g)) {
24307
+ const body = m[1];
24308
+ if (body === void 0) continue;
24309
+ const range = /([A-Za-z0-9])-[A-Za-z0-9]/.exec(body);
24310
+ const rangeStart = range?.[1];
24311
+ if (rangeStart !== void 0) fuel.add(rangeStart);
24312
+ else {
24313
+ const literal2 = body.replace(/\\/g, "")[0];
24314
+ if (literal2 !== void 0 && literal2 !== "^") fuel.add(literal2);
24315
+ }
24316
+ }
24317
+ if (pattern.includes("\\w")) fuel.add("a");
24318
+ if (pattern.includes("\\d")) fuel.add("0");
24319
+ if (pattern.includes("\\s")) fuel.add(" ");
24320
+ if (/(?<!\\)\./.test(pattern)) fuel.add("a");
24321
+ if (fuel.size === 0) fuel.add("a");
24322
+ return [...fuel];
24323
+ }
24324
+ function derivedProbes(pattern) {
24325
+ const prefix = literalPrefix(pattern);
24326
+ const fuel = fuelChars(pattern);
24327
+ const terminators = ["!", "#", "~", "\n"];
24328
+ const probes = [];
24329
+ for (const f of fuel) {
24330
+ for (const term of terminators) {
24331
+ if (term === f) continue;
24332
+ for (const len of [23, 25]) probes.push(prefix + f.repeat(len) + term);
24333
+ }
24334
+ }
24335
+ return probes;
24336
+ }
24337
+ function probesFor(rule) {
24338
+ const derived = rule.matcher.type === "regex" ? derivedProbes(rule.matcher.pattern) : [];
24339
+ return [...derived, ...EXPONENTIAL_PROBES, ...POLYNOMIAL_PROBES];
24340
+ }
24341
+ function worstProbeMs(rule) {
24342
+ let ms = 0;
24343
+ let probe = "";
24344
+ for (const text of probesFor(rule)) {
24345
+ const start = performance.now();
24346
+ scan(text, [rule]);
24347
+ const elapsed = performance.now() - start;
24348
+ if (elapsed > ms) {
24349
+ ms = elapsed;
24350
+ probe = text;
24351
+ }
24352
+ if (ms >= BUDGET_MS) break;
24353
+ }
24354
+ return { ms, probe };
24355
+ }
24356
+ function checkRuleTiming(rule) {
24357
+ const { ms, probe } = worstProbeMs(rule);
24358
+ return { safe: ms < BUDGET_MS, worstMs: ms, probe };
24359
+ }
24360
+
23230
24361
  // ../../rules/code-flaws/auth-jwt-no-verify.json
23231
24362
  var auth_jwt_no_verify_default = {
23232
24363
  specVersion: 1,
@@ -25389,6 +26520,10 @@ import { arch, hostname as hostname3, platform, release } from "os";
25389
26520
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25390
26521
  import { join as join8 } from "path";
25391
26522
 
26523
+ // ../../packages/plugin-sdk/src/paths.ts
26524
+ import { readdirSync as readdirSync2, realpathSync as realpathSync2 } from "fs";
26525
+ import { basename as basename3, dirname as dirname2, sep as sep3 } from "path";
26526
+
25392
26527
  // ../../packages/plugin-sdk/src/posture.ts
25393
26528
  function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
25394
26529
  for (const category of Object.keys(posture)) {
@@ -25401,8 +26536,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
25401
26536
 
25402
26537
  // ../../packages/plugin-sdk/src/project-files.ts
25403
26538
  var import_ignore = __toESM(require_ignore(), 1);
25404
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25405
- import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
26539
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
26540
+ import { basename as basename4, join as join9, relative, sep as sep4 } from "path";
25406
26541
 
25407
26542
  // ../../packages/plugin-sdk/src/raw-egress.ts
25408
26543
  var MIN_RAW_LEN = 4;
@@ -25414,6 +26549,59 @@ function safeMaskedMatch(rawMatch) {
25414
26549
  return masked;
25415
26550
  }
25416
26551
 
26552
+ // ../../packages/plugin-sdk/src/rule-quarantine.ts
26553
+ var PASS_BUDGET_MS = 2e3;
26554
+ function ruleProbeKey(rule) {
26555
+ if (rule.matcher.type !== "regex") return void 0;
26556
+ return contentHashOf(`${rule.matcher.pattern} ${rule.matcher.flags}`);
26557
+ }
26558
+ function warnQuarantined(rule, worstMs) {
26559
+ const timing = worstMs === void 0 ? "not verified in time" : `${worstMs.toFixed(1)}ms`;
26560
+ process.stderr.write(
26561
+ `[aka] quarantined rule "${rule.id}": regex matcher exceeded the ReDoS timing budget (${timing}); excluded from this scan.
26562
+ `
26563
+ );
26564
+ }
26565
+ async function filterUnsafeRules(rules, gateway, opts) {
26566
+ const passBudgetMs = opts?.passBudgetMs ?? PASS_BUDGET_MS;
26567
+ const passStart = performance.now();
26568
+ const safe = [];
26569
+ for (const rule of rules) {
26570
+ const key = ruleProbeKey(rule);
26571
+ if (key === void 0) {
26572
+ safe.push(rule);
26573
+ continue;
26574
+ }
26575
+ let cached2;
26576
+ try {
26577
+ cached2 = await gateway.getRuleProbeVerdict(key);
26578
+ } catch {
26579
+ cached2 = void 0;
26580
+ }
26581
+ if (cached2) {
26582
+ if (cached2.verdict === "safe") safe.push(rule);
26583
+ else warnQuarantined(rule, cached2.worstProbeMs);
26584
+ continue;
26585
+ }
26586
+ if (performance.now() - passStart >= passBudgetMs) {
26587
+ warnQuarantined(rule, void 0);
26588
+ continue;
26589
+ }
26590
+ let isSafe;
26591
+ let worstMs;
26592
+ try {
26593
+ ({ safe: isSafe, worstMs } = checkRuleTiming(rule));
26594
+ } catch {
26595
+ isSafe = false;
26596
+ worstMs = Number.POSITIVE_INFINITY;
26597
+ }
26598
+ await gateway.setRuleProbeVerdict(key, isSafe ? "safe" : "quarantined", worstMs);
26599
+ if (isSafe) safe.push(rule);
26600
+ else warnQuarantined(rule, worstMs);
26601
+ }
26602
+ return safe;
26603
+ }
26604
+
25417
26605
  // ../../packages/plugin-sdk/src/runtime.ts
25418
26606
  import { randomUUID as randomUUID10 } from "crypto";
25419
26607
  var ENFORCEMENT_CEILING_ENABLED = false;
@@ -25456,7 +26644,17 @@ function createPluginRuntime(gateway, settings, opts) {
25456
26644
  categoryActionIndex.set(p.target.category, p.action);
25457
26645
  }
25458
26646
  }
25459
- rules = bundle.rulesComplete ? bundle.rules ?? [] : [...getLoadedRules(), ...bundle.rules ?? []];
26647
+ const bundledProbeKeys = new Set(
26648
+ getLoadedRules().map(ruleProbeKey).filter((key) => key !== void 0)
26649
+ );
26650
+ const incoming = bundle.rules ?? [];
26651
+ const ciVerified = incoming.filter((rule) => {
26652
+ const key = ruleProbeKey(rule);
26653
+ return key !== void 0 && bundledProbeKeys.has(key);
26654
+ });
26655
+ const needsGate = incoming.filter((rule) => !ciVerified.includes(rule));
26656
+ const safeBundleRules = [...ciVerified, ...await filterUnsafeRules(needsGate, gateway)];
26657
+ rules = bundle.rulesComplete ? safeBundleRules : [...getLoadedRules(), ...safeBundleRules];
25460
26658
  bundleExceptions = bundle.exceptions ?? [];
25461
26659
  initialized = true;
25462
26660
  }
@@ -25703,12 +26901,12 @@ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeF
25703
26901
  import { join as join10 } from "path";
25704
26902
 
25705
26903
  // src/command-registry.ts
25706
- import { readdirSync as readdirSync3 } from "fs";
26904
+ import { readdirSync as readdirSync4 } from "fs";
25707
26905
  import { fileURLToPath } from "url";
25708
26906
  var COMMAND_NAMESPACE = "aka";
25709
26907
  var COMMANDS_DIR = fileURLToPath(new URL("../commands", import.meta.url));
25710
26908
  function readRegisteredCommands() {
25711
- return readdirSync3(COMMANDS_DIR).filter((f) => f.endsWith(".md")).map((f) => `/${COMMAND_NAMESPACE}:${f.replace(/\.md$/, "")}`);
26909
+ return readdirSync4(COMMANDS_DIR).filter((f) => f.endsWith(".md")).map((f) => `/${COMMAND_NAMESPACE}:${f.replace(/\.md$/, "")}`);
25712
26910
  }
25713
26911
  function selectRegisteredCommands(curated, registry2) {
25714
26912
  const registered = new Set(registry2);
@@ -25802,8 +27000,8 @@ function table(headers, rows, opts = {}) {
25802
27000
  const widths = headers.map(
25803
27001
  (h, i) => Math.max(visibleLength(h), ...rows.map((r) => visibleLength(r[i] ?? "")))
25804
27002
  );
25805
- const sep4 = " ".repeat(gap);
25806
- const fmt = (cells) => cells.map((cell, i) => padEnd(cell, widths[i] ?? 0)).join(sep4);
27003
+ const sep5 = " ".repeat(gap);
27004
+ const fmt = (cells) => cells.map((cell, i) => padEnd(cell, widths[i] ?? 0)).join(sep5);
25807
27005
  const headerLine = fmt(headers.map((h) => h.toUpperCase()));
25808
27006
  if (opts.rowSep === true) {
25809
27007
  const fullWidth = widths.reduce((n, w) => n + w, 0) + gap * Math.max(0, widths.length - 1);
@@ -25815,7 +27013,7 @@ function table(headers, rows, opts = {}) {
25815
27013
  });
25816
27014
  return [headerLine, rule, ...body].join("\n");
25817
27015
  }
25818
- const ruleLine = widths.map((w) => "\u2500".repeat(w)).join(sep4);
27016
+ const ruleLine = widths.map((w) => "\u2500".repeat(w)).join(sep5);
25819
27017
  return [headerLine, ruleLine, ...rows.map(fmt)].join("\n");
25820
27018
  }
25821
27019
  function fenced(body) {
@@ -26355,6 +27553,13 @@ var StandaloneDataGateway = class {
26355
27553
  this.db.scanLedger.upsertEntries(entries);
26356
27554
  return Promise.resolve();
26357
27555
  }
27556
+ getRuleProbeVerdict(ruleKey) {
27557
+ return Promise.resolve(this.db.ruleProbeCache.getVerdict(ruleKey));
27558
+ }
27559
+ setRuleProbeVerdict(ruleKey, verdict, worstProbeMs2) {
27560
+ this.db.ruleProbeCache.setVerdict(ruleKey, verdict, worstProbeMs2);
27561
+ return Promise.resolve();
27562
+ }
26358
27563
  openAtRestKeysForPath(path) {
26359
27564
  return Promise.resolve(this.db.resolutions.openAtRestKeysForPath(path));
26360
27565
  }
@@ -26365,6 +27570,12 @@ var StandaloneDataGateway = class {
26365
27570
  this.db.resolutions.insertResolution(input);
26366
27571
  return Promise.resolve();
26367
27572
  }
27573
+ // Bare forward — no toggle read here. The plugin-path kill-switch is
27574
+ // enforced by the caller, which already holds the parsed workspace
27575
+ // settings; this class only ever sees `dataDir`, not the settings base.
27576
+ recordProjectEgress(input) {
27577
+ return Promise.resolve(this.db.shares.recordProjectEgress(input));
27578
+ }
26368
27579
  close() {
26369
27580
  this.db.close();
26370
27581
  return Promise.resolve();
@@ -26382,7 +27593,7 @@ import { randomUUID as randomUUID12 } from "crypto";
26382
27593
  var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
26383
27594
 
26384
27595
  // src/history/transcripts.ts
26385
- import { readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
27596
+ import { readdirSync as readdirSync5, readFileSync as readFileSync7 } from "fs";
26386
27597
  import { homedir as homedir3 } from "os";
26387
27598
  import { join as join12 } from "path";
26388
27599
  function transcriptsDir(home) {
@@ -26398,7 +27609,7 @@ function deriveProvider(ruleId) {
26398
27609
  }
26399
27610
 
26400
27611
  // src/remediation/redact.ts
26401
- import { readFileSync as readFileSync8, realpathSync as realpathSync2, renameSync as renameSync5, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
27612
+ import { readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
26402
27613
  import { isAbsolute as isAbsolute2, relative as relative2, resolve } from "path";
26403
27614
  var REDACTED_PLACEHOLDER = "[REDACTED:SECRET]";
26404
27615
  function platformRedactionScope(home) {
@@ -26406,7 +27617,7 @@ function platformRedactionScope(home) {
26406
27617
  }
26407
27618
  function realPathOrNull(path) {
26408
27619
  try {
26409
- return realpathSync2(path);
27620
+ return realpathSync3(path);
26410
27621
  } catch {
26411
27622
  return null;
26412
27623
  }