@spool-lab/core 0.4.12 → 0.5.0

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.
Files changed (87) hide show
  1. package/README.md +2 -2
  2. package/dist/db/db.d.ts +10 -2
  3. package/dist/db/db.d.ts.map +1 -1
  4. package/dist/db/db.js +119 -10
  5. package/dist/db/db.js.map +1 -1
  6. package/dist/db/queries.d.ts +5 -5
  7. package/dist/db/queries.d.ts.map +1 -1
  8. package/dist/db/queries.js +32 -14
  9. package/dist/db/queries.js.map +1 -1
  10. package/dist/index.d.ts +4 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/observability/exporter-file.d.ts +30 -0
  15. package/dist/observability/exporter-file.d.ts.map +1 -0
  16. package/dist/observability/exporter-file.js +125 -0
  17. package/dist/observability/exporter-file.js.map +1 -0
  18. package/dist/observability/exporter-pretty.d.ts +27 -0
  19. package/dist/observability/exporter-pretty.d.ts.map +1 -0
  20. package/dist/observability/exporter-pretty.js +84 -0
  21. package/dist/observability/exporter-pretty.js.map +1 -0
  22. package/dist/observability/index.d.ts +2 -0
  23. package/dist/observability/index.d.ts.map +1 -0
  24. package/dist/observability/index.js +2 -0
  25. package/dist/observability/index.js.map +1 -0
  26. package/dist/observability/layer.d.ts +29 -0
  27. package/dist/observability/layer.d.ts.map +1 -0
  28. package/dist/observability/layer.js +45 -0
  29. package/dist/observability/layer.js.map +1 -0
  30. package/dist/parsers/opencode.d.ts +23 -0
  31. package/dist/parsers/opencode.d.ts.map +1 -0
  32. package/dist/parsers/opencode.js +358 -0
  33. package/dist/parsers/opencode.js.map +1 -0
  34. package/dist/projects/sessions.d.ts +29 -1
  35. package/dist/projects/sessions.d.ts.map +1 -1
  36. package/dist/projects/sessions.js +115 -11
  37. package/dist/projects/sessions.js.map +1 -1
  38. package/dist/security/index.d.ts +14 -0
  39. package/dist/security/index.d.ts.map +1 -0
  40. package/dist/security/index.js +7 -0
  41. package/dist/security/index.js.map +1 -0
  42. package/dist/security/maintenance.d.ts +15 -0
  43. package/dist/security/maintenance.d.ts.map +1 -0
  44. package/dist/security/maintenance.js +68 -0
  45. package/dist/security/maintenance.js.map +1 -0
  46. package/dist/security/profile.d.ts +47 -0
  47. package/dist/security/profile.d.ts.map +1 -0
  48. package/dist/security/profile.js +131 -0
  49. package/dist/security/profile.js.map +1 -0
  50. package/dist/security/purge.d.ts +68 -0
  51. package/dist/security/purge.d.ts.map +1 -0
  52. package/dist/security/purge.js +193 -0
  53. package/dist/security/purge.js.map +1 -0
  54. package/dist/security/repo.d.ts +190 -0
  55. package/dist/security/repo.d.ts.map +1 -0
  56. package/dist/security/repo.js +594 -0
  57. package/dist/security/repo.js.map +1 -0
  58. package/dist/security/scan.d.ts +50 -0
  59. package/dist/security/scan.d.ts.map +1 -0
  60. package/dist/security/scan.js +122 -0
  61. package/dist/security/scan.js.map +1 -0
  62. package/dist/security/types.d.ts +146 -0
  63. package/dist/security/types.d.ts.map +1 -0
  64. package/dist/security/types.js +10 -0
  65. package/dist/security/types.js.map +1 -0
  66. package/dist/security/worker.d.ts +69 -0
  67. package/dist/security/worker.d.ts.map +1 -0
  68. package/dist/security/worker.js +228 -0
  69. package/dist/security/worker.js.map +1 -0
  70. package/dist/sync/source-paths.d.ts +1 -0
  71. package/dist/sync/source-paths.d.ts.map +1 -1
  72. package/dist/sync/source-paths.js +48 -6
  73. package/dist/sync/source-paths.js.map +1 -1
  74. package/dist/sync/syncer.d.ts +9 -1
  75. package/dist/sync/syncer.d.ts.map +1 -1
  76. package/dist/sync/syncer.js +128 -16
  77. package/dist/sync/syncer.js.map +1 -1
  78. package/dist/sync/watcher.d.ts.map +1 -1
  79. package/dist/sync/watcher.js +8 -2
  80. package/dist/sync/watcher.js.map +1 -1
  81. package/dist/types.d.ts +15 -1
  82. package/dist/types.d.ts.map +1 -1
  83. package/dist/util/resolve-bin.d.ts +34 -0
  84. package/dist/util/resolve-bin.d.ts.map +1 -1
  85. package/dist/util/resolve-bin.js +175 -20
  86. package/dist/util/resolve-bin.js.map +1 -1
  87. package/package.json +11 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/security/scan.ts"],"names":[],"mappings":"AAaA,OAAO,EAAQ,MAAM,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAWvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;;;;AAEhD,qBAAa,SAAU,SAAQ,eAA8B;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,mBAAmB,GAAG,iBAAiB,GAAG,WAAW,CAAA;CACvE,CAAC;CAAG;AAOL,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAA;IACrB;;0CAEsC;IACtC,SAAS,EAAE,SAAS,cAAc,EAAE,CAAA;IACpC;2BACuB;IACvB,cAAc,EAAE,MAAM,CAAA;IACtB;;;oEAGgE;IAChE,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC;+CAC2C;IAC3C,OAAO,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACxD;;;wCAGoC;IACpC,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACpC;AAED;;2CAE2C;AAC3C,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,eAAe,GACpB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,CAsFtC"}
