@spool-lab/core 0.4.1 → 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 (152) hide show
  1. package/dist/db/db.d.ts +28 -1
  2. package/dist/db/db.d.ts.map +1 -1
  3. package/dist/db/db.js +320 -17
  4. package/dist/db/db.js.map +1 -1
  5. package/dist/db/queries.d.ts +36 -11
  6. package/dist/db/queries.d.ts.map +1 -1
  7. package/dist/db/queries.js +118 -83
  8. package/dist/db/queries.js.map +1 -1
  9. package/dist/db/share-drafts.d.ts +51 -0
  10. package/dist/db/share-drafts.d.ts.map +1 -0
  11. package/dist/db/share-drafts.js +47 -0
  12. package/dist/db/share-drafts.js.map +1 -0
  13. package/dist/doctor/checks/config.d.ts +3 -0
  14. package/dist/doctor/checks/config.d.ts.map +1 -0
  15. package/dist/doctor/checks/config.js +123 -0
  16. package/dist/doctor/checks/config.js.map +1 -0
  17. package/dist/doctor/checks/db.d.ts +3 -0
  18. package/dist/doctor/checks/db.d.ts.map +1 -0
  19. package/dist/doctor/checks/db.js +225 -0
  20. package/dist/doctor/checks/db.js.map +1 -0
  21. package/dist/doctor/checks/env.d.ts +3 -0
  22. package/dist/doctor/checks/env.d.ts.map +1 -0
  23. package/dist/doctor/checks/env.js +70 -0
  24. package/dist/doctor/checks/env.js.map +1 -0
  25. package/dist/doctor/checks/index.d.ts +3 -0
  26. package/dist/doctor/checks/index.d.ts.map +1 -0
  27. package/dist/doctor/checks/index.js +13 -0
  28. package/dist/doctor/checks/index.js.map +1 -0
  29. package/dist/doctor/checks/native.d.ts +3 -0
  30. package/dist/doctor/checks/native.d.ts.map +1 -0
  31. package/dist/doctor/checks/native.js +42 -0
  32. package/dist/doctor/checks/native.js.map +1 -0
  33. package/dist/doctor/checks/versions.d.ts +3 -0
  34. package/dist/doctor/checks/versions.d.ts.map +1 -0
  35. package/dist/doctor/checks/versions.js +79 -0
  36. package/dist/doctor/checks/versions.js.map +1 -0
  37. package/dist/doctor/index.d.ts +3 -0
  38. package/dist/doctor/index.d.ts.map +1 -0
  39. package/dist/doctor/index.js +3 -0
  40. package/dist/doctor/index.js.map +1 -0
  41. package/dist/doctor/preflight.d.ts +18 -0
  42. package/dist/doctor/preflight.d.ts.map +1 -0
  43. package/dist/doctor/preflight.js +27 -0
  44. package/dist/doctor/preflight.js.map +1 -0
  45. package/dist/doctor/runner.d.ts +12 -0
  46. package/dist/doctor/runner.d.ts.map +1 -0
  47. package/dist/doctor/runner.js +37 -0
  48. package/dist/doctor/runner.js.map +1 -0
  49. package/dist/doctor/types.d.ts +27 -0
  50. package/dist/doctor/types.d.ts.map +1 -0
  51. package/dist/doctor/types.js +2 -0
  52. package/dist/doctor/types.js.map +1 -0
  53. package/dist/index.d.ts +7 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +7 -1
  56. package/dist/index.js.map +1 -1
  57. package/dist/migrations/agent-search-cleanup.d.ts +36 -0
  58. package/dist/migrations/agent-search-cleanup.d.ts.map +1 -0
  59. package/dist/migrations/agent-search-cleanup.js +182 -0
  60. package/dist/migrations/agent-search-cleanup.js.map +1 -0
  61. package/dist/migrations/worktree-identity-upgrade.d.ts +31 -0
  62. package/dist/migrations/worktree-identity-upgrade.d.ts.map +1 -0
  63. package/dist/migrations/worktree-identity-upgrade.js +22 -0
  64. package/dist/migrations/worktree-identity-upgrade.js.map +1 -0
  65. package/dist/parsers/claude.d.ts.map +1 -1
  66. package/dist/parsers/claude.js +25 -12
  67. package/dist/parsers/claude.js.map +1 -1
  68. package/dist/parsers/codex.d.ts.map +1 -1
  69. package/dist/parsers/codex.js +32 -5
  70. package/dist/parsers/codex.js.map +1 -1
  71. package/dist/parsers/gemini.d.ts.map +1 -1
  72. package/dist/parsers/gemini.js +5 -5
  73. package/dist/parsers/gemini.js.map +1 -1
  74. package/dist/parsers/spool-prelude.d.ts +16 -0
  75. package/dist/parsers/spool-prelude.d.ts.map +1 -0
  76. package/dist/parsers/spool-prelude.js +21 -0
  77. package/dist/parsers/spool-prelude.js.map +1 -0
  78. package/dist/projects/display-name.d.ts +2 -0
  79. package/dist/projects/display-name.d.ts.map +1 -0
  80. package/dist/projects/display-name.js +8 -0
  81. package/dist/projects/display-name.js.map +1 -0
  82. package/dist/projects/fs.d.ts +3 -0
  83. package/dist/projects/fs.d.ts.map +1 -0
  84. package/dist/projects/fs.js +20 -0
  85. package/dist/projects/fs.js.map +1 -0
  86. package/dist/projects/groups.d.ts +4 -0
  87. package/dist/projects/groups.d.ts.map +1 -0
  88. package/dist/projects/groups.js +20 -0
  89. package/dist/projects/groups.js.map +1 -0
  90. package/dist/projects/identity-synthesizers.d.ts +22 -0
  91. package/dist/projects/identity-synthesizers.d.ts.map +1 -0
  92. package/dist/projects/identity-synthesizers.js +29 -0
  93. package/dist/projects/identity-synthesizers.js.map +1 -0
  94. package/dist/projects/identity.d.ts +16 -0
  95. package/dist/projects/identity.d.ts.map +1 -0
  96. package/dist/projects/identity.js +172 -0
  97. package/dist/projects/identity.js.map +1 -0
  98. package/dist/projects/sessions.d.ts +40 -0
  99. package/dist/projects/sessions.d.ts.map +1 -0
  100. package/dist/projects/sessions.js +151 -0
  101. package/dist/projects/sessions.js.map +1 -0
  102. package/dist/projects/worktree-resolvers.d.ts +19 -0
  103. package/dist/projects/worktree-resolvers.d.ts.map +1 -0
  104. package/dist/projects/worktree-resolvers.js +65 -0
  105. package/dist/projects/worktree-resolvers.js.map +1 -0
  106. package/dist/security/index.d.ts +14 -0
  107. package/dist/security/index.d.ts.map +1 -0
  108. package/dist/security/index.js +7 -0
  109. package/dist/security/index.js.map +1 -0
  110. package/dist/security/maintenance.d.ts +15 -0
  111. package/dist/security/maintenance.d.ts.map +1 -0
  112. package/dist/security/maintenance.js +68 -0
  113. package/dist/security/maintenance.js.map +1 -0
  114. package/dist/security/profile.d.ts +47 -0
  115. package/dist/security/profile.d.ts.map +1 -0
  116. package/dist/security/profile.js +122 -0
  117. package/dist/security/profile.js.map +1 -0
  118. package/dist/security/purge.d.ts +49 -0
  119. package/dist/security/purge.d.ts.map +1 -0
  120. package/dist/security/purge.js +159 -0
  121. package/dist/security/purge.js.map +1 -0
  122. package/dist/security/repo.d.ts +141 -0
  123. package/dist/security/repo.d.ts.map +1 -0
  124. package/dist/security/repo.js +469 -0
  125. package/dist/security/repo.js.map +1 -0
  126. package/dist/security/scan.d.ts +50 -0
  127. package/dist/security/scan.d.ts.map +1 -0
  128. package/dist/security/scan.js +120 -0
  129. package/dist/security/scan.js.map +1 -0
  130. package/dist/security/types.d.ts +128 -0
  131. package/dist/security/types.d.ts.map +1 -0
  132. package/dist/security/types.js +10 -0
  133. package/dist/security/types.js.map +1 -0
  134. package/dist/security/worker.d.ts +53 -0
  135. package/dist/security/worker.d.ts.map +1 -0
  136. package/dist/security/worker.js +180 -0
  137. package/dist/security/worker.js.map +1 -0
  138. package/dist/sync/source-paths.d.ts +1 -0
  139. package/dist/sync/source-paths.d.ts.map +1 -1
  140. package/dist/sync/source-paths.js +13 -3
  141. package/dist/sync/source-paths.js.map +1 -1
  142. package/dist/sync/syncer.d.ts +8 -1
  143. package/dist/sync/syncer.d.ts.map +1 -1
  144. package/dist/sync/syncer.js +37 -17
  145. package/dist/sync/syncer.js.map +1 -1
  146. package/dist/types.d.ts +28 -6
  147. package/dist/types.d.ts.map +1 -1
  148. package/dist/util/resolve-bin.d.ts +34 -0
  149. package/dist/util/resolve-bin.d.ts.map +1 -1
  150. package/dist/util/resolve-bin.js +175 -20
  151. package/dist/util/resolve-bin.js.map +1 -1
  152. 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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"}