@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.
- package/README.md +2 -2
- package/dist/db/db.d.ts +10 -2
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +119 -10
- package/dist/db/db.js.map +1 -1
- package/dist/db/queries.d.ts +5 -5
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +32 -14
- package/dist/db/queries.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/observability/exporter-file.d.ts +30 -0
- package/dist/observability/exporter-file.d.ts.map +1 -0
- package/dist/observability/exporter-file.js +125 -0
- package/dist/observability/exporter-file.js.map +1 -0
- package/dist/observability/exporter-pretty.d.ts +27 -0
- package/dist/observability/exporter-pretty.d.ts.map +1 -0
- package/dist/observability/exporter-pretty.js +84 -0
- package/dist/observability/exporter-pretty.js.map +1 -0
- package/dist/observability/index.d.ts +2 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +2 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/layer.d.ts +29 -0
- package/dist/observability/layer.d.ts.map +1 -0
- package/dist/observability/layer.js +45 -0
- package/dist/observability/layer.js.map +1 -0
- package/dist/parsers/opencode.d.ts +23 -0
- package/dist/parsers/opencode.d.ts.map +1 -0
- package/dist/parsers/opencode.js +358 -0
- package/dist/parsers/opencode.js.map +1 -0
- 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 +131 -0
- package/dist/security/profile.js.map +1 -0
- package/dist/security/purge.d.ts +68 -0
- package/dist/security/purge.d.ts.map +1 -0
- package/dist/security/purge.js +193 -0
- package/dist/security/purge.js.map +1 -0
- package/dist/security/repo.d.ts +190 -0
- package/dist/security/repo.d.ts.map +1 -0
- package/dist/security/repo.js +594 -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 +122 -0
- package/dist/security/scan.js.map +1 -0
- package/dist/security/types.d.ts +146 -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 +69 -0
- package/dist/security/worker.d.ts.map +1 -0
- package/dist/security/worker.js +228 -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 +48 -6
- package/dist/sync/source-paths.js.map +1 -1
- package/dist/sync/syncer.d.ts +9 -1
- package/dist/sync/syncer.d.ts.map +1 -1
- package/dist/sync/syncer.js +128 -16
- package/dist/sync/syncer.js.map +1 -1
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +8 -2
- package/dist/sync/watcher.js.map +1 -1
- package/dist/types.d.ts +15 -1
- 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 +11 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/security/scan.ts"],"names":[],"mappings":"AAaA,OAAO,EAAQ,MAAM,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAWvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;;;;AAEhD,qBAAa,SAAU,SAAQ,eAA8B;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,mBAAmB,GAAG,iBAAiB,GAAG,WAAW,CAAA;CACvE,CAAC;CAAG;AAOL,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAA;IACrB;;0CAEsC;IACtC,SAAS,EAAE,SAAS,cAAc,EAAE,CAAA;IACpC;2BACuB;IACvB,cAAc,EAAE,MAAM,CAAA;IACtB;;;oEAGgE;IAChE,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC;+CAC2C;IAC3C,OAAO,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACxD;;;wCAGoC;IACpC,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACpC;AAED;;2CAE2C;AAC3C,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,eAAe,GACpB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,CAsFtC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Per-session scan pipeline.
|
|
2
|
+
//
|
|
3
|
+
// Effect-shaped so the worker can compose it with Queue / Stream /
|
|
4
|
+
// supervisor primitives. The detection itself runs against
|
|
5
|
+
// `messages.content_text` only — Purge later mutates that field,
|
|
6
|
+
// findings re-detect, and the audit trail stays consistent.
|
|
7
|
+
//
|
|
8
|
+
// Transaction shape: detection runs *outside* the transaction (it
|
|
9
|
+
// can take seconds with ML); only the final apply step is atomic.
|
|
10
|
+
// If the worker dies mid-detection, scan_profile was never updated
|
|
11
|
+
// → restart re-enqueues from scratch. If it dies inside the
|
|
12
|
+
// transaction, SQLite atomicity guarantees no half-applied state.
|
|
13
|
+
import { Data, Effect } from 'effect';
|
|
14
|
+
import { hashValueForRedactExclude } from '@spool-lab/redact';
|
|
15
|
+
import { deleteRefreshableFindings, insertFindings, setSessionScanProfile, updateSessionCounts, getAllowlists, isAllowlisted, } from './repo.js';
|
|
16
|
+
export class ScanError extends Data.TaggedError('ScanError') {
|
|
17
|
+
}
|
|
18
|
+
/** Scan one session end to end. Idempotent: running twice produces
|
|
19
|
+
* the same findings (same value_hash + same offsets → same rows
|
|
20
|
+
* after the delete-then-insert reset). */
|
|
21
|
+
export function scanSession(sessionId, deps) {
|
|
22
|
+
return Effect.gen(function* () {
|
|
23
|
+
// 1. Load messages outside the transaction.
|
|
24
|
+
const messages = yield* Effect.try({
|
|
25
|
+
try: () => deps.db.prepare(`SELECT id, content_text
|
|
26
|
+
FROM messages
|
|
27
|
+
WHERE session_id = ?
|
|
28
|
+
ORDER BY seq ASC`).all(sessionId),
|
|
29
|
+
catch: (cause) => new ScanError({ sessionId, cause, reason: 'db-failed' }),
|
|
30
|
+
});
|
|
31
|
+
yield* Effect.annotateCurrentSpan('messageCount', messages.length);
|
|
32
|
+
if (messages.length === 0) {
|
|
33
|
+
// Session has no messages yet. Still mark it scanned with the
|
|
34
|
+
// current profile so we don't loop on it; future sync will
|
|
35
|
+
// invalidate the profile and re-enqueue.
|
|
36
|
+
yield* applyEmpty(sessionId, deps);
|
|
37
|
+
yield* deps.publish({ type: 'session-rescanned', sessionId });
|
|
38
|
+
return { sessionId, inserted: 0, profile: deps.currentProfile };
|
|
39
|
+
}
|
|
40
|
+
// 2. Run providers per message. The flat list is enough — we
|
|
41
|
+
// dedupe later by (kind, value_hash, start_offset) so producers
|
|
42
|
+
// can be naive about overlap.
|
|
43
|
+
const allMatches = [];
|
|
44
|
+
for (const msg of messages) {
|
|
45
|
+
for (const provider of deps.providers) {
|
|
46
|
+
if (!provider.available())
|
|
47
|
+
continue;
|
|
48
|
+
const matches = yield* Effect.tryPromise({
|
|
49
|
+
try: () => provider.analyze(msg.content_text),
|
|
50
|
+
catch: (cause) => new ScanError({ sessionId, cause, reason: 'provider-failed' }),
|
|
51
|
+
});
|
|
52
|
+
for (const m of matches) {
|
|
53
|
+
allMatches.push({ ...m, messageId: msg.id });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// 3. Dedupe across providers — pick the highest-confidence match
|
|
58
|
+
// for each (kind, value_hash, start_offset).
|
|
59
|
+
const merged = mergeMatches(allMatches);
|
|
60
|
+
// 4. Allowlist join → assign initial state.
|
|
61
|
+
const allow = yield* Effect.try({
|
|
62
|
+
try: () => getAllowlists(deps.db, sessionId),
|
|
63
|
+
catch: (cause) => new ScanError({ sessionId, cause, reason: 'db-failed' }),
|
|
64
|
+
});
|
|
65
|
+
const inputs = merged.map((m) => ({
|
|
66
|
+
sessionId,
|
|
67
|
+
messageId: m.messageId,
|
|
68
|
+
kind: m.kind,
|
|
69
|
+
valueHash: m.valueHash,
|
|
70
|
+
confidence: m.confidence,
|
|
71
|
+
provider: m.provider,
|
|
72
|
+
startOffset: m.start,
|
|
73
|
+
endOffset: m.end,
|
|
74
|
+
state: (deps.kindAllowlist?.has(m.kind) || isAllowlisted(allow, m.kind, m.valueHash))
|
|
75
|
+
? 'dismissed'
|
|
76
|
+
: 'active',
|
|
77
|
+
}));
|
|
78
|
+
// 5. Apply atomically.
|
|
79
|
+
yield* Effect.try({
|
|
80
|
+
try: () => {
|
|
81
|
+
deps.db.transaction(() => {
|
|
82
|
+
deleteRefreshableFindings(deps.db, sessionId, deps.providerNames);
|
|
83
|
+
insertFindings(deps.db, inputs);
|
|
84
|
+
setSessionScanProfile(deps.db, sessionId, deps.currentProfile, new Date().toISOString());
|
|
85
|
+
updateSessionCounts(deps.db, sessionId);
|
|
86
|
+
})();
|
|
87
|
+
},
|
|
88
|
+
catch: (cause) => new ScanError({ sessionId, cause, reason: 'db-failed' }),
|
|
89
|
+
});
|
|
90
|
+
yield* deps.publish({ type: 'session-rescanned', sessionId });
|
|
91
|
+
yield* Effect.annotateCurrentSpan('inserted', inputs.length);
|
|
92
|
+
return { sessionId, inserted: inputs.length, profile: deps.currentProfile };
|
|
93
|
+
}).pipe(Effect.withSpan('security.scan.session', { attributes: { sessionId } }));
|
|
94
|
+
}
|
|
95
|
+
function applyEmpty(sessionId, deps) {
|
|
96
|
+
return Effect.try({
|
|
97
|
+
try: () => {
|
|
98
|
+
deps.db.transaction(() => {
|
|
99
|
+
deleteRefreshableFindings(deps.db, sessionId, deps.providerNames);
|
|
100
|
+
setSessionScanProfile(deps.db, sessionId, deps.currentProfile, new Date().toISOString());
|
|
101
|
+
updateSessionCounts(deps.db, sessionId);
|
|
102
|
+
})();
|
|
103
|
+
},
|
|
104
|
+
catch: (cause) => new ScanError({ sessionId, cause, reason: 'db-failed' }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Pick the highest-confidence match per (messageId, kind, value_hash,
|
|
108
|
+
* start). The same vendor token detected by two providers should
|
|
109
|
+
* produce one finding, attributed to whoever was most confident. */
|
|
110
|
+
function mergeMatches(matches) {
|
|
111
|
+
const byKey = new Map();
|
|
112
|
+
for (const m of matches) {
|
|
113
|
+
const hash = hashValueForRedactExclude(m.value);
|
|
114
|
+
const key = `${m.messageId}|${m.kind}|${hash}|${m.start}`;
|
|
115
|
+
const existing = byKey.get(key);
|
|
116
|
+
if (!existing || m.confidence > existing.confidence) {
|
|
117
|
+
byKey.set(key, { ...m, valueHash: hash });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return Array.from(byKey.values());
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/security/scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { SensitiveKind, Severity } from '@spool-lab/redact';
|
|
2
|
+
/** A finding's lifecycle state.
|
|
3
|
+
*
|
|
4
|
+
* active — surfaced in Security page and session strip
|
|
5
|
+
* dismissed — user opted to ignore; suppressed from default views
|
|
6
|
+
* purged — raw value rewritten with a mask in
|
|
7
|
+
* messages.content_text; row remains as an audit record
|
|
8
|
+
*/
|
|
9
|
+
export type FindingState = 'active' | 'dismissed' | 'purged';
|
|
10
|
+
/** One row of the `findings` table, mapped to camelCase for the
|
|
11
|
+
* renderer. The raw sensitive value is NEVER part of this payload —
|
|
12
|
+
* consumers that need to render the original text fetch it via
|
|
13
|
+
* `getFindingValue(findingId)`, which reads
|
|
14
|
+
* `messages.content_text[startOffset..endOffset]` at request time. */
|
|
15
|
+
export interface FindingRow {
|
|
16
|
+
id: number;
|
|
17
|
+
sessionId: number;
|
|
18
|
+
messageId: number | null;
|
|
19
|
+
kind: SensitiveKind;
|
|
20
|
+
valueHash: string;
|
|
21
|
+
confidence: number;
|
|
22
|
+
provider: string;
|
|
23
|
+
startOffset: number;
|
|
24
|
+
endOffset: number;
|
|
25
|
+
state: FindingState;
|
|
26
|
+
detectedAt: string;
|
|
27
|
+
stateChangedAt: string | null;
|
|
28
|
+
}
|
|
29
|
+
/** A session row joined with its denormalised scan counts. Used by
|
|
30
|
+
* the Security page list and the Library row badge. */
|
|
31
|
+
export interface SessionWithFindingCounts {
|
|
32
|
+
id: number;
|
|
33
|
+
sessionUuid: string;
|
|
34
|
+
title: string | null;
|
|
35
|
+
startedAt: string;
|
|
36
|
+
/** Active findings of any severity. */
|
|
37
|
+
findingCount: number;
|
|
38
|
+
/** Active findings whose kind is in `HIGH_SEVERITY_KINDS`. */
|
|
39
|
+
highCount: number;
|
|
40
|
+
/** Lifetime count of purged findings on this session. Combined with
|
|
41
|
+
* scan_completed_at + a zero findingCount, lets the Library row
|
|
42
|
+
* show an "all-resolved" check rather than a stale red badge. */
|
|
43
|
+
purgedCount: number;
|
|
44
|
+
scanCompletedAt: string | null;
|
|
45
|
+
/** Sources name ('claude' / 'codex' / 'gemini'). */
|
|
46
|
+
source: string;
|
|
47
|
+
/** Number of non-sidechain messages in the session. */
|
|
48
|
+
messageCount: number;
|
|
49
|
+
/** Model identifier as captured at sync time. May be null for
|
|
50
|
+
* sessions where the parser couldn't infer it. */
|
|
51
|
+
model: string | null;
|
|
52
|
+
/** Working directory captured when the session started; used to
|
|
53
|
+
* build the resume-CLI command and the "Copy terminal command"
|
|
54
|
+
* menu action. Null when the parser couldn't infer it. */
|
|
55
|
+
cwd: string | null;
|
|
56
|
+
/** Human-readable project name, e.g. "spool". Surfaced at the head
|
|
57
|
+
* of the session card's meta row so users can locate the session
|
|
58
|
+
* in the Library at a glance. */
|
|
59
|
+
projectDisplayName: string | null;
|
|
60
|
+
}
|
|
61
|
+
/** Row in the Security page's Risk-by-category panel. One per kind
|
|
62
|
+
* that has ≥ 1 active finding; categories with zero counts are not
|
|
63
|
+
* emitted. */
|
|
64
|
+
export interface RiskByCategoryRow {
|
|
65
|
+
kind: SensitiveKind;
|
|
66
|
+
severity: Severity;
|
|
67
|
+
count: number;
|
|
68
|
+
/** Distinct sessions containing ≥ 1 active finding of this kind. */
|
|
69
|
+
sessions: number;
|
|
70
|
+
}
|
|
71
|
+
/** One session in which a given `(kind, valueHash)` secret appears —
|
|
72
|
+
* the cross-session "blast radius" of a single leaked value. Counts
|
|
73
|
+
* only ACTIVE findings (dismissed/purged occurrences don't expose the
|
|
74
|
+
* value through Spool's surfaces). */
|
|
75
|
+
export interface OccurrenceBySession {
|
|
76
|
+
sessionId: number;
|
|
77
|
+
sessionUuid: string;
|
|
78
|
+
sessionTitle: string | null;
|
|
79
|
+
/** Agent source (claude / codex / gemini / opencode) — drives the
|
|
80
|
+
* source badge so each row reads as a session, not loose text. */
|
|
81
|
+
source: string;
|
|
82
|
+
/** Project display name, null for the Loose bucket. */
|
|
83
|
+
project: string | null;
|
|
84
|
+
/** Active occurrences of this value in this session. */
|
|
85
|
+
count: number;
|
|
86
|
+
/** Most recent detected_at across this session's occurrences (ISO). */
|
|
87
|
+
lastSeen: string;
|
|
88
|
+
}
|
|
89
|
+
/** Scope of a Dismiss action.
|
|
90
|
+
* session — add to allowlist_session for this finding's session
|
|
91
|
+
* global — add to allowlist_global; all matching findings across
|
|
92
|
+
* every session auto-dismiss on next scan */
|
|
93
|
+
export interface DismissScope {
|
|
94
|
+
scope: 'session' | 'global';
|
|
95
|
+
/** Required when scope === 'session'. */
|
|
96
|
+
sessionId?: number;
|
|
97
|
+
}
|
|
98
|
+
/** Change events emitted on the worker's PubSub stream and forwarded
|
|
99
|
+
* to renderers via IPC. Consumers should treat unknown `type` values
|
|
100
|
+
* as no-ops (forward-compat). */
|
|
101
|
+
export type FindingsChange = {
|
|
102
|
+
type: 'inserted';
|
|
103
|
+
sessionId: number;
|
|
104
|
+
} | {
|
|
105
|
+
type: 'state-changed';
|
|
106
|
+
sessionId: number;
|
|
107
|
+
findingId: number;
|
|
108
|
+
state: FindingState;
|
|
109
|
+
} | {
|
|
110
|
+
type: 'session-rescanned';
|
|
111
|
+
sessionId: number;
|
|
112
|
+
};
|
|
113
|
+
/** Snapshot of the scan worker's transient state. The DB stores
|
|
114
|
+
* scan_profile / scan_completed_at; queued and scanning are
|
|
115
|
+
* worker-memory only — they reset on restart. */
|
|
116
|
+
export interface ScanStatus {
|
|
117
|
+
/** Sessions waiting for a slot in the worker queue. */
|
|
118
|
+
queued: number;
|
|
119
|
+
/** Session currently being scanned, or null when idle. */
|
|
120
|
+
scanning: number | null;
|
|
121
|
+
/** When in backfill mode, sessions still to process; 0 otherwise. */
|
|
122
|
+
backfillRemaining: number;
|
|
123
|
+
/** High-water mark for the active scan burst — the total session
|
|
124
|
+
* count this burst has set out to scan. Held until the worker
|
|
125
|
+
* goes fully idle (queued + scanning + backfillRemaining all 0),
|
|
126
|
+
* then resets to 0. Lets the renderer's progress bar survive
|
|
127
|
+
* navigation away from + back to the Security page mid-scan
|
|
128
|
+
* without "restarting" at the remaining-now count. */
|
|
129
|
+
backfillTotal: number;
|
|
130
|
+
/** True for the lifetime of a user-initiated `Rescan all` burst
|
|
131
|
+
* (set by `worker.rescanAll()`; cleared automatically when the
|
|
132
|
+
* worker goes fully idle). The renderer reads this directly
|
|
133
|
+
* instead of tracking the click locally — a renderer-side flag
|
|
134
|
+
* would race against any auto sync-driven enqueue that completed
|
|
135
|
+
* between the click and the IPC reaching the worker, causing
|
|
136
|
+
* the manual ACK banner to be hijacked + cleared by the auto
|
|
137
|
+
* burst's idle edge. Tying it to the worker keeps a single
|
|
138
|
+
* source of truth. */
|
|
139
|
+
manualBurstInFlight: boolean;
|
|
140
|
+
/** Composite identifier for the active provider set, e.g.
|
|
141
|
+
* 'regex@3' or 'regex@3,pf@1.5b-q4'. Persisted on
|
|
142
|
+
* sessions.scan_profile to detect rescan candidates after
|
|
143
|
+
* provider configuration changes. */
|
|
144
|
+
currentProfile: string;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/security/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Shared types for the Security Scan subsystem.
|
|
2
|
+
//
|
|
3
|
+
// These are the cross-boundary payloads — worker → IPC → renderer.
|
|
4
|
+
// They live in one module so:
|
|
5
|
+
// 1. The worker (main process), IPC adapters, and renderer hooks
|
|
6
|
+
// can import a single source of truth.
|
|
7
|
+
// 2. Promoting them to `Schema.Struct` later (for a Scope-B Effect
|
|
8
|
+
// RPC migration) is a local refactor, not a consumer rewrite.
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/security/types.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,mEAAmE;AACnE,8BAA8B;AAC9B,mEAAmE;AACnE,4CAA4C;AAC5C,qEAAqE;AACrE,mEAAmE"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Effect, Fiber, Stream, Scope } from 'effect';
|
|
2
|
+
import type Database from 'better-sqlite3';
|
|
3
|
+
import type { RedactProvider } from '@spool-lab/redact';
|
|
4
|
+
import type { FindingsChange, ScanStatus } from './types.js';
|
|
5
|
+
export interface WorkerConfig {
|
|
6
|
+
db: Database.Database;
|
|
7
|
+
providers: readonly RedactProvider[];
|
|
8
|
+
/** Profile string for `providers`. The worker rejects mismatched
|
|
9
|
+
* pairs (you can't claim profile = 'regex@3,pf@1.5b' with only
|
|
10
|
+
* regexProvider in `providers`).
|
|
11
|
+
*
|
|
12
|
+
* Pass either a static string (test fixtures) or a thunk (prod —
|
|
13
|
+
* lets the profile track pref changes like `kindAllowlist` so
|
|
14
|
+
* toggling a kind triggers backfill via `listSessionsNeedingScan`
|
|
15
|
+
* on the next call). The worker resolves the thunk on every read
|
|
16
|
+
* rather than caching, so a pref save → SET_PREFS handler can
|
|
17
|
+
* invalidate sessions and immediately see them as stale. */
|
|
18
|
+
currentProfile: string | (() => string);
|
|
19
|
+
/** Names of providers active in this profile. */
|
|
20
|
+
providerNames: readonly string[];
|
|
21
|
+
/** Live accessor for the kind-level allowlist. Read on every scan
|
|
22
|
+
* (cheap — a Set lookup per match), so changing the preference
|
|
23
|
+
* takes effect on the next rescan without restarting the worker. */
|
|
24
|
+
getKindAllowlist?: () => ReadonlySet<string>;
|
|
25
|
+
}
|
|
26
|
+
export interface ScanWorker {
|
|
27
|
+
/** Drop a session at the back of the queue. */
|
|
28
|
+
enqueue: (sessionId: number) => Effect.Effect<void>;
|
|
29
|
+
/** Set every session's profile to NULL and enqueue them — used by
|
|
30
|
+
* "Rescan all" and by provider-set changes. */
|
|
31
|
+
rescanAll: () => Effect.Effect<number>;
|
|
32
|
+
/** Enqueue every session whose scan_profile != currentProfile.
|
|
33
|
+
* Called on boot AND on every profile-affecting pref change
|
|
34
|
+
* (kindAllowlist toggle, PF on/off, etc.).
|
|
35
|
+
*
|
|
36
|
+
* Treated as a fresh burst: any leftover items still in the queue
|
|
37
|
+
* from the previous burst are discarded (they were enqueued against
|
|
38
|
+
* an outdated profile; the current `listSessionsNeedingScan` call
|
|
39
|
+
* is the authoritative new work set). `backfillTotal` resets to the
|
|
40
|
+
* new stale-set size instead of sticky-maxing with the old burst's
|
|
41
|
+
* value, so the renderer's progress bar reflects the new work
|
|
42
|
+
* rather than the union of the two.
|
|
43
|
+
*
|
|
44
|
+
* Pass `userInitiated: true` when the caller is a SET_PREFS / PF
|
|
45
|
+
* toggle handler so the renderer's busy→idle edge surfaces a
|
|
46
|
+
* "Scan complete" result banner the same way it does for the
|
|
47
|
+
* Rescan-all button. */
|
|
48
|
+
backfill: (opts?: {
|
|
49
|
+
userInitiated?: boolean;
|
|
50
|
+
}) => Effect.Effect<number>;
|
|
51
|
+
/** Stream of finding change events. Translator in main process
|
|
52
|
+
* subscribes and forwards over IPC. */
|
|
53
|
+
changes: Stream.Stream<FindingsChange>;
|
|
54
|
+
/** Stream of status updates — emitted on every queued / scanning /
|
|
55
|
+
* backfillRemaining mutation. Lets the UI replace polling with a
|
|
56
|
+
* push subscription. */
|
|
57
|
+
statusChanges: Stream.Stream<ScanStatus>;
|
|
58
|
+
/** Current snapshot. UI uses this for the first read; subsequent
|
|
59
|
+
* updates land via `statusChanges`. */
|
|
60
|
+
getStatus: Effect.Effect<ScanStatus>;
|
|
61
|
+
}
|
|
62
|
+
/** Build a worker as a Scoped effect: when the scope closes, the
|
|
63
|
+
* draining fiber is interrupted cleanly and the PubSub shuts down. */
|
|
64
|
+
export declare function makeScanWorker(config: WorkerConfig): Effect.Effect<ScanWorker, never, Scope.Scope>;
|
|
65
|
+
/** Helper for tests / callers that want a complete drain promise
|
|
66
|
+
* (e.g. wait for the queue to empty after a backfill). */
|
|
67
|
+
export declare function waitForIdle(worker: ScanWorker): Effect.Effect<void>;
|
|
68
|
+
export { Fiber };
|
|
69
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/security/worker.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAsB,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACzE,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAGvD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5D,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAA;IACrB,SAAS,EAAE,SAAS,cAAc,EAAE,CAAA;IACpC;;;;;;;;;iEAS6D;IAC7D,cAAc,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IACvC,iDAAiD;IACjD,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC;;yEAEqE;IACrE,gBAAgB,CAAC,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;CAC7C;AAQD,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACnD;oDACgD;IAChD,SAAS,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACtC;;;;;;;;;;;;;;;6BAeyB;IACzB,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACvE;4CACwC;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAA;IACtC;;6BAEyB;IACzB,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACxC;4CACwC;IACxC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;CACrC;AAED;uEACuE;AACvE,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,GACnB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAoO/C;AAED;2DAC2D;AAC3D,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAQnE;AAGD,OAAO,EAAE,KAAK,EAAE,CAAA"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Scan worker — the long-lived coordinator that drains a session
|
|
2
|
+
// queue, runs `scanSession` per item, and publishes change events.
|
|
3
|
+
//
|
|
4
|
+
// Implementation: Effect Scope A. Inside this module is Effect-shaped
|
|
5
|
+
// (Queue, Stream, PubSub, Ref, Scope). The boundary — IPC handlers
|
|
6
|
+
// in `packages/app/src/main/ipc/security.ts` — wraps `runFiber` /
|
|
7
|
+
// `runPromise` so renderers see plain Promise-shaped channels.
|
|
8
|
+
//
|
|
9
|
+
// Single-fiber by design (`concurrency: 1` on the queue stream).
|
|
10
|
+
// Better-sqlite3 is synchronous, so multiple parallel fibers compete
|
|
11
|
+
// for the DB lock and gain nothing. Backfill UX surfaces "session N
|
|
12
|
+
// of M" progress for the user.
|
|
13
|
+
import { Effect, Fiber, PubSub, Queue, Ref, Stream } from 'effect';
|
|
14
|
+
import { scanSession } from './scan.js';
|
|
15
|
+
import { listSessionsNeedingScan } from './repo.js';
|
|
16
|
+
function resolveProfile(config) {
|
|
17
|
+
return typeof config.currentProfile === 'function'
|
|
18
|
+
? config.currentProfile()
|
|
19
|
+
: config.currentProfile;
|
|
20
|
+
}
|
|
21
|
+
/** Build a worker as a Scoped effect: when the scope closes, the
|
|
22
|
+
* draining fiber is interrupted cleanly and the PubSub shuts down. */
|
|
23
|
+
export function makeScanWorker(config) {
|
|
24
|
+
return Effect.gen(function* () {
|
|
25
|
+
const queue = yield* Queue.unbounded();
|
|
26
|
+
const events = yield* PubSub.unbounded();
|
|
27
|
+
const statusEvents = yield* PubSub.unbounded();
|
|
28
|
+
const statusRef = yield* Ref.make({
|
|
29
|
+
queued: 0,
|
|
30
|
+
scanning: null,
|
|
31
|
+
backfillRemaining: 0,
|
|
32
|
+
backfillTotal: 0,
|
|
33
|
+
manualBurstInFlight: false,
|
|
34
|
+
currentProfile: resolveProfile(config),
|
|
35
|
+
});
|
|
36
|
+
const publish = (c) => PubSub.publish(events, c).pipe(Effect.asVoid);
|
|
37
|
+
// Update statusRef AND publish the new snapshot so subscribers get
|
|
38
|
+
// a push event for every mutation. One helper at the seam avoids
|
|
39
|
+
// forgetting to publish from a future Ref.update site.
|
|
40
|
+
//
|
|
41
|
+
// `backfillTotal` is derived here so individual mutation sites
|
|
42
|
+
// (enqueue / rescanAll / backfill / scanOne) don't have to
|
|
43
|
+
// remember to bump it. The renderer's progress bar reads
|
|
44
|
+
// `backfillRemaining + (scanning ? 1 : 0)` as the active count;
|
|
45
|
+
// we track the high-water mark of that quantity since the last
|
|
46
|
+
// full-idle state, and reset to 0 once everything drains. Survives
|
|
47
|
+
// navigation: a user who switches tabs mid-burst and comes back
|
|
48
|
+
// gets the true original total via `getStatus`, not the
|
|
49
|
+
// remaining-at-remount-time approximation.
|
|
50
|
+
const updateStatus = (f) => Ref.update(statusRef, (prev) => {
|
|
51
|
+
const after = f(prev);
|
|
52
|
+
// High-water mark of *backfillRemaining alone*. Including the
|
|
53
|
+
// scanning slot (br + 1 when scanning != null) bumped the
|
|
54
|
+
// total by one every time a scanOne started, and dropped it
|
|
55
|
+
// again when scanning went back to null between sessions —
|
|
56
|
+
// turning the renderer's `(total - remaining)/total` into a
|
|
57
|
+
// non-monotonic value that visibly rewound the progress bar
|
|
58
|
+
// on every cross-session tick. backfillRemaining itself is
|
|
59
|
+
// strictly monotonic (only decrements at scanOne end), so
|
|
60
|
+
// anchoring the total to it makes the progress bar strictly
|
|
61
|
+
// forward.
|
|
62
|
+
const fullyIdle = after.backfillRemaining === 0 &&
|
|
63
|
+
after.queued === 0 &&
|
|
64
|
+
after.scanning === null;
|
|
65
|
+
const backfillTotal = fullyIdle
|
|
66
|
+
? 0
|
|
67
|
+
: Math.max(prev.backfillTotal, after.backfillRemaining);
|
|
68
|
+
// `manualBurstInFlight` rides along with the burst. The
|
|
69
|
+
// worker.rescanAll() mutation flips it on; this wrapper
|
|
70
|
+
// flips it off the instant the worker drains, so the
|
|
71
|
+
// renderer's "your manual scan finished" detection never
|
|
72
|
+
// sees a stale truth across burst boundaries.
|
|
73
|
+
const manualBurstInFlight = fullyIdle ? false : after.manualBurstInFlight;
|
|
74
|
+
return { ...after, backfillTotal, manualBurstInFlight };
|
|
75
|
+
}).pipe(Effect.zipRight(Ref.get(statusRef)), Effect.flatMap((s) => PubSub.publish(statusEvents, s)), Effect.asVoid);
|
|
76
|
+
// Burst-reset variant for backfill() (and any future caller that
|
|
77
|
+
// means "start a fresh burst, discard whatever the previous one
|
|
78
|
+
// had queued"). Two differences from updateStatus:
|
|
79
|
+
//
|
|
80
|
+
// 1. The `Math.max(prev.backfillTotal, after.backfillRemaining)`
|
|
81
|
+
// sticky guard is BYPASSED — the caller is declaring a brand-
|
|
82
|
+
// new burst, so its new `backfillRemaining` becomes the
|
|
83
|
+
// authoritative `backfillTotal`. Without this, a toggle-pref
|
|
84
|
+
// mid-scan would leave the renderer showing the OLD burst's
|
|
85
|
+
// total while the new burst's much smaller stale set drives
|
|
86
|
+
// the remaining count → progress bar visually pinned at
|
|
87
|
+
// ~100% for as long as the queue takes to drain.
|
|
88
|
+
//
|
|
89
|
+
// 2. The Effect Queue is drained synchronously. Items left in
|
|
90
|
+
// it were enqueued against the previous profile and were
|
|
91
|
+
// already accounted for in the OLD burst's
|
|
92
|
+
// backfillRemaining. With the new totals in force they'd
|
|
93
|
+
// keep `nowBusy = queued > 0` true (banner-stuck-at-100%
|
|
94
|
+
// problem) and waste regex-only scans on sessions whose
|
|
95
|
+
// profile already matches the current one.
|
|
96
|
+
const resetBurst = (f) => Effect.gen(function* () {
|
|
97
|
+
yield* Queue.takeAll(queue);
|
|
98
|
+
yield* Ref.update(statusRef, (prev) => {
|
|
99
|
+
const after = f(prev);
|
|
100
|
+
const fullyIdle = after.backfillRemaining === 0 &&
|
|
101
|
+
after.queued === 0 &&
|
|
102
|
+
after.scanning === null;
|
|
103
|
+
const backfillTotal = fullyIdle ? 0 : after.backfillRemaining;
|
|
104
|
+
const manualBurstInFlight = fullyIdle ? false : after.manualBurstInFlight;
|
|
105
|
+
return { ...after, backfillTotal, manualBurstInFlight };
|
|
106
|
+
});
|
|
107
|
+
const snap = yield* Ref.get(statusRef);
|
|
108
|
+
yield* PubSub.publish(statusEvents, snap);
|
|
109
|
+
});
|
|
110
|
+
const scanOne = (sessionId) => Effect.gen(function* () {
|
|
111
|
+
yield* updateStatus((s) => ({
|
|
112
|
+
...s,
|
|
113
|
+
scanning: sessionId,
|
|
114
|
+
queued: Math.max(0, s.queued - 1),
|
|
115
|
+
}));
|
|
116
|
+
const result = yield* scanSession(sessionId, {
|
|
117
|
+
db: config.db,
|
|
118
|
+
providers: config.providers,
|
|
119
|
+
currentProfile: resolveProfile(config),
|
|
120
|
+
providerNames: config.providerNames,
|
|
121
|
+
publish,
|
|
122
|
+
...(config.getKindAllowlist ? { kindAllowlist: config.getKindAllowlist() } : {}),
|
|
123
|
+
}).pipe(Effect.catchAll((err) => Effect.logError(`scanSession failed for ${sessionId}: ${err.reason}`, err).pipe(Effect.as(null))));
|
|
124
|
+
yield* updateStatus((s) => ({
|
|
125
|
+
...s,
|
|
126
|
+
scanning: null,
|
|
127
|
+
backfillRemaining: Math.max(0, s.backfillRemaining - 1),
|
|
128
|
+
}));
|
|
129
|
+
return result;
|
|
130
|
+
});
|
|
131
|
+
// The drain fiber runs for the worker's lifetime; Scope.addFinalizer
|
|
132
|
+
// interrupts it when the runtime tears down.
|
|
133
|
+
//
|
|
134
|
+
// Throttle: yield 50ms between scans. better-sqlite3 is synchronous
|
|
135
|
+
// so the DB lock is held for the full scanOne duration (~50-100ms
|
|
136
|
+
// per session). Without a yield, a backfill of N sessions becomes
|
|
137
|
+
// ~N*scanTime of uninterrupted lock contention — foreground reads
|
|
138
|
+
// (listSessions, FTS search) get queued behind every scan. The
|
|
139
|
+
// sleep gives the event loop a window to flush foreground IPC
|
|
140
|
+
// handlers before grabbing the next session. Doubles backfill
|
|
141
|
+
// duration but keeps the app responsive.
|
|
142
|
+
const drain = Stream.fromQueue(queue).pipe(Stream.mapEffect((sessionId) => scanOne(sessionId).pipe(Effect.tap(() => Effect.sleep('50 millis'))), { concurrency: 1 }), Stream.runDrain);
|
|
143
|
+
yield* Effect.forkScoped(drain);
|
|
144
|
+
const enqueue = (sessionId) => Effect.gen(function* () {
|
|
145
|
+
yield* Queue.offer(queue, sessionId);
|
|
146
|
+
yield* updateStatus((s) => ({ ...s, queued: s.queued + 1 }));
|
|
147
|
+
});
|
|
148
|
+
const rescanAll = () => Effect.gen(function* () {
|
|
149
|
+
// SELECT and UPDATE share one transaction so a concurrent sync
|
|
150
|
+
// that inserts a new session can't slip between them — the
|
|
151
|
+
// new row would otherwise miss the profile NULL pass AND be
|
|
152
|
+
// omitted from `all`, leaving it silently skipped by the
|
|
153
|
+
// drain fiber even though the user pressed "Rescan all". With
|
|
154
|
+
// a transaction the worker either sees every session present
|
|
155
|
+
// at the moment of the click, or sees the post-insert state
|
|
156
|
+
// (in which case the new session participates).
|
|
157
|
+
const all = yield* Effect.sync(() => config.db.transaction(() => {
|
|
158
|
+
const rows = config.db
|
|
159
|
+
.prepare('SELECT id FROM sessions ORDER BY started_at DESC')
|
|
160
|
+
.all();
|
|
161
|
+
config.db.prepare('UPDATE sessions SET scan_profile = NULL').run();
|
|
162
|
+
return rows;
|
|
163
|
+
})());
|
|
164
|
+
yield* updateStatus((s) => ({
|
|
165
|
+
...s,
|
|
166
|
+
backfillRemaining: all.length,
|
|
167
|
+
// Flag this as a user-initiated burst — the renderer
|
|
168
|
+
// hangs its result-banner ACK off this field instead of
|
|
169
|
+
// a click-local flag.
|
|
170
|
+
manualBurstInFlight: true,
|
|
171
|
+
}));
|
|
172
|
+
for (const r of all) {
|
|
173
|
+
yield* enqueue(r.id);
|
|
174
|
+
}
|
|
175
|
+
yield* Effect.annotateCurrentSpan('enqueued', all.length);
|
|
176
|
+
return all.length;
|
|
177
|
+
}).pipe(Effect.withSpan('security.worker.rescan_all'));
|
|
178
|
+
const backfill = (opts) => Effect.gen(function* () {
|
|
179
|
+
// Re-resolve the profile each call so a SET_PREFS handler that
|
|
180
|
+
// changed kindAllowlist sees the updated hash before invoking
|
|
181
|
+
// backfill.
|
|
182
|
+
const profile = resolveProfile(config);
|
|
183
|
+
const stale = yield* Effect.sync(() => listSessionsNeedingScan(config.db, profile));
|
|
184
|
+
// Fresh burst: drain any leftover queue items (they belong to
|
|
185
|
+
// a previous profile snapshot — see `resetBurst` notes), reset
|
|
186
|
+
// queued/backfillRemaining/backfillTotal to this burst's stale
|
|
187
|
+
// count, and stamp the user-initiated flag if applicable. The
|
|
188
|
+
// wrapper bypasses the sticky-max guard so the renderer's
|
|
189
|
+
// progress bar tracks the new burst, not the old.
|
|
190
|
+
yield* resetBurst((s) => ({
|
|
191
|
+
...s,
|
|
192
|
+
currentProfile: profile,
|
|
193
|
+
queued: 0,
|
|
194
|
+
backfillRemaining: stale.length,
|
|
195
|
+
manualBurstInFlight: opts?.userInitiated === true,
|
|
196
|
+
}));
|
|
197
|
+
for (const id of stale) {
|
|
198
|
+
yield* enqueue(id);
|
|
199
|
+
}
|
|
200
|
+
yield* Effect.annotateCurrentSpan('enqueued', stale.length);
|
|
201
|
+
yield* Effect.annotateCurrentSpan('userInitiated', opts?.userInitiated === true);
|
|
202
|
+
return stale.length;
|
|
203
|
+
}).pipe(Effect.withSpan('security.worker.backfill'));
|
|
204
|
+
return {
|
|
205
|
+
enqueue,
|
|
206
|
+
rescanAll,
|
|
207
|
+
backfill,
|
|
208
|
+
changes: Stream.fromPubSub(events),
|
|
209
|
+
statusChanges: Stream.fromPubSub(statusEvents),
|
|
210
|
+
getStatus: Ref.get(statusRef),
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/** Helper for tests / callers that want a complete drain promise
|
|
215
|
+
* (e.g. wait for the queue to empty after a backfill). */
|
|
216
|
+
export function waitForIdle(worker) {
|
|
217
|
+
return Effect.gen(function* () {
|
|
218
|
+
while (true) {
|
|
219
|
+
const s = yield* worker.getStatus;
|
|
220
|
+
if (s.queued === 0 && s.scanning === null && s.backfillRemaining === 0)
|
|
221
|
+
return;
|
|
222
|
+
yield* Effect.sleep('25 millis');
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// Re-export Fiber for callers that want to manage runtime lifecycle.
|
|
227
|
+
export { Fiber };
|
|
228
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/security/worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,4 +2,5 @@ import type { SessionSource } from '../types.js';
|
|
|
2
2
|
export declare function getSessionRoots(source: SessionSource): string[];
|
|
3
3
|
export declare function detectSessionSource(filePath: string, sourceRoots?: Record<SessionSource, string[]>): SessionSource | undefined;
|
|
4
4
|
export declare function getSessionWatchPatterns(source: SessionSource, roots?: string[]): string[];
|
|
5
|
+
export declare function isSessionFileForSource(source: SessionSource, filePath: string, root: string): boolean;
|
|
5
6
|
//# sourceMappingURL=source-paths.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-paths.d.ts","sourceRoot":"","sources":["../../src/sync/source-paths.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"source-paths.d.ts","sourceRoot":"","sources":["../../src/sync/source-paths.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAyBhD,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,EAAE,CAgC/D;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAK1C,GACA,aAAa,GAAG,SAAS,CAO3B;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,aAAa,EACrB,KAAK,WAA0B,GAC9B,MAAM,EAAE,CAOV;AAiED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAoBrG"}
|