@@ -0,0 +1,122 @@
1
+ // Per-session scan pipeline.
2
+ //
3
+ // Effect-shaped so the worker can compose it with Queue / Stream /
4
+ // supervisor primitives. The detection itself runs against
5
+ // `messages.content_text` only — Purge later mutates that field,
6
+ // findings re-detect, and the audit trail stays consistent.
7
+ //
8
+ // Transaction shape: detection runs *outside* the transaction (it
9
+ // can take seconds with ML); only the final apply step is atomic.
10
+ // If the worker dies mid-detection, scan_profile was never updated
11
+ // → restart re-enqueues from scratch. If it dies inside the
12
+ // transaction, SQLite atomicity guarantees no half-applied state.
13
+ import { Data, Effect } from 'effect';
14
+ import { hashValueForRedactExclude } from '@spool-lab/redact';
15
+ import { deleteRefreshableFindings, insertFindings, setSessionScanProfile, updateSessionCounts, getAllowlists, isAllowlisted, } from './repo.js';
16
+ export class ScanError extends Data.TaggedError('ScanError') {
17
+ }
18
+ /** Scan one session end to end. Idempotent: running twice produces
19
+ * the same findings (same value_hash + same offsets → same rows
20
+ * after the delete-then-insert reset). */
21
+ export function scanSession(sessionId, deps) {
22
+ return Effect.gen(function* () {
23
+ // 1. Load messages outside the transaction.
24
+ const messages = yield* Effect.try({
25
+ try: () => deps.db.prepare(`SELECT id, content_text
26
+ FROM messages
27
+ WHERE session_id = ?
28
+ ORDER BY seq ASC`).all(sessionId),
29
+ catch: (cause) => new ScanError({ sessionId, cause, reason: 'db-failed' }),
30
+ });
31
+ yield* Effect.annotateCurrentSpan('messageCount', messages.length);
32
+ if (messages.length === 0) {
33
+ // Session has no messages yet. Still mark it scanned with the
34
+ // current profile so we don't loop on it; future sync will
35
+ // invalidate the profile and re-enqueue.
36
+ yield* applyEmpty(sessionId, deps);
37
+ yield* deps.publish({ type: 'session-rescanned', sessionId });
38
+ return { sessionId, inserted: 0, profile: deps.currentProfile };
39
+ }
40
+ // 2. Run providers per message. The flat list is enough — we
41
+ // dedupe later by (kind, value_hash, start_offset) so producers
42
+ // can be naive about overlap.
43
+ const allMatches = [];
44
+ for (const msg of messages) {
45
+ for (const provider of deps.providers) {
46
+ if (!provider.available())
47
+ continue;
48
+ const matches = yield* Effect.tryPromise({
49
+ try: () => provider.analyze(msg.content_text),
50
+ catch: (cause) => new ScanError({ sessionId, cause, reason: 'provider-failed' }),
51
+ });
52
+ for (const m of matches) {
53
+ allMatches.push({ ...m, messageId: msg.id });
54
+ }
55
+ }
56
+ }
57
+ // 3. Dedupe across providers — pick the highest-confidence match
58
+ // for each (kind, value_hash, start_offset).
59
+ const merged = mergeMatches(allMatches);
60
+ // 4. Allowlist join → assign initial state.
61
+ const allow = yield* Effect.try({
62
+ try: () => getAllowlists(deps.db, sessionId),
63
+ catch: (cause) => new ScanError({ sessionId, cause, reason: 'db-failed' }),
64
+ });
65
+ const inputs = merged.map((m) => ({
66
+ sessionId,
67
+ messageId: m.messageId,
68
+ kind: m.kind,
69
+ valueHash: m.valueHash,
70
+ confidence: m.confidence,
71
+ provider: m.provider,
72
+ startOffset: m.start,
73
+ endOffset: m.end,
74
+ state: (deps.kindAllowlist?.has(m.kind) || isAllowlisted(allow, m.kind, m.valueHash))
75
+ ? 'dismissed'
76
+ : 'active',
77
+ }));
78
+ // 5. Apply atomically.
79
+ yield* Effect.try({
80
+ try: () => {
81
+ deps.db.transaction(() => {
82
+ deleteRefreshableFindings(deps.db, sessionId, deps.providerNames);
83
+ insertFindings(deps.db, inputs);
84
+ setSessionScanProfile(deps.db, sessionId, deps.currentProfile, new Date().toISOString());
85
+ updateSessionCounts(deps.db, sessionId);
86
+ })();
87
+ },
88
+ catch: (cause) => new ScanError({ sessionId, cause, reason: 'db-failed' }),
89
+ });
90
+ yield* deps.publish({ type: 'session-rescanned', sessionId });
91
+ yield* Effect.annotateCurrentSpan('inserted', inputs.length);
92
+ return { sessionId, inserted: inputs.length, profile: deps.currentProfile };
93
+ }).pipe(Effect.withSpan('security.scan.session', { attributes: { sessionId } }));
94
+ }
95
+ function applyEmpty(sessionId, deps) {
96
+ return Effect.try({
97
+ try: () => {
98
+ deps.db.transaction(() => {
99
+ deleteRefreshableFindings(deps.db, sessionId, deps.providerNames);
100
+ setSessionScanProfile(deps.db, sessionId, deps.currentProfile, new Date().toISOString());
101
+ updateSessionCounts(deps.db, sessionId);
102
+ })();
103
+ },
104
+ catch: (cause) => new ScanError({ sessionId, cause, reason: 'db-failed' }),
105
+ });
106
+ }
107
+ /** Pick the highest-confidence match per (messageId, kind, value_hash,
108
+ * start). The same vendor token detected by two providers should
109
+ * produce one finding, attributed to whoever was most confident. */
110
+ function mergeMatches(matches) {
111
+ const byKey = new Map();
112
+ for (const m of matches) {
113
+ const hash = hashValueForRedactExclude(m.value);
114
+ const key = `${m.messageId}|${m.kind}|${hash}|${m.start}`;
115
+ const existing = byKey.get(key);
116
+ if (!existing || m.confidence > existing.confidence) {
117
+ byKey.set(key, { ...m, valueHash: hash });
118
+ }
119
+ }
120
+ return Array.from(byKey.values());
121
+ }
122
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/security/scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,146 @@
1
+ import type { SensitiveKind, Severity } from '@spool-lab/redact';
2
+ /** A finding's lifecycle state.
3
+ *
4
+ * active — surfaced in Security page and session strip
5
+ * dismissed — user opted to ignore; suppressed from default views
6
+ * purged — raw value rewritten with a mask in
7
+ * messages.content_text; row remains as an audit record
8
+ */
9
+ export type FindingState = 'active' | 'dismissed' | 'purged';
10
+ /** One row of the `findings` table, mapped to camelCase for the
11
+ * renderer. The raw sensitive value is NEVER part of this payload —
12
+ * consumers that need to render the original text fetch it via
13
+ * `getFindingValue(findingId)`, which reads
14
+ * `messages.content_text[startOffset..endOffset]` at request time. */
15
+ export interface FindingRow {
16
+ id: number;
17
+ sessionId: number;
18
+ messageId: number | null;
19
+ kind: SensitiveKind;
20
+ valueHash: string;
21
+ confidence: number;
22
+ provider: string;
23
+ startOffset: number;
24
+ endOffset: number;
25
+ state: FindingState;
26
+ detectedAt: string;
27
+ stateChangedAt: string | null;
28
+ }
29
+ /** A session row joined with its denormalised scan counts. Used by
30
+ * the Security page list and the Library row badge. */
31
+ export interface SessionWithFindingCounts {
32
+ id: number;
33
+ sessionUuid: string;
34
+ title: string | null;
35
+ startedAt: string;
36
+ /** Active findings of any severity. */
37
+ findingCount: number;
38
+ /** Active findings whose kind is in `HIGH_SEVERITY_KINDS`. */
39
+ highCount: number;
40
+ /** Lifetime count of purged findings on this session. Combined with
41
+ * scan_completed_at + a zero findingCount, lets the Library row
42
+ * show an "all-resolved" check rather than a stale red badge. */
43
+ purgedCount: number;
44
+ scanCompletedAt: string | null;
45
+ /** Sources name ('claude' / 'codex' / 'gemini'). */
46
+ source: string;
47
+ /** Number of non-sidechain messages in the session. */
48
+ messageCount: number;
49
+ /** Model identifier as captured at sync time. May be null for
50
+ * sessions where the parser couldn't infer it. */
51
+ model: string | null;
52
+ /** Working directory captured when the session started; used to
53
+ * build the resume-CLI command and the "Copy terminal command"
54
+ * menu action. Null when the parser couldn't infer it. */
55
+ cwd: string | null;
56
+ /** Human-readable project name, e.g. "spool". Surfaced at the head
57
+ * of the session card's meta row so users can locate the session
58
+ * in the Library at a glance. */
59
+ projectDisplayName: string | null;
60
+ }
61
+ /** Row in the Security page's Risk-by-category panel. One per kind
62
+ * that has ≥ 1 active finding; categories with zero counts are not
63
+ * emitted. */
64
+ export interface RiskByCategoryRow {
65
+ kind: SensitiveKind;
66
+ severity: Severity;
67
+ count: number;
68
+ /** Distinct sessions containing ≥ 1 active finding of this kind. */
69
+ sessions: number;
70
+ }
71
+ /** One session in which a given `(kind, valueHash)` secret appears —
72
+ * the cross-session "blast radius" of a single leaked value. Counts
73
+ * only ACTIVE findings (dismissed/purged occurrences don't expose the
74
+ * value through Spool's surfaces). */
75
+ export interface OccurrenceBySession {
76
+ sessionId: number;
77
+ sessionUuid: string;
78
+ sessionTitle: string | null;
79
+ /** Agent source (claude / codex / gemini / opencode) — drives the
80
+ * source badge so each row reads as a session, not loose text. */
81
+ source: string;
82
+ /** Project display name, null for the Loose bucket. */
83
+ project: string | null;
84
+ /** Active occurrences of this value in this session. */
85
+ count: number;
86
+ /** Most recent detected_at across this session's occurrences (ISO). */
87
+ lastSeen: string;
88
+ }
89
+ /** Scope of a Dismiss action.
90
+ * session — add to allowlist_session for this finding's session
91
+ * global — add to allowlist_global; all matching findings across
92
+ * every session auto-dismiss on next scan */
93
+ export interface DismissScope {
94
+ scope: 'session' | 'global';
95
+ /** Required when scope === 'session'. */
96
+ sessionId?: number;
97
+ }
98
+ /** Change events emitted on the worker's PubSub stream and forwarded
99
+ * to renderers via IPC. Consumers should treat unknown `type` values
100
+ * as no-ops (forward-compat). */
101
+ export type FindingsChange = {
102
+ type: 'inserted';
103
+ sessionId: number;
104
+ } | {
105
+ type: 'state-changed';
106
+ sessionId: number;
107
+ findingId: number;
108
+ state: FindingState;
109
+ } | {
110
+ type: 'session-rescanned';
111
+ sessionId: number;
112
+ };
113
+ /** Snapshot of the scan worker's transient state. The DB stores
114
+ * scan_profile / scan_completed_at; queued and scanning are
115
+ * worker-memory only — they reset on restart. */
116
+ export interface ScanStatus {
117
+ /** Sessions waiting for a slot in the worker queue. */
118
+ queued: number;
119
+ /** Session currently being scanned, or null when idle. */
120
+ scanning: number | null;
121
+ /** When in backfill mode, sessions still to process; 0 otherwise. */
122
+ backfillRemaining: number;
123
+ /** High-water mark for the active scan burst — the total session
124
+ * count this burst has set out to scan. Held until the worker
125
+ * goes fully idle (queued + scanning + backfillRemaining all 0),
126
+ * then resets to 0. Lets the renderer's progress bar survive
127
+ * navigation away from + back to the Security page mid-scan
128
+ * without "restarting" at the remaining-now count. */
129
+ backfillTotal: number;
130
+ /** True for the lifetime of a user-initiated `Rescan all` burst
131
+ * (set by `worker.rescanAll()`; cleared automatically when the
132
+ * worker goes fully idle). The renderer reads this directly
133
+ * instead of tracking the click locally — a renderer-side flag
134
+ * would race against any auto sync-driven enqueue that completed
135
+ * between the click and the IPC reaching the worker, causing
136
+ * the manual ACK banner to be hijacked + cleared by the auto
137
+ * burst's idle edge. Tying it to the worker keeps a single
138
+ * source of truth. */
139
+ manualBurstInFlight: boolean;
140
+ /** Composite identifier for the active provider set, e.g.
141
+ * 'regex@3' or 'regex@3,pf@1.5b-q4'. Persisted on
142
+ * sessions.scan_profile to detect rescan candidates after
143
+ * provider configuration changes. */
144
+ currentProfile: string;
145
+ }
146
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/security/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ // Shared types for the Security Scan subsystem.
2
+ //
3
+ // These are the cross-boundary payloads — worker → IPC → renderer.
4
+ // They live in one module so:
5
+ // 1. The worker (main process), IPC adapters, and renderer hooks
6
+ // can import a single source of truth.
7
+ // 2. Promoting them to `Schema.Struct` later (for a Scope-B Effect
8
+ // RPC migration) is a local refactor, not a consumer rewrite.
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/security/types.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,mEAAmE;AACnE,8BAA8B;AAC9B,mEAAmE;AACnE,4CAA4C;AAC5C,qEAAqE;AACrE,mEAAmE"}
@@ -0,0 +1,69 @@
1
+ import { Effect, Fiber, Stream, Scope } from 'effect';
2
+ import type Database from 'better-sqlite3';
3
+ import type { RedactProvider } from '@spool-lab/redact';
4
+ import type { FindingsChange, ScanStatus } from './types.js';
5
+ export interface WorkerConfig {
6
+ db: Database.Database;
7
+ providers: readonly RedactProvider[];
8
+ /** Profile string for `providers`. The worker rejects mismatched
9
+ * pairs (you can't claim profile = 'regex@3,pf@1.5b' with only
10
+ * regexProvider in `providers`).
11
+ *
12
+ * Pass either a static string (test fixtures) or a thunk (prod —
13
+ * lets the profile track pref changes like `kindAllowlist` so
14
+ * toggling a kind triggers backfill via `listSessionsNeedingScan`
15
+ * on the next call). The worker resolves the thunk on every read
16
+ * rather than caching, so a pref save → SET_PREFS handler can
17
+ * invalidate sessions and immediately see them as stale. */
18
+ currentProfile: string | (() => string);
19
+ /** Names of providers active in this profile. */
20
+ providerNames: readonly string[];
21
+ /** Live accessor for the kind-level allowlist. Read on every scan
22
+ * (cheap — a Set lookup per match), so changing the preference
23
+ * takes effect on the next rescan without restarting the worker. */
24
+ getKindAllowlist?: () => ReadonlySet<string>;
25
+ }
26
+ export interface ScanWorker {
27
+ /** Drop a session at the back of the queue. */
28
+ enqueue: (sessionId: number) => Effect.Effect<void>;
29
+ /** Set every session's profile to NULL and enqueue them — used by
30
+ * "Rescan all" and by provider-set changes. */
31
+ rescanAll: () => Effect.Effect<number>;
32
+ /** Enqueue every session whose scan_profile != currentProfile.
33
+ * Called on boot AND on every profile-affecting pref change
34
+ * (kindAllowlist toggle, PF on/off, etc.).
35
+ *
36
+ * Treated as a fresh burst: any leftover items still in the queue
37
+ * from the previous burst are discarded (they were enqueued against
38
+ * an outdated profile; the current `listSessionsNeedingScan` call
39
+ * is the authoritative new work set). `backfillTotal` resets to the
40
+ * new stale-set size instead of sticky-maxing with the old burst's
41
+ * value, so the renderer's progress bar reflects the new work
42
+ * rather than the union of the two.
43
+ *
44
+ * Pass `userInitiated: true` when the caller is a SET_PREFS / PF
45
+ * toggle handler so the renderer's busy→idle edge surfaces a
46
+ * "Scan complete" result banner the same way it does for the
47
+ * Rescan-all button. */
48
+ backfill: (opts?: {
49
+ userInitiated?: boolean;
50
+ }) => Effect.Effect<number>;
51
+ /** Stream of finding change events. Translator in main process
52
+ * subscribes and forwards over IPC. */
53
+ changes: Stream.Stream<FindingsChange>;
54
+ /** Stream of status updates — emitted on every queued / scanning /
55
+ * backfillRemaining mutation. Lets the UI replace polling with a
56
+ * push subscription. */
57
+ statusChanges: Stream.Stream<ScanStatus>;
58
+ /** Current snapshot. UI uses this for the first read; subsequent
59
+ * updates land via `statusChanges`. */
60
+ getStatus: Effect.Effect<ScanStatus>;
61
+ }
62
+ /** Build a worker as a Scoped effect: when the scope closes, the
63
+ * draining fiber is interrupted cleanly and the PubSub shuts down. */
64
+ export declare function makeScanWorker(config: WorkerConfig): Effect.Effect<ScanWorker, never, Scope.Scope>;
65
+ /** Helper for tests / callers that want a complete drain promise
66
+ * (e.g. wait for the queue to empty after a backfill). */
67
+ export declare function waitForIdle(worker: ScanWorker): Effect.Effect<void>;
68
+ export { Fiber };
69
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/security/worker.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAsB,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACzE,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAGvD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5D,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAA;IACrB,SAAS,EAAE,SAAS,cAAc,EAAE,CAAA;IACpC;;;;;;;;;iEAS6D;IAC7D,cAAc,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IACvC,iDAAiD;IACjD,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC;;yEAEqE;IACrE,gBAAgB,CAAC,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;CAC7C;AAQD,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACnD;oDACgD;IAChD,SAAS,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACtC;;;;;;;;;;;;;;;6BAeyB;IACzB,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACvE;4CACwC;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAA;IACtC;;6BAEyB;IACzB,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACxC;4CACwC;IACxC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;CACrC;AAED;uEACuE;AACvE,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,GACnB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAoO/C;AAED;2DAC2D;AAC3D,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAQnE;AAGD,OAAO,EAAE,KAAK,EAAE,CAAA"}
@@ -0,0 +1,228 @@
1
+ // Scan worker — the long-lived coordinator that drains a session
2
+ // queue, runs `scanSession` per item, and publishes change events.
3
+ //
4
+ // Implementation: Effect Scope A. Inside this module is Effect-shaped
5
+ // (Queue, Stream, PubSub, Ref, Scope). The boundary — IPC handlers
6
+ // in `packages/app/src/main/ipc/security.ts` — wraps `runFiber` /
7
+ // `runPromise` so renderers see plain Promise-shaped channels.
8
+ //
9
+ // Single-fiber by design (`concurrency: 1` on the queue stream).
10
+ // Better-sqlite3 is synchronous, so multiple parallel fibers compete
11
+ // for the DB lock and gain nothing. Backfill UX surfaces "session N
12
+ // of M" progress for the user.
13
+ import { Effect, Fiber, PubSub, Queue, Ref, Stream } from 'effect';
14
+ import { scanSession } from './scan.js';
15
+ import { listSessionsNeedingScan } from './repo.js';
16
+ function resolveProfile(config) {
17
+ return typeof config.currentProfile === 'function'
18
+ ? config.currentProfile()
19
+ : config.currentProfile;
20
+ }
21
+ /** Build a worker as a Scoped effect: when the scope closes, the
22
+ * draining fiber is interrupted cleanly and the PubSub shuts down. */
23
+ export function makeScanWorker(config) {
24
+ return Effect.gen(function* () {
25
+ const queue = yield* Queue.unbounded();
26
+ const events = yield* PubSub.unbounded();
27
+ const statusEvents = yield* PubSub.unbounded();
28
+ const statusRef = yield* Ref.make({
29
+ queued: 0,
30
+ scanning: null,
31
+ backfillRemaining: 0,
32
+ backfillTotal: 0,
33
+ manualBurstInFlight: false,
34
+ currentProfile: resolveProfile(config),
35
+ });
36
+ const publish = (c) => PubSub.publish(events, c).pipe(Effect.asVoid);
37
+ // Update statusRef AND publish the new snapshot so subscribers get
38
+ // a push event for every mutation. One helper at the seam avoids
39
+ // forgetting to publish from a future Ref.update site.
40
+ //
41
+ // `backfillTotal` is derived here so individual mutation sites
42
+ // (enqueue / rescanAll / backfill / scanOne) don't have to
43
+ // remember to bump it. The renderer's progress bar reads
44
+ // `backfillRemaining + (scanning ? 1 : 0)` as the active count;
45
+ // we track the high-water mark of that quantity since the last
46
+ // full-idle state, and reset to 0 once everything drains. Survives
47
+ // navigation: a user who switches tabs mid-burst and comes back
48
+ // gets the true original total via `getStatus`, not the
49
+ // remaining-at-remount-time approximation.
50
+ const updateStatus = (f) => Ref.update(statusRef, (prev) => {
51
+ const after = f(prev);
52
+ // High-water mark of *backfillRemaining alone*. Including the
53
+ // scanning slot (br + 1 when scanning != null) bumped the
54
+ // total by one every time a scanOne started, and dropped it
55
+ // again when scanning went back to null between sessions —
56
+ // turning the renderer's `(total - remaining)/total` into a
57
+ // non-monotonic value that visibly rewound the progress bar
58
+ // on every cross-session tick. backfillRemaining itself is
59
+ // strictly monotonic (only decrements at scanOne end), so
60
+ // anchoring the total to it makes the progress bar strictly
61
+ // forward.
62
+ const fullyIdle = after.backfillRemaining === 0 &&
63
+ after.queued === 0 &&
64
+ after.scanning === null;
65
+ const backfillTotal = fullyIdle
66
+ ? 0
67
+ : Math.max(prev.backfillTotal, after.backfillRemaining);
68
+ // `manualBurstInFlight` rides along with the burst. The
69
+ // worker.rescanAll() mutation flips it on; this wrapper
70
+ // flips it off the instant the worker drains, so the
71
+ // renderer's "your manual scan finished" detection never
72
+ // sees a stale truth across burst boundaries.
73
+ const manualBurstInFlight = fullyIdle ? false : after.manualBurstInFlight;
74
+ return { ...after, backfillTotal, manualBurstInFlight };
75
+ }).pipe(Effect.zipRight(Ref.get(statusRef)), Effect.flatMap((s) => PubSub.publish(statusEvents, s)), Effect.asVoid);
76
+ // Burst-reset variant for backfill() (and any future caller that
77
+ // means "start a fresh burst, discard whatever the previous one
78
+ // had queued"). Two differences from updateStatus:
79
+ //
80
+ // 1. The `Math.max(prev.backfillTotal, after.backfillRemaining)`
81
+ // sticky guard is BYPASSED — the caller is declaring a brand-
82
+ // new burst, so its new `backfillRemaining` becomes the
83
+ // authoritative `backfillTotal`. Without this, a toggle-pref
84
+ // mid-scan would leave the renderer showing the OLD burst's
85
+ // total while the new burst's much smaller stale set drives
86
+ // the remaining count → progress bar visually pinned at
87
+ // ~100% for as long as the queue takes to drain.
88
+ //
89
+ // 2. The Effect Queue is drained synchronously. Items left in
90
+ // it were enqueued against the previous profile and were
91
+ // already accounted for in the OLD burst's
92
+ // backfillRemaining. With the new totals in force they'd
93
+ // keep `nowBusy = queued > 0` true (banner-stuck-at-100%
94
+ // problem) and waste regex-only scans on sessions whose
95
+ // profile already matches the current one.
96
+ const resetBurst = (f) => Effect.gen(function* () {
97
+ yield* Queue.takeAll(queue);
98
+ yield* Ref.update(statusRef, (prev) => {
99
+ const after = f(prev);
100
+ const fullyIdle = after.backfillRemaining === 0 &&
101
+ after.queued === 0 &&
102
+ after.scanning === null;
103
+ const backfillTotal = fullyIdle ? 0 : after.backfillRemaining;
104
+ const manualBurstInFlight = fullyIdle ? false : after.manualBurstInFlight;
105
+ return { ...after, backfillTotal, manualBurstInFlight };
106
+ });
107
+ const snap = yield* Ref.get(statusRef);
108
+ yield* PubSub.publish(statusEvents, snap);
109
+ });
110
+ const scanOne = (sessionId) => Effect.gen(function* () {
111
+ yield* updateStatus((s) => ({
112
+ ...s,
113
+ scanning: sessionId,
114
+ queued: Math.max(0, s.queued - 1),
115
+ }));
116
+ const result = yield* scanSession(sessionId, {
117
+ db: config.db,
118
+ providers: config.providers,
119
+ currentProfile: resolveProfile(config),
120
+ providerNames: config.providerNames,
121
+ publish,
122
+ ...(config.getKindAllowlist ? { kindAllowlist: config.getKindAllowlist() } : {}),
123
+ }).pipe(Effect.catchAll((err) => Effect.logError(`scanSession failed for ${sessionId}: ${err.reason}`, err).pipe(Effect.as(null))));
124
+ yield* updateStatus((s) => ({
125
+ ...s,
126
+ scanning: null,
127
+ backfillRemaining: Math.max(0, s.backfillRemaining - 1),
128
+ }));
129
+ return result;
130
+ });
131
+ // The drain fiber runs for the worker's lifetime; Scope.addFinalizer
132
+ // interrupts it when the runtime tears down.
133
+ //
134
+ // Throttle: yield 50ms between scans. better-sqlite3 is synchronous
135
+ // so the DB lock is held for the full scanOne duration (~50-100ms
136
+ // per session). Without a yield, a backfill of N sessions becomes
137
+ // ~N*scanTime of uninterrupted lock contention — foreground reads
138
+ // (listSessions, FTS search) get queued behind every scan. The
139
+ // sleep gives the event loop a window to flush foreground IPC
140
+ // handlers before grabbing the next session. Doubles backfill
141
+ // duration but keeps the app responsive.
142
+ const drain = Stream.fromQueue(queue).pipe(Stream.mapEffect((sessionId) => scanOne(sessionId).pipe(Effect.tap(() => Effect.sleep('50 millis'))), { concurrency: 1 }), Stream.runDrain);
143
+ yield* Effect.forkScoped(drain);
144
+ const enqueue = (sessionId) => Effect.gen(function* () {
145
+ yield* Queue.offer(queue, sessionId);
146
+ yield* updateStatus((s) => ({ ...s, queued: s.queued + 1 }));
147
+ });
148
+ const rescanAll = () => Effect.gen(function* () {
149
+ // SELECT and UPDATE share one transaction so a concurrent sync
150
+ // that inserts a new session can't slip between them — the
151
+ // new row would otherwise miss the profile NULL pass AND be
152
+ // omitted from `all`, leaving it silently skipped by the
153
+ // drain fiber even though the user pressed "Rescan all". With
154
+ // a transaction the worker either sees every session present
155
+ // at the moment of the click, or sees the post-insert state
156
+ // (in which case the new session participates).
157
+ const all = yield* Effect.sync(() => config.db.transaction(() => {
158
+ const rows = config.db
159
+ .prepare('SELECT id FROM sessions ORDER BY started_at DESC')
160
+ .all();
161
+ config.db.prepare('UPDATE sessions SET scan_profile = NULL').run();
162
+ return rows;
163
+ })());
164
+ yield* updateStatus((s) => ({
165
+ ...s,
166
+ backfillRemaining: all.length,
167
+ // Flag this as a user-initiated burst — the renderer
168
+ // hangs its result-banner ACK off this field instead of
169
+ // a click-local flag.
170
+ manualBurstInFlight: true,
171
+ }));
172
+ for (const r of all) {
173
+ yield* enqueue(r.id);
174
+ }
175
+ yield* Effect.annotateCurrentSpan('enqueued', all.length);
176
+ return all.length;
177
+ }).pipe(Effect.withSpan('security.worker.rescan_all'));
178
+ const backfill = (opts) => Effect.gen(function* () {
179
+ // Re-resolve the profile each call so a SET_PREFS handler that
180
+ // changed kindAllowlist sees the updated hash before invoking
181
+ // backfill.
182
+ const profile = resolveProfile(config);
183
+ const stale = yield* Effect.sync(() => listSessionsNeedingScan(config.db, profile));
184
+ // Fresh burst: drain any leftover queue items (they belong to
185
+ // a previous profile snapshot — see `resetBurst` notes), reset
186
+ // queued/backfillRemaining/backfillTotal to this burst's stale
187
+ // count, and stamp the user-initiated flag if applicable. The
188
+ // wrapper bypasses the sticky-max guard so the renderer's
189
+ // progress bar tracks the new burst, not the old.
190
+ yield* resetBurst((s) => ({
191
+ ...s,
192
+ currentProfile: profile,
193
+ queued: 0,
194
+ backfillRemaining: stale.length,
195
+ manualBurstInFlight: opts?.userInitiated === true,
196
+ }));
197
+ for (const id of stale) {
198
+ yield* enqueue(id);
199
+ }
200
+ yield* Effect.annotateCurrentSpan('enqueued', stale.length);
201
+ yield* Effect.annotateCurrentSpan('userInitiated', opts?.userInitiated === true);
202
+ return stale.length;
203
+ }).pipe(Effect.withSpan('security.worker.backfill'));
204
+ return {
205
+ enqueue,
206
+ rescanAll,
207
+ backfill,
208
+ changes: Stream.fromPubSub(events),
209
+ statusChanges: Stream.fromPubSub(statusEvents),
210
+ getStatus: Ref.get(statusRef),
211
+ };
212
+ });
213
+ }
214
+ /** Helper for tests / callers that want a complete drain promise
215
+ * (e.g. wait for the queue to empty after a backfill). */
216
+ export function waitForIdle(worker) {
217
+ return Effect.gen(function* () {
218
+ while (true) {
219
+ const s = yield* worker.getStatus;
220
+ if (s.queued === 0 && s.scanning === null && s.backfillRemaining === 0)
221
+ return;
222
+ yield* Effect.sleep('25 millis');
223
+ }
224
+ });
225
+ }
226
+ // Re-export Fiber for callers that want to manage runtime lifecycle.
227
+ export { Fiber };
228
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/security/worker.ts"],"names":[],"mappings":"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"}
@@ -2,4 +2,5 @@ import type { SessionSource } from '../types.js';
2
2
  export declare function getSessionRoots(source: SessionSource): string[];
3
3
  export declare function detectSessionSource(filePath: string, sourceRoots?: Record<SessionSource, string[]>): SessionSource | undefined;
4
4
  export declare function getSessionWatchPatterns(source: SessionSource, roots?: string[]): string[];
5
+ export declare function isSessionFileForSource(source: SessionSource, filePath: string, root: string): boolean;
5
6
  //# sourceMappingURL=source-paths.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"source-paths.d.ts","sourceRoot":"","sources":["../../src/sync/source-paths.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAuBhD,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,EAAE,CA4B/D;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAI1C,GACA,aAAa,GAAG,SAAS,CAO3B;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,aAAa,EACrB,KAAK,WAA0B,GAC9B,MAAM,EAAE,CAGV"}
1
+ {"version":3,"file":"source-paths.d.ts","sourceRoot":"","sources":["../../src/sync/source-paths.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAyBhD,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,EAAE,CAgC/D;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAK1C,GACA,aAAa,GAAG,SAAS,CAO3B;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,aAAa,EACrB,KAAK,WAA0B,GAC9B,MAAM,EAAE,CAOV;AAiED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAoBrG"}