@skillsmith/core 0.10.0 → 0.11.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/CHANGELOG.md +4 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/analysis/tree-sitter/pythonIncremental.d.ts +23 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.d.ts.map +1 -1
- package/dist/src/analysis/tree-sitter/pythonIncremental.hardening.test.js +137 -0
- package/dist/src/analysis/tree-sitter/pythonIncremental.hardening.test.js.map +1 -1
- package/dist/src/analysis/tree-sitter/pythonIncremental.js +83 -10
- package/dist/src/analysis/tree-sitter/pythonIncremental.js.map +1 -1
- package/dist/src/analysis/tree-sitter/queryExtractionMatchesOrExceedsRegex.test.js +6 -1
- package/dist/src/analysis/tree-sitter/queryExtractionMatchesOrExceedsRegex.test.js.map +1 -1
- package/dist/src/config/audit-notify-state.d.ts +46 -0
- package/dist/src/config/audit-notify-state.d.ts.map +1 -0
- package/dist/src/config/audit-notify-state.js +55 -0
- package/dist/src/config/audit-notify-state.js.map +1 -0
- package/dist/src/config/audit-notify-state.test.d.ts +9 -0
- package/dist/src/config/audit-notify-state.test.d.ts.map +1 -0
- package/dist/src/config/audit-notify-state.test.js +63 -0
- package/dist/src/config/audit-notify-state.test.js.map +1 -0
- package/dist/src/config/index.d.ts +10 -0
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js.map +1 -1
- package/dist/src/exports/services.d.ts +1 -1
- package/dist/src/exports/services.d.ts.map +1 -1
- package/dist/src/exports/services.js +3 -1
- package/dist/src/exports/services.js.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/logging/context.d.ts +58 -0
- package/dist/src/logging/context.d.ts.map +1 -0
- package/dist/src/logging/context.js +62 -0
- package/dist/src/logging/context.js.map +1 -0
- package/dist/src/logging/context.test.d.ts +14 -0
- package/dist/src/logging/context.test.d.ts.map +1 -0
- package/dist/src/logging/context.test.js +88 -0
- package/dist/src/logging/context.test.js.map +1 -0
- package/dist/src/logging/index.d.ts +13 -0
- package/dist/src/logging/index.d.ts.map +1 -0
- package/dist/src/logging/index.js +12 -0
- package/dist/src/logging/index.js.map +1 -0
- package/dist/src/logging/logger.d.ts +71 -0
- package/dist/src/logging/logger.d.ts.map +1 -0
- package/dist/src/logging/logger.js +264 -0
- package/dist/src/logging/logger.js.map +1 -0
- package/dist/src/logging/logger.test.d.ts +9 -0
- package/dist/src/logging/logger.test.d.ts.map +1 -0
- package/dist/src/logging/logger.test.js +320 -0
- package/dist/src/logging/logger.test.js.map +1 -0
- package/dist/src/logging/redact.d.ts +35 -0
- package/dist/src/logging/redact.d.ts.map +1 -0
- package/dist/src/logging/redact.js +152 -0
- package/dist/src/logging/redact.js.map +1 -0
- package/dist/src/logging/redact.test.d.ts +8 -0
- package/dist/src/logging/redact.test.d.ts.map +1 -0
- package/dist/src/logging/redact.test.js +330 -0
- package/dist/src/logging/redact.test.js.map +1 -0
- package/dist/src/logging/rotation.d.ts +75 -0
- package/dist/src/logging/rotation.d.ts.map +1 -0
- package/dist/src/logging/rotation.js +284 -0
- package/dist/src/logging/rotation.js.map +1 -0
- package/dist/src/logging/rotation.test.d.ts +11 -0
- package/dist/src/logging/rotation.test.d.ts.map +1 -0
- package/dist/src/logging/rotation.test.js +132 -0
- package/dist/src/logging/rotation.test.js.map +1 -0
- package/dist/src/logging/types.d.ts +69 -0
- package/dist/src/logging/types.d.ts.map +1 -0
- package/dist/src/logging/types.js +9 -0
- package/dist/src/logging/types.js.map +1 -0
- package/dist/src/security/index.d.ts +2 -0
- package/dist/src/security/index.d.ts.map +1 -1
- package/dist/src/security/index.js +4 -0
- package/dist/src/security/index.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.hostile-update.d.ts +46 -0
- package/dist/src/security/scanner/SecurityScanner.hostile-update.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.hostile-update.js +209 -0
- package/dist/src/security/scanner/SecurityScanner.hostile-update.js.map +1 -0
- package/dist/src/security/scanner/index.d.ts +2 -1
- package/dist/src/security/scanner/index.d.ts.map +1 -1
- package/dist/src/security/scanner/index.js +2 -0
- package/dist/src/security/scanner/index.js.map +1 -1
- package/dist/src/security/scanner/types.d.ts +21 -0
- package/dist/src/security/scanner/types.d.ts.map +1 -1
- package/dist/src/services/skill-installation.io.d.ts.map +1 -1
- package/dist/src/services/skill-installation.io.js +19 -2
- package/dist/src/services/skill-installation.io.js.map +1 -1
- package/dist/src/sync/access-token.d.ts +27 -0
- package/dist/src/sync/access-token.d.ts.map +1 -0
- package/dist/src/sync/access-token.js +40 -0
- package/dist/src/sync/access-token.js.map +1 -0
- package/dist/src/sync/audit-notify-client.d.ts +83 -0
- package/dist/src/sync/audit-notify-client.d.ts.map +1 -0
- package/dist/src/sync/audit-notify-client.js +126 -0
- package/dist/src/sync/audit-notify-client.js.map +1 -0
- package/dist/src/sync/audit-notify-client.test.d.ts +14 -0
- package/dist/src/sync/audit-notify-client.test.d.ts.map +1 -0
- package/dist/src/sync/audit-notify-client.test.js +133 -0
- package/dist/src/sync/audit-notify-client.test.js.map +1 -0
- package/dist/src/sync/index.d.ts +1 -0
- package/dist/src/sync/index.d.ts.map +1 -1
- package/dist/src/sync/index.js +2 -0
- package/dist/src/sync/index.js.map +1 -1
- package/dist/src/sync/inventory-client.d.ts.map +1 -1
- package/dist/src/sync/inventory-client.js +5 -16
- package/dist/src/sync/inventory-client.js.map +1 -1
- package/dist/src/telemetry/posthog.d.ts +15 -0
- package/dist/src/telemetry/posthog.d.ts.map +1 -1
- package/dist/src/telemetry/posthog.js +6 -1
- package/dist/src/telemetry/posthog.js.map +1 -1
- package/dist/src/telemetry/wrap.bench.js +5 -0
- package/dist/src/telemetry/wrap.bench.js.map +1 -1
- package/dist/src/telemetry/wrap.correlation.test.d.ts +20 -0
- package/dist/src/telemetry/wrap.correlation.test.d.ts.map +1 -0
- package/dist/src/telemetry/wrap.correlation.test.js +174 -0
- package/dist/src/telemetry/wrap.correlation.test.js.map +1 -0
- package/dist/src/telemetry/wrap.d.ts.map +1 -1
- package/dist/src/telemetry/wrap.gate.test.js +5 -0
- package/dist/src/telemetry/wrap.gate.test.js.map +1 -1
- package/dist/src/telemetry/wrap.js +111 -52
- package/dist/src/telemetry/wrap.js.map +1 -1
- package/dist/src/telemetry/wrap.marker.test.js +5 -0
- package/dist/src/telemetry/wrap.marker.test.js.map +1 -1
- package/dist/src/telemetry/wrap.test.js +10 -0
- package/dist/src/telemetry/wrap.test.js.map +1 -1
- package/dist/src/types.d.ts +6 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/tests/security/scanner-regression-guard.test.js +179 -1
- package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-5615: Per-surface serialized JSONL writer with daily/size rollover and
|
|
3
|
+
* retention sweep.
|
|
4
|
+
*
|
|
5
|
+
* Write-serialization invariant (F2, from
|
|
6
|
+
* docs/internal/implementation/production-error-logging.md — the plan's
|
|
7
|
+
* original P-5 draft omitted this; it was the Opus `concurrency-auditor`
|
|
8
|
+
* plan-audit's most likely real-world race):
|
|
9
|
+
*
|
|
10
|
+
* A redacted error record (stack ≤20 frames) can exceed `PIPE_BUF`
|
|
11
|
+
* (4096 bytes), so concurrent, independent `appendFile`/`appendFileSync`
|
|
12
|
+
* calls from parallel async tool invocations are NOT safe here — two
|
|
13
|
+
* large concurrent writes can interleave/tear into one invalid JSONL line.
|
|
14
|
+
*
|
|
15
|
+
* Fix: one long-lived `fs.createWriteStream(path, { flags: 'a' })` per
|
|
16
|
+
* surface, created lazily on first write and cached in a module-scoped
|
|
17
|
+
* map. ALL writes for a given surface go through THIS single stream.
|
|
18
|
+
*
|
|
19
|
+
* That alone (Node serializing writes queued on one stream) would already
|
|
20
|
+
* prevent tearing between two writes to the SAME stream object. This
|
|
21
|
+
* module goes one step further for simplicity and an even stronger
|
|
22
|
+
* guarantee: every write is wrapped in an async mutex (`runExclusive`) keyed
|
|
23
|
+
* by surface, so at most one write is ever in flight to a surface's
|
|
24
|
+
* underlying file descriptor at a time — including the write that decides
|
|
25
|
+
* whether a daily/size rollover is needed. That decision-and-possible-swap
|
|
26
|
+
* (open a new stream, `end()` the old one) happens INSIDE the same
|
|
27
|
+
* exclusive section as the write that triggered it, so a rotation can
|
|
28
|
+
* never race a write: no write can land on a stream that has already been
|
|
29
|
+
* told to `end()`, and no two writers can simultaneously decide "today's
|
|
30
|
+
* file doesn't exist yet" and both create a stream.
|
|
31
|
+
*
|
|
32
|
+
* Daily rollover: `skillsmith-<surface>-<YYYY-MM-DD>.jsonl`, keyed off the
|
|
33
|
+
* calendar date at write time (mocked by `vi.setSystemTime` in tests — no
|
|
34
|
+
* separate clock seam needed).
|
|
35
|
+
*
|
|
36
|
+
* Size cap: ~10MB per file. When the currently-open file for TODAY would
|
|
37
|
+
* exceed the cap, roll to a `.1`/`.2`/... suffixed continuation file for the
|
|
38
|
+
* same day (`skillsmith-<surface>-<YYYY-MM-DD>.jsonl.<n>`), through the same
|
|
39
|
+
* serialization point as everything else.
|
|
40
|
+
*
|
|
41
|
+
* Retention: on module init, asynchronously (NOT awaited — never blocks
|
|
42
|
+
* import) sweep `~/.skillsmith/logs/` and delete files whose mtime is older
|
|
43
|
+
* than 14 days. `pruneExpiredLogs` is also exported directly so tests can
|
|
44
|
+
* await a deterministic run against a temp directory instead of racing the
|
|
45
|
+
* fire-and-forget module-init sweep.
|
|
46
|
+
*/
|
|
47
|
+
import { createWriteStream, existsSync, statSync } from 'node:fs';
|
|
48
|
+
import { mkdir, readdir, stat, unlink } from 'node:fs/promises';
|
|
49
|
+
import { homedir } from 'node:os';
|
|
50
|
+
import { join } from 'node:path';
|
|
51
|
+
const SIZE_CAP_BYTES = 10 * 1024 * 1024; // ~10MB per file (F2)
|
|
52
|
+
const RETENTION_DAYS = 14;
|
|
53
|
+
/**
|
|
54
|
+
* Log directory. Overridable via `SKILLSMITH_LOG_DIR` — a test-only seam
|
|
55
|
+
* (not part of the plan's documented env-var surface) so `rotation.test.ts`
|
|
56
|
+
* can point writes/sweeps at a temp directory instead of the real
|
|
57
|
+
* `~/.skillsmith/logs/`. Read lazily (not frozen at import time) so a test
|
|
58
|
+
* can set the env var before its first write without needing
|
|
59
|
+
* `vi.resetModules()`.
|
|
60
|
+
*/
|
|
61
|
+
function getLogDir() {
|
|
62
|
+
return process.env.SKILLSMITH_LOG_DIR || join(homedir(), '.skillsmith', 'logs');
|
|
63
|
+
}
|
|
64
|
+
function dailyFilePath(surface, date) {
|
|
65
|
+
return join(getLogDir(), `skillsmith-${surface}-${date}.jsonl`);
|
|
66
|
+
}
|
|
67
|
+
/** Next available `.N` continuation suffix for today's file, same day only. */
|
|
68
|
+
function nextRolledFilePath(surface, date) {
|
|
69
|
+
const base = dailyFilePath(surface, date);
|
|
70
|
+
let n = 1;
|
|
71
|
+
while (existsSync(`${base}.${n}`))
|
|
72
|
+
n++;
|
|
73
|
+
return `${base}.${n}`;
|
|
74
|
+
}
|
|
75
|
+
function statSizeOrZero(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
return statSync(filePath).size;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function todayDateString() {
|
|
84
|
+
return new Date().toISOString().split('T')[0];
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Async mutex — one FIFO queue per surface. `runExclusive` serializes both
|
|
88
|
+
// the rollover decision AND the write itself, so at most one write is ever
|
|
89
|
+
// in flight per surface (F2).
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
const queueTails = new Map();
|
|
92
|
+
function runExclusive(surface, task) {
|
|
93
|
+
const tail = queueTails.get(surface) ?? Promise.resolve();
|
|
94
|
+
// Run `task` after the prior tail settles, whether it resolved or
|
|
95
|
+
// rejected — one surface's failed write must not permanently jam the
|
|
96
|
+
// queue for that surface's subsequent writes.
|
|
97
|
+
const result = tail.then(task, task);
|
|
98
|
+
// Keep the queue's tail alive regardless of `task`'s outcome; the real
|
|
99
|
+
// outcome is still observable to the caller via `result`.
|
|
100
|
+
queueTails.set(surface, result.then(() => undefined, () => undefined));
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
const states = new Map();
|
|
104
|
+
function getState(surface) {
|
|
105
|
+
let state = states.get(surface);
|
|
106
|
+
if (!state) {
|
|
107
|
+
state = { stream: null, date: null, sizeBytes: 0 };
|
|
108
|
+
states.set(surface, state);
|
|
109
|
+
}
|
|
110
|
+
return state;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Ends a stream and resolves once it has fully finished (or errored) — never
|
|
114
|
+
* rejects, so a close failure can't block the caller from opening the
|
|
115
|
+
* replacement stream.
|
|
116
|
+
*/
|
|
117
|
+
function closeStream(stream) {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
const done = () => resolve();
|
|
120
|
+
stream.once('error', done);
|
|
121
|
+
stream.end(done);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Opens `filePath` for append and resolves once the underlying fd is open
|
|
126
|
+
* (or rejects if `open()` itself fails, e.g. permission denied) — waiting
|
|
127
|
+
* for `'open'`/`'error'` rather than trusting `createWriteStream`'s
|
|
128
|
+
* synchronous return lets an unwritable path surface as a rejected Promise
|
|
129
|
+
* instead of a later unhandled `'error'` event.
|
|
130
|
+
*/
|
|
131
|
+
function openStream(filePath) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const stream = createWriteStream(filePath, { flags: 'a' });
|
|
134
|
+
const onOpen = () => {
|
|
135
|
+
cleanup();
|
|
136
|
+
resolve(stream);
|
|
137
|
+
};
|
|
138
|
+
const onError = (err) => {
|
|
139
|
+
cleanup();
|
|
140
|
+
reject(err);
|
|
141
|
+
};
|
|
142
|
+
function cleanup() {
|
|
143
|
+
stream.off('open', onOpen);
|
|
144
|
+
stream.off('error', onError);
|
|
145
|
+
}
|
|
146
|
+
stream.once('open', onOpen);
|
|
147
|
+
stream.once('error', onError);
|
|
148
|
+
}).then((stream) => {
|
|
149
|
+
// Persistent handler for errors that occur AFTER open (disk full, etc.)
|
|
150
|
+
// — required so an unlistened 'error' event never crashes the process.
|
|
151
|
+
// Actual write failures are still propagated to the caller via the
|
|
152
|
+
// write() callback in `writeChunk`; this is a pure safety net.
|
|
153
|
+
stream.on('error', (err) => {
|
|
154
|
+
console.error('[skillsmith] log stream error:', err instanceof Error ? err.message : err);
|
|
155
|
+
});
|
|
156
|
+
return stream;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function writeChunk(stream, buf) {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
stream.write(buf, (err) => {
|
|
162
|
+
if (err)
|
|
163
|
+
reject(err);
|
|
164
|
+
else
|
|
165
|
+
resolve();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Resolves the stream to write to for `surface` RIGHT NOW, opening a new one
|
|
171
|
+
* (and ending the old one) if the calendar day changed or the current file
|
|
172
|
+
* has exceeded the size cap. Must only be called from inside
|
|
173
|
+
* `runExclusive(surface, ...)` — it is not itself lock-protected.
|
|
174
|
+
*/
|
|
175
|
+
async function resolveStream(surface) {
|
|
176
|
+
const state = getState(surface);
|
|
177
|
+
const today = todayDateString();
|
|
178
|
+
const isNewDay = state.stream === null || state.date !== today;
|
|
179
|
+
const isOversized = !isNewDay && state.sizeBytes >= SIZE_CAP_BYTES;
|
|
180
|
+
if (isNewDay || isOversized) {
|
|
181
|
+
if (state.stream) {
|
|
182
|
+
await closeStream(state.stream);
|
|
183
|
+
}
|
|
184
|
+
await mkdir(getLogDir(), { recursive: true });
|
|
185
|
+
const filePath = isOversized
|
|
186
|
+
? nextRolledFilePath(surface, today)
|
|
187
|
+
: dailyFilePath(surface, today);
|
|
188
|
+
const stream = await openStream(filePath);
|
|
189
|
+
state.stream = stream;
|
|
190
|
+
state.date = today;
|
|
191
|
+
// Stat the target file so reopening a same-day file across a process
|
|
192
|
+
// restart still enforces the cap accurately, instead of resetting to 0
|
|
193
|
+
// and silently growing the already-oversized base file further.
|
|
194
|
+
state.sizeBytes = statSizeOrZero(filePath);
|
|
195
|
+
}
|
|
196
|
+
// `resolveStream` only sets `state.stream` to a non-null value above, and
|
|
197
|
+
// this function is the sole place that ever nulls or reassigns it, so the
|
|
198
|
+
// non-null assertion here is safe.
|
|
199
|
+
return state.stream;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Serializes `line` for `surface`: acquires that surface's exclusive queue,
|
|
203
|
+
* resolves (opening/rotating as needed) the current stream, and writes.
|
|
204
|
+
* Resolves once the OS-level write has completed; rejects (never throws
|
|
205
|
+
* synchronously) if the directory/file couldn't be created or the write
|
|
206
|
+
* failed — callers (see `logger.ts`) must handle rejection to honor the
|
|
207
|
+
* "logger never throws" invariant.
|
|
208
|
+
*/
|
|
209
|
+
export function writeLogLine(surface, line) {
|
|
210
|
+
return runExclusive(surface, async () => {
|
|
211
|
+
const stream = await resolveStream(surface);
|
|
212
|
+
const buf = Buffer.from(line.endsWith('\n') ? line : `${line}\n`, 'utf8');
|
|
213
|
+
await writeChunk(stream, buf);
|
|
214
|
+
getState(surface).sizeBytes += buf.length;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Retention sweep
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
/**
|
|
221
|
+
* Deletes files under the log directory whose mtime is older than
|
|
222
|
+
* `RETENTION_DAYS`. Best-effort: a missing directory or a per-file stat/
|
|
223
|
+
* unlink failure (permissions, a concurrent process already deleted it,
|
|
224
|
+
* etc.) is swallowed rather than thrown — this must never be able to crash
|
|
225
|
+
* module init or take down a caller that awaits it in a test.
|
|
226
|
+
*
|
|
227
|
+
* Exported (not just fired at module init) so tests can await a
|
|
228
|
+
* deterministic run against a temp directory rather than racing the
|
|
229
|
+
* fire-and-forget call below.
|
|
230
|
+
*/
|
|
231
|
+
export async function pruneExpiredLogs() {
|
|
232
|
+
const dir = getLogDir();
|
|
233
|
+
try {
|
|
234
|
+
if (!existsSync(dir))
|
|
235
|
+
return;
|
|
236
|
+
const entries = await readdir(dir);
|
|
237
|
+
const cutoff = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
238
|
+
await Promise.all(entries.map(async (name) => {
|
|
239
|
+
const full = join(dir, name);
|
|
240
|
+
try {
|
|
241
|
+
const info = await stat(full);
|
|
242
|
+
if (info.isFile() && info.mtimeMs < cutoff) {
|
|
243
|
+
await unlink(full);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Best-effort per-file; ignore and move on.
|
|
248
|
+
}
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Best-effort overall; never throw from module init.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Fire-and-forget at module load — async, non-blocking, intentionally not
|
|
256
|
+
// awaited (F2: "unrelated to the active writer"). Skipped under Vitest
|
|
257
|
+
// (`process.env.VITEST` is set automatically by the test runner, not
|
|
258
|
+
// something call sites configure) and via an explicit opt-out, so merely
|
|
259
|
+
// IMPORTING this module — which every `logger.test.ts`/`redact.test.ts` run
|
|
260
|
+
// does transitively — never sweeps the real `~/.skillsmith/logs/` directory
|
|
261
|
+
// as a side effect. Tests call `pruneExpiredLogs()` directly against
|
|
262
|
+
// `SKILLSMITH_LOG_DIR` instead, for a deterministic, awaitable run.
|
|
263
|
+
if (process.env.VITEST !== 'true' && process.env.SKILLSMITH_LOG_DISABLE_STARTUP_SWEEP !== '1') {
|
|
264
|
+
void pruneExpiredLogs();
|
|
265
|
+
}
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Test-only reset
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
/**
|
|
270
|
+
* Closes any open streams and clears all in-memory state. Test-only — used
|
|
271
|
+
* by `rotation.test.ts` between cases so state from one temp-dir scenario
|
|
272
|
+
* never leaks into the next.
|
|
273
|
+
*/
|
|
274
|
+
export async function __resetLoggingStateForTests() {
|
|
275
|
+
const closes = [];
|
|
276
|
+
for (const state of states.values()) {
|
|
277
|
+
if (state.stream)
|
|
278
|
+
closes.push(closeStream(state.stream));
|
|
279
|
+
}
|
|
280
|
+
await Promise.all(closes);
|
|
281
|
+
states.clear();
|
|
282
|
+
queueTails.clear();
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=rotation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotation.js","sourceRoot":"","sources":["../../../src/logging/rotation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-5615: Tests for the per-surface serialized JSONL writer (`rotation.ts`).
|
|
3
|
+
*
|
|
4
|
+
* Covers the plan's P-5 required test ("fire N concurrent large ...
|
|
5
|
+
* writes ... assert every line parses as valid JSON with no
|
|
6
|
+
* interleaved/torn content"), plus daily rollover, size-cap rollover, and
|
|
7
|
+
* the 14-day retention sweep. All tests point `SKILLSMITH_LOG_DIR` at a temp
|
|
8
|
+
* directory — never the real `~/.skillsmith/logs/`.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=rotation.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotation.test.d.ts","sourceRoot":"","sources":["../../../src/logging/rotation.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-5615: Tests for the per-surface serialized JSONL writer (`rotation.ts`).
|
|
3
|
+
*
|
|
4
|
+
* Covers the plan's P-5 required test ("fire N concurrent large ...
|
|
5
|
+
* writes ... assert every line parses as valid JSON with no
|
|
6
|
+
* interleaved/torn content"), plus daily rollover, size-cap rollover, and
|
|
7
|
+
* the 14-day retention sweep. All tests point `SKILLSMITH_LOG_DIR` at a temp
|
|
8
|
+
* directory — never the real `~/.skillsmith/logs/`.
|
|
9
|
+
*/
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, utimesSync, writeFileSync, } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { __resetLoggingStateForTests, pruneExpiredLogs, writeLogLine } from './rotation.js';
|
|
15
|
+
let tempDir;
|
|
16
|
+
let originalLogDir;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tempDir = mkdtempSync(join(tmpdir(), 'skillsmith-log-rotation-'));
|
|
19
|
+
originalLogDir = process.env.SKILLSMITH_LOG_DIR;
|
|
20
|
+
process.env.SKILLSMITH_LOG_DIR = tempDir;
|
|
21
|
+
});
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await __resetLoggingStateForTests();
|
|
24
|
+
if (originalLogDir === undefined) {
|
|
25
|
+
delete process.env.SKILLSMITH_LOG_DIR;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
process.env.SKILLSMITH_LOG_DIR = originalLogDir;
|
|
29
|
+
}
|
|
30
|
+
vi.useRealTimers();
|
|
31
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
describe('rotation.ts — write serialization (F2)', () => {
|
|
34
|
+
it('serializes N concurrent large (>PIPE_BUF) writes without interleaving or tearing', async () => {
|
|
35
|
+
const N = 25;
|
|
36
|
+
const PAYLOAD_SIZE = 6000; // > PIPE_BUF (4096 bytes)
|
|
37
|
+
const writes = Array.from({ length: N }, (_, i) => writeLogLine('mcp', JSON.stringify({ id: i, blob: 'x'.repeat(PAYLOAD_SIZE) })));
|
|
38
|
+
await Promise.all(writes);
|
|
39
|
+
const files = readdirSync(tempDir).filter((f) => f.startsWith('skillsmith-mcp-'));
|
|
40
|
+
expect(files).toHaveLength(1);
|
|
41
|
+
const content = readFileSync(join(tempDir, files[0]), 'utf8');
|
|
42
|
+
const lines = content.split('\n').filter((line) => line.length > 0);
|
|
43
|
+
expect(lines).toHaveLength(N);
|
|
44
|
+
const seenIds = new Set();
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
// JSON.parse throws on a torn/interleaved line — this is the actual
|
|
47
|
+
// no-tearing assertion, not just the id-uniqueness check below.
|
|
48
|
+
const parsed = JSON.parse(line);
|
|
49
|
+
expect(parsed.blob).toHaveLength(PAYLOAD_SIZE);
|
|
50
|
+
seenIds.add(parsed.id);
|
|
51
|
+
}
|
|
52
|
+
expect(seenIds.size).toBe(N);
|
|
53
|
+
});
|
|
54
|
+
it('keeps concurrent writes to different surfaces independent', async () => {
|
|
55
|
+
await Promise.all([
|
|
56
|
+
writeLogLine('mcp', JSON.stringify({ surface: 'mcp' })),
|
|
57
|
+
writeLogLine('cli', JSON.stringify({ surface: 'cli' })),
|
|
58
|
+
writeLogLine('vscode', JSON.stringify({ surface: 'vscode' })),
|
|
59
|
+
]);
|
|
60
|
+
const files = readdirSync(tempDir);
|
|
61
|
+
expect(files.some((f) => f.startsWith('skillsmith-mcp-'))).toBe(true);
|
|
62
|
+
expect(files.some((f) => f.startsWith('skillsmith-cli-'))).toBe(true);
|
|
63
|
+
expect(files.some((f) => f.startsWith('skillsmith-vscode-'))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('rotation.ts — daily rollover', () => {
|
|
67
|
+
it('rolls to a new dated file when the calendar date changes', async () => {
|
|
68
|
+
vi.useFakeTimers();
|
|
69
|
+
vi.setSystemTime(new Date('2026-01-01T23:59:00Z'));
|
|
70
|
+
await writeLogLine('cli', JSON.stringify({ day: 1 }));
|
|
71
|
+
vi.setSystemTime(new Date('2026-01-02T00:01:00Z'));
|
|
72
|
+
await writeLogLine('cli', JSON.stringify({ day: 2 }));
|
|
73
|
+
const files = readdirSync(tempDir)
|
|
74
|
+
.filter((f) => f.startsWith('skillsmith-cli-'))
|
|
75
|
+
.sort();
|
|
76
|
+
expect(files).toEqual(['skillsmith-cli-2026-01-01.jsonl', 'skillsmith-cli-2026-01-02.jsonl']);
|
|
77
|
+
const day1 = readFileSync(join(tempDir, 'skillsmith-cli-2026-01-01.jsonl'), 'utf8').trim();
|
|
78
|
+
const day2 = readFileSync(join(tempDir, 'skillsmith-cli-2026-01-02.jsonl'), 'utf8').trim();
|
|
79
|
+
expect(JSON.parse(day1)).toEqual({ day: 1 });
|
|
80
|
+
expect(JSON.parse(day2)).toEqual({ day: 2 });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('rotation.ts — size-cap rollover', () => {
|
|
84
|
+
it('rolls to a .1 continuation file once the current file reaches the ~10MB cap', async () => {
|
|
85
|
+
vi.useFakeTimers();
|
|
86
|
+
vi.setSystemTime(new Date('2026-02-01T12:00:00Z'));
|
|
87
|
+
// First write pushes the base file to (just over) the 10MB cap in one
|
|
88
|
+
// shot — far faster than looping thousands of small writes to get there,
|
|
89
|
+
// and exercises the same in-memory size-tracking path either way.
|
|
90
|
+
const bigPayload = 'x'.repeat(10 * 1024 * 1024);
|
|
91
|
+
await writeLogLine('cli', JSON.stringify({ big: bigPayload }));
|
|
92
|
+
// Second write, same day: resolveStream sees sizeBytes >= cap and rolls
|
|
93
|
+
// to a `.1` continuation file instead of continuing to grow the base file.
|
|
94
|
+
await writeLogLine('cli', JSON.stringify({ rolled: true }));
|
|
95
|
+
const baseName = 'skillsmith-cli-2026-02-01.jsonl';
|
|
96
|
+
expect(existsSync(join(tempDir, `${baseName}.1`))).toBe(true);
|
|
97
|
+
const rolledContent = readFileSync(join(tempDir, `${baseName}.1`), 'utf8').trim();
|
|
98
|
+
expect(JSON.parse(rolledContent)).toEqual({ rolled: true });
|
|
99
|
+
const baseLines = readFileSync(join(tempDir, baseName), 'utf8')
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter((line) => line.length > 0);
|
|
102
|
+
expect(baseLines).toHaveLength(1);
|
|
103
|
+
}, 15000);
|
|
104
|
+
});
|
|
105
|
+
describe('rotation.ts — retention sweep', () => {
|
|
106
|
+
it('deletes files older than 14 days and keeps recent ones', async () => {
|
|
107
|
+
const oldFile = join(tempDir, 'skillsmith-mcp-2020-01-01.jsonl');
|
|
108
|
+
const recentFile = join(tempDir, 'skillsmith-mcp-2026-01-01.jsonl');
|
|
109
|
+
writeFileSync(oldFile, '{}\n');
|
|
110
|
+
writeFileSync(recentFile, '{}\n');
|
|
111
|
+
const fifteenDaysAgoSec = (Date.now() - 15 * 24 * 60 * 60 * 1000) / 1000;
|
|
112
|
+
const oneDayAgoSec = (Date.now() - 1 * 24 * 60 * 60 * 1000) / 1000;
|
|
113
|
+
utimesSync(oldFile, fifteenDaysAgoSec, fifteenDaysAgoSec);
|
|
114
|
+
utimesSync(recentFile, oneDayAgoSec, oneDayAgoSec);
|
|
115
|
+
await pruneExpiredLogs();
|
|
116
|
+
expect(existsSync(oldFile)).toBe(false);
|
|
117
|
+
expect(existsSync(recentFile)).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it('does not delete directories, only files', async () => {
|
|
120
|
+
const oldDir = join(tempDir, 'not-a-log-file');
|
|
121
|
+
mkdirSync(oldDir);
|
|
122
|
+
const fifteenDaysAgoSec = (Date.now() - 15 * 24 * 60 * 60 * 1000) / 1000;
|
|
123
|
+
utimesSync(oldDir, fifteenDaysAgoSec, fifteenDaysAgoSec);
|
|
124
|
+
await expect(pruneExpiredLogs()).resolves.toBeUndefined();
|
|
125
|
+
expect(existsSync(oldDir)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
it('is a no-op (never throws) when the log directory does not exist', async () => {
|
|
128
|
+
process.env.SKILLSMITH_LOG_DIR = join(tempDir, 'does-not-exist');
|
|
129
|
+
await expect(pruneExpiredLogs()).resolves.toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=rotation.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotation.test.js","sourceRoot":"","sources":["../../../src/logging/rotation.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-5615: Shared types for the production error-logging module
|
|
3
|
+
* (`packages/core/src/logging/`).
|
|
4
|
+
*
|
|
5
|
+
* Field list and shape verified against
|
|
6
|
+
* docs/internal/implementation/production-error-logging.md §1.
|
|
7
|
+
*/
|
|
8
|
+
/** Log severity, ordered `debug < info < warn < error`. */
|
|
9
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
10
|
+
/** Which invocation surface produced a record — one JSONL file per surface. */
|
|
11
|
+
export type Surface = 'mcp' | 'cli' | 'vscode';
|
|
12
|
+
/** Normalized, already-redacted error shape embedded in a `LogRecord`. */
|
|
13
|
+
export interface LogRecordError {
|
|
14
|
+
name: string;
|
|
15
|
+
message: string;
|
|
16
|
+
/** Redacted, capped to the first 20 frames. */
|
|
17
|
+
stack?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* One JSON-line record written to
|
|
21
|
+
* `~/.skillsmith/logs/skillsmith-<surface>-<YYYY-MM-DD>.jsonl`.
|
|
22
|
+
*
|
|
23
|
+
* Field order mirrors the plan's listing (ts, level, surface, event, msg,
|
|
24
|
+
* err, correlationId, toolOrCommand, skillId, version, pid) — `logger.ts`
|
|
25
|
+
* constructs records in this order so `JSON.stringify` output (insertion
|
|
26
|
+
* order for string keys) reads consistently on disk.
|
|
27
|
+
*/
|
|
28
|
+
export interface LogRecord {
|
|
29
|
+
ts: string;
|
|
30
|
+
level: LogLevel;
|
|
31
|
+
surface: Surface;
|
|
32
|
+
/** Short machine-readable event/category tag. Defaults to `level` when omitted. */
|
|
33
|
+
event: string;
|
|
34
|
+
/** Redacted human-readable message. */
|
|
35
|
+
msg: string;
|
|
36
|
+
err?: LogRecordError;
|
|
37
|
+
correlationId?: string;
|
|
38
|
+
toolOrCommand?: string;
|
|
39
|
+
skillId?: string;
|
|
40
|
+
version: string;
|
|
41
|
+
pid: number;
|
|
42
|
+
/**
|
|
43
|
+
* Additional structured context beyond the plan's core field list —
|
|
44
|
+
* carries forward the old (dead) `mcp-server/src/logger.ts`'s freeform
|
|
45
|
+
* `details` capability so callers don't lose arbitrary context. Redacted
|
|
46
|
+
* via `redactSensitiveObject` like every other field. Omitted entirely
|
|
47
|
+
* when the caller passes no extra keys.
|
|
48
|
+
*/
|
|
49
|
+
details?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The `details` argument accepted by every `Logger` method. Recognized keys
|
|
53
|
+
* are lifted into their own `LogRecord` fields; anything else is folded into
|
|
54
|
+
* `LogRecord.details` (redacted). Matches the old logger's
|
|
55
|
+
* `(message: string, details?: unknown)` call shape while giving structure to
|
|
56
|
+
* the fields the new record format cares about.
|
|
57
|
+
*/
|
|
58
|
+
export interface LogDetails {
|
|
59
|
+
/** Short machine-readable event/category tag for this record. */
|
|
60
|
+
event?: string;
|
|
61
|
+
/** Raw error to normalize into `LogRecord.err` (redacted). */
|
|
62
|
+
err?: unknown;
|
|
63
|
+
/** MCP tool name or CLI command name associated with this record. */
|
|
64
|
+
toolOrCommand?: string;
|
|
65
|
+
/** Skill ID associated with this record, when applicable. */
|
|
66
|
+
skillId?: string;
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/logging/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,2DAA2D;AAC3D,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAE1D,+EAA+E;AAC/E,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAA;AAE9C,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,QAAQ,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;IAChB,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-5615: Shared types for the production error-logging module
|
|
3
|
+
* (`packages/core/src/logging/`).
|
|
4
|
+
*
|
|
5
|
+
* Field list and shape verified against
|
|
6
|
+
* docs/internal/implementation/production-error-logging.md §1.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/logging/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export { SecurityScanner } from './scanner/index.js';
|
|
11
11
|
export type { SecurityFinding, SecurityFindingType, SecuritySeverity, ScanReport, ScannerOptions, RiskScoreBreakdown, } from './scanner/index.js';
|
|
12
|
+
export { compareScanReports, DEFAULT_RISK_THRESHOLD } from './scanner/index.js';
|
|
13
|
+
export type { HostileUpdateVerdict } from './scanner/index.js';
|
|
12
14
|
export { sanitizeHtml, sanitizeFileName, sanitizePath, sanitizeUrl, sanitizeText, isValidStripeId, sanitizeStripeCustomerId, sanitizeStripeSubscriptionId, sanitizeStripePriceId, sanitizeStripeInvoiceId, sanitizeStripeEventId, } from './sanitization.js';
|
|
13
15
|
export { validateDbPath, validateDbPathOrThrow, isPathSafe, DEFAULT_ALLOWED_DIRS, } from './pathValidation.js';
|
|
14
16
|
export type { PathValidationOptions, PathValidationResult } from './pathValidation.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/security/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/security/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAI3B,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC/E,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAG9D,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,YAAY,EAEZ,eAAe,EACf,wBAAwB,EACxB,4BAA4B,EAC5B,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,UAAU,EACV,oBAAoB,GACrB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAGtF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,YAAY,EACV,aAAa,EACb,cAAc,EACd,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,UAAU,GACX,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EACL,WAAW,EACX,wBAAwB,EACxB,kBAAkB,EAClB,2BAA2B,EAC3B,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,yBAAyB,CAAA;AAChC,YAAY,EACV,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,yBAAyB,CAAA;AAGhC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGtD,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACtF,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA"}
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
// Scanner module
|
|
11
11
|
export { SecurityScanner } from './scanner/index.js';
|
|
12
|
+
// SMI-5535 R0 Wave 2A: the hostile-update (rug-pull) comparator is a public
|
|
13
|
+
// feature; surface it (+ its default threshold + verdict type) through the
|
|
14
|
+
// security barrel so it is reachable from `@skillsmith/core`.
|
|
15
|
+
export { compareScanReports, DEFAULT_RISK_THRESHOLD } from './scanner/index.js';
|
|
12
16
|
// Sanitization
|
|
13
17
|
export { sanitizeHtml, sanitizeFileName, sanitizePath, sanitizeUrl, sanitizeText,
|
|
14
18
|
// SMI-1062: Stripe ID validators
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/security/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,iBAAiB;AACjB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/security/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,iBAAiB;AACjB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AASpD,4EAA4E;AAC5E,2EAA2E;AAC3E,8DAA8D;AAC9D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAG/E,eAAe;AACf,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,YAAY;AACZ,iCAAiC;AACjC,eAAe,EACf,wBAAwB,EACxB,4BAA4B,EAC5B,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,mBAAmB,CAAA;AAE1B,kBAAkB;AAClB,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,UAAU,EACV,oBAAoB,GACrB,MAAM,qBAAqB,CAAA;AAG5B,eAAe;AACf,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAU9C,sBAAsB;AACtB,OAAO,EACL,WAAW,EACX,wBAAwB,EACxB,kBAAkB,EAClB,2BAA2B,EAC3B,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,yBAAyB,CAAA;AAQhC,iCAAiC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGjD,0CAA0C;AAC1C,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hostile-Update Detection — R0 Wave 2A (SMI-5535)
|
|
3
|
+
* @module @skillsmith/core/security/scanner/SecurityScanner.hostile-update
|
|
4
|
+
*
|
|
5
|
+
* A PURE comparator (no I/O, no clock, no mutation of its inputs) that detects a
|
|
6
|
+
* benign→malicious "rug-pull" between two SecurityScanner scans of the SAME
|
|
7
|
+
* skill. The scanner already blocks a skill that is malicious on first sight;
|
|
8
|
+
* this detector catches the harder case where an initially-clean skill is
|
|
9
|
+
* silently updated to exfiltrate credentials or execute remote payloads.
|
|
10
|
+
*
|
|
11
|
+
* The verdict is delta-based: it never re-scores content, it only reasons about
|
|
12
|
+
* what changed between `previous` and `current`. "hostile" is reserved for the
|
|
13
|
+
* benign→malicious transition — a skill that was ALREADY flagged and merely got
|
|
14
|
+
* worse is a worsening (`suspicious`), not a fresh rug-pull.
|
|
15
|
+
*/
|
|
16
|
+
import type { HostileUpdateVerdict, ScanReport } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Default risk-score threshold. Mirrors SecurityScanner's `riskThreshold`
|
|
19
|
+
* default (SMI-5359) so the "crossed the failure bar" signal here lines up with
|
|
20
|
+
* the `passed` computation in `SecurityScanner.scan`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEFAULT_RISK_THRESHOLD = 40;
|
|
23
|
+
/**
|
|
24
|
+
* Compare two scans of the same skill and classify the transition.
|
|
25
|
+
*
|
|
26
|
+
* Ordering is hostile → suspicious → benign so the strongest signal wins.
|
|
27
|
+
*
|
|
28
|
+
* @param previous Scan of the prior (trusted) version.
|
|
29
|
+
* @param current Scan of the incoming version.
|
|
30
|
+
* @param threshold Risk-score failure bar; defaults to {@link DEFAULT_RISK_THRESHOLD}.
|
|
31
|
+
*
|
|
32
|
+
* CALLER CONTRACT (LOW-2): this value MUST equal the `riskThreshold` the
|
|
33
|
+
* `SecurityScanner` instance used to produce BOTH `previous` and `current`
|
|
34
|
+
* (i.e. the same `ScannerOptions.riskThreshold` — or the scanner's own
|
|
35
|
+
* default of 40, which is why this parameter also defaults to
|
|
36
|
+
* {@link DEFAULT_RISK_THRESHOLD}). `previouslyBenign` below is derived from
|
|
37
|
+
* `previous.passed`, which was computed by the scanner against ITS OWN
|
|
38
|
+
* `riskThreshold` at scan time — if the caller passes a `threshold` here
|
|
39
|
+
* that differs from that value, `crossedThreshold`'s "did the score cross
|
|
40
|
+
* the bar" signal and `previouslyBenign`'s "was it passing" signal are
|
|
41
|
+
* reasoning about two different bars, and the hostile/suspicious/benign
|
|
42
|
+
* verdict can disagree with what `previous.passed` / `current.passed`
|
|
43
|
+
* would say under a freshly-run scan.
|
|
44
|
+
*/
|
|
45
|
+
export declare function compareScanReports(previous: ScanReport, current: ScanReport, threshold?: number): HostileUpdateVerdict;
|
|
46
|
+
//# sourceMappingURL=SecurityScanner.hostile-update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecurityScanner.hostile-update.d.ts","sourceRoot":"","sources":["../../../../src/security/scanner/SecurityScanner.hostile-update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EAIX,MAAM,YAAY,CAAA;AAEnB;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAA;AA4GxC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,UAAU,EACnB,SAAS,GAAE,MAA+B,GACzC,oBAAoB,CA+EtB"}
|