@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.
- package/dist/db/db.d.ts +10 -2
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +111 -3
- package/dist/db/db.js.map +1 -1
- package/dist/db/queries.d.ts +1 -2
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +23 -9
- package/dist/db/queries.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/projects/sessions.d.ts +29 -1
- package/dist/projects/sessions.d.ts.map +1 -1
- package/dist/projects/sessions.js +115 -11
- package/dist/projects/sessions.js.map +1 -1
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +7 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/maintenance.d.ts +15 -0
- package/dist/security/maintenance.d.ts.map +1 -0
- package/dist/security/maintenance.js +68 -0
- package/dist/security/maintenance.js.map +1 -0
- package/dist/security/profile.d.ts +47 -0
- package/dist/security/profile.d.ts.map +1 -0
- package/dist/security/profile.js +122 -0
- package/dist/security/profile.js.map +1 -0
- package/dist/security/purge.d.ts +49 -0
- package/dist/security/purge.d.ts.map +1 -0
- package/dist/security/purge.js +159 -0
- package/dist/security/purge.js.map +1 -0
- package/dist/security/repo.d.ts +141 -0
- package/dist/security/repo.d.ts.map +1 -0
- package/dist/security/repo.js +469 -0
- package/dist/security/repo.js.map +1 -0
- package/dist/security/scan.d.ts +50 -0
- package/dist/security/scan.d.ts.map +1 -0
- package/dist/security/scan.js +120 -0
- package/dist/security/scan.js.map +1 -0
- package/dist/security/types.d.ts +128 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security/types.js +10 -0
- package/dist/security/types.js.map +1 -0
- package/dist/security/worker.d.ts +53 -0
- package/dist/security/worker.d.ts.map +1 -0
- package/dist/security/worker.js +180 -0
- package/dist/security/worker.js.map +1 -0
- package/dist/sync/source-paths.d.ts +1 -0
- package/dist/sync/source-paths.d.ts.map +1 -1
- package/dist/sync/source-paths.js +13 -3
- package/dist/sync/source-paths.js.map +1 -1
- package/dist/sync/syncer.d.ts +8 -1
- package/dist/sync/syncer.d.ts.map +1 -1
- package/dist/sync/syncer.js +26 -14
- package/dist/sync/syncer.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/util/resolve-bin.d.ts +34 -0
- package/dist/util/resolve-bin.d.ts.map +1 -1
- package/dist/util/resolve-bin.js +175 -20
- package/dist/util/resolve-bin.js.map +1 -1
- 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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
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"}
|