@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
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
});
|
|
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
|
+
return out;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Produce a purge order that's safe against offset drift. Findings
|
|
89
|
+
* with `message_id IS NULL` (orphan rows that the schema permits but
|
|
90
|
+
* should never happen in practice) fall to the end so they don't
|
|
91
|
+
* interfere with message-grouped batches.
|
|
92
|
+
*
|
|
93
|
+
* Exported only for tests. */
|
|
94
|
+
export function orderForBulkPurge(db, findingIds) {
|
|
95
|
+
if (findingIds.length === 0)
|
|
96
|
+
return [];
|
|
97
|
+
const placeholders = findingIds.map(() => '?').join(',');
|
|
98
|
+
const rows = db.prepare(`SELECT id, message_id, start_offset
|
|
99
|
+
FROM findings
|
|
100
|
+
WHERE id IN (${placeholders})`).all(...findingIds);
|
|
101
|
+
const byMessage = new Map();
|
|
102
|
+
for (const r of rows) {
|
|
103
|
+
const key = r.message_id;
|
|
104
|
+
let bucket = byMessage.get(key);
|
|
105
|
+
if (!bucket) {
|
|
106
|
+
bucket = [];
|
|
107
|
+
byMessage.set(key, bucket);
|
|
108
|
+
}
|
|
109
|
+
bucket.push({ id: r.id, start_offset: r.start_offset });
|
|
110
|
+
}
|
|
111
|
+
for (const bucket of byMessage.values()) {
|
|
112
|
+
bucket.sort((a, b) => b.start_offset - a.start_offset);
|
|
113
|
+
}
|
|
114
|
+
// Stable iteration order over the messages doesn't matter for
|
|
115
|
+
// correctness — purges in different messages can't shift each
|
|
116
|
+
// other's offsets. We sort by message_id (nulls last) for
|
|
117
|
+
// determinism in tests.
|
|
118
|
+
const messageKeys = [...byMessage.keys()].sort((a, b) => {
|
|
119
|
+
if (a === null)
|
|
120
|
+
return 1;
|
|
121
|
+
if (b === null)
|
|
122
|
+
return -1;
|
|
123
|
+
return a - b;
|
|
124
|
+
});
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const key of messageKeys) {
|
|
127
|
+
for (const r of byMessage.get(key))
|
|
128
|
+
out.push(r.id);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function applyPurgeTxn(db, finding, messageId, kind) {
|
|
133
|
+
const msg = db.prepare('SELECT content_text FROM messages WHERE id = ?').get(messageId);
|
|
134
|
+
if (!msg) {
|
|
135
|
+
throw new Error(`Message ${messageId} disappeared between read and purge`);
|
|
136
|
+
}
|
|
137
|
+
const original = msg.content_text.slice(finding.start_offset, finding.end_offset);
|
|
138
|
+
const mask = maskValueByKind(original, kind);
|
|
139
|
+
const purgedAt = new Date().toISOString();
|
|
140
|
+
db.transaction(() => {
|
|
141
|
+
const newText = msg.content_text.slice(0, finding.start_offset) +
|
|
142
|
+
mask +
|
|
143
|
+
msg.content_text.slice(finding.end_offset);
|
|
144
|
+
db.prepare('UPDATE messages SET content_text = ? WHERE id = ?')
|
|
145
|
+
.run(newText, messageId);
|
|
146
|
+
db.prepare(`UPDATE findings
|
|
147
|
+
SET state = 'purged',
|
|
148
|
+
state_changed_at = ?
|
|
149
|
+
WHERE id = ?`).run(purgedAt, finding.id);
|
|
150
|
+
updateSessionCounts(db, finding.session_id);
|
|
151
|
+
})();
|
|
152
|
+
return {
|
|
153
|
+
findingId: finding.id,
|
|
154
|
+
sessionId: finding.session_id,
|
|
155
|
+
maskUsed: mask,
|
|
156
|
+
purgedAt,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
//# 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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"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { SensitiveKind } from '@spool-lab/redact';
|
|
3
|
+
import type { FindingRow, FindingState, RiskByCategoryRow, SessionWithFindingCounts } from './types.js';
|
|
4
|
+
export interface FindingFilter {
|
|
5
|
+
sessionId?: number;
|
|
6
|
+
kind?: SensitiveKind;
|
|
7
|
+
/** Multi-select kinds. When non-empty takes precedence over `kind`.
|
|
8
|
+
* Used by the Security page filter pills which support clicking
|
|
9
|
+
* multiple chips to OR them together. */
|
|
10
|
+
kinds?: readonly SensitiveKind[];
|
|
11
|
+
state?: FindingState | 'any';
|
|
12
|
+
severity?: 'high' | 'low';
|
|
13
|
+
limit?: number;
|
|
14
|
+
offset?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SessionFindingFilter {
|
|
17
|
+
kind?: SensitiveKind;
|
|
18
|
+
kinds?: readonly SensitiveKind[];
|
|
19
|
+
state?: FindingState | 'any';
|
|
20
|
+
severity?: 'high' | 'low';
|
|
21
|
+
/** Free-text on session title. */
|
|
22
|
+
text?: string;
|
|
23
|
+
limit?: number;
|
|
24
|
+
offset?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface Page<T> {
|
|
27
|
+
rows: T[];
|
|
28
|
+
hasMore: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface FindingInput {
|
|
31
|
+
sessionId: number;
|
|
32
|
+
messageId: number | null;
|
|
33
|
+
kind: SensitiveKind;
|
|
34
|
+
valueHash: string;
|
|
35
|
+
confidence: number;
|
|
36
|
+
provider: string;
|
|
37
|
+
startOffset: number;
|
|
38
|
+
endOffset: number;
|
|
39
|
+
/** Allowlist hit at insert time → 'dismissed'; else 'active'. */
|
|
40
|
+
state: FindingState;
|
|
41
|
+
}
|
|
42
|
+
/** Insert a batch of findings. Caller wraps in a transaction. */
|
|
43
|
+
export declare function insertFindings(db: Database.Database, rows: readonly FindingInput[]): void;
|
|
44
|
+
/** Delete all non-purged rows for the providers being rescanned —
|
|
45
|
+
* i.e. wipe the producer's previous output so the upcoming
|
|
46
|
+
* `insertFindings` is the canonical truth.
|
|
47
|
+
*
|
|
48
|
+
* Includes `state='dismissed'` deliberately: those rows are derived
|
|
49
|
+
* state. The user's "ignore" decisions live in `allowlist_session`
|
|
50
|
+
* and `allowlist_global` (preserved here), and the scan path
|
|
51
|
+
* re-emits findings as `state='dismissed'` whenever it encounters a
|
|
52
|
+
* hit that matches an allowlist row. Re-inserting also keeps
|
|
53
|
+
* `findings.value_hash` aligned with the per-kind allowlist
|
|
54
|
+
* preference (security.json `kindAllowlist`) — without this delete,
|
|
55
|
+
* a mute → unmute cycle would accumulate phantom dismissed rows
|
|
56
|
+
* (one set per cycle), inflating audit counts and breaking
|
|
57
|
+
* `riskByCategory` totals over time.
|
|
58
|
+
*
|
|
59
|
+
* Purged rows are the only state that's NOT producer-derived —
|
|
60
|
+
* they correspond to destructive `messages.content_text` rewrites
|
|
61
|
+
* the user explicitly approved. Preserving them is the audit
|
|
62
|
+
* contract documented in the design doc. */
|
|
63
|
+
export declare function deleteRefreshableFindings(db: Database.Database, sessionId: number, providers: readonly string[]): void;
|
|
64
|
+
/** @deprecated Renamed to {@link deleteRefreshableFindings} after the
|
|
65
|
+
* active-only filter was found to leak phantom dismissed rows
|
|
66
|
+
* across mute→unmute cycles. Kept as a thin alias so callers
|
|
67
|
+
* outside the repo don't break mid-stack. */
|
|
68
|
+
export declare const deleteActiveFindings: typeof deleteRefreshableFindings;
|
|
69
|
+
/** Recompute sessions.scan_finding_count + scan_high_count from the
|
|
70
|
+
* findings table.
|
|
71
|
+
*
|
|
72
|
+
* Counts EXCLUDE info-tier kinds (absolute-path, ip, internal-host).
|
|
73
|
+
* Those are stored — useful for an opt-in "Show informational
|
|
74
|
+
* signals" toggle — but they don't drive the Library row badge
|
|
75
|
+
* because their pattern-only signal has too high a false-positive
|
|
76
|
+
* rate to be meaningful at first glance. */
|
|
77
|
+
export declare function updateSessionCounts(db: Database.Database, sessionId: number): void;
|
|
78
|
+
export declare function setSessionScanProfile(db: Database.Database, sessionId: number, profile: string, completedAt: string): void;
|
|
79
|
+
/** Set every session's scan_profile to NULL — used by "Rescan all" so
|
|
80
|
+
* every session re-enqueues. */
|
|
81
|
+
export declare function invalidateAllScanProfiles(db: Database.Database): number;
|
|
82
|
+
/** Drop the scan profile for one session — used by the sync cascade
|
|
83
|
+
* when a session's messages change after first scan. */
|
|
84
|
+
export declare function invalidateSessionScanProfile(db: Database.Database, sessionId: number): void;
|
|
85
|
+
/** Sessions whose stored profile doesn't structurally match
|
|
86
|
+
* `currentProfile`. Used at boot for backfill enqueue. */
|
|
87
|
+
export declare function listSessionsNeedingScan(db: Database.Database, currentProfile: string): number[];
|
|
88
|
+
export declare function listFindings(db: Database.Database, filter: FindingFilter): FindingRow[];
|
|
89
|
+
export declare function listFindingsPage(db: Database.Database, filter: FindingFilter): Page<FindingRow>;
|
|
90
|
+
export declare function listSessionsWithFindings(db: Database.Database, filter: SessionFindingFilter): SessionWithFindingCounts[];
|
|
91
|
+
export declare function listSessionsWithFindingsPage(db: Database.Database, filter: SessionFindingFilter): Page<SessionWithFindingCounts>;
|
|
92
|
+
/** Total distinct sessions matching the same filter as
|
|
93
|
+
* `listSessionsWithFindings` — used by the renderer to show
|
|
94
|
+
* "涉及 N 个会话" without loading every page. Cheaper than the list
|
|
95
|
+
* query: no SELECT projection, no GROUP BY, no ORDER BY. */
|
|
96
|
+
export declare function countSessionsWithFindings(db: Database.Database, filter: SessionFindingFilter): number;
|
|
97
|
+
/** Risk by category — one row per kind that has ≥ 1 active finding.
|
|
98
|
+
* Drives the Watchtower-style panel on the Security page. */
|
|
99
|
+
export declare function riskByCategory(db: Database.Database): RiskByCategoryRow[];
|
|
100
|
+
/** Read the live raw value for one finding. Used by the UI's review
|
|
101
|
+
* panel and the Purge confirm dialog. Returns null when the finding
|
|
102
|
+
* no longer points at a valid message (race against session
|
|
103
|
+
* deletion) or has been purged (offsets now point at the mask). */
|
|
104
|
+
export declare function getFindingValue(db: Database.Database, findingId: number): string | null;
|
|
105
|
+
/** Bulk version of `getFindingValue` — one SQL query for N finding
|
|
106
|
+
* ids instead of N round-trips. Caller fans the result map back out
|
|
107
|
+
* by id; missing keys are treated as `null` (purged / vanished). */
|
|
108
|
+
export declare function getFindingValues(db: Database.Database, findingIds: readonly number[]): Record<number, string | null>;
|
|
109
|
+
export interface AllowlistSnapshot {
|
|
110
|
+
/** (kind|value_hash) keys for this session. */
|
|
111
|
+
session: Set<string>;
|
|
112
|
+
/** (kind|value_hash) keys, global scope. */
|
|
113
|
+
global: Set<string>;
|
|
114
|
+
}
|
|
115
|
+
export declare function getAllowlists(db: Database.Database, sessionId: number): AllowlistSnapshot;
|
|
116
|
+
export declare function isAllowlisted(allow: AllowlistSnapshot, kind: SensitiveKind, valueHash: string): boolean;
|
|
117
|
+
export declare function addAllowlistSession(db: Database.Database, sessionId: number, kind: SensitiveKind, valueHash: string): void;
|
|
118
|
+
export declare function addAllowlistGlobal(db: Database.Database, kind: SensitiveKind, valueHash: string): void;
|
|
119
|
+
export declare function removeAllowlistSession(db: Database.Database, sessionId: number, kind: SensitiveKind, valueHash: string): void;
|
|
120
|
+
export declare function removeAllowlistGlobal(db: Database.Database, kind: SensitiveKind, valueHash: string): void;
|
|
121
|
+
export interface AllowlistEntryRow {
|
|
122
|
+
scope: 'session' | 'global';
|
|
123
|
+
kind: SensitiveKind;
|
|
124
|
+
valueHash: string;
|
|
125
|
+
createdAt: string;
|
|
126
|
+
/** Session row context — null for global entries. */
|
|
127
|
+
sessionUuid: string | null;
|
|
128
|
+
sessionTitle: string | null;
|
|
129
|
+
}
|
|
130
|
+
/** All allowlist rows from both tables, joined with the originating
|
|
131
|
+
* session metadata. Drives the Settings → Security pane's "Manage…"
|
|
132
|
+
* modal so the user can review and revoke past decisions. */
|
|
133
|
+
export declare function listAllowlistEntries(db: Database.Database): AllowlistEntryRow[];
|
|
134
|
+
/** Flip a finding to 'dismissed' and, depending on scope, write the
|
|
135
|
+
* allowlist entry so future rescans honor the decision. Caller
|
|
136
|
+
* wraps in a transaction. */
|
|
137
|
+
export declare function dismissFinding(db: Database.Database, findingId: number, scope: 'session' | 'global'): void;
|
|
138
|
+
/** Re-activate a dismissed finding and remove the allowlist entry
|
|
139
|
+
* that pinned it (both scopes, since UI doesn't always know which). */
|
|
140
|
+
export declare function undismissFinding(db: Database.Database, findingId: number): void;
|
|
141
|
+
//# sourceMappingURL=repo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/security/repo.ts"],"names":[],"mappings":"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"}
|