@npm-safe/core-dsh 1.0.5
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/LICENSE +204 -0
- package/dist/index.d.ts +513 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +711 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/anthropic.d.ts +47 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +161 -0
- package/dist/llm/anthropic.js.map +1 -0
- package/dist/llm/gemini.d.ts +47 -0
- package/dist/llm/gemini.d.ts.map +1 -0
- package/dist/llm/gemini.js +165 -0
- package/dist/llm/gemini.js.map +1 -0
- package/dist/llm/llm-config.d.ts +97 -0
- package/dist/llm/llm-config.d.ts.map +1 -0
- package/dist/llm/llm-config.js +188 -0
- package/dist/llm/llm-config.js.map +1 -0
- package/dist/llm/parse.d.ts +95 -0
- package/dist/llm/parse.d.ts.map +1 -0
- package/dist/llm/parse.js +158 -0
- package/dist/llm/parse.js.map +1 -0
- package/dist/llm/provider.d.ts +122 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +206 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/registry/client.d.ts +164 -0
- package/dist/registry/client.d.ts.map +1 -0
- package/dist/registry/client.js +378 -0
- package/dist/registry/client.js.map +1 -0
- package/dist/registry/types.d.ts +226 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +32 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/registry/validator.d.ts +87 -0
- package/dist/registry/validator.d.ts.map +1 -0
- package/dist/registry/validator.js +214 -0
- package/dist/registry/validator.js.map +1 -0
- package/dist/scanner/ci-scan.d.ts +82 -0
- package/dist/scanner/ci-scan.d.ts.map +1 -0
- package/dist/scanner/ci-scan.js +130 -0
- package/dist/scanner/ci-scan.js.map +1 -0
- package/dist/scanner/rule-config.d.ts +61 -0
- package/dist/scanner/rule-config.d.ts.map +1 -0
- package/dist/scanner/rule-config.js +103 -0
- package/dist/scanner/rule-config.js.map +1 -0
- package/dist/scanner/rule-loader.d.ts +28 -0
- package/dist/scanner/rule-loader.d.ts.map +1 -0
- package/dist/scanner/rule-loader.js +67 -0
- package/dist/scanner/rule-loader.js.map +1 -0
- package/dist/scanner/static-rules.d.ts +88 -0
- package/dist/scanner/static-rules.d.ts.map +1 -0
- package/dist/scanner/static-rules.js +723 -0
- package/dist/scanner/static-rules.js.map +1 -0
- package/dist/scanner/types.d.ts +177 -0
- package/dist/scanner/types.d.ts.map +1 -0
- package/dist/scanner/types.js +53 -0
- package/dist/scanner/types.js.map +1 -0
- package/dist/scheduler/rate-limiter.d.ts +74 -0
- package/dist/scheduler/rate-limiter.d.ts.map +1 -0
- package/dist/scheduler/rate-limiter.js +182 -0
- package/dist/scheduler/rate-limiter.js.map +1 -0
- package/dist/scheduler/refresh-scheduler.d.ts +201 -0
- package/dist/scheduler/refresh-scheduler.d.ts.map +1 -0
- package/dist/scheduler/refresh-scheduler.js +295 -0
- package/dist/scheduler/refresh-scheduler.js.map +1 -0
- package/dist/store/cache-manager.d.ts +166 -0
- package/dist/store/cache-manager.d.ts.map +1 -0
- package/dist/store/cache-manager.js +356 -0
- package/dist/store/cache-manager.js.map +1 -0
- package/dist/store/database.d.ts +81 -0
- package/dist/store/database.d.ts.map +1 -0
- package/dist/store/database.js +182 -0
- package/dist/store/database.js.map +1 -0
- package/dist/store/schema.d.ts +42 -0
- package/dist/store/schema.d.ts.map +1 -0
- package/dist/store/schema.js +126 -0
- package/dist/store/schema.js.map +1 -0
- package/dist/translator/provider.d.ts +152 -0
- package/dist/translator/provider.d.ts.map +1 -0
- package/dist/translator/provider.js +159 -0
- package/dist/translator/provider.js.map +1 -0
- package/dist/translator/types.d.ts +83 -0
- package/dist/translator/types.d.ts.map +1 -0
- package/dist/translator/types.js +58 -0
- package/dist/translator/types.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-manager.d.ts","sourceRoot":"","sources":["../../src/store/cache-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache read/write layer for @npm-safe/core.
|
|
3
|
+
*
|
|
4
|
+
* {@link CacheManager} sits on top of a {@link DatabaseManager} connection and
|
|
5
|
+
* provides typed accessors for the cached npm metadata, security scan reports,
|
|
6
|
+
* watchlist, and key-value settings tables. It owns the TTL policy for the
|
|
7
|
+
* `packages` table: every {@link CacheManager.setPackage} call stamps the row
|
|
8
|
+
* with an expiry timestamp (`ttl_until`) computed as `now + cacheTtlMs`, and
|
|
9
|
+
* {@link CacheManager.getPackage} refuses to return rows whose TTL has
|
|
10
|
+
* elapsed. Scheduling of refreshes is intentionally out of scope — that is
|
|
11
|
+
* the job of the refresh-scheduler; this module only reads and writes.
|
|
12
|
+
*
|
|
13
|
+
* All methods return `Promise`s for API consistency and future async backing
|
|
14
|
+
* stores, even though the underlying `better-sqlite3` calls are synchronous.
|
|
15
|
+
*
|
|
16
|
+
* @module store/cache-manager
|
|
17
|
+
*/
|
|
18
|
+
import { SecurityLevel, } from "../scanner/types.js";
|
|
19
|
+
/**
|
|
20
|
+
* Default cache time-to-live for package metadata: 1 hour in milliseconds.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_CACHE_TTL_MS = 3_600_000;
|
|
23
|
+
/**
|
|
24
|
+
* Derives a {@link SecurityLevel} from a numeric static-scan score using the
|
|
25
|
+
* same thresholds as the static analyzer: `>=80` Safe, `>=50` Suspicious,
|
|
26
|
+
* `>=20` Dangerous, otherwise Unknown. The `security_reports` table persists
|
|
27
|
+
* only the numeric score (not the enum), so the level is reconstructed on
|
|
28
|
+
* read to satisfy the {@link StaticScanReport} shape.
|
|
29
|
+
*
|
|
30
|
+
* @param score - Numeric score from 0 to 100 (higher is safer).
|
|
31
|
+
* @returns The security level matching the score band.
|
|
32
|
+
*/
|
|
33
|
+
function scoreToLevel(score) {
|
|
34
|
+
if (score >= 80)
|
|
35
|
+
return SecurityLevel.Safe;
|
|
36
|
+
if (score >= 50)
|
|
37
|
+
return SecurityLevel.Suspicious;
|
|
38
|
+
if (score >= 20)
|
|
39
|
+
return SecurityLevel.Dangerous;
|
|
40
|
+
return SecurityLevel.Unknown;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Normalizes a {@link PackageRepository} value to a plain string for storage
|
|
44
|
+
* in the `packages.repository` TEXT column. Structured descriptors are
|
|
45
|
+
* rendered as `type:url`; shorthand strings are stored verbatim.
|
|
46
|
+
*
|
|
47
|
+
* @param repo - The repository descriptor from registry metadata.
|
|
48
|
+
* @returns A string representation suitable for storage.
|
|
49
|
+
*/
|
|
50
|
+
function repositoryToString(repo) {
|
|
51
|
+
if (repo === undefined)
|
|
52
|
+
return "";
|
|
53
|
+
if (typeof repo === "string")
|
|
54
|
+
return repo;
|
|
55
|
+
return `${repo.type}:${repo.url}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Cache read/write manager backed by a {@link DatabaseManager} connection.
|
|
59
|
+
*
|
|
60
|
+
* The manager is stateless beyond the TTL configuration and the injected
|
|
61
|
+
* database handle, so it is safe to construct one per request or to share a
|
|
62
|
+
* long-lived instance. All public methods are async-returning for API
|
|
63
|
+
* consistency; the underlying `better-sqlite3` calls are synchronous.
|
|
64
|
+
*/
|
|
65
|
+
export class CacheManager {
|
|
66
|
+
/** Underlying better-sqlite3 connection accessor. */
|
|
67
|
+
db;
|
|
68
|
+
/** Cache TTL in milliseconds applied to newly written package rows. */
|
|
69
|
+
cacheTtlMs;
|
|
70
|
+
/**
|
|
71
|
+
* @param database - The {@link DatabaseManager} supplying the connection.
|
|
72
|
+
* @param options - Optional configuration; see {@link CacheManagerOptions}.
|
|
73
|
+
*/
|
|
74
|
+
constructor(database, options) {
|
|
75
|
+
this.db = database.getDb();
|
|
76
|
+
this.cacheTtlMs = options?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns cached package metadata for `name` if the row is still fresh
|
|
80
|
+
* (i.e. `ttl_until >= now`). Returns `null` when the row is missing or
|
|
81
|
+
* stale; callers are expected to re-fetch from the registry and call
|
|
82
|
+
* {@link CacheManager.setPackage} to refresh the cache.
|
|
83
|
+
*
|
|
84
|
+
* @param name - Fully-qualified package name (scope included when scoped).
|
|
85
|
+
* @returns The cached {@link PackageMetadata}, or `null` if absent/stale.
|
|
86
|
+
*/
|
|
87
|
+
async getPackage(name) {
|
|
88
|
+
const row = this.db
|
|
89
|
+
.prepare("SELECT name, latest_version, description, homepage, repository, registry_data, cached_at, ttl_until FROM packages WHERE name = ? AND ttl_until >= strftime('%Y-%m-%d %H:%M:%f', 'now')")
|
|
90
|
+
.get(name);
|
|
91
|
+
if (row === undefined)
|
|
92
|
+
return null;
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(row.registry_data);
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Upserts a package metadata row, stamping it with a fresh TTL computed as
|
|
103
|
+
* `now + cacheTtlMs`. The full packument is serialized into the
|
|
104
|
+
* `registry_data` JSON column; the scalar columns (`latest_version`,
|
|
105
|
+
* `description`, `homepage`, `repository`) are denormalized for cheap
|
|
106
|
+
* listing queries.
|
|
107
|
+
*
|
|
108
|
+
* @param meta - The packument returned by the npm registry.
|
|
109
|
+
*/
|
|
110
|
+
async setPackage(meta) {
|
|
111
|
+
const latest = meta["dist-tags"].latest;
|
|
112
|
+
const description = meta.description ?? "";
|
|
113
|
+
const homepage = meta.homepage ?? "";
|
|
114
|
+
const repository = repositoryToString(meta.repository);
|
|
115
|
+
const registryData = JSON.stringify(meta);
|
|
116
|
+
// Fractional (sub-second) TTLs are supported: `datetime('now', ...)` would
|
|
117
|
+
// truncate fractional seconds to whole seconds, so the expiry is stamped
|
|
118
|
+
// via `strftime('%Y-%m-%d %H:%M:%f', ...)` which preserves milliseconds.
|
|
119
|
+
// The value is a number derived from configuration, not user input, so
|
|
120
|
+
// interpolating it into the SQL string is safe.
|
|
121
|
+
const ttlSeconds = this.cacheTtlMs / 1000;
|
|
122
|
+
this.db
|
|
123
|
+
.prepare(`INSERT INTO packages (name, latest_version, description, homepage, repository, registry_data, cached_at, ttl_until)
|
|
124
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), strftime('%Y-%m-%d %H:%M:%f', 'now', '+${ttlSeconds} seconds'))
|
|
125
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
126
|
+
latest_version = excluded.latest_version,
|
|
127
|
+
description = excluded.description,
|
|
128
|
+
homepage = excluded.homepage,
|
|
129
|
+
repository = excluded.repository,
|
|
130
|
+
registry_data = excluded.registry_data,
|
|
131
|
+
cached_at = datetime('now'),
|
|
132
|
+
ttl_until = strftime('%Y-%m-%d %H:%M:%f', 'now', '+${ttlSeconds} seconds')`)
|
|
133
|
+
.run(meta.name, latest, description, homepage, repository, registryData);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Returns the most recent static security report for `pkg`@`version`, or
|
|
137
|
+
* `null` if none is cached. The {@link SecurityLevel} is reconstructed from
|
|
138
|
+
* the persisted numeric score because the `security_reports` table stores
|
|
139
|
+
* only the score, not the enum.
|
|
140
|
+
*
|
|
141
|
+
* @param pkg - Fully-qualified package name.
|
|
142
|
+
* @param version - Semver version string.
|
|
143
|
+
* @returns The cached {@link StaticScanReport}, or `null` if absent.
|
|
144
|
+
*/
|
|
145
|
+
async getSecurityReport(pkg, version) {
|
|
146
|
+
const row = this.db
|
|
147
|
+
.prepare("SELECT id, package_name, version, scan_type, overall_score, findings_json, summary, scanned_at FROM security_reports WHERE package_name = ? AND version = ? AND scan_type = 'static'")
|
|
148
|
+
.get(pkg, version);
|
|
149
|
+
if (row === undefined)
|
|
150
|
+
return null;
|
|
151
|
+
let findings = [];
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(row.findings_json);
|
|
154
|
+
if (Array.isArray(parsed)) {
|
|
155
|
+
findings = parsed;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
findings = [];
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
packageName: row.package_name,
|
|
163
|
+
version: row.version,
|
|
164
|
+
overallLevel: scoreToLevel(row.overall_score),
|
|
165
|
+
score: row.overall_score,
|
|
166
|
+
findings,
|
|
167
|
+
scannedAt: row.scanned_at,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Upserts a static security report into the `security_reports` table. The
|
|
172
|
+
* findings array is serialized to JSON in the `findings_json` column; the
|
|
173
|
+
* numeric score is stored in `overall_score`. The row is keyed by
|
|
174
|
+
* `(package_name, version, scan_type)` via the table's UNIQUE constraint.
|
|
175
|
+
*
|
|
176
|
+
* @param report - The static scan report to persist.
|
|
177
|
+
*/
|
|
178
|
+
async setSecurityReport(report) {
|
|
179
|
+
const findingsJson = JSON.stringify(report.findings);
|
|
180
|
+
this.db
|
|
181
|
+
.prepare(`INSERT INTO security_reports (package_name, version, scan_type, overall_score, findings_json, summary, scanned_at)
|
|
182
|
+
VALUES (?, ?, 'static', ?, ?, '', ?)
|
|
183
|
+
ON CONFLICT(package_name, version, scan_type) DO UPDATE SET
|
|
184
|
+
overall_score = excluded.overall_score,
|
|
185
|
+
findings_json = excluded.findings_json,
|
|
186
|
+
scanned_at = excluded.scanned_at`)
|
|
187
|
+
.run(report.packageName, report.version, report.score, findingsJson, report.scannedAt);
|
|
188
|
+
}
|
|
189
|
+
async getLlmScanReport(pkg, version) {
|
|
190
|
+
const row = this.db
|
|
191
|
+
.prepare("SELECT findings_json, summary, scanned_at FROM security_reports WHERE package_name = ? AND version = ? AND scan_type = 'llm'")
|
|
192
|
+
.get(pkg, version);
|
|
193
|
+
if (!row)
|
|
194
|
+
return null;
|
|
195
|
+
let findings = [];
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(row.findings_json);
|
|
198
|
+
if (Array.isArray(parsed))
|
|
199
|
+
findings = parsed;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
findings = [];
|
|
203
|
+
}
|
|
204
|
+
let details = {};
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(row.summary);
|
|
207
|
+
if (parsed && typeof parsed === "object") {
|
|
208
|
+
details = parsed;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
details = { reason: row.summary };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
enabled: details.enabled ?? false,
|
|
216
|
+
reason: details.reason,
|
|
217
|
+
summary: details.summary,
|
|
218
|
+
functionalMatch: details.functionalMatch,
|
|
219
|
+
suspiciousScore: details.suspiciousScore,
|
|
220
|
+
findings,
|
|
221
|
+
scannedAt: row.scanned_at,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async setLlmScanReport(packageName, version, report) {
|
|
225
|
+
const summary = JSON.stringify({
|
|
226
|
+
enabled: report.enabled,
|
|
227
|
+
summary: report.summary,
|
|
228
|
+
reason: report.reason,
|
|
229
|
+
functionalMatch: report.functionalMatch,
|
|
230
|
+
suspiciousScore: report.suspiciousScore,
|
|
231
|
+
});
|
|
232
|
+
this.db
|
|
233
|
+
.prepare(`INSERT INTO security_reports (package_name, version, scan_type, overall_score, findings_json, summary, scanned_at)
|
|
234
|
+
VALUES (?, ?, 'llm', ?, ?, ?, ?)
|
|
235
|
+
ON CONFLICT(package_name, version, scan_type) DO UPDATE SET
|
|
236
|
+
overall_score = excluded.overall_score,
|
|
237
|
+
findings_json = excluded.findings_json,
|
|
238
|
+
summary = excluded.summary,
|
|
239
|
+
scanned_at = excluded.scanned_at`)
|
|
240
|
+
.run(packageName, version, report.suspiciousScore ?? 0, JSON.stringify(report.findings ?? []), summary, report.scannedAt ?? new Date().toISOString());
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Returns the list of package names currently on the user's watchlist.
|
|
244
|
+
*
|
|
245
|
+
* @returns All watched package names, in insertion order.
|
|
246
|
+
*/
|
|
247
|
+
async getWatchlist() {
|
|
248
|
+
const rows = this.db
|
|
249
|
+
.prepare("SELECT package_name FROM watchlist ORDER BY added_at ASC")
|
|
250
|
+
.all();
|
|
251
|
+
return rows.map((r) => r.package_name);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Adds `name` to the watchlist. Idempotent: inserting a name that is
|
|
255
|
+
* already watched is a no-op (INSERT OR IGNORE).
|
|
256
|
+
*
|
|
257
|
+
* @param name - Fully-qualified package name to watch.
|
|
258
|
+
*/
|
|
259
|
+
async addToWatchlist(name) {
|
|
260
|
+
this.db
|
|
261
|
+
.prepare("INSERT OR IGNORE INTO watchlist (package_name) VALUES (?)")
|
|
262
|
+
.run(name);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Removes `name` from the watchlist. No-op if the name was not watched.
|
|
266
|
+
*
|
|
267
|
+
* @param name - Fully-qualified package name to stop watching.
|
|
268
|
+
*/
|
|
269
|
+
async removeFromWatchlist(name) {
|
|
270
|
+
this.db
|
|
271
|
+
.prepare("DELETE FROM watchlist WHERE package_name = ?")
|
|
272
|
+
.run(name);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Returns the value stored under `key` in the settings table, or `null`
|
|
276
|
+
* if the key is unset.
|
|
277
|
+
*
|
|
278
|
+
* @param key - Settings key.
|
|
279
|
+
* @returns The stored value, or `null` if absent.
|
|
280
|
+
*/
|
|
281
|
+
async getSetting(key) {
|
|
282
|
+
const row = this.db
|
|
283
|
+
.prepare("SELECT key, value FROM settings WHERE key = ?")
|
|
284
|
+
.get(key);
|
|
285
|
+
if (row === undefined)
|
|
286
|
+
return null;
|
|
287
|
+
return row.value;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Upserts `value` under `key` in the settings table (INSERT OR REPLACE).
|
|
291
|
+
*
|
|
292
|
+
* @param key - Settings key.
|
|
293
|
+
* @param value - Settings value to persist.
|
|
294
|
+
*/
|
|
295
|
+
async setSetting(key, value) {
|
|
296
|
+
this.db
|
|
297
|
+
.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
|
298
|
+
.run(key, value);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Returns the names of all cached package rows whose TTL has elapsed
|
|
302
|
+
* (`ttl_until < now`). These are candidates for an incremental refresh by
|
|
303
|
+
* the refresh-scheduler.
|
|
304
|
+
*
|
|
305
|
+
* @returns Package names with expired cache entries.
|
|
306
|
+
*/
|
|
307
|
+
async getStalePackages() {
|
|
308
|
+
const rows = this.db
|
|
309
|
+
.prepare("SELECT name FROM packages WHERE ttl_until < strftime('%Y-%m-%d %H:%M:%f', 'now')")
|
|
310
|
+
.all();
|
|
311
|
+
return rows.map((r) => r.name);
|
|
312
|
+
}
|
|
313
|
+
// --------------------------------------------------------------------------
|
|
314
|
+
// Check history
|
|
315
|
+
// --------------------------------------------------------------------------
|
|
316
|
+
/**
|
|
317
|
+
* Append an entry to the persistent check history, keeping only the most
|
|
318
|
+
* recent {@link MAX_CHECK_HISTORY} entries.
|
|
319
|
+
*/
|
|
320
|
+
async addHistoryEntry(entry) {
|
|
321
|
+
this.db
|
|
322
|
+
.prepare(`INSERT INTO check_history (package_name, level, score, timestamp)
|
|
323
|
+
VALUES (?, ?, ?, ?)`)
|
|
324
|
+
.run(entry.packageName, entry.level, entry.score, entry.timestamp);
|
|
325
|
+
this.db
|
|
326
|
+
.prepare(`DELETE FROM check_history WHERE id NOT IN (
|
|
327
|
+
SELECT id FROM check_history ORDER BY timestamp DESC, id DESC LIMIT ${MAX_CHECK_HISTORY}
|
|
328
|
+
)`)
|
|
329
|
+
.run();
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Return the persistent check history, newest first.
|
|
333
|
+
*/
|
|
334
|
+
async getHistory(limit) {
|
|
335
|
+
const capped = Math.max(1, Math.min(limit ?? MAX_CHECK_HISTORY, MAX_CHECK_HISTORY));
|
|
336
|
+
const rows = this.db
|
|
337
|
+
.prepare(`SELECT package_name, level, score, timestamp FROM check_history
|
|
338
|
+
ORDER BY timestamp DESC, id DESC LIMIT ${capped}`)
|
|
339
|
+
.all();
|
|
340
|
+
return rows.map((r) => ({
|
|
341
|
+
packageName: r.package_name,
|
|
342
|
+
level: r.level,
|
|
343
|
+
score: r.score,
|
|
344
|
+
timestamp: r.timestamp,
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Remove every entry from the persistent check history.
|
|
349
|
+
*/
|
|
350
|
+
async clearHistory() {
|
|
351
|
+
this.db.prepare("DELETE FROM check_history").run();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/** Maximum number of check-history entries retained in the database. */
|
|
355
|
+
export const MAX_CHECK_HISTORY = 1000;
|
|
356
|
+
//# sourceMappingURL=cache-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-manager.js","sourceRoot":"","sources":["../../src/store/cache-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite connection manager for @npm-safe/core.
|
|
3
|
+
*
|
|
4
|
+
* Owns a single `better-sqlite3` connection, configures WAL-mode pragmas for
|
|
5
|
+
* safe concurrent reads, and runs the migration list declared in
|
|
6
|
+
* {@link ./schema.ts} on construction. This module is pure connection +
|
|
7
|
+
* migration management — no query helpers, no caching, no business logic.
|
|
8
|
+
*
|
|
9
|
+
* @module store/database
|
|
10
|
+
*/
|
|
11
|
+
import Database from "better-sqlite3";
|
|
12
|
+
/**
|
|
13
|
+
* Error thrown by {@link DatabaseManager} when the underlying `better-sqlite3`
|
|
14
|
+
* operation fails (open, pragma, migration). Wraps the original error so
|
|
15
|
+
* callers can branch on a single typed error while still inspecting the cause.
|
|
16
|
+
*/
|
|
17
|
+
export declare class DatabaseManagerError extends Error {
|
|
18
|
+
readonly cause?: unknown | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* @param message - Human-readable error message.
|
|
21
|
+
* @param cause - The underlying error, if any.
|
|
22
|
+
*/
|
|
23
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Manages the lifecycle of a single SQLite database connection.
|
|
27
|
+
*
|
|
28
|
+
* On construction it opens the database file (creating it if missing),
|
|
29
|
+
* applies WAL-mode and safety pragmas, then runs any unapplied migrations
|
|
30
|
+
* from {@link getMigrationList} inside a transaction. The connection is
|
|
31
|
+
* kept open and accessible via {@link DatabaseManager.getDb} until
|
|
32
|
+
* {@link DatabaseManager.close} is called.
|
|
33
|
+
*/
|
|
34
|
+
export declare class DatabaseManager {
|
|
35
|
+
/** Underlying better-sqlite3 connection, or `null` after `close()`. */
|
|
36
|
+
private db;
|
|
37
|
+
/**
|
|
38
|
+
* Opens (or creates) the database at `dbPath`, configures pragmas, and
|
|
39
|
+
* runs pending migrations. Throws {@link DatabaseManagerError} on any
|
|
40
|
+
* failure during open, pragma, or migration.
|
|
41
|
+
*
|
|
42
|
+
* @param dbPath - Filesystem path to the SQLite database file.
|
|
43
|
+
*/
|
|
44
|
+
constructor(dbPath: string);
|
|
45
|
+
/**
|
|
46
|
+
* Apply the connection-level pragmas: WAL journal mode, a 5s busy timeout,
|
|
47
|
+
* NORMAL synchronous (safe under WAL), and foreign-key enforcement.
|
|
48
|
+
*/
|
|
49
|
+
private applyPragmas;
|
|
50
|
+
/**
|
|
51
|
+
* Run all migrations from {@link getMigrationList} that have not yet been
|
|
52
|
+
* recorded in the `_migrations` table. The `_migrations` tracking table is
|
|
53
|
+
* created idempotently first (it is also part of {@link SCHEMA_SQL}, but
|
|
54
|
+
* we create it here so the very first migration can be recorded even when
|
|
55
|
+
* the schema is empty).
|
|
56
|
+
*
|
|
57
|
+
* Each migration is applied in its own transaction; on success its name is
|
|
58
|
+
* inserted into `_migrations`.
|
|
59
|
+
*/
|
|
60
|
+
private runMigrations;
|
|
61
|
+
/**
|
|
62
|
+
* Returns the underlying `better-sqlite3` connection. Throws if the
|
|
63
|
+
* connection has been closed.
|
|
64
|
+
*
|
|
65
|
+
* @returns The active better-sqlite3 `Database` instance.
|
|
66
|
+
*/
|
|
67
|
+
getDb(): Database.Database;
|
|
68
|
+
/**
|
|
69
|
+
* Closes the database connection. Safe to call multiple times; subsequent
|
|
70
|
+
* calls are no-ops. Throws {@link DatabaseManagerError} if the underlying
|
|
71
|
+
* close fails.
|
|
72
|
+
*/
|
|
73
|
+
close(): void;
|
|
74
|
+
/**
|
|
75
|
+
* Whether the database connection is currently open.
|
|
76
|
+
*
|
|
77
|
+
* @returns `true` if the connection has not been closed.
|
|
78
|
+
*/
|
|
79
|
+
isOpen(): boolean;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/store/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAQtC;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;aAO3B,KAAK,CAAC,EAAE,OAAO;IANjC;;;OAGG;gBAED,OAAO,EAAE,MAAM,EACC,KAAK,CAAC,EAAE,OAAO,YAAA;CAKlC;AAoBD;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAC1B,uEAAuE;IACvE,OAAO,CAAC,EAAE,CAA2B;IAErC;;;;;;OAMG;gBACS,MAAM,EAAE,MAAM;IAc1B;;;OAGG;IACH,OAAO,CAAC,YAAY;IAmBpB;;;;;;;;;OASG;IACH,OAAO,CAAC,aAAa;IAiDrB;;;;;OAKG;IACH,KAAK,IAAI,QAAQ,CAAC,QAAQ;IAO1B;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAgBb;;;;OAIG;IACH,MAAM,IAAI,OAAO;CAGlB"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite connection manager for @npm-safe/core.
|
|
3
|
+
*
|
|
4
|
+
* Owns a single `better-sqlite3` connection, configures WAL-mode pragmas for
|
|
5
|
+
* safe concurrent reads, and runs the migration list declared in
|
|
6
|
+
* {@link ./schema.ts} on construction. This module is pure connection +
|
|
7
|
+
* migration management — no query helpers, no caching, no business logic.
|
|
8
|
+
*
|
|
9
|
+
* @module store/database
|
|
10
|
+
*/
|
|
11
|
+
import Database from "better-sqlite3";
|
|
12
|
+
import { getMigrationList, getInitialMigration, getCheckHistoryMigration, } from "./schema.js";
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown by {@link DatabaseManager} when the underlying `better-sqlite3`
|
|
15
|
+
* operation fails (open, pragma, migration). Wraps the original error so
|
|
16
|
+
* callers can branch on a single typed error while still inspecting the cause.
|
|
17
|
+
*/
|
|
18
|
+
export class DatabaseManagerError extends Error {
|
|
19
|
+
cause;
|
|
20
|
+
/**
|
|
21
|
+
* @param message - Human-readable error message.
|
|
22
|
+
* @param cause - The underlying error, if any.
|
|
23
|
+
*/
|
|
24
|
+
constructor(message, cause) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.cause = cause;
|
|
27
|
+
this.name = "DatabaseManagerError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Maps a migration file name to the SQL it should execute. Today only the
|
|
32
|
+
* initial migration exists; future migrations add entries here.
|
|
33
|
+
*
|
|
34
|
+
* @param name - Migration file name from {@link getMigrationList}.
|
|
35
|
+
* @returns The SQL string to run for that migration.
|
|
36
|
+
*/
|
|
37
|
+
function getMigrationSql(name) {
|
|
38
|
+
switch (name) {
|
|
39
|
+
case "001_initial.sql":
|
|
40
|
+
return getInitialMigration();
|
|
41
|
+
case "002_check_history.sql":
|
|
42
|
+
return getCheckHistoryMigration();
|
|
43
|
+
default:
|
|
44
|
+
throw new DatabaseManagerError(`Unknown migration: ${name}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Manages the lifecycle of a single SQLite database connection.
|
|
49
|
+
*
|
|
50
|
+
* On construction it opens the database file (creating it if missing),
|
|
51
|
+
* applies WAL-mode and safety pragmas, then runs any unapplied migrations
|
|
52
|
+
* from {@link getMigrationList} inside a transaction. The connection is
|
|
53
|
+
* kept open and accessible via {@link DatabaseManager.getDb} until
|
|
54
|
+
* {@link DatabaseManager.close} is called.
|
|
55
|
+
*/
|
|
56
|
+
export class DatabaseManager {
|
|
57
|
+
/** Underlying better-sqlite3 connection, or `null` after `close()`. */
|
|
58
|
+
db;
|
|
59
|
+
/**
|
|
60
|
+
* Opens (or creates) the database at `dbPath`, configures pragmas, and
|
|
61
|
+
* runs pending migrations. Throws {@link DatabaseManagerError} on any
|
|
62
|
+
* failure during open, pragma, or migration.
|
|
63
|
+
*
|
|
64
|
+
* @param dbPath - Filesystem path to the SQLite database file.
|
|
65
|
+
*/
|
|
66
|
+
constructor(dbPath) {
|
|
67
|
+
try {
|
|
68
|
+
this.db = new Database(dbPath, { fileMustExist: false });
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
throw new DatabaseManagerError(`Failed to open database at ${dbPath}: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
72
|
+
}
|
|
73
|
+
this.applyPragmas();
|
|
74
|
+
this.runMigrations();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Apply the connection-level pragmas: WAL journal mode, a 5s busy timeout,
|
|
78
|
+
* NORMAL synchronous (safe under WAL), and foreign-key enforcement.
|
|
79
|
+
*/
|
|
80
|
+
applyPragmas() {
|
|
81
|
+
const pragmas = [
|
|
82
|
+
"journal_mode=WAL",
|
|
83
|
+
"busy_timeout=5000",
|
|
84
|
+
"synchronous=NORMAL",
|
|
85
|
+
"foreign_keys=ON",
|
|
86
|
+
];
|
|
87
|
+
for (const stmt of pragmas) {
|
|
88
|
+
try {
|
|
89
|
+
this.db.pragma(stmt);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
throw new DatabaseManagerError(`Failed to apply pragma "${stmt}": ${err instanceof Error ? err.message : String(err)}`, err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Run all migrations from {@link getMigrationList} that have not yet been
|
|
98
|
+
* recorded in the `_migrations` table. The `_migrations` tracking table is
|
|
99
|
+
* created idempotently first (it is also part of {@link SCHEMA_SQL}, but
|
|
100
|
+
* we create it here so the very first migration can be recorded even when
|
|
101
|
+
* the schema is empty).
|
|
102
|
+
*
|
|
103
|
+
* Each migration is applied in its own transaction; on success its name is
|
|
104
|
+
* inserted into `_migrations`.
|
|
105
|
+
*/
|
|
106
|
+
runMigrations() {
|
|
107
|
+
const db = this.db;
|
|
108
|
+
try {
|
|
109
|
+
db.exec(`
|
|
110
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
111
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
112
|
+
name TEXT NOT NULL UNIQUE,
|
|
113
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
114
|
+
);
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
throw new DatabaseManagerError(`Failed to create _migrations tracking table: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
119
|
+
}
|
|
120
|
+
const selectApplied = db.prepare("SELECT name FROM _migrations WHERE name = ?");
|
|
121
|
+
const insertMigration = db.prepare("INSERT INTO _migrations (name) VALUES (?)");
|
|
122
|
+
const applyOne = (name, sql) => {
|
|
123
|
+
const tx = db.transaction(() => {
|
|
124
|
+
db.exec(sql);
|
|
125
|
+
insertMigration.run(name);
|
|
126
|
+
});
|
|
127
|
+
tx();
|
|
128
|
+
};
|
|
129
|
+
for (const name of getMigrationList()) {
|
|
130
|
+
const existing = selectApplied.get(name);
|
|
131
|
+
if (existing !== undefined) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
applyOne(name, getMigrationSql(name));
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
throw new DatabaseManagerError(`Failed to apply migration "${name}": ${err instanceof Error ? err.message : String(err)}`, err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Returns the underlying `better-sqlite3` connection. Throws if the
|
|
144
|
+
* connection has been closed.
|
|
145
|
+
*
|
|
146
|
+
* @returns The active better-sqlite3 `Database` instance.
|
|
147
|
+
*/
|
|
148
|
+
getDb() {
|
|
149
|
+
if (this.db === null) {
|
|
150
|
+
throw new DatabaseManagerError("Database is not open");
|
|
151
|
+
}
|
|
152
|
+
return this.db;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Closes the database connection. Safe to call multiple times; subsequent
|
|
156
|
+
* calls are no-ops. Throws {@link DatabaseManagerError} if the underlying
|
|
157
|
+
* close fails.
|
|
158
|
+
*/
|
|
159
|
+
close() {
|
|
160
|
+
if (this.db === null) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
this.db.close();
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
throw new DatabaseManagerError(`Failed to close database: ${err instanceof Error ? err.message : String(err)}`, err);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
this.db = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether the database connection is currently open.
|
|
175
|
+
*
|
|
176
|
+
* @returns `true` if the connection has not been closed.
|
|
177
|
+
*/
|
|
178
|
+
isOpen() {
|
|
179
|
+
return this.db !== null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/store/database.ts"],"names":[],"mappings":"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"}
|