@vltpkg/security-archive 1.4.0 → 1.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 CHANGED
@@ -23,7 +23,9 @@ const graph = actual.load({
23
23
  projectRoot: process.cwd(),
24
24
  })
25
25
 
26
- const archive = await SecurityArchive.start({ graph, specOptions })
26
+ const archive = await SecurityArchive.start({
27
+ nodes: [...graph.nodes.values()],
28
+ })
27
29
 
28
30
  if (archive.ok) {
29
31
  for (const node of graph.nodes.values()) {
@@ -36,3 +38,14 @@ if (archive.ok) {
36
38
  console.warn('Failed to start the SecurityArchive')
37
39
  }
38
40
  ```
41
+
42
+ ## Options
43
+
44
+ - `path`: sqlite db location. Defaults to the vlt XDG cache dir.
45
+ - `retries`: retry attempts on failed API requests. Defaults to `3`.
46
+ - `timeout`: max ms to wait for the API, retries included. Defaults to
47
+ `VLT_SECURITY_ARCHIVE_TIMEOUT` env or `30000`. Must be a positive
48
+ integer, other values are ignored. On timeout a warning is printed,
49
+ missing packages stay unscanned, `ok` is `false`, `timedOut` is
50
+ `true` and nothing is cached.
51
+ - `ttl`: ms to cache newly fetched entries for. Defaults to 3 hours.
package/dist/index.d.ts CHANGED
@@ -24,6 +24,14 @@ export type SecurityArchiveOptions = LRUCache.OptionsBase<DepID, PackageReportDa
24
24
  * Number of retries attempts to reach the remote security API.
25
25
  */
26
26
  retries?: number;
27
+ /**
28
+ * Max ms to wait for the remote security API, incl. retries.
29
+ * Defaults to `VLT_SECURITY_ARCHIVE_TIMEOUT` env or
30
+ * {@link SecurityArchive.defaultTimeout}. Positive integers only,
31
+ * other values are ignored. On timeout, missing packages are left
32
+ * unscanned and a warning is printed.
33
+ */
34
+ timeout?: number;
27
35
  };
28
36
  /**
29
37
  * A database of security information for given packages from a list of nodes.
@@ -31,7 +39,8 @@ export type SecurityArchiveOptions = LRUCache.OptionsBase<DepID, PackageReportDa
31
39
  * Using the SecurityArchive.refresh() method will update the local cache
32
40
  * with information from the socket.dev APIs or load from the local storage
33
41
  * if available. Information about package security is then available
34
- * using the SecurityArchive.get() method.
42
+ * using the SecurityArchive.get() method. Remote requests are bounded
43
+ * by the `timeout` option.
35
44
  */
36
45
  export declare class SecurityArchive extends LRUCache<DepID, PackageReportData> implements SecurityArchiveLike {
37
46
  #private;
@@ -40,6 +49,10 @@ export declare class SecurityArchive extends LRUCache<DepID, PackageReportData>
40
49
  * for all public registry packages from the initial list of nodes.
41
50
  */
42
51
  ok: boolean;
52
+ /**
53
+ * True if the last refresh() timed out waiting for the remote API.
54
+ */
55
+ timedOut: boolean;
43
56
  /**
44
57
  * Creates a new security archive instance and starts the refresh process.
45
58
  */
@@ -52,6 +65,10 @@ export declare class SecurityArchive extends LRUCache<DepID, PackageReportData>
52
65
  * By default, entries are cached for 3 hours.
53
66
  */
54
67
  static get defaultTtl(): number;
68
+ /**
69
+ * By default, remote requests time out after 30 seconds.
70
+ */
71
+ static get defaultTimeout(): number;
55
72
  constructor(options?: SecurityArchiveOptions);
56
73
  /**
57
74
  * Get the report data for a node, keyed by its base {@link DepID}.
package/dist/index.js CHANGED
@@ -16,18 +16,27 @@ export * from "./types.js";
16
16
  export { npmRegistryURL, usesNpmRegistry } from "./browser.js";
17
17
  const SOCKET_API_V0_URL = 'https://api.socket.dev/v0/purl?alerts=true';
18
18
  const SOCKET_PUBLIC_API_TOKEN = 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api';
19
+ // AbortSignal.timeout throws on <0/NaN and overflows to 1ms above 2^31-1
20
+ const asTimeout = (raw) => {
21
+ const n = Number(raw);
22
+ return Number.isInteger(n) && n > 0 && n <= 2_147_483_647 ?
23
+ n
24
+ : undefined;
25
+ };
19
26
  /**
20
27
  * A database of security information for given packages from a list of nodes.
21
28
  *
22
29
  * Using the SecurityArchive.refresh() method will update the local cache
23
30
  * with information from the socket.dev APIs or load from the local storage
24
31
  * if available. Information about package security is then available
25
- * using the SecurityArchive.get() method.
32
+ * using the SecurityArchive.get() method. Remote requests are bounded
33
+ * by the `timeout` option.
26
34
  */
27
35
  export class SecurityArchive extends LRUCache {
28
36
  #expired = new Set();
29
37
  #path;
30
38
  #retries;
39
+ #timeout;
31
40
  #nodesByName = new Map();
32
41
  #nodesByID = new Map();
33
42
  /**
@@ -35,6 +44,10 @@ export class SecurityArchive extends LRUCache {
35
44
  * for all public registry packages from the initial list of nodes.
36
45
  */
37
46
  ok = false;
47
+ /**
48
+ * True if the last refresh() timed out waiting for the remote API.
49
+ */
50
+ timedOut = false;
38
51
  /**
39
52
  * Creates a new security archive instance and starts the refresh process.
40
53
  */
@@ -55,6 +68,12 @@ export class SecurityArchive extends LRUCache {
55
68
  static get defaultTtl() {
56
69
  return 1000 * 60 * 60 * 3;
57
70
  }
71
+ /**
72
+ * By default, remote requests time out after 30 seconds.
73
+ */
74
+ static get defaultTimeout() {
75
+ return 30_000;
76
+ }
58
77
  constructor(options = {}) {
59
78
  super({
60
79
  max: _a.defaultMax,
@@ -65,6 +84,10 @@ export class SecurityArchive extends LRUCache {
65
84
  this.#path =
66
85
  options.path ?? new XDG('vlt').cache('security-archive.db');
67
86
  this.#retries = options.retries ?? 3;
87
+ this.#timeout =
88
+ asTimeout(options.timeout) ??
89
+ asTimeout(process.env.VLT_SECURITY_ARCHIVE_TIMEOUT) ??
90
+ _a.defaultTimeout;
68
91
  }
69
92
  /**
70
93
  * Get the report data for a node, keyed by its base {@link DepID}.
@@ -187,6 +210,7 @@ export class SecurityArchive extends LRUCache {
187
210
  const payload = {
188
211
  dbPath: this.#path,
189
212
  retries: this.#retries,
213
+ timeout: this.#timeout,
190
214
  ttl: _a.defaultTtl,
191
215
  expired,
192
216
  };
@@ -235,7 +259,7 @@ export class SecurityArchive extends LRUCache {
235
259
  * Fetches security information from the socket.dev API.
236
260
  * Returns a string of NDJSON data.
237
261
  */
238
- async #retrieveRemoteData(queue) {
262
+ async #retrieveRemoteData(queue, signal) {
239
263
  // fetch information from the socket.dev API
240
264
  const req = await fetch(SOCKET_API_V0_URL, {
241
265
  method: 'POST',
@@ -246,6 +270,7 @@ export class SecurityArchive extends LRUCache {
246
270
  body: JSON.stringify({
247
271
  components: Array.from(queue),
248
272
  }),
273
+ signal,
249
274
  });
250
275
  // on missing valid auth or API, it should abort the retry logic
251
276
  if (req.status === 404) {
@@ -258,6 +283,23 @@ export class SecurityArchive extends LRUCache {
258
283
  const str = await req.text();
259
284
  return str.trim() + '\n';
260
285
  }
286
+ /**
287
+ * Fetches remote data within the configured timeout, retries included.
288
+ * Returns `undefined` on timeout so that queued packages stay unscanned.
289
+ */
290
+ async #fetchRemoteData(queue) {
291
+ const signal = AbortSignal.timeout(this.#timeout);
292
+ try {
293
+ return await pRetry(() => this.#retrieveRemoteData(queue, signal), { retries: this.#retries, signal });
294
+ }
295
+ catch (err) {
296
+ if (!signal.aborted)
297
+ throw err;
298
+ this.timedOut = true;
299
+ // eslint-disable-next-line no-console
300
+ console.warn(`security-archive: timed out after ${this.#timeout}ms fetching security data; ${queue.size} package(s) left unscanned (use :scanned to exclude them). Set VLT_SECURITY_ARCHIVE_TIMEOUT=<ms> to wait longer.`);
301
+ }
302
+ }
261
303
  /**
262
304
  * Parses and load NDJSON data into the in-memory cache.
263
305
  * Returns a set of dep ids that were successfully loaded.
@@ -345,6 +387,7 @@ export class SecurityArchive extends LRUCache {
345
387
  async refresh({ nodes }) {
346
388
  // should start by clearing the current in-memory cache
347
389
  this.clear();
390
+ this.timedOut = false;
348
391
  const db = this.#openDatabase();
349
392
  // indexes nodes by name and id for quick lookup
350
393
  this.#nodesByName.clear();
@@ -362,10 +405,12 @@ export class SecurityArchive extends LRUCache {
362
405
  const queue = this.#queueUpRequiredPackages();
363
406
  // only reach for the remote API if there are packages queued up
364
407
  if (queue.size > 0) {
365
- // Parse the response data
366
- const res = await pRetry(() => this.#retrieveRemoteData(queue), { retries: this.#retries });
367
- const ids = this.#loadFromNDJSON(res);
368
- this.#storeNewItemsToDatabase(db, ids);
408
+ const res = await this.#fetchRemoteData(queue);
409
+ // on timeout nothing is loaded nor cached
410
+ if (res !== undefined) {
411
+ const ids = this.#loadFromNDJSON(res);
412
+ this.#storeNewItemsToDatabase(db, ids);
413
+ }
369
414
  }
370
415
  // validates the refresh process was successful
371
416
  this.#validateReportData();
@@ -9,6 +9,8 @@ export type UpdateExpiredPayload = {
9
9
  dbPath: string;
10
10
  /** Number of retries for fetching remote data */
11
11
  retries: number;
12
+ /** Max ms for the remote request, incl. retries */
13
+ timeout: number;
12
14
  /** TTL in ms to use for new entries */
13
15
  ttl: number;
14
16
  /** Expired entries to revalidate */
@@ -17,7 +17,8 @@ const isMain = (path) => path === __CODE_SPLIT_SCRIPT_NAME ||
17
17
  /**
18
18
  * Fetches security information from the socket.dev API.
19
19
  */
20
- const retrieveRemoteData = async (queue, retries) => {
20
+ const retrieveRemoteData = async (queue, retries, timeout) => {
21
+ const signal = AbortSignal.timeout(timeout);
21
22
  return pRetry(async () => {
22
23
  const req = await fetch(SOCKET_API_V0_URL, {
23
24
  method: 'POST',
@@ -28,6 +29,7 @@ const retrieveRemoteData = async (queue, retries) => {
28
29
  body: JSON.stringify({
29
30
  components: Array.from(queue),
30
31
  }),
32
+ signal,
31
33
  });
32
34
  if (req.status === 404) {
33
35
  throw new AbortError('Missing API');
@@ -39,7 +41,7 @@ const retrieveRemoteData = async (queue, retries) => {
39
41
  }
40
42
  const str = await req.text();
41
43
  return str.trim() + '\n';
42
- }, { retries });
44
+ }, { retries, signal });
43
45
  };
44
46
  /**
45
47
  * Main entrypoint for the detached process.
@@ -68,7 +70,7 @@ export const main = async (input = process.stdin) => {
68
70
  /* c8 ignore next */
69
71
  input.on('error', reject);
70
72
  });
71
- const { dbPath, retries, ttl, expired } = payload;
73
+ const { dbPath, retries, timeout, ttl, expired } = payload;
72
74
  if (!expired.length) {
73
75
  return false;
74
76
  }
@@ -82,7 +84,7 @@ export const main = async (input = process.stdin) => {
82
84
  entryByNameVersion.set(`${entry.name}@${entry.version}`, entry);
83
85
  }
84
86
  // Fetch updated data
85
- const res = await retrieveRemoteData(expiredQueue, retries);
87
+ const res = await retrieveRemoteData(expiredQueue, retries, timeout);
86
88
  // Parse NDJSON response and collect results
87
89
  const now = Date.now();
88
90
  const results = [];
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.4.0",
4
+ "version": "1.5.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,12 +13,12 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/dep-id": "1.4.0",
17
- "@vltpkg/error-cause": "1.4.0",
18
- "@vltpkg/spec": "1.4.0",
19
- "@vltpkg/types": "1.4.0",
20
- "@vltpkg/user-agent": "1.4.0",
21
- "@vltpkg/xdg": "1.4.0",
16
+ "@vltpkg/dep-id": "1.5.0",
17
+ "@vltpkg/error-cause": "1.5.0",
18
+ "@vltpkg/spec": "1.5.0",
19
+ "@vltpkg/types": "1.5.0",
20
+ "@vltpkg/user-agent": "1.5.0",
21
+ "@vltpkg/xdg": "1.5.0",
22
22
  "lru-cache": "^11.2.4",
23
23
  "p-retry": "^7.1.1"
24
24
  },