@spool-lab/core 0.4.12 → 0.4.20

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 (63) hide show
  1. package/dist/db/db.d.ts +10 -2
  2. package/dist/db/db.d.ts.map +1 -1
  3. package/dist/db/db.js +111 -3
  4. package/dist/db/db.js.map +1 -1
  5. package/dist/db/queries.d.ts +1 -2
  6. package/dist/db/queries.d.ts.map +1 -1
  7. package/dist/db/queries.js +23 -9
  8. package/dist/db/queries.js.map +1 -1
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/projects/sessions.d.ts +29 -1
  14. package/dist/projects/sessions.d.ts.map +1 -1
  15. package/dist/projects/sessions.js +115 -11
  16. package/dist/projects/sessions.js.map +1 -1
  17. package/dist/security/index.d.ts +14 -0
  18. package/dist/security/index.d.ts.map +1 -0
  19. package/dist/security/index.js +7 -0
  20. package/dist/security/index.js.map +1 -0
  21. package/dist/security/maintenance.d.ts +15 -0
  22. package/dist/security/maintenance.d.ts.map +1 -0
  23. package/dist/security/maintenance.js +68 -0
  24. package/dist/security/maintenance.js.map +1 -0
  25. package/dist/security/profile.d.ts +47 -0
  26. package/dist/security/profile.d.ts.map +1 -0
  27. package/dist/security/profile.js +122 -0
  28. package/dist/security/profile.js.map +1 -0
  29. package/dist/security/purge.d.ts +49 -0
  30. package/dist/security/purge.d.ts.map +1 -0
  31. package/dist/security/purge.js +159 -0
  32. package/dist/security/purge.js.map +1 -0
  33. package/dist/security/repo.d.ts +141 -0
  34. package/dist/security/repo.d.ts.map +1 -0
  35. package/dist/security/repo.js +469 -0
  36. package/dist/security/repo.js.map +1 -0
  37. package/dist/security/scan.d.ts +50 -0
  38. package/dist/security/scan.d.ts.map +1 -0
  39. package/dist/security/scan.js +120 -0
  40. package/dist/security/scan.js.map +1 -0
  41. package/dist/security/types.d.ts +128 -0
  42. package/dist/security/types.d.ts.map +1 -0
  43. package/dist/security/types.js +10 -0
  44. package/dist/security/types.js.map +1 -0
  45. package/dist/security/worker.d.ts +53 -0
  46. package/dist/security/worker.d.ts.map +1 -0
  47. package/dist/security/worker.js +180 -0
  48. package/dist/security/worker.js.map +1 -0
  49. package/dist/sync/source-paths.d.ts +1 -0
  50. package/dist/sync/source-paths.d.ts.map +1 -1
  51. package/dist/sync/source-paths.js +13 -3
  52. package/dist/sync/source-paths.js.map +1 -1
  53. package/dist/sync/syncer.d.ts +8 -1
  54. package/dist/sync/syncer.d.ts.map +1 -1
  55. package/dist/sync/syncer.js +26 -14
  56. package/dist/sync/syncer.js.map +1 -1
  57. package/dist/types.d.ts +13 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/util/resolve-bin.d.ts +34 -0
  60. package/dist/util/resolve-bin.d.ts.map +1 -1
  61. package/dist/util/resolve-bin.js +175 -20
  62. package/dist/util/resolve-bin.js.map +1 -1
  63. package/package.json +5 -2
@@ -1,6 +1,7 @@
1
1
  import { SESSION_SELECT, rowToSession } from '../db/queries.js';
2
+ const DEFAULT_PAGE_SIZE = 50;
2
3
  export function listSessionsByIdentity(db, identityKey, options = {}) {
3
- const { sources, sortOrder = 'recent', limit = 500, excludePinned = false } = options;
4
+ const { sources, sortOrder = 'recent', limit = DEFAULT_PAGE_SIZE, excludePinned = false, cursor, } = options;
4
5
  const conditions = ['p.identity_key = ?', 's.message_count > 0'];
5
6
  const params = [identityKey];
6
7
  if (sources && sources.length > 0) {
@@ -11,16 +12,52 @@ export function listSessionsByIdentity(db, identityKey, options = {}) {
11
12
  if (excludePinned) {
12
13
  conditions.push('NOT EXISTS (SELECT 1 FROM pins WHERE pins.session_uuid = s.session_uuid)');
13
14
  }
14
- const orderBy = orderByClause(sortOrder);
15
+ if (cursor) {
16
+ const c = cursorWhere(sortOrder, cursor);
17
+ conditions.push(c.sql);
18
+ params.push(...c.params);
19
+ }
20
+ return executePage(db, conditions, params, sortOrder, limit);
21
+ }
22
+ export function listRecentSessionsPage(db, options = {}) {
23
+ const { limit = DEFAULT_PAGE_SIZE, cursor } = options;
24
+ const conditions = ['s.message_count > 0'];
25
+ const params = [];
26
+ if (cursor) {
27
+ const c = cursorWhere('recent', cursor);
28
+ conditions.push(c.sql);
29
+ params.push(...c.params);
30
+ }
31
+ return executePage(db, conditions, params, 'recent', limit);
32
+ }
33
+ /**
34
+ * Per-cwd counts across the full project. Drives the chip strip's badges
35
+ * so they show the true distribution rather than just the loaded page.
36
+ * Honors the same `sources` filter as `listSessionsByIdentity`.
37
+ */
38
+ export function listProjectDirectoryCounts(db, identityKey, options = {}) {
39
+ const { sources } = options;
40
+ const conditions = ['p.identity_key = ?', 's.message_count > 0'];
41
+ const params = [identityKey];
42
+ if (sources && sources.length > 0) {
43
+ const placeholders = sources.map(() => '?').join(',');
44
+ conditions.push(`src.name IN (${placeholders})`);
45
+ params.push(...sources);
46
+ }
15
47
  const sql = `
16
- ${SESSION_SELECT}
48
+ SELECT
49
+ COALESCE(NULLIF(s.cwd, ''), '(unknown)') AS cwd,
50
+ COUNT(*) AS cnt,
51
+ MAX(s.started_at) AS last_at
52
+ FROM sessions s
53
+ JOIN sources src ON src.id = s.source_id
54
+ JOIN projects p ON p.id = s.project_id
17
55
  WHERE ${conditions.join(' AND ')}
18
- ORDER BY ${orderBy}
19
- LIMIT ?
56
+ GROUP BY cwd
57
+ ORDER BY MAX(s.started_at) DESC
20
58
  `;
21
- params.push(limit);
22
59
  const rows = db.prepare(sql).all(...params);
23
- return rows.map(rowToSession);
60
+ return rows.map(r => ({ cwd: r.cwd, sessionCount: r.cnt, lastSessionAt: r.last_at }));
24
61
  }
25
62
  export function listPinnedSessionsByIdentity(db, identityKey) {
26
63
  const rows = db.prepare(`
@@ -31,17 +68,84 @@ export function listPinnedSessionsByIdentity(db, identityKey) {
31
68
  `).all(identityKey);
32
69
  return rows.map(rowToSession);
33
70
  }
71
+ function executePage(db, conditions, params, sortOrder, limit) {
72
+ const sql = `
73
+ ${SESSION_SELECT}
74
+ WHERE ${conditions.join(' AND ')}
75
+ ORDER BY ${orderByClause(sortOrder)}
76
+ LIMIT ?
77
+ `;
78
+ // Fetch limit+1 so we can detect "more rows exist" without a count query.
79
+ const rows = db.prepare(sql).all(...params, limit + 1);
80
+ const hasMore = rows.length > limit;
81
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
82
+ const sessions = pageRows.map(rowToSession);
83
+ const last = sessions.at(-1);
84
+ const nextCursor = hasMore && last
85
+ ? {
86
+ startedAt: last.startedAt,
87
+ sessionUuid: last.sessionUuid,
88
+ messageCount: last.messageCount,
89
+ title: last.title ?? '',
90
+ }
91
+ : null;
92
+ return { sessions, nextCursor };
93
+ }
34
94
  function orderByClause(sortOrder) {
95
+ // session_uuid is the unique tiebreaker so the page boundary is deterministic
96
+ // and keyset pagination skips exactly the rows already shown.
97
+ switch (sortOrder) {
98
+ case 'oldest':
99
+ return 's.started_at ASC, s.session_uuid ASC';
100
+ case 'most_messages':
101
+ return 's.message_count DESC, s.started_at DESC, s.session_uuid ASC';
102
+ case 'title':
103
+ return "COALESCE(NULLIF(s.title, ''), '') ASC, s.started_at DESC, s.session_uuid ASC";
104
+ case 'recent':
105
+ default:
106
+ return 's.started_at DESC, s.session_uuid ASC';
107
+ }
108
+ }
109
+ function cursorWhere(sortOrder, c) {
110
+ const titleExpr = "COALESCE(NULLIF(s.title, ''), '')";
35
111
  switch (sortOrder) {
36
112
  case 'oldest':
37
- return 's.started_at ASC';
113
+ return {
114
+ sql: '(s.started_at > ? OR (s.started_at = ? AND s.session_uuid > ?))',
115
+ params: [c.startedAt, c.startedAt, c.sessionUuid],
116
+ };
38
117
  case 'most_messages':
39
- return 's.message_count DESC, s.started_at DESC';
118
+ return {
119
+ sql: `(
120
+ s.message_count < ?
121
+ OR (s.message_count = ? AND s.started_at < ?)
122
+ OR (s.message_count = ? AND s.started_at = ? AND s.session_uuid > ?)
123
+ )`,
124
+ params: [
125
+ c.messageCount,
126
+ c.messageCount, c.startedAt,
127
+ c.messageCount, c.startedAt, c.sessionUuid,
128
+ ],
129
+ };
40
130
  case 'title':
41
- return 'COALESCE(NULLIF(s.title, \'\'), \'\') ASC, s.started_at DESC';
131
+ return {
132
+ sql: `(
133
+ ${titleExpr} > ?
134
+ OR (${titleExpr} = ? AND s.started_at < ?)
135
+ OR (${titleExpr} = ? AND s.started_at = ? AND s.session_uuid > ?)
136
+ )`,
137
+ params: [
138
+ c.title,
139
+ c.title, c.startedAt,
140
+ c.title, c.startedAt, c.sessionUuid,
141
+ ],
142
+ };
42
143
  case 'recent':
43
144
  default:
44
- return 's.started_at DESC';
145
+ return {
146
+ sql: '(s.started_at < ? OR (s.started_at = ? AND s.session_uuid > ?))',
147
+ params: [c.startedAt, c.startedAt, c.sessionUuid],
148
+ };
45
149
  }
46
150
  }
47
151
  //# sourceMappingURL=sessions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/projects/sessions.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAW/D,MAAM,UAAU,sBAAsB,CACpC,EAAqB,EACrB,WAAmB,EACnB,UAAyC,EAAE;IAE3C,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,QAAQ,EAAE,KAAK,GAAG,GAAG,EAAE,aAAa,GAAG,KAAK,EAAE,GAAG,OAAO,CAAA;IAErF,MAAM,UAAU,GAAa,CAAC,oBAAoB,EAAE,qBAAqB,CAAC,CAAA;IAC1E,MAAM,MAAM,GAAc,CAAC,WAAW,CAAC,CAAA;IAEvC,IAAI,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACrD,UAAU,CAAC,IAAI,CAAC,gBAAgB,YAAY,GAAG,CAAC,CAAA;QAChD,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAA;IACzB,CAAC;IAED,IAAI,aAAa,EAAE,CAAC;QAClB,UAAU,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAA;IAC7F,CAAC;IAED,MAAM,OAAO,GAAG,aAAa,CAAC,SAAS,CAAC,CAAA;IACxC,MAAM,GAAG,GAAG;MACR,cAAc;YACR,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC;eACrB,OAAO;;GAEnB,CAAA;IACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAElB,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAmC,CAAA;IAC7E,OAAO,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;AAC/B,CAAC;AAED,MAAM,UAAU,4BAA4B,CAC1C,EAAqB,EACrB,WAAmB;IAEnB,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC;MACpB,cAAc;;;;GAIjB,CAAC,CAAC,GAAG,CAAC,WAAW,CAAmC,CAAA;IACrD,OAAO,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;AAC/B,CAAC;AAED,SAAS,aAAa,CAAC,SAAkC;IACvD,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,QAAQ;YACX,OAAO,kBAAkB,CAAA;QAC3B,KAAK,eAAe;YAClB,OAAO,yCAAyC,CAAA;QAClD,KAAK,OAAO;YACV,OAAO,8DAA8D,CAAA;QACvE,KAAK,QAAQ,CAAC;QACd;YACE,OAAO,mBAAmB,CAAA;IAC9B,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/projects/sessions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export type { FindingState, FindingRow, SessionWithFindingCounts, RiskByCategoryRow, DismissScope, FindingsChange, ScanStatus, } from './types.js';
2
+ export { REDACT_DETECTOR_VERSION, currentProfileString, parseProfile, profilesMatch, providersInProfile, } from './profile.js';
3
+ export type { ProfileOpts, ParsedProfile } from './profile.js';
4
+ export { insertFindings, deleteActiveFindings, deleteRefreshableFindings, updateSessionCounts, setSessionScanProfile, invalidateAllScanProfiles, invalidateSessionScanProfile, listSessionsNeedingScan, listFindings, listFindingsPage, listSessionsWithFindings, listSessionsWithFindingsPage, countSessionsWithFindings, riskByCategory, getFindingValue, getFindingValues, getAllowlists, isAllowlisted, addAllowlistSession, addAllowlistGlobal, removeAllowlistSession, removeAllowlistGlobal, listAllowlistEntries, dismissFinding, undismissFinding, } from './repo.js';
5
+ export type { FindingFilter, SessionFindingFilter, FindingInput, AllowlistEntryRow, AllowlistSnapshot, Page, } from './repo.js';
6
+ export { scanSession, ScanError } from './scan.js';
7
+ export type { ScanResult, ScanSessionDeps } from './scan.js';
8
+ export { purgeFinding, purgeFindings, orderForBulkPurge, PurgeError } from './purge.js';
9
+ export type { PurgeResult, PurgeDeps } from './purge.js';
10
+ export { makeScanWorker, waitForIdle } from './worker.js';
11
+ export type { ScanWorker, WorkerConfig } from './worker.js';
12
+ export { listBackups, deleteBackups, backupDirFor } from './maintenance.js';
13
+ export type { BackupFileInfo, DeleteBackupsResult, } from './maintenance.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/security/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,UAAU,EACV,wBAAwB,EACxB,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,UAAU,GACX,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,YAAY,EACZ,aAAa,EACb,kBAAkB,GACnB,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE9D,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,YAAY,EACZ,gBAAgB,EAChB,wBAAwB,EACxB,4BAA4B,EAC5B,yBAAyB,EACzB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,GACjB,MAAM,WAAW,CAAA;AAClB,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,IAAI,GACL,MAAM,WAAW,CAAA;AAElB,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAClD,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAE5D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvF,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAExD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE3D,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC3E,YAAY,EACV,cAAc,EACd,mBAAmB,GACpB,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,7 @@
1
+ export { REDACT_DETECTOR_VERSION, currentProfileString, parseProfile, profilesMatch, providersInProfile, } from './profile.js';
2
+ export { insertFindings, deleteActiveFindings, deleteRefreshableFindings, updateSessionCounts, setSessionScanProfile, invalidateAllScanProfiles, invalidateSessionScanProfile, listSessionsNeedingScan, listFindings, listFindingsPage, listSessionsWithFindings, listSessionsWithFindingsPage, countSessionsWithFindings, riskByCategory, getFindingValue, getFindingValues, getAllowlists, isAllowlisted, addAllowlistSession, addAllowlistGlobal, removeAllowlistSession, removeAllowlistGlobal, listAllowlistEntries, dismissFinding, undismissFinding, } from './repo.js';
3
+ export { scanSession, ScanError } from './scan.js';
4
+ export { purgeFinding, purgeFindings, orderForBulkPurge, PurgeError } from './purge.js';
5
+ export { makeScanWorker, waitForIdle } from './worker.js';
6
+ export { listBackups, deleteBackups, backupDirFor } from './maintenance.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/security/index.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,YAAY,EACZ,aAAa,EACb,kBAAkB,GACnB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,YAAY,EACZ,gBAAgB,EAChB,wBAAwB,EACxB,4BAA4B,EAC5B,yBAAyB,EACzB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACd,gBAAgB,GACjB,MAAM,WAAW,CAAA;AAUlB,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAGlD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAGvF,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGzD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,15 @@
1
+ import type Database from 'better-sqlite3';
2
+ export interface BackupFileInfo {
3
+ name: string;
4
+ sizeBytes: number;
5
+ mtimeMs: number;
6
+ kind: 'auto' | 'manual';
7
+ }
8
+ export interface DeleteBackupsResult {
9
+ deleted: number;
10
+ bytesFreed: number;
11
+ }
12
+ export declare function backupDirFor(db: Database.Database): string | null;
13
+ export declare function listBackups(db: Database.Database): BackupFileInfo[];
14
+ export declare function deleteBackups(db: Database.Database, names: readonly string[]): DeleteBackupsResult;
15
+ //# sourceMappingURL=maintenance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/security/maintenance.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAS1C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAA;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,GAAG,IAAI,CAKjE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,cAAc,EAAE,CAsBnE;AAED,wBAAgB,aAAa,CAC3B,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,mBAAmB,CAwBrB"}
@@ -0,0 +1,68 @@
1
+ import { existsSync, readdirSync, rmSync, statSync, } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+ // Listed in the UI: any snapshot Spool itself produced under
4
+ // ~/.spool/backups/. The narrower regex below distinguishes automated
5
+ // schema-migration snapshots (kind: 'auto') from human-named rollback
6
+ // points (kind: 'manual') so the UI can surface that difference.
7
+ const SPOOL_BACKUP_RE = /^spool-pre-.+\.db$/;
8
+ const AUTO_BACKUP_RE = /^spool-pre-v\d+-/;
9
+ export function backupDirFor(db) {
10
+ if (db.memory)
11
+ return null;
12
+ const dbPath = db.name;
13
+ if (!dbPath)
14
+ return null;
15
+ return join(dirname(dbPath), 'backups');
16
+ }
17
+ export function listBackups(db) {
18
+ const backupDir = backupDirFor(db);
19
+ if (!backupDir || !existsSync(backupDir))
20
+ return [];
21
+ return readdirSync(backupDir)
22
+ .filter((name) => SPOOL_BACKUP_RE.test(name))
23
+ .map((name) => {
24
+ try {
25
+ const st = statSync(join(backupDir, name));
26
+ return {
27
+ name,
28
+ sizeBytes: st.size,
29
+ mtimeMs: st.mtimeMs,
30
+ kind: AUTO_BACKUP_RE.test(name) ? 'auto' : 'manual',
31
+ };
32
+ }
33
+ catch {
34
+ // stat may race with a concurrent delete; skip on error.
35
+ return null;
36
+ }
37
+ })
38
+ .filter((e) => e !== null)
39
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
40
+ }
41
+ export function deleteBackups(db, names) {
42
+ const backupDir = backupDirFor(db);
43
+ if (!backupDir || !existsSync(backupDir) || names.length === 0) {
44
+ return { deleted: 0, bytesFreed: 0 };
45
+ }
46
+ let deleted = 0;
47
+ let bytesFreed = 0;
48
+ for (const name of names) {
49
+ // Reject path traversal: a malicious or buggy caller must not be
50
+ // able to delete files outside the backups dir.
51
+ if (basename(name) !== name)
52
+ continue;
53
+ if (!SPOOL_BACKUP_RE.test(name))
54
+ continue;
55
+ const full = join(backupDir, name);
56
+ try {
57
+ const st = statSync(full);
58
+ rmSync(full, { force: true });
59
+ deleted += 1;
60
+ bytesFreed += st.size;
61
+ }
62
+ catch {
63
+ // best-effort; missing file = already gone, treat as no-op.
64
+ }
65
+ }
66
+ return { deleted, bytesFreed };
67
+ }
68
+ //# sourceMappingURL=maintenance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maintenance.js","sourceRoot":"","sources":["../../src/security/maintenance.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ export declare const REDACT_DETECTOR_VERSION = 4;
2
+ export interface ProfileOpts {
3
+ /** Regex detector revision. Bump in lockstep with rule changes. */
4
+ regexVersion?: number;
5
+ /** Whether the Privacy Filter ML provider is enabled. */
6
+ pfEnabled?: boolean;
7
+ /** Privacy Filter model + quant level, e.g. '1.5b-q4'. Required
8
+ * when `pfEnabled` is true. */
9
+ pfVersion?: string;
10
+ /** Per-kind allowlist (Settings → Security → "Don't report"). Hashed
11
+ * into the profile string so flipping a kind triggers a real
12
+ * re-scan via `listSessionsNeedingScan` — otherwise sessions
13
+ * scanned before the toggle would never re-evaluate their findings
14
+ * and stale `state='active'` rows would linger for the now-
15
+ * allowlisted kind.
16
+ *
17
+ * Order-insensitive: we sort + dedupe before hashing. Empty list
18
+ * collapses the segment so the profile stays `regex@4` (not
19
+ * `regex@4,allow@0`) — backward-compat with sessions stamped
20
+ * before the allowlist feature existed. */
21
+ kindAllowlist?: readonly string[];
22
+ }
23
+ export interface ParsedProfile {
24
+ regex: number;
25
+ pf?: string;
26
+ /** Short hex digest of the sorted kind allowlist; only present when
27
+ * the user has configured a non-empty list. */
28
+ allow?: string;
29
+ }
30
+ /** Build the canonical profile string for the current detector set.
31
+ * Order is fixed (regex → pf → allow) so equality is structural. */
32
+ export declare function currentProfileString(opts?: ProfileOpts): string;
33
+ /** Parse a profile string back into structured fields. Returns null
34
+ * for inputs that don't look like a profile (e.g. legacy values,
35
+ * manual edits). Strict — unknown tokens cause a null return so the
36
+ * worker treats them as "stale, rescan". */
37
+ export declare function parseProfile(s: string | null | undefined): ParsedProfile | null;
38
+ /** Structural equality on profile strings — order-insensitive but
39
+ * same-content. */
40
+ export declare function profilesMatch(a: string | null | undefined, b: string | null | undefined): boolean;
41
+ /** Names of the providers active in a profile. Used by
42
+ * `deleteActiveFindings(... providers)` so rescanning with a smaller
43
+ * provider set doesn't clobber prior findings the user may still
44
+ * want (e.g. disabling pf shouldn't delete pf's historical hits;
45
+ * they just stop being refreshed). */
46
+ export declare function providersInProfile(s: string): string[];
47
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/security/profile.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,yDAAyD;IACzD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;oCACgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;;;gDAU4C;IAC5C,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,EAAE,CAAC,EAAE,MAAM,CAAA;IACX;oDACgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;qEACqE;AACrE,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,WAAgB,GAAG,MAAM,CAYnE;AAqBD;;;6CAG6C;AAC7C,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,aAAa,GAAG,IAAI,CA6B/E;AAED;oBACoB;AACpB,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAKjG;AAED;;;;uCAIuC;AACvC,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAMtD"}
@@ -0,0 +1,122 @@
1
+ // Scan profile — a composite identifier for the set of providers
2
+ // that produced the findings in a session.
3
+ //
4
+ // Persisted on `sessions.scan_profile`. Compare to the worker's
5
+ // current profile string at startup to detect rescan candidates:
6
+ //
7
+ // stored = NULL → never scanned, enqueue
8
+ // stored = 'regex@3' → matches current 'regex@3', skip
9
+ // stored = 'regex@3,pf@1.5b-q4' → user disabled pf, enqueue rescan
10
+ //
11
+ // Provider versions are bumped manually when detection logic changes.
12
+ // REDACT_DETECTOR_VERSION lives next to the regex detector in
13
+ // `@spool-lab/redact` (added in PR 1a); we re-export it here for
14
+ // callers that only depend on @spool-lab/core.
15
+ // v4: tightened IPv6 (rejects HH:MM:SS look-alikes) + dropped
16
+ // `.local` from internal-host TLDs (collides with `.env.local`).
17
+ // Bumping the version forces a rescan on every session, so old
18
+ // findings under the v3 rules get replaced with the cleaner v4 set.
19
+ export const REDACT_DETECTOR_VERSION = 4;
20
+ /** Build the canonical profile string for the current detector set.
21
+ * Order is fixed (regex → pf → allow) so equality is structural. */
22
+ export function currentProfileString(opts = {}) {
23
+ const regexVersion = opts.regexVersion ?? REDACT_DETECTOR_VERSION;
24
+ const parts = [`regex@${regexVersion}`];
25
+ if (opts.pfEnabled) {
26
+ if (!opts.pfVersion) {
27
+ throw new Error('currentProfileString: pfVersion is required when pfEnabled is true');
28
+ }
29
+ parts.push(`pf@${opts.pfVersion}`);
30
+ }
31
+ const allowHash = hashKindAllowlist(opts.kindAllowlist);
32
+ if (allowHash)
33
+ parts.push(`allow@${allowHash}`);
34
+ return parts.join(',');
35
+ }
36
+ /** FNV-1a 32-bit over the sorted+deduped kind list. Returns null for
37
+ * empty / undefined so the profile string stays minimal when no kinds
38
+ * are allowlisted (the common case). 8-char hex; collision risk is
39
+ * irrelevant — drift, not authenticity, is what we're detecting. */
40
+ function hashKindAllowlist(kinds) {
41
+ if (!kinds || kinds.length === 0)
42
+ return null;
43
+ const sorted = [...new Set(kinds)].sort();
44
+ let h = 0x811c9dc5;
45
+ for (const k of sorted) {
46
+ for (let i = 0; i < k.length; i++) {
47
+ h ^= k.charCodeAt(i);
48
+ h = Math.imul(h, 0x01000193);
49
+ }
50
+ h ^= 0x2c; // ',' delimiter so ['a','bc'] !== ['ab','c']
51
+ h = Math.imul(h, 0x01000193);
52
+ }
53
+ return (h >>> 0).toString(16).padStart(8, '0');
54
+ }
55
+ /** Parse a profile string back into structured fields. Returns null
56
+ * for inputs that don't look like a profile (e.g. legacy values,
57
+ * manual edits). Strict — unknown tokens cause a null return so the
58
+ * worker treats them as "stale, rescan". */
59
+ export function parseProfile(s) {
60
+ if (!s)
61
+ return null;
62
+ const parts = s.split(',').map(p => p.trim()).filter(Boolean);
63
+ let regex;
64
+ let pf;
65
+ let allow;
66
+ for (const part of parts) {
67
+ const at = part.indexOf('@');
68
+ if (at <= 0)
69
+ return null;
70
+ const name = part.slice(0, at);
71
+ const ver = part.slice(at + 1);
72
+ if (!ver)
73
+ return null;
74
+ if (name === 'regex') {
75
+ const n = Number(ver);
76
+ if (!Number.isInteger(n) || n < 1)
77
+ return null;
78
+ regex = n;
79
+ }
80
+ else if (name === 'pf') {
81
+ pf = ver;
82
+ }
83
+ else if (name === 'allow') {
84
+ allow = ver;
85
+ }
86
+ else {
87
+ return null;
88
+ }
89
+ }
90
+ if (regex === undefined)
91
+ return null;
92
+ const result = { regex };
93
+ if (pf !== undefined)
94
+ result.pf = pf;
95
+ if (allow !== undefined)
96
+ result.allow = allow;
97
+ return result;
98
+ }
99
+ /** Structural equality on profile strings — order-insensitive but
100
+ * same-content. */
101
+ export function profilesMatch(a, b) {
102
+ const pa = parseProfile(a);
103
+ const pb = parseProfile(b);
104
+ if (!pa || !pb)
105
+ return false;
106
+ return pa.regex === pb.regex && pa.pf === pb.pf && pa.allow === pb.allow;
107
+ }
108
+ /** Names of the providers active in a profile. Used by
109
+ * `deleteActiveFindings(... providers)` so rescanning with a smaller
110
+ * provider set doesn't clobber prior findings the user may still
111
+ * want (e.g. disabling pf shouldn't delete pf's historical hits;
112
+ * they just stop being refreshed). */
113
+ export function providersInProfile(s) {
114
+ const parsed = parseProfile(s);
115
+ if (!parsed)
116
+ return [];
117
+ const out = ['regex'];
118
+ if (parsed.pf)
119
+ out.push('pf');
120
+ return out;
121
+ }
122
+ //# sourceMappingURL=profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/security/profile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import { Effect } from 'effect';
2
+ import type Database from 'better-sqlite3';
3
+ import type { FindingsChange } from './types.js';
4
+ declare const PurgeError_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 & {
5
+ readonly _tag: "PurgeError";
6
+ } & Readonly<A>;
7
+ export declare class PurgeError extends PurgeError_base<{
8
+ readonly findingId: number;
9
+ readonly reason: 'not-found' | 'already-purged' | 'message-missing' | 'db-failed';
10
+ readonly cause?: unknown;
11
+ }> {
12
+ }
13
+ export interface PurgeResult {
14
+ findingId: number;
15
+ sessionId: number;
16
+ /** The mask string that replaced the raw value. */
17
+ maskUsed: string;
18
+ purgedAt: string;
19
+ }
20
+ export interface PurgeDeps {
21
+ db: Database.Database;
22
+ publish: (change: FindingsChange) => Effect.Effect<void>;
23
+ }
24
+ /** Purge one finding. Idempotent on "already purged" → returns a
25
+ * PurgeError so the UI can surface a no-op. */
26
+ export declare function purgeFinding(findingId: number, deps: PurgeDeps): Effect.Effect<PurgeResult, PurgeError>;
27
+ /** Bulk purge across many findings. Caller passes an arbitrary
28
+ * ordering; we re-sort to the only correct order:
29
+ *
30
+ * 1. Group by `message_id`
31
+ * 2. Within each group, apply in **descending** `start_offset` so
32
+ * earlier offsets stay valid as the string shifts.
33
+ *
34
+ * Without this re-sort, two findings inside the same message at
35
+ * offsets [10..20] and [40..60] would corrupt: purging [10..20]
36
+ * first shifts everything after offset 20 by `mask.length - 10`,
37
+ * and the second slice [40..60] now points at the wrong bytes
38
+ * (often leaking part of the second secret into the mask, or
39
+ * losing it entirely). */
40
+ export declare function purgeFindings(findingIds: readonly number[], deps: PurgeDeps): Effect.Effect<PurgeResult[], PurgeError>;
41
+ /** Produce a purge order that's safe against offset drift. Findings
42
+ * with `message_id IS NULL` (orphan rows that the schema permits but
43
+ * should never happen in practice) fall to the end so they don't
44
+ * interfere with message-grouped batches.
45
+ *
46
+ * Exported only for tests. */
47
+ export declare function orderForBulkPurge(db: Database.Database, findingIds: readonly number[]): number[];
48
+ export {};
49
+ //# sourceMappingURL=purge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"purge.d.ts","sourceRoot":"","sources":["../../src/security/purge.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAQ,MAAM,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAI1C,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,YAAY,CAAA;;;;AAE9D,qBAAa,UAAW,SAAQ,gBAA+B;IAC7D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,gBAAgB,GAAG,iBAAiB,GAAG,WAAW,CAAA;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CACzB,CAAC;CAAG;AAEL,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAYD,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAA;IACrB,OAAO,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CACzD;AAED;gDACgD;AAChD,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,SAAS,GACd,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,CAoCxC;AAED;;;;;;;;;;;;2BAY2B;AAC3B,wBAAgB,aAAa,CAC3B,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,IAAI,EAAE,SAAS,GACd,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,UAAU,CAAC,CAsB1C;AAED;;;;;+BAK+B;AAC/B,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,MAAM,EAAE,CAkCV"}