@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,594 @@
1
+ // Findings + allowlist repository.
2
+ //
3
+ // Plain better-sqlite3 prepared statements. No Effect here — this
4
+ // module is called from both the scan worker (Effect, wraps these in
5
+ // Effect.sync) and the IPC handlers (Promise-shaped). Keeping it
6
+ // effect-free preserves the worker / boundary split documented in
7
+ // the spec's "Implementation: Effect TS scope" section.
8
+ //
9
+ // Invariant: `findings.value_hash` is FNV-1a, computed via the
10
+ // share-kit-aligned `hashValueForRedactExclude` so identity matches
11
+ // across surfaces (share editor's exclude list, security allowlists).
12
+ import { HIGH_SEVERITY_KINDS, INFO_SEVERITY_KINDS, severityOf, } from '@spool-lab/redact';
13
+ function rowToFinding(r) {
14
+ return {
15
+ id: r.id,
16
+ sessionId: r.session_id,
17
+ messageId: r.message_id,
18
+ kind: r.kind,
19
+ valueHash: r.value_hash,
20
+ confidence: r.confidence,
21
+ provider: r.provider,
22
+ startOffset: r.start_offset,
23
+ endOffset: r.end_offset,
24
+ state: r.state,
25
+ detectedAt: r.detected_at,
26
+ stateChangedAt: r.state_changed_at,
27
+ };
28
+ }
29
+ /** Insert a batch of findings. Caller wraps in a transaction. */
30
+ export function insertFindings(db, rows) {
31
+ if (rows.length === 0)
32
+ return;
33
+ const stmt = db.prepare(`INSERT INTO findings
34
+ (session_id, message_id, kind, value_hash, confidence, provider,
35
+ start_offset, end_offset, state, state_changed_at)
36
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
37
+ for (const r of rows) {
38
+ stmt.run(r.sessionId, r.messageId, r.kind, r.valueHash, r.confidence, r.provider, r.startOffset, r.endOffset, r.state, r.state !== 'active' ? new Date().toISOString() : null);
39
+ }
40
+ }
41
+ /** Delete all non-purged rows for the providers being rescanned —
42
+ * i.e. wipe the producer's previous output so the upcoming
43
+ * `insertFindings` is the canonical truth.
44
+ *
45
+ * Includes `state='dismissed'` deliberately: those rows are derived
46
+ * state. The user's "ignore" decisions live in `allowlist_session`
47
+ * and `allowlist_global` (preserved here), and the scan path
48
+ * re-emits findings as `state='dismissed'` whenever it encounters a
49
+ * hit that matches an allowlist row. Re-inserting also keeps
50
+ * `findings.value_hash` aligned with the per-kind allowlist
51
+ * preference (security.json `kindAllowlist`) — without this delete,
52
+ * a mute → unmute cycle would accumulate phantom dismissed rows
53
+ * (one set per cycle), inflating audit counts and breaking
54
+ * `riskByCategory` totals over time.
55
+ *
56
+ * Purged rows are the only state that's NOT producer-derived —
57
+ * they correspond to destructive `messages.content_text` rewrites
58
+ * the user explicitly approved. Preserving them is the audit
59
+ * contract documented in the design doc. */
60
+ export function deleteRefreshableFindings(db, sessionId, providers) {
61
+ if (providers.length === 0)
62
+ return;
63
+ const placeholders = providers.map(() => '?').join(',');
64
+ db.prepare(`DELETE FROM findings
65
+ WHERE session_id = ?
66
+ AND state IN ('active','dismissed')
67
+ AND provider IN (${placeholders})`).run(sessionId, ...providers);
68
+ }
69
+ /** @deprecated Renamed to {@link deleteRefreshableFindings} after the
70
+ * active-only filter was found to leak phantom dismissed rows
71
+ * across mute→unmute cycles. Kept as a thin alias so callers
72
+ * outside the repo don't break mid-stack. */
73
+ export const deleteActiveFindings = deleteRefreshableFindings;
74
+ // ─── Counts (denormalised on sessions) ────────────────────────────
75
+ /** Recompute sessions.scan_finding_count + scan_high_count from the
76
+ * findings table.
77
+ *
78
+ * Counts EXCLUDE info-tier kinds (absolute-path, ip, internal-host).
79
+ * Those are stored — useful for an opt-in "Show informational
80
+ * signals" toggle — but they don't drive the Library row badge
81
+ * because their pattern-only signal has too high a false-positive
82
+ * rate to be meaningful at first glance. */
83
+ export function updateSessionCounts(db, sessionId) {
84
+ const active = db.prepare(`SELECT
85
+ SUM(CASE WHEN kind NOT IN (${infoKindsPlaceholders()}) THEN 1 ELSE 0 END) AS total,
86
+ SUM(CASE WHEN kind IN (${highKindsPlaceholders()}) THEN 1 ELSE 0 END) AS high
87
+ FROM findings
88
+ WHERE session_id = ? AND state = 'active'`).get(...INFO_SEVERITY_KINDS_ARRAY, ...HIGH_SEVERITY_KINDS_ARRAY, sessionId);
89
+ const purged = db.prepare(`SELECT COUNT(*) AS c
90
+ FROM findings
91
+ WHERE session_id = ? AND state = 'purged'`).get(sessionId);
92
+ db.prepare(`UPDATE sessions
93
+ SET scan_finding_count = ?,
94
+ scan_high_count = ?,
95
+ scan_purged_count = ?
96
+ WHERE id = ?`).run(active.total ?? 0, active.high ?? 0, purged.c, sessionId);
97
+ }
98
+ const HIGH_SEVERITY_KINDS_ARRAY = Array.from(HIGH_SEVERITY_KINDS);
99
+ const INFO_SEVERITY_KINDS_ARRAY = Array.from(INFO_SEVERITY_KINDS);
100
+ function highKindsPlaceholders() {
101
+ return HIGH_SEVERITY_KINDS_ARRAY.map(() => '?').join(',');
102
+ }
103
+ function infoKindsPlaceholders() {
104
+ return INFO_SEVERITY_KINDS_ARRAY.map(() => '?').join(',');
105
+ }
106
+ export function setSessionScanProfile(db, sessionId, profile, completedAt) {
107
+ db.prepare(`UPDATE sessions
108
+ SET scan_profile = ?, scan_completed_at = ?
109
+ WHERE id = ?`).run(profile, completedAt, sessionId);
110
+ }
111
+ /** Set every session's scan_profile to NULL — used by "Rescan all" so
112
+ * every session re-enqueues. */
113
+ export function invalidateAllScanProfiles(db) {
114
+ const r = db.prepare('UPDATE sessions SET scan_profile = NULL').run();
115
+ return r.changes;
116
+ }
117
+ /** Drop the scan profile for one session — used by the sync cascade
118
+ * when a session's messages change after first scan. */
119
+ export function invalidateSessionScanProfile(db, sessionId) {
120
+ db.prepare(`UPDATE sessions
121
+ SET scan_profile = NULL,
122
+ scan_completed_at = NULL
123
+ WHERE id = ?`).run(sessionId);
124
+ }
125
+ /** Sessions whose stored profile doesn't structurally match
126
+ * `currentProfile`. Used at boot for backfill enqueue. */
127
+ export function listSessionsNeedingScan(db, currentProfile) {
128
+ const rows = db.prepare(`SELECT id, scan_profile
129
+ FROM sessions
130
+ ORDER BY started_at DESC`).all();
131
+ const out = [];
132
+ for (const r of rows) {
133
+ if (r.scan_profile === null || r.scan_profile !== currentProfile) {
134
+ out.push(r.id);
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ // ─── Reads (UI path) ──────────────────────────────────────────────
140
+ export function listFindings(db, filter) {
141
+ const where = [];
142
+ const params = [];
143
+ if (filter.sessionId !== undefined) {
144
+ where.push('session_id = ?');
145
+ params.push(filter.sessionId);
146
+ }
147
+ if (filter.kinds && filter.kinds.length > 0) {
148
+ const placeholders = filter.kinds.map(() => '?').join(',');
149
+ where.push(`kind IN (${placeholders})`);
150
+ params.push(...filter.kinds);
151
+ }
152
+ else if (filter.kind !== undefined) {
153
+ where.push('kind = ?');
154
+ params.push(filter.kind);
155
+ }
156
+ if (filter.state && filter.state !== 'any') {
157
+ where.push('state = ?');
158
+ params.push(filter.state);
159
+ }
160
+ else if (!filter.state) {
161
+ // Default: active only.
162
+ where.push("state = 'active'");
163
+ }
164
+ if (filter.severity === 'high') {
165
+ where.push(`kind IN (${highKindsPlaceholders()})`);
166
+ params.push(...HIGH_SEVERITY_KINDS_ARRAY);
167
+ }
168
+ else if (filter.severity === 'low') {
169
+ where.push(`kind NOT IN (${highKindsPlaceholders()})`);
170
+ params.push(...HIGH_SEVERITY_KINDS_ARRAY);
171
+ }
172
+ // Skip info-tier kinds unless the caller is explicitly asking for
173
+ // one. Saves the renderer from paging through 800+ absolute-path
174
+ // findings before reaching the api-key that actually matters.
175
+ const explicitKindsInclude = (kinds, kind) => {
176
+ if (kinds && kinds.length > 0)
177
+ return kinds.some(k => INFO_SEVERITY_KINDS.has(k));
178
+ if (kind !== undefined)
179
+ return INFO_SEVERITY_KINDS.has(kind);
180
+ return false;
181
+ };
182
+ if (filter.excludeInfo && !explicitKindsInclude(filter.kinds, filter.kind)) {
183
+ where.push(`kind NOT IN (${infoKindsPlaceholders()})`);
184
+ params.push(...INFO_SEVERITY_KINDS_ARRAY);
185
+ }
186
+ let pagination = '';
187
+ if (filter.limit !== undefined) {
188
+ pagination = ' LIMIT ? OFFSET ?';
189
+ params.push(filter.limit, filter.offset ?? 0);
190
+ }
191
+ const sql = `SELECT * FROM findings
192
+ ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
193
+ ORDER BY detected_at DESC, id DESC${pagination}`;
194
+ const rows = db.prepare(sql).all(...params);
195
+ return rows.map(rowToFinding);
196
+ }
197
+ /** limit+1 peek to set `hasMore` without an extra COUNT round-trip. */
198
+ function paginate(filter, fetch) {
199
+ if (filter.limit === undefined)
200
+ return { rows: fetch(filter), hasMore: false };
201
+ const peeked = fetch({ ...filter, limit: filter.limit + 1 });
202
+ const hasMore = peeked.length > filter.limit;
203
+ return { rows: hasMore ? peeked.slice(0, filter.limit) : peeked, hasMore };
204
+ }
205
+ export function listFindingsPage(db, filter) {
206
+ return paginate(filter, f => listFindings(db, f));
207
+ }
208
+ /** Shared WHERE-clause builder for the session-findings join. Used by
209
+ * both `listSessionsWithFindings` (paginated rows) and
210
+ * `countSessionsWithFindings` (total) so the two stay consistent. */
211
+ function buildSessionFindingWhereSql(filter) {
212
+ const params = [];
213
+ const stateCondition = filter.state && filter.state !== 'any'
214
+ ? 'f.state = ?'
215
+ : "f.state = 'active'";
216
+ if (filter.state && filter.state !== 'any')
217
+ params.push(filter.state);
218
+ let kindCondition = '';
219
+ const explicitKinds = filter.kinds && filter.kinds.length > 0
220
+ ? filter.kinds
221
+ : filter.kind !== undefined ? [filter.kind] : undefined;
222
+ if (explicitKinds) {
223
+ const placeholders = explicitKinds.map(() => '?').join(',');
224
+ kindCondition = `AND f.kind IN (${placeholders})`;
225
+ params.push(...explicitKinds);
226
+ }
227
+ let severityCondition = '';
228
+ if (filter.severity === 'high') {
229
+ severityCondition = `AND f.kind IN (${highKindsPlaceholders()})`;
230
+ params.push(...HIGH_SEVERITY_KINDS_ARRAY);
231
+ }
232
+ else if (filter.severity === 'low') {
233
+ // Exclude both HIGH (not low) and INFO (noisy infra signals) so
234
+ // `severity:low` returns only the identity-tier kinds (email,
235
+ // phone, person-name, etc.).
236
+ severityCondition = `AND f.kind NOT IN (${highKindsPlaceholders()}) AND f.kind NOT IN (${infoKindsPlaceholders()})`;
237
+ params.push(...HIGH_SEVERITY_KINDS_ARRAY, ...INFO_SEVERITY_KINDS_ARRAY);
238
+ }
239
+ // Default exclusion: info-tier findings don't surface a session on
240
+ // their own — they're stored as an audit record (see Info drawer at
241
+ // the bottom of the page). Only when the user has explicitly pinned
242
+ // an info kind via filter.kind/filter.kinds do we let them through.
243
+ let infoExclusion = '';
244
+ if (!explicitKinds && filter.severity !== 'high' && filter.severity !== 'low') {
245
+ infoExclusion = `AND f.kind NOT IN (${infoKindsPlaceholders()})`;
246
+ params.push(...INFO_SEVERITY_KINDS_ARRAY);
247
+ }
248
+ let textCondition = '';
249
+ if (filter.text && filter.text.trim().length > 0) {
250
+ textCondition = `AND s.title LIKE ?`;
251
+ params.push(`%${filter.text.trim()}%`);
252
+ }
253
+ const whereClause = `${stateCondition} ${kindCondition} ${severityCondition} ${infoExclusion} ${textCondition}
254
+ AND COALESCE(s.message_count, 0) > 0`;
255
+ return { whereClause, params };
256
+ }
257
+ export function listSessionsWithFindings(db, filter) {
258
+ // For category/severity-aware filtering we need to compute counts
259
+ // from the findings table directly (denormalised counters can't
260
+ // express "only api-key findings"). We always recompute here.
261
+ const { whereClause, params } = buildSessionFindingWhereSql(filter);
262
+ const sql = `
263
+ SELECT
264
+ s.id AS id,
265
+ s.session_uuid AS session_uuid,
266
+ s.title AS title,
267
+ s.started_at AS started_at,
268
+ s.scan_completed_at AS scan_completed_at,
269
+ s.scan_purged_count AS purged_count,
270
+ s.message_count AS message_count,
271
+ s.model AS model,
272
+ s.cwd AS cwd,
273
+ src.name AS source,
274
+ p.display_name AS project_display_name,
275
+ SUM(1) AS finding_count,
276
+ SUM(CASE WHEN f.kind IN (${highKindsPlaceholders()}) THEN 1 ELSE 0 END) AS high_count
277
+ FROM sessions s
278
+ JOIN findings f ON f.session_id = s.id
279
+ JOIN sources src ON src.id = s.source_id
280
+ JOIN projects p ON p.id = s.project_id
281
+ WHERE ${whereClause}
282
+ GROUP BY s.id
283
+ ORDER BY high_count DESC, finding_count DESC, s.started_at DESC, s.id DESC
284
+ ${filter.limit !== undefined ? 'LIMIT ? OFFSET ?' : ''}
285
+ `;
286
+ // Placeholder order matches SQL: SELECT's CASE WHEN IN (?) binds first,
287
+ // then the WHERE clause's params.
288
+ const allParams = [...HIGH_SEVERITY_KINDS_ARRAY, ...params];
289
+ if (filter.limit !== undefined) {
290
+ allParams.push(filter.limit, filter.offset ?? 0);
291
+ }
292
+ const rows = db.prepare(sql).all(...allParams);
293
+ return rows.map(r => ({
294
+ id: r.id,
295
+ sessionUuid: r.session_uuid,
296
+ title: r.title,
297
+ startedAt: r.started_at,
298
+ scanCompletedAt: r.scan_completed_at,
299
+ findingCount: r.finding_count,
300
+ highCount: r.high_count ?? 0,
301
+ purgedCount: r.purged_count ?? 0,
302
+ source: r.source,
303
+ messageCount: r.message_count ?? 0,
304
+ model: r.model,
305
+ cwd: r.cwd,
306
+ projectDisplayName: r.project_display_name,
307
+ }));
308
+ }
309
+ export function listSessionsWithFindingsPage(db, filter) {
310
+ return paginate(filter, f => listSessionsWithFindings(db, f));
311
+ }
312
+ /** Total distinct sessions matching the same filter as
313
+ * `listSessionsWithFindings` — used by the renderer to show
314
+ * "涉及 N 个会话" without loading every page. Cheaper than the list
315
+ * query: no SELECT projection, no GROUP BY, no ORDER BY. */
316
+ export function countSessionsWithFindings(db, filter) {
317
+ const { whereClause, params } = buildSessionFindingWhereSql(filter);
318
+ const sql = `
319
+ SELECT COUNT(DISTINCT s.id) AS total
320
+ FROM sessions s
321
+ JOIN findings f ON f.session_id = s.id
322
+ JOIN sources src ON src.id = s.source_id
323
+ JOIN projects p ON p.id = s.project_id
324
+ WHERE ${whereClause}
325
+ `;
326
+ const row = db.prepare(sql).get(...params);
327
+ return row.total;
328
+ }
329
+ /** Risk by category — one row per kind that has ≥ 1 active finding.
330
+ * Drives the Watchtower-style panel on the Security page. */
331
+ export function riskByCategory(db) {
332
+ const rows = db.prepare(`SELECT kind,
333
+ COUNT(*) AS count,
334
+ COUNT(DISTINCT session_id) AS sessions
335
+ FROM findings
336
+ WHERE state = 'active'
337
+ GROUP BY kind
338
+ ORDER BY count DESC`).all();
339
+ return rows.map(r => ({
340
+ kind: r.kind,
341
+ severity: severityOf(r.kind),
342
+ count: r.count,
343
+ sessions: r.sessions,
344
+ }));
345
+ }
346
+ /** Cross-session blast radius for one leaked value. Returns every
347
+ * session that has ≥ 1 ACTIVE finding sharing the given
348
+ * `(kind, valueHash)`, with the per-session occurrence count, project,
349
+ * and most-recent detection time. Ordered most-recent-first.
350
+ *
351
+ * Spool already collapses repeated occurrences of a value WITHIN a
352
+ * session into one row (×N); this extends that view across the whole
353
+ * archive so the user can see "this same key also leaked in 4 other
354
+ * sessions". Dismissed/purged occurrences are excluded — they no
355
+ * longer expose the value through search / AI / browse. */
356
+ export function occurrencesByValueHash(db, kind, valueHash) {
357
+ const rows = db.prepare(`SELECT
358
+ s.id AS session_id,
359
+ s.session_uuid AS session_uuid,
360
+ s.title AS session_title,
361
+ src.name AS source,
362
+ p.display_name AS project,
363
+ COUNT(*) AS count,
364
+ MAX(f.detected_at) AS last_seen
365
+ FROM findings f
366
+ JOIN sessions s ON s.id = f.session_id
367
+ JOIN sources src ON src.id = s.source_id
368
+ JOIN projects p ON p.id = s.project_id
369
+ WHERE f.kind = ? AND f.value_hash = ? AND f.state = 'active'
370
+ GROUP BY s.id
371
+ ORDER BY last_seen DESC, s.id DESC`).all(kind, valueHash);
372
+ return rows.map(r => ({
373
+ sessionId: r.session_id,
374
+ sessionUuid: r.session_uuid,
375
+ sessionTitle: r.session_title,
376
+ source: r.source,
377
+ project: r.project,
378
+ count: r.count,
379
+ lastSeen: r.last_seen,
380
+ }));
381
+ }
382
+ /** The most recent `scan_completed_at` across ALL sessions, regardless
383
+ * of whether they currently have active findings. Drives the Security
384
+ * page's "scanned X ago" label. Deriving it from the filtered,
385
+ * paginated findings list undercounts: a session scanned + cleaned
386
+ * (no active findings) drops off that list and stops contributing, so
387
+ * the label could read older than the true last scan. Returns null
388
+ * when nothing has been scanned yet. */
389
+ export function lastScanCompletedAt(db) {
390
+ const row = db.prepare(`SELECT MAX(scan_completed_at) AS last
391
+ FROM sessions
392
+ WHERE scan_completed_at IS NOT NULL`).get();
393
+ return row.last;
394
+ }
395
+ /** Read the live raw value for one finding. Used by the UI's review
396
+ * panel and the Purge confirm dialog. Returns null when the finding
397
+ * no longer points at a valid message (race against session
398
+ * deletion) or has been purged (offsets now point at the mask). */
399
+ export function getFindingValue(db, findingId) {
400
+ const row = db.prepare(`SELECT f.start_offset, f.end_offset, f.state, m.content_text
401
+ FROM findings f
402
+ LEFT JOIN messages m ON m.id = f.message_id
403
+ WHERE f.id = ?`).get(findingId);
404
+ if (!row || row.content_text === null)
405
+ return null;
406
+ if (row.state === 'purged')
407
+ return null;
408
+ return row.content_text.slice(row.start_offset, row.end_offset);
409
+ }
410
+ /** Bulk version of `getFindingValue` — one SQL query for N finding
411
+ * ids instead of N round-trips. Caller fans the result map back out
412
+ * by id; missing keys are treated as `null` (purged / vanished). */
413
+ export function getFindingValues(db, findingIds) {
414
+ const out = {};
415
+ if (findingIds.length === 0)
416
+ return out;
417
+ const placeholders = findingIds.map(() => '?').join(',');
418
+ const rows = db.prepare(`SELECT f.id, f.start_offset, f.end_offset, f.state, m.content_text
419
+ FROM findings f
420
+ LEFT JOIN messages m ON m.id = f.message_id
421
+ WHERE f.id IN (${placeholders})`).all(...findingIds);
422
+ for (const r of rows) {
423
+ if (r.content_text === null || r.state === 'purged') {
424
+ out[r.id] = null;
425
+ }
426
+ else {
427
+ out[r.id] = r.content_text.slice(r.start_offset, r.end_offset);
428
+ }
429
+ }
430
+ // Fill in nulls for ids that didn't come back (deleted / cascade).
431
+ for (const id of findingIds) {
432
+ if (!(id in out))
433
+ out[id] = null;
434
+ }
435
+ return out;
436
+ }
437
+ const allowKey = (kind, hash) => `${kind}|${hash}`;
438
+ export function getAllowlists(db, sessionId) {
439
+ const sessionRows = db.prepare('SELECT kind, value_hash FROM allowlist_session WHERE session_id = ?').all(sessionId);
440
+ const globalRows = db.prepare('SELECT kind, value_hash FROM allowlist_global').all();
441
+ return {
442
+ session: new Set(sessionRows.map(r => allowKey(r.kind, r.value_hash))),
443
+ global: new Set(globalRows.map(r => allowKey(r.kind, r.value_hash))),
444
+ };
445
+ }
446
+ export function isAllowlisted(allow, kind, valueHash) {
447
+ const key = allowKey(kind, valueHash);
448
+ return allow.session.has(key) || allow.global.has(key);
449
+ }
450
+ export function addAllowlistSession(db, sessionId, kind, valueHash) {
451
+ db.prepare(`INSERT OR IGNORE INTO allowlist_session (session_id, kind, value_hash)
452
+ VALUES (?, ?, ?)`).run(sessionId, kind, valueHash);
453
+ }
454
+ export function addAllowlistGlobal(db, kind, valueHash) {
455
+ db.prepare(`INSERT OR IGNORE INTO allowlist_global (kind, value_hash)
456
+ VALUES (?, ?)`).run(kind, valueHash);
457
+ }
458
+ export function removeAllowlistSession(db, sessionId, kind, valueHash) {
459
+ db.prepare(`DELETE FROM allowlist_session
460
+ WHERE session_id = ? AND kind = ? AND value_hash = ?`).run(sessionId, kind, valueHash);
461
+ }
462
+ export function removeAllowlistGlobal(db, kind, valueHash) {
463
+ db.prepare(`DELETE FROM allowlist_global WHERE kind = ? AND value_hash = ?`).run(kind, valueHash);
464
+ }
465
+ /** All allowlist rows from both tables, joined with the originating
466
+ * session metadata. Drives the Settings → Security pane's "Manage…"
467
+ * modal so the user can review and revoke past decisions.
468
+ *
469
+ * Each entry's display value is reconstructed live from a matching
470
+ * finding's message text — the same plaintext the findings view
471
+ * shows (blurred). Nothing is persisted on the allowlist row. */
472
+ export function listAllowlistEntries(db) {
473
+ const sessionRows = db.prepare(`SELECT a.session_id AS session_id,
474
+ a.kind AS kind,
475
+ a.value_hash AS value_hash,
476
+ a.created_at AS created_at,
477
+ s.session_uuid AS session_uuid,
478
+ s.title AS session_title
479
+ FROM allowlist_session a
480
+ JOIN sessions s ON s.id = a.session_id
481
+ ORDER BY a.created_at DESC`).all();
482
+ const globalRows = db.prepare(`SELECT kind, value_hash, created_at
483
+ FROM allowlist_global
484
+ ORDER BY created_at DESC`).all();
485
+ // Reconstruct the live value from any surviving non-purged finding
486
+ // whose message text we can still read. N is small (the allowlist is
487
+ // a hand-curated list), so a prepared statement per row is fine.
488
+ const sessionValueStmt = db.prepare(`SELECT substr(m.content_text, f.start_offset + 1, f.end_offset - f.start_offset) AS value
489
+ FROM findings f
490
+ LEFT JOIN messages m ON m.id = f.message_id
491
+ WHERE f.session_id = ? AND f.kind = ? AND f.value_hash = ?
492
+ AND f.state != 'purged' AND m.content_text IS NOT NULL
493
+ LIMIT 1`);
494
+ const globalValueStmt = db.prepare(`SELECT substr(m.content_text, f.start_offset + 1, f.end_offset - f.start_offset) AS value
495
+ FROM findings f
496
+ LEFT JOIN messages m ON m.id = f.message_id
497
+ WHERE f.kind = ? AND f.value_hash = ?
498
+ AND f.state != 'purged' AND m.content_text IS NOT NULL
499
+ LIMIT 1`);
500
+ return [
501
+ ...globalRows.map((r) => ({
502
+ scope: 'global',
503
+ kind: r.kind,
504
+ valueHash: r.value_hash,
505
+ createdAt: r.created_at,
506
+ sessionUuid: null,
507
+ sessionTitle: null,
508
+ value: globalValueStmt.get(r.kind, r.value_hash)?.value ?? null,
509
+ })),
510
+ ...sessionRows.map((r) => ({
511
+ scope: 'session',
512
+ kind: r.kind,
513
+ valueHash: r.value_hash,
514
+ createdAt: r.created_at,
515
+ sessionUuid: r.session_uuid,
516
+ sessionTitle: r.session_title,
517
+ value: sessionValueStmt.get(r.session_id, r.kind, r.value_hash)?.value ?? null,
518
+ })),
519
+ ];
520
+ }
521
+ /** Total allowlist rows across both scopes. A cheap header badge —
522
+ * two `SELECT COUNT(*)` summed, no per-row value reconstruction.
523
+ * Use this instead of `listAllowlistEntries().length` for counts. */
524
+ export function countAllowlistEntries(db) {
525
+ const session = db.prepare('SELECT COUNT(*) AS c FROM allowlist_session').get();
526
+ const global = db.prepare('SELECT COUNT(*) AS c FROM allowlist_global').get();
527
+ return session.c + global.c;
528
+ }
529
+ // ─── Mutations called from IPC dismiss handlers ───────────────────
530
+ /** Flip a finding to 'dismissed' and, depending on scope, write the
531
+ * allowlist entry so future rescans honor the decision. Caller
532
+ * wraps in a transaction. */
533
+ export function dismissFinding(db, findingId, scope, recomputeCounts = true) {
534
+ const f = db.prepare(`SELECT session_id, kind, value_hash FROM findings WHERE id = ?`).get(findingId);
535
+ if (!f)
536
+ return null;
537
+ db.prepare(`UPDATE findings
538
+ SET state = 'dismissed',
539
+ state_changed_at = datetime('now')
540
+ WHERE id = ?`).run(findingId);
541
+ if (scope === 'session') {
542
+ addAllowlistSession(db, f.session_id, f.kind, f.value_hash);
543
+ }
544
+ else {
545
+ addAllowlistGlobal(db, f.kind, f.value_hash);
546
+ }
547
+ if (recomputeCounts)
548
+ updateSessionCounts(db, f.session_id);
549
+ return f.session_id;
550
+ }
551
+ /** Batch variant of {@link dismissFinding}. Dismisses many findings in a
552
+ * single transaction — one allowlist write + one `updateSessionCounts`
553
+ * per affected session instead of N parallel IPC round-trips, each of
554
+ * which recomputes counts. Mirrors the bulk-purge pattern
555
+ * (`purgeFindings`). Returns the distinct session ids touched, in
556
+ * first-seen order, so the IPC layer can emit one change event per
557
+ * session. Unknown ids are skipped. */
558
+ export function dismissFindings(db, findingIds, scope) {
559
+ if (findingIds.length === 0)
560
+ return [];
561
+ const touched = [];
562
+ const seen = new Set();
563
+ const txn = db.transaction((ids) => {
564
+ for (const id of ids) {
565
+ // Defer count recompute — many ids usually share one session, so
566
+ // recomputing per-id would redo the same aggregate N times.
567
+ const sessionId = dismissFinding(db, id, scope, false);
568
+ if (sessionId != null && !seen.has(sessionId)) {
569
+ seen.add(sessionId);
570
+ touched.push(sessionId);
571
+ }
572
+ }
573
+ for (const sessionId of touched)
574
+ updateSessionCounts(db, sessionId);
575
+ });
576
+ txn(findingIds);
577
+ return touched;
578
+ }
579
+ /** Re-activate a dismissed finding and remove the allowlist entry
580
+ * that pinned it (both scopes, since UI doesn't always know which). */
581
+ export function undismissFinding(db, findingId) {
582
+ const f = db.prepare('SELECT session_id, kind, value_hash FROM findings WHERE id = ?').get(findingId);
583
+ if (!f)
584
+ return null;
585
+ db.prepare(`UPDATE findings
586
+ SET state = 'active',
587
+ state_changed_at = datetime('now')
588
+ WHERE id = ?`).run(findingId);
589
+ removeAllowlistSession(db, f.session_id, f.kind, f.value_hash);
590
+ removeAllowlistGlobal(db, f.kind, f.value_hash);
591
+ updateSessionCounts(db, f.session_id);
592
+ return f.session_id;
593
+ }
594
+ //# sourceMappingURL=repo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo.js","sourceRoot":"","sources":["../../src/security/repo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { Effect } from 'effect';
2
+ import type Database from 'better-sqlite3';
3
+ import type { RedactProvider } from '@spool-lab/redact';
4
+ import type { FindingsChange } from './types.js';
5
+ declare const ScanError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
6
+ readonly _tag: "ScanError";
7
+ } & Readonly<A>;
8
+ export declare class ScanError extends ScanError_base<{
9
+ readonly sessionId: number;
10
+ readonly cause: unknown;
11
+ readonly reason: 'session-not-found' | 'provider-failed' | 'db-failed';
12
+ }> {
13
+ }
14
+ /** Result of one session scan — useful for tests and progress UIs. */
15
+ export interface ScanResult {
16
+ sessionId: number;
17
+ inserted: number;
18
+ /** Profile string written to sessions.scan_profile. */
19
+ profile: string;
20
+ }
21
+ export interface ScanSessionDeps {
22
+ /** SQLite database handle (same instance the Syncer uses). */
23
+ db: Database.Database;
24
+ /** Active providers, evaluated in priority order. First entry wins
25
+ * on overlap (regex before pf — pattern matches are authoritative
26
+ * for known credential prefixes). */
27
+ providers: readonly RedactProvider[];
28
+ /** Profile string for the active provider set; persisted on
29
+ * successful scan. */
30
+ currentProfile: string;
31
+ /** Names of providers whose prior `active` findings should be
32
+ * deleted before re-insert. Almost always equals the names in
33
+ * `providers`; passed explicitly so the worker can choose to
34
+ * preserve historical pf findings even when pf is disabled. */
35
+ providerNames: readonly string[];
36
+ /** Sink for change notifications. The worker layer wraps PubSub
37
+ * here; tests pass a simple collector. */
38
+ publish: (change: FindingsChange) => Effect.Effect<void>;
39
+ /** Kind-level allowlist. Findings whose kind is in this set get
40
+ * inserted with state='dismissed' (instead of 'active') without
41
+ * needing a per-value allowlist row. Driven by the Settings →
42
+ * Security pane's multi-select. */
43
+ kindAllowlist?: ReadonlySet<string>;
44
+ }
45
+ /** Scan one session end to end. Idempotent: running twice produces
46
+ * the same findings (same value_hash + same offsets → same rows
47
+ * after the delete-then-insert reset). */
48
+ export declare function scanSession(sessionId: number, deps: ScanSessionDeps): Effect.Effect<ScanResult, ScanError>;
49
+ export {};
50
+ //# sourceMappingURL=scan.d.ts.map