@vltpkg/security-archive 1.0.0-rc.24 → 1.0.0-rc.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
+ var _a;
1
2
  import { DatabaseSync } from 'node:sqlite';
3
+ import { spawn } from 'node:child_process';
2
4
  import { mkdirSync } from 'node:fs';
3
5
  import { dirname } from 'node:path';
4
6
  import { LRUCache } from 'lru-cache';
@@ -8,6 +10,7 @@ import { asDepID, splitDepID, baseDepID } from '@vltpkg/dep-id';
8
10
  import { error } from '@vltpkg/error-cause';
9
11
  import { XDG } from '@vltpkg/xdg';
10
12
  import { asPackageReportData } from "./types.js";
13
+ import { __CODE_SPLIT_SCRIPT_NAME } from "./update-expired.js";
11
14
  export * from "./types.js";
12
15
  const SOCKET_API_V0_URL = 'https://api.socket.dev/v0/purl?alerts=true';
13
16
  const SOCKET_PUBLIC_API_TOKEN = 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api';
@@ -39,7 +42,6 @@ export const { version } = loadPackageJson(import.meta.filename, process.env.__V
39
42
  */
40
43
  export class SecurityArchive extends LRUCache {
41
44
  #expired = new Set();
42
- #pUpdateExpired;
43
45
  #path;
44
46
  #retries;
45
47
  #nodesByName = new Map();
@@ -53,7 +55,7 @@ export class SecurityArchive extends LRUCache {
53
55
  * Creates a new security archive instance and starts the refresh process.
54
56
  */
55
57
  static async start(options) {
56
- const archive = new SecurityArchive(options);
58
+ const archive = new _a(options);
57
59
  await archive.refresh(options);
58
60
  return archive;
59
61
  }
@@ -71,8 +73,8 @@ export class SecurityArchive extends LRUCache {
71
73
  }
72
74
  constructor(options = {}) {
73
75
  super({
74
- max: SecurityArchive.defaultMax,
75
- ttl: SecurityArchive.defaultTtl,
76
+ max: _a.defaultMax,
77
+ ttl: _a.defaultTtl,
76
78
  ...options,
77
79
  allowStale: true,
78
80
  });
@@ -141,35 +143,66 @@ export class SecurityArchive extends LRUCache {
141
143
  db.prepare('DELETE FROM cache WHERE depID = ?').run(depID);
142
144
  }
143
145
  }
144
- // TODO: we need to move this to a detached process in order for the
145
- // cli commands that make usage of the security-archive, e.g: vlt ls,
146
- // vlt query to not hang while waiting for stale-while-revalidate
147
- // request to finish and close the db connection
148
- this.#pUpdateExpired = this.#updateExpired(db);
146
+ // Spawn a detached process to revalidate expired entries in the
147
+ // background so that cli commands (vlt ls, vlt query) don't hang
148
+ // waiting for stale-while-revalidate requests to complete.
149
+ this.#spawnUpdateExpired();
149
150
  }
150
151
  /**
151
- * Updates the database with renewed entries for expired packages.
152
+ * Spawns a detached child process to revalidate expired entries.
153
+ * The process runs independently — the main process does not wait for it.
152
154
  */
153
- async #updateExpired(db) {
154
- const expiredQueue = new Set();
155
+ #spawnUpdateExpired() {
156
+ const expired = [];
155
157
  for (const depID of this.#expired) {
156
158
  const node = this.#nodesByID.get(baseDepID(depID));
157
159
  /* c8 ignore start */
158
- if (!node?.version) {
159
- // skip any missing node or nodes without a version, this
160
+ if (!node?.version || !node.name) {
161
+ // skip any missing node or nodes without a version/name, this
160
162
  // should not happen, but some optional dependencies might
161
163
  // end up in this state when reading from the lockfile
162
164
  continue;
163
165
  }
164
166
  /* c8 ignore stop */
165
- const purl = `pkg:npm/${node.name}@${node.version}`;
166
- expiredQueue.add({ purl });
167
+ expired.push({
168
+ depID: baseDepID(depID),
169
+ name: node.name,
170
+ version: node.version,
171
+ });
167
172
  }
168
- if (expiredQueue.size > 0) {
169
- const res = await pRetry(() => this.#retrieveRemoteData(expiredQueue), { retries: this.#retries });
170
- const ids = this.#loadFromNDJSON(res);
171
- this.#storeNewItemsToDatabase(db, ids);
173
+ if (!expired.length) {
174
+ return;
172
175
  }
176
+ const isDeno = globalThis.Deno !=
177
+ undefined;
178
+ const payload = {
179
+ dbPath: this.#path,
180
+ retries: this.#retries,
181
+ ttl: _a.defaultTtl,
182
+ expired,
183
+ };
184
+ const args = [];
185
+ // Deno on Windows does not support detached processes
186
+ /* c8 ignore next */
187
+ const detached = !(isDeno && process.platform === 'win32');
188
+ /* c8 ignore start */
189
+ if (isDeno) {
190
+ args.push('--unstable-node-globals', '--unstable-bare-node-builtins');
191
+ }
192
+ /* c8 ignore stop */
193
+ args.push(__CODE_SPLIT_SCRIPT_NAME);
194
+ const proc = spawn(process.execPath, args, {
195
+ detached,
196
+ stdio: ['pipe', 'ignore', 'ignore'],
197
+ env: { ...process.env },
198
+ });
199
+ proc.stdin.write(JSON.stringify(payload));
200
+ proc.stdin.end();
201
+ /* c8 ignore start */
202
+ if (detached) {
203
+ proc.unref();
204
+ }
205
+ /* c8 ignore stop */
173
206
  }
174
207
  /**
175
208
  * Queues up all required packages that are missing from the archive.
@@ -335,26 +368,18 @@ export class SecurityArchive extends LRUCache {
335
368
  this.#validateReportData();
336
369
  }
337
370
  finally {
338
- // TODO: once we move the stale-while-revalidate to a detached process
339
- // the this.#close method no longer needs to be async
340
- void this.#close(db);
371
+ this.#close(db);
341
372
  }
342
373
  }
343
374
  /**
344
375
  * Closes the database connection and cleans up the internal state.
345
376
  */
346
- async #close(db) {
347
- // the revalidation of stale entries might not yet be completed
348
- // so in case we have a pending promise for that, we need to wait
349
- await this.#pUpdateExpired;
377
+ #close(db) {
350
378
  // close db connection
351
379
  db.exec('PRAGMA optimize');
352
380
  db.close();
353
381
  // clean up internal state
354
382
  this.#expired.clear();
355
- this.#pUpdateExpired = undefined;
356
- // TODO: at this point we could spawn a deref process to
357
- // remove entries with expired ttl values and vaccum the db
358
383
  }
359
384
  /**
360
385
  * Outputs the current in-memory cache as a JSON object.
@@ -367,3 +392,4 @@ export class SecurityArchive extends LRUCache {
367
392
  return obj;
368
393
  }
369
394
  }
395
+ _a = SecurityArchive;
@@ -0,0 +1,32 @@
1
+ import type { DepID } from '@vltpkg/dep-id';
2
+ import type EventEmitter from 'node:events';
3
+ export declare const __CODE_SPLIT_SCRIPT_NAME: string;
4
+ export declare const version: string;
5
+ /**
6
+ * Serialized payload sent to the detached process via stdin.
7
+ */
8
+ export type UpdateExpiredPayload = {
9
+ /** Path to the sqlite database */
10
+ dbPath: string;
11
+ /** Number of retries for fetching remote data */
12
+ retries: number;
13
+ /** TTL in ms to use for new entries */
14
+ ttl: number;
15
+ /** Expired entries to revalidate */
16
+ expired: ExpiredEntry[];
17
+ };
18
+ /**
19
+ * Minimal serialized information for each expired entry.
20
+ * Only carries what's needed for the API call + DB write.
21
+ */
22
+ export type ExpiredEntry = {
23
+ depID: DepID;
24
+ name: string;
25
+ version: string;
26
+ };
27
+ /**
28
+ * Main entrypoint for the detached process.
29
+ * Reads a JSON payload from stdin, fetches updated security data,
30
+ * and writes it back to the SQLite database.
31
+ */
32
+ export declare const main: (input?: EventEmitter) => Promise<boolean>;
@@ -0,0 +1,153 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import pRetry, { AbortError } from 'p-retry';
6
+ import { asDepID, baseDepID } from '@vltpkg/dep-id';
7
+ import { error } from '@vltpkg/error-cause';
8
+ import { loadPackageJson } from 'package-json-from-dist';
9
+ import { asPackageReportData } from "./types.js";
10
+ export const __CODE_SPLIT_SCRIPT_NAME = import.meta.filename;
11
+ const SOCKET_API_V0_URL = 'https://api.socket.dev/v0/purl?alerts=true';
12
+ const SOCKET_PUBLIC_API_TOKEN = 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api';
13
+ export const { version } = loadPackageJson(import.meta.filename, process.env.__VLT_INTERNAL_CLI_PACKAGE_JSON);
14
+ /* c8 ignore start */
15
+ const isMain = (path) => path === __CODE_SPLIT_SCRIPT_NAME ||
16
+ path === pathToFileURL(__CODE_SPLIT_SCRIPT_NAME).toString();
17
+ /* c8 ignore stop */
18
+ /**
19
+ * Fetches security information from the socket.dev API.
20
+ */
21
+ const retrieveRemoteData = async (queue, retries) => {
22
+ return pRetry(async () => {
23
+ const req = await fetch(SOCKET_API_V0_URL, {
24
+ method: 'POST',
25
+ headers: {
26
+ Authorization: `Basic ${Buffer.from(`${SOCKET_PUBLIC_API_TOKEN}:`).toString('base64url')}`,
27
+ 'User-Agent': `@vltpkg/security-archive/${version}`,
28
+ },
29
+ body: JSON.stringify({
30
+ components: Array.from(queue),
31
+ }),
32
+ });
33
+ if (req.status === 404) {
34
+ throw new AbortError('Missing API');
35
+ }
36
+ if (!req.ok || !(req.status >= 200 && req.status <= 299)) {
37
+ throw error('Failed to fetch security data', {
38
+ response: req,
39
+ });
40
+ }
41
+ const str = await req.text();
42
+ return str.trim() + '\n';
43
+ }, { retries });
44
+ };
45
+ /**
46
+ * Main entrypoint for the detached process.
47
+ * Reads a JSON payload from stdin, fetches updated security data,
48
+ * and writes it back to the SQLite database.
49
+ */
50
+ export const main = async (input = process.stdin) => {
51
+ const payload = await new Promise((resolve, reject) => {
52
+ const chunks = [];
53
+ let chunkLen = 0;
54
+ input.on('data', (chunk) => {
55
+ chunks.push(chunk);
56
+ chunkLen += chunk.length;
57
+ });
58
+ input.on('end', () => {
59
+ try {
60
+ const raw = Buffer.concat(chunks, chunkLen).toString();
61
+ resolve(JSON.parse(raw));
62
+ /* c8 ignore start */
63
+ }
64
+ catch (err) {
65
+ reject(err);
66
+ }
67
+ /* c8 ignore stop */
68
+ });
69
+ /* c8 ignore next */
70
+ input.on('error', reject);
71
+ });
72
+ const { dbPath, retries, ttl, expired } = payload;
73
+ if (!expired.length) {
74
+ return false;
75
+ }
76
+ // Build purl queue from expired entries
77
+ const expiredQueue = new Set();
78
+ // Build a lookup map: name@version -> depID
79
+ const entryByNameVersion = new Map();
80
+ for (const entry of expired) {
81
+ const purl = `pkg:npm/${entry.name}@${entry.version}`;
82
+ expiredQueue.add({ purl });
83
+ entryByNameVersion.set(`${entry.name}@${entry.version}`, entry);
84
+ }
85
+ // Fetch updated data
86
+ const res = await retrieveRemoteData(expiredQueue, retries);
87
+ // Parse NDJSON response and collect results
88
+ const now = Date.now();
89
+ const results = [];
90
+ const json = res.split('}\n');
91
+ for (const line of json) {
92
+ if (!line.trim())
93
+ continue;
94
+ const data = JSON.parse(line + '}');
95
+ const scope = data.namespace ? `${data.namespace}/` : '';
96
+ const name = `${scope}${data.name}`;
97
+ const key = `${name}@${data.version}`;
98
+ const entry = entryByNameVersion.get(key);
99
+ if (!entry) {
100
+ // eslint-disable-next-line no-console
101
+ console.warn(`security-archive: failed to find entry for ${key} found in the response.`);
102
+ continue;
103
+ }
104
+ // Calculate average score from all score components
105
+ const scoreComponents = [
106
+ data.score.license,
107
+ data.score.maintenance,
108
+ data.score.quality,
109
+ data.score.supplyChain,
110
+ data.score.vulnerability,
111
+ ];
112
+ const newAverageScore = Number((scoreComponents.reduce((sum, score) => sum + score, 0) /
113
+ scoreComponents.length).toFixed(2));
114
+ const reportData = asPackageReportData({
115
+ ...data,
116
+ score: { ...data.score, overall: newAverageScore },
117
+ });
118
+ results.push({
119
+ depID: baseDepID(asDepID(entry.depID)),
120
+ report: JSON.stringify(reportData),
121
+ start: now,
122
+ ttl,
123
+ });
124
+ }
125
+ if (!results.length) {
126
+ return false;
127
+ }
128
+ // Write to database
129
+ mkdirSync(dirname(dbPath), { recursive: true });
130
+ const db = new DatabaseSync(dbPath);
131
+ db.exec('CREATE TABLE IF NOT EXISTS cache ' +
132
+ '(depID TEXT PRIMARY KEY, report TEXT, ttl INTEGER, start INTEGER) ' +
133
+ 'WITHOUT ROWID');
134
+ db.exec('PRAGMA journal_mode = WAL');
135
+ db.exec('PRAGMA synchronous = NORMAL');
136
+ const dbWrite = db.prepare('INSERT OR REPLACE INTO cache (depID, report, start, ttl) ' +
137
+ 'VALUES (?, ?, ?, ?)');
138
+ for (const { depID, report, start, ttl: entryTtl } of results) {
139
+ dbWrite.run(depID, report, start, entryTtl);
140
+ }
141
+ db.exec('PRAGMA optimize');
142
+ db.close();
143
+ return true;
144
+ };
145
+ /* c8 ignore start */
146
+ if (isMain(process.argv[1])) {
147
+ process.title = 'vlt-security-archive-update';
148
+ const res = await main(process.stdin);
149
+ if (!res) {
150
+ process.exit(1);
151
+ }
152
+ }
153
+ /* c8 ignore stop */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/security-archive",
3
3
  "description": "A database of security information of packages",
4
- "version": "1.0.0-rc.24",
4
+ "version": "1.0.0-rc.26",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -9,14 +9,15 @@
9
9
  },
10
10
  "author": {
11
11
  "name": "vlt technology inc.",
12
- "email": "support@vlt.sh"
12
+ "email": "support@vlt.sh",
13
+ "url": "http://vlt.sh"
13
14
  },
14
15
  "dependencies": {
15
- "@vltpkg/dep-id": "1.0.0-rc.24",
16
- "@vltpkg/error-cause": "1.0.0-rc.24",
17
- "@vltpkg/spec": "1.0.0-rc.24",
18
- "@vltpkg/types": "1.0.0-rc.24",
19
- "@vltpkg/xdg": "1.0.0-rc.24",
16
+ "@vltpkg/dep-id": "1.0.0-rc.26",
17
+ "@vltpkg/error-cause": "1.0.0-rc.26",
18
+ "@vltpkg/spec": "1.0.0-rc.26",
19
+ "@vltpkg/types": "1.0.0-rc.26",
20
+ "@vltpkg/xdg": "1.0.0-rc.26",
20
21
  "lru-cache": "^11.2.4",
21
22
  "p-retry": "^7.1.1",
22
23
  "package-json-from-dist": "^1.0.1"
@@ -63,6 +64,11 @@
63
64
  "import": {
64
65
  "default": "./dist/browser.js"
65
66
  }
67
+ },
68
+ "./update-expired": {
69
+ "import": {
70
+ "default": "./dist/update-expired.js"
71
+ }
66
72
  }
67
73
  },
68
74
  "files": [