@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,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 = 2;
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":"AA4BA,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,131 @@
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@1' → matches current 'regex@1', skip
9
+ // stored = 'regex@1,pf@1.5b-q4.r2' → 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
+ // Cache-nonce for the regex detector rule set. Bumping it makes
16
+ // `currentProfileString()` differ from what's stored on
17
+ // `sessions.scan_profile`, which the backfill loop reads as "this
18
+ // session needs a rescan". No semantic meaning outside that diff —
19
+ // it's NOT a user-visible "version of the security feature".
20
+ //
21
+ // Scope: consumed only by code gated behind VITE_FEATURE_SECURITY.
22
+ // Bump on every rule change once the feature ships so users with old
23
+ // stamps automatically pick up the new rules.
24
+ // 1 — initial release
25
+ // 2 — 2026-05-28: env-var detector now rejects values containing
26
+ // non-ASCII letters (CJK / Cyrillic / Hangul / Arabic / emoji
27
+ // are placeholder description text, never real env-var secrets).
28
+ export const REDACT_DETECTOR_VERSION = 2;
29
+ /** Build the canonical profile string for the current detector set.
30
+ * Order is fixed (regex → pf → allow) so equality is structural. */
31
+ export function currentProfileString(opts = {}) {
32
+ const regexVersion = opts.regexVersion ?? REDACT_DETECTOR_VERSION;
33
+ const parts = [`regex@${regexVersion}`];
34
+ if (opts.pfEnabled) {
35
+ if (!opts.pfVersion) {
36
+ throw new Error('currentProfileString: pfVersion is required when pfEnabled is true');
37
+ }
38
+ parts.push(`pf@${opts.pfVersion}`);
39
+ }
40
+ const allowHash = hashKindAllowlist(opts.kindAllowlist);
41
+ if (allowHash)
42
+ parts.push(`allow@${allowHash}`);
43
+ return parts.join(',');
44
+ }
45
+ /** FNV-1a 32-bit over the sorted+deduped kind list. Returns null for
46
+ * empty / undefined so the profile string stays minimal when no kinds
47
+ * are allowlisted (the common case). 8-char hex; collision risk is
48
+ * irrelevant — drift, not authenticity, is what we're detecting. */
49
+ function hashKindAllowlist(kinds) {
50
+ if (!kinds || kinds.length === 0)
51
+ return null;
52
+ const sorted = [...new Set(kinds)].sort();
53
+ let h = 0x811c9dc5;
54
+ for (const k of sorted) {
55
+ for (let i = 0; i < k.length; i++) {
56
+ h ^= k.charCodeAt(i);
57
+ h = Math.imul(h, 0x01000193);
58
+ }
59
+ h ^= 0x2c; // ',' delimiter so ['a','bc'] !== ['ab','c']
60
+ h = Math.imul(h, 0x01000193);
61
+ }
62
+ return (h >>> 0).toString(16).padStart(8, '0');
63
+ }
64
+ /** Parse a profile string back into structured fields. Returns null
65
+ * for inputs that don't look like a profile (e.g. legacy values,
66
+ * manual edits). Strict — unknown tokens cause a null return so the
67
+ * worker treats them as "stale, rescan". */
68
+ export function parseProfile(s) {
69
+ if (!s)
70
+ return null;
71
+ const parts = s.split(',').map(p => p.trim()).filter(Boolean);
72
+ let regex;
73
+ let pf;
74
+ let allow;
75
+ for (const part of parts) {
76
+ const at = part.indexOf('@');
77
+ if (at <= 0)
78
+ return null;
79
+ const name = part.slice(0, at);
80
+ const ver = part.slice(at + 1);
81
+ if (!ver)
82
+ return null;
83
+ if (name === 'regex') {
84
+ const n = Number(ver);
85
+ if (!Number.isInteger(n) || n < 1)
86
+ return null;
87
+ regex = n;
88
+ }
89
+ else if (name === 'pf') {
90
+ pf = ver;
91
+ }
92
+ else if (name === 'allow') {
93
+ allow = ver;
94
+ }
95
+ else {
96
+ return null;
97
+ }
98
+ }
99
+ if (regex === undefined)
100
+ return null;
101
+ const result = { regex };
102
+ if (pf !== undefined)
103
+ result.pf = pf;
104
+ if (allow !== undefined)
105
+ result.allow = allow;
106
+ return result;
107
+ }
108
+ /** Structural equality on profile strings — order-insensitive but
109
+ * same-content. */
110
+ export function profilesMatch(a, b) {
111
+ const pa = parseProfile(a);
112
+ const pb = parseProfile(b);
113
+ if (!pa || !pb)
114
+ return false;
115
+ return pa.regex === pb.regex && pa.pf === pb.pf && pa.allow === pb.allow;
116
+ }
117
+ /** Names of the providers active in a profile. Used by
118
+ * `deleteActiveFindings(... providers)` so rescanning with a smaller
119
+ * provider set doesn't clobber prior findings the user may still
120
+ * want (e.g. disabling pf shouldn't delete pf's historical hits;
121
+ * they just stop being refreshed). */
122
+ export function providersInProfile(s) {
123
+ const parsed = parseProfile(s);
124
+ if (!parsed)
125
+ return [];
126
+ const out = ['regex'];
127
+ if (parsed.pf)
128
+ out.push('pf');
129
+ return out;
130
+ }
131
+ //# 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,68 @@
1
+ import { Effect } from 'effect';
2
+ import type Database from 'better-sqlite3';
3
+ import type { SensitiveKind } from '@spool-lab/redact';
4
+ import type { FindingsChange } from './types.js';
5
+ 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 & {
6
+ readonly _tag: "PurgeError";
7
+ } & Readonly<A>;
8
+ export declare class PurgeError extends PurgeError_base<{
9
+ readonly findingId: number;
10
+ readonly reason: 'not-found' | 'already-purged' | 'message-missing' | 'db-failed';
11
+ readonly cause?: unknown;
12
+ }> {
13
+ }
14
+ export interface PurgeResult {
15
+ findingId: number;
16
+ sessionId: number;
17
+ /** The mask string that replaced the raw value. */
18
+ maskUsed: string;
19
+ purgedAt: string;
20
+ }
21
+ export interface PurgeDeps {
22
+ db: Database.Database;
23
+ publish: (change: FindingsChange) => Effect.Effect<void>;
24
+ }
25
+ /** Purge one finding. Idempotent on "already purged" → returns a
26
+ * PurgeError so the UI can surface a no-op. */
27
+ export declare function purgeFinding(findingId: number, deps: PurgeDeps): Effect.Effect<PurgeResult, PurgeError>;
28
+ /** Bulk purge across many findings. Caller passes an arbitrary
29
+ * ordering; we re-sort to the only correct order:
30
+ *
31
+ * 1. Group by `message_id`
32
+ * 2. Within each group, apply in **descending** `start_offset` so
33
+ * earlier offsets stay valid as the string shifts.
34
+ *
35
+ * Without this re-sort, two findings inside the same message at
36
+ * offsets [10..20] and [40..60] would corrupt: purging [10..20]
37
+ * first shifts everything after offset 20 by `mask.length - 10`,
38
+ * and the second slice [40..60] now points at the wrong bytes
39
+ * (often leaking part of the second secret into the mask, or
40
+ * losing it entirely). */
41
+ export declare function purgeFindings(findingIds: readonly number[], deps: PurgeDeps): Effect.Effect<PurgeResult[], PurgeError>;
42
+ /** Purge EVERY active occurrence of one leaked value — the
43
+ * `(kind, value_hash)` pair — across all sessions in the archive, in a
44
+ * single logical operation. The natural follow-through to "I've rotated
45
+ * this key at the source → now scrub every copy from Spool's surfaces".
46
+ *
47
+ * Delegates to {@link purgeFindings}, which re-orders the collected ids
48
+ * via {@link orderForBulkPurge} so per-message offsets stay valid. The
49
+ * original `~/.claude` session files are NOT touched — this masks only
50
+ * Spool's stored copies (DB + FTS), closing the search / AI / browse
51
+ * exposure.
52
+ *
53
+ * Returns the purge results plus the distinct session ids touched, in
54
+ * first-seen order, so the IPC layer can emit one change event per
55
+ * session. */
56
+ export declare function purgeEverywhere(kind: SensitiveKind, valueHash: string, deps: PurgeDeps): Effect.Effect<{
57
+ results: PurgeResult[];
58
+ sessionIds: number[];
59
+ }, PurgeError>;
60
+ /** Produce a purge order that's safe against offset drift. Findings
61
+ * with `message_id IS NULL` (orphan rows that the schema permits but
62
+ * should never happen in practice) fall to the end so they don't
63
+ * interfere with message-grouped batches.
64
+ *
65
+ * Exported only for tests. */
66
+ export declare function orderForBulkPurge(db: Database.Database, findingIds: readonly number[]): number[];
67
+ export {};
68
+ //# 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;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAGtD,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,CAsCxC;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,CAyB1C;AAED;;;;;;;;;;;;;eAae;AACf,wBAAgB,eAAe,CAC7B,IAAI,EAAE,aAAa,EACnB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,SAAS,GACd,MAAM,CAAC,MAAM,CAAC;IAAE,OAAO,EAAE,WAAW,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,EAAE,UAAU,CAAC,CAoB7E;AAED;;;;;+BAK+B;AAC/B,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,MAAM,EAAE,CAkCV"}
@@ -0,0 +1,193 @@
1
+ // Purge mechanism — the only destructive action in the Security
2
+ // Scan feature.
3
+ //
4
+ // What it does: rewrites the raw secret in `messages.content_text`
5
+ // with the per-kind mask from @spool-lab/redact (e.g. `[redacted:
6
+ // AWS key]`), flips the finding to `state='purged'`, recomputes the
7
+ // session's denormalised counts, and lets the existing FTS triggers
8
+ // sync messages_fts automatically.
9
+ //
10
+ // What it does NOT do: touch the source transcript file in
11
+ // `~/.claude/sessions/`, the `.spool` exports the user may already
12
+ // have published, or OS backups of `spool.db`. Those are Phase 2/3
13
+ // vault concerns documented in the spec.
14
+ //
15
+ // Bulk-by-message ordering: when several findings sit in the same
16
+ // message, apply them in DESCENDING start_offset order so earlier
17
+ // offsets stay valid as the string shifts.
18
+ import { Data, Effect } from 'effect';
19
+ import { maskValueByKind } from '@spool-lab/redact';
20
+ import { updateSessionCounts } from './repo.js';
21
+ export class PurgeError extends Data.TaggedError('PurgeError') {
22
+ }
23
+ /** Purge one finding. Idempotent on "already purged" → returns a
24
+ * PurgeError so the UI can surface a no-op. */
25
+ export function purgeFinding(findingId, deps) {
26
+ return Effect.gen(function* () {
27
+ const row = yield* Effect.try({
28
+ try: () => deps.db.prepare(`SELECT id, session_id, message_id, kind, start_offset, end_offset, state
29
+ FROM findings WHERE id = ?`).get(findingId),
30
+ catch: (cause) => new PurgeError({ findingId, reason: 'db-failed', cause }),
31
+ });
32
+ if (!row) {
33
+ return yield* Effect.fail(new PurgeError({ findingId, reason: 'not-found' }));
34
+ }
35
+ if (row.state === 'purged') {
36
+ return yield* Effect.fail(new PurgeError({ findingId, reason: 'already-purged' }));
37
+ }
38
+ if (row.message_id === null) {
39
+ return yield* Effect.fail(new PurgeError({ findingId, reason: 'message-missing' }));
40
+ }
41
+ const messageId = row.message_id;
42
+ const kind = row.kind;
43
+ const result = yield* Effect.try({
44
+ try: () => applyPurgeTxn(deps.db, row, messageId, kind),
45
+ catch: (cause) => new PurgeError({ findingId, reason: 'db-failed', cause }),
46
+ });
47
+ yield* deps.publish({
48
+ type: 'state-changed',
49
+ sessionId: row.session_id,
50
+ findingId: row.id,
51
+ state: 'purged',
52
+ });
53
+ return result;
54
+ }).pipe(Effect.withSpan('security.purge.single', { attributes: { findingId } }));
55
+ }
56
+ /** Bulk purge across many findings. Caller passes an arbitrary
57
+ * ordering; we re-sort to the only correct order:
58
+ *
59
+ * 1. Group by `message_id`
60
+ * 2. Within each group, apply in **descending** `start_offset` so
61
+ * earlier offsets stay valid as the string shifts.
62
+ *
63
+ * Without this re-sort, two findings inside the same message at
64
+ * offsets [10..20] and [40..60] would corrupt: purging [10..20]
65
+ * first shifts everything after offset 20 by `mask.length - 10`,
66
+ * and the second slice [40..60] now points at the wrong bytes
67
+ * (often leaking part of the second secret into the mask, or
68
+ * losing it entirely). */
69
+ export function purgeFindings(findingIds, deps) {
70
+ return Effect.gen(function* () {
71
+ // Resolve offsets/message ids up front so we can deterministically
72
+ // order. The Effect.try preserves db-failure → PurgeError mapping.
73
+ const ordered = yield* Effect.try({
74
+ try: () => orderForBulkPurge(deps.db, findingIds),
75
+ catch: (cause) => new PurgeError({ findingId: -1, reason: 'db-failed', cause }),
76
+ });
77
+ const out = [];
78
+ for (const id of ordered) {
79
+ const result = yield* purgeFinding(id, deps).pipe(Effect.catchTag('PurgeError', (err) => err.reason === 'already-purged'
80
+ ? Effect.succeed(null)
81
+ : Effect.fail(err)));
82
+ if (result)
83
+ out.push(result);
84
+ }
85
+ yield* Effect.annotateCurrentSpan('purged', out.length);
86
+ return out;
87
+ }).pipe(Effect.withSpan('security.purge.bulk', { attributes: { requested: findingIds.length } }));
88
+ }
89
+ /** Purge EVERY active occurrence of one leaked value — the
90
+ * `(kind, value_hash)` pair — across all sessions in the archive, in a
91
+ * single logical operation. The natural follow-through to "I've rotated
92
+ * this key at the source → now scrub every copy from Spool's surfaces".
93
+ *
94
+ * Delegates to {@link purgeFindings}, which re-orders the collected ids
95
+ * via {@link orderForBulkPurge} so per-message offsets stay valid. The
96
+ * original `~/.claude` session files are NOT touched — this masks only
97
+ * Spool's stored copies (DB + FTS), closing the search / AI / browse
98
+ * exposure.
99
+ *
100
+ * Returns the purge results plus the distinct session ids touched, in
101
+ * first-seen order, so the IPC layer can emit one change event per
102
+ * session. */
103
+ export function purgeEverywhere(kind, valueHash, deps) {
104
+ return Effect.gen(function* () {
105
+ const ids = yield* Effect.try({
106
+ try: () => deps.db.prepare(`SELECT id FROM findings
107
+ WHERE kind = ? AND value_hash = ? AND state = 'active'`).all(kind, valueHash).map(r => r.id),
108
+ catch: (cause) => new PurgeError({ findingId: -1, reason: 'db-failed', cause }),
109
+ });
110
+ const results = yield* purgeFindings(ids, deps);
111
+ const seen = new Set();
112
+ const sessionIds = [];
113
+ for (const r of results) {
114
+ if (!seen.has(r.sessionId)) {
115
+ seen.add(r.sessionId);
116
+ sessionIds.push(r.sessionId);
117
+ }
118
+ }
119
+ return { results, sessionIds };
120
+ }).pipe(Effect.withSpan('security.purge.everywhere', { attributes: { kind } }));
121
+ }
122
+ /** Produce a purge order that's safe against offset drift. Findings
123
+ * with `message_id IS NULL` (orphan rows that the schema permits but
124
+ * should never happen in practice) fall to the end so they don't
125
+ * interfere with message-grouped batches.
126
+ *
127
+ * Exported only for tests. */
128
+ export function orderForBulkPurge(db, findingIds) {
129
+ if (findingIds.length === 0)
130
+ return [];
131
+ const placeholders = findingIds.map(() => '?').join(',');
132
+ const rows = db.prepare(`SELECT id, message_id, start_offset
133
+ FROM findings
134
+ WHERE id IN (${placeholders})`).all(...findingIds);
135
+ const byMessage = new Map();
136
+ for (const r of rows) {
137
+ const key = r.message_id;
138
+ let bucket = byMessage.get(key);
139
+ if (!bucket) {
140
+ bucket = [];
141
+ byMessage.set(key, bucket);
142
+ }
143
+ bucket.push({ id: r.id, start_offset: r.start_offset });
144
+ }
145
+ for (const bucket of byMessage.values()) {
146
+ bucket.sort((a, b) => b.start_offset - a.start_offset);
147
+ }
148
+ // Stable iteration order over the messages doesn't matter for
149
+ // correctness — purges in different messages can't shift each
150
+ // other's offsets. We sort by message_id (nulls last) for
151
+ // determinism in tests.
152
+ const messageKeys = [...byMessage.keys()].sort((a, b) => {
153
+ if (a === null)
154
+ return 1;
155
+ if (b === null)
156
+ return -1;
157
+ return a - b;
158
+ });
159
+ const out = [];
160
+ for (const key of messageKeys) {
161
+ for (const r of byMessage.get(key))
162
+ out.push(r.id);
163
+ }
164
+ return out;
165
+ }
166
+ function applyPurgeTxn(db, finding, messageId, kind) {
167
+ const msg = db.prepare('SELECT content_text FROM messages WHERE id = ?').get(messageId);
168
+ if (!msg) {
169
+ throw new Error(`Message ${messageId} disappeared between read and purge`);
170
+ }
171
+ const original = msg.content_text.slice(finding.start_offset, finding.end_offset);
172
+ const mask = maskValueByKind(original, kind);
173
+ const purgedAt = new Date().toISOString();
174
+ db.transaction(() => {
175
+ const newText = msg.content_text.slice(0, finding.start_offset) +
176
+ mask +
177
+ msg.content_text.slice(finding.end_offset);
178
+ db.prepare('UPDATE messages SET content_text = ? WHERE id = ?')
179
+ .run(newText, messageId);
180
+ db.prepare(`UPDATE findings
181
+ SET state = 'purged',
182
+ state_changed_at = ?
183
+ WHERE id = ?`).run(purgedAt, finding.id);
184
+ updateSessionCounts(db, finding.session_id);
185
+ })();
186
+ return {
187
+ findingId: finding.id,
188
+ sessionId: finding.session_id,
189
+ maskUsed: mask,
190
+ purgedAt,
191
+ };
192
+ }
193
+ //# sourceMappingURL=purge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"purge.js","sourceRoot":"","sources":["../../src/security/purge.ts"],"names":[],"mappings":"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"}