@vltpkg/security-archive 0.0.0-4

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 ADDED
@@ -0,0 +1,15 @@
1
+ Copyright (c) vlt technology, Inc.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
7
+ Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by:
8
+
9
+ (a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of contributors, in source or binary form) alone; or
10
+ (b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution.
11
+ Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this license, whether expressly, by implication, estoppel or otherwise.
12
+
13
+ DISCLAIMER
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @vltpkg/security-archive
2
+
3
+ A key/value storage that holds security data for unique package
4
+ versions that are coming from a public registry.
5
+
6
+ This package serves as the backend for `@vltpkg/query` when using
7
+ pseudo-selectors that rely on security data.
8
+
9
+ Security data is provided in partnership with
10
+ [Socket](https://socket.dev/).
11
+
12
+ ## Usage
13
+
14
+ ```
15
+ import { actual } from '@vltpkg/graph'
16
+ import { SecurityArchive } from '@vltpkg/security-archive'
17
+
18
+ const specOptions = {
19
+ registry: 'https://registry.npmjs.org/',
20
+ }
21
+ const graph = actual.load({
22
+ ...specOptions,
23
+ projectRoot: process.cwd(),
24
+ })
25
+
26
+ const archive = await SecurityArchive.start({ graph, specOptions })
27
+
28
+ if (archive.ok) {
29
+ for (const node of graph.nodes.values()) {
30
+ const securityData = archive.get(node.id)
31
+ if (securityData) {
32
+ console.log('securityData', securityData)
33
+ }
34
+ }
35
+ } else {
36
+ console.warn('Failed to start the SecurityArchive')
37
+ }
38
+ ```
@@ -0,0 +1,13 @@
1
+ import type { PackageReportData, SecurityArchiveLike } from './types.ts';
2
+ import type { DepID } from '@vltpkg/dep-id';
3
+ export type * from './types.ts';
4
+ /**
5
+ * A database of security information for given packages in a graph.
6
+ */
7
+ export declare class SecurityArchive extends Map<DepID, PackageReportData> implements SecurityArchiveLike {
8
+ /**
9
+ * Loads a security archive from a valid JSON dump.
10
+ */
11
+ static load(dump: unknown): SecurityArchive;
12
+ }
13
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/browser.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAE3C,mBAAmB,YAAY,CAAA;AAsB/B;;GAEG;AACH,qBAAa,eACX,SAAQ,GAAG,CAAC,KAAK,EAAE,iBAAiB,CACpC,YAAW,mBAAmB;IAE9B;;OAEG;IACH,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO;CAQ1B"}
@@ -0,0 +1,29 @@
1
+ import { asDepID } from '@vltpkg/dep-id/browser';
2
+ import { error } from '@vltpkg/error-cause';
3
+ import { asPackageReportData } from "./types.js";
4
+ const isObj = (o) => !!o && typeof o === 'object';
5
+ const isSecurityArchiveJSON = (json) => isObj(json) &&
6
+ Object.entries(json).every(([key, value]) => typeof key === 'string' && isObj(value));
7
+ const asSecurityArchiveJSON = (json) => {
8
+ if (!isSecurityArchiveJSON(json)) {
9
+ throw error('Invalid security archive JSON', { found: json });
10
+ }
11
+ return json;
12
+ };
13
+ /**
14
+ * A database of security information for given packages in a graph.
15
+ */
16
+ export class SecurityArchive extends Map {
17
+ /**
18
+ * Loads a security archive from a valid JSON dump.
19
+ */
20
+ static load(dump) {
21
+ const archive = new SecurityArchive();
22
+ const json = asSecurityArchiveJSON(dump);
23
+ for (const [key, value] of Object.entries(json)) {
24
+ archive.set(asDepID(key), asPackageReportData(value));
25
+ }
26
+ return archive;
27
+ }
28
+ }
29
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAUhD,MAAM,KAAK,GAAG,CAAC,CAAU,EAAgC,EAAE,CACzD,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAA;AAE9B,MAAM,qBAAqB,GAAG,CAC5B,IAAa,EACsB,EAAE,CACrC,KAAK,CAAC,IAAI,CAAC;IACX,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CACxB,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,CAC1D,CAAA;AAEH,MAAM,qBAAqB,GAAG,CAC5B,IAAa,EACc,EAAE;IAC7B,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,CAAC,+BAA+B,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAC/D,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,OAAO,eACX,SAAQ,GAA6B;IAGrC;;OAEG;IACH,MAAM,CAAC,IAAI,CAAC,IAAa;QACvB,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAA;QACrC,MAAM,IAAI,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAA;QACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAChD,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAA;QACvD,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC;CACF","sourcesContent":["import { asDepID } from '@vltpkg/dep-id/browser'\nimport { error } from '@vltpkg/error-cause'\nimport { asPackageReportData } from './types.ts'\nimport type { JSONField } from '@vltpkg/types'\nimport type {\n PackageReportData,\n SecurityArchiveLike,\n} from './types.ts'\nimport type { DepID } from '@vltpkg/dep-id'\n\nexport type * from './types.ts'\n\nconst isObj = (o: unknown): o is Record<string, unknown> =>\n !!o && typeof o === 'object'\n\nconst isSecurityArchiveJSON = (\n json: unknown,\n): json is Record<string, JSONField> =>\n isObj(json) &&\n Object.entries(json).every(\n ([key, value]) => typeof key === 'string' && isObj(value),\n )\n\nconst asSecurityArchiveJSON = (\n json: unknown,\n): Record<string, JSONField> => {\n if (!isSecurityArchiveJSON(json)) {\n throw error('Invalid security archive JSON', { found: json })\n }\n return json\n}\n\n/**\n * A database of security information for given packages in a graph.\n */\nexport class SecurityArchive\n extends Map<DepID, PackageReportData>\n implements SecurityArchiveLike\n{\n /**\n * Loads a security archive from a valid JSON dump.\n */\n static load(dump: unknown) {\n const archive = new SecurityArchive()\n const json = asSecurityArchiveJSON(dump)\n for (const [key, value] of Object.entries(json)) {\n archive.set(asDepID(key), asPackageReportData(value))\n }\n return archive\n }\n}\n"]}
@@ -0,0 +1,74 @@
1
+ import { LRUCache } from 'lru-cache';
2
+ import type { DepID } from '@vltpkg/dep-id';
3
+ import type { PackageReportData, SecurityArchiveLike, SecurityArchiveRefreshOptions } from './types.ts';
4
+ export type * from './types.ts';
5
+ export declare const targetSecurityRegisty = "https://registry.npmjs.org/";
6
+ export type JSONItemResponse = {
7
+ namespace?: `@{string}`;
8
+ name: string;
9
+ version: string;
10
+ };
11
+ export type DBReadEntry = {
12
+ depID: string;
13
+ report: string;
14
+ start: number;
15
+ ttl: number;
16
+ };
17
+ export type DBWriteEntry = [string, string, number, number];
18
+ export type SecurityArchiveOptions = LRUCache.OptionsBase<DepID, PackageReportData, unknown> & {
19
+ /**
20
+ * Security archive does not supports a fetch-on-demand model.
21
+ */
22
+ fetchMethod?: undefined;
23
+ /**
24
+ * An optional value for the path in which to store the sqlite db.
25
+ */
26
+ path?: string;
27
+ /**
28
+ * Number of retries attempts to reach the remote security API.
29
+ */
30
+ retries?: number;
31
+ };
32
+ export declare const version: string;
33
+ /**
34
+ * A database of security information for given packages in a graph.
35
+ *
36
+ * Using the SecurityArchive.refresh() method will update the local cache
37
+ * with information from the socket.dev APIs or load from the local storage
38
+ * if available. Information about package security is then available
39
+ * using the SecurityArchive.get() method.
40
+ */
41
+ export declare class SecurityArchive extends LRUCache<DepID, PackageReportData> implements SecurityArchiveLike {
42
+ #private;
43
+ /**
44
+ * True if the refresh process was successful and report data
45
+ * is available for all public registry packages in the graph.
46
+ */
47
+ ok: boolean;
48
+ /**
49
+ * Creates a new security archive instance and starts the refresh process.
50
+ */
51
+ static start(options: SecurityArchiveOptions & SecurityArchiveRefreshOptions): Promise<SecurityArchive>;
52
+ /**
53
+ * By default, limits to 100K entries in the in-memory archive.
54
+ */
55
+ static get defaultMax(): number;
56
+ /**
57
+ * By default, entries are cached for 3 hours.
58
+ */
59
+ static get defaultTtl(): number;
60
+ constructor(options?: SecurityArchiveOptions);
61
+ /**
62
+ * Starts the security archive by providing a {@link GraphLike} instance,
63
+ * its registry-based nodes are going to be used as valid potential entries.
64
+ *
65
+ * Any entry that is missing from the persisted cached values are going
66
+ * to be requested in a batch-request to the remote socket.dev API.
67
+ */
68
+ refresh({ graph, specOptions, }: SecurityArchiveRefreshOptions): Promise<void>;
69
+ /**
70
+ * Outputs the current in-memory cache as a JSON object.
71
+ */
72
+ toJSON(): Record<DepID, PackageReportData> | undefined;
73
+ }
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAOpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAG3C,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACnB,6BAA6B,EAC9B,MAAM,YAAY,CAAA;AAEnB,mBAAmB,YAAY,CAAA;AAM/B,eAAO,MAAM,qBAAqB,gCAAgC,CAAA;AAElE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,CAAC,EAAE,WAAW,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAE3D,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC,WAAW,CACvD,KAAK,EACL,iBAAiB,EACjB,OAAO,CACR,GAAG;IACF;;OAEG;IACH,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAyBD,eAAO,MAAQ,OAAO,QAKrB,CAAA;AAED;;;;;;;GAOG;AACH,qBAAa,eACX,SAAQ,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CACzC,YAAW,mBAAmB;;IAM9B;;;OAGG;IACH,EAAE,UAAQ;IAEV;;OAEG;WACU,KAAK,CAChB,OAAO,EAAE,sBAAsB,GAAG,6BAA6B;IAOjE;;OAEG;IACH,MAAM,KAAK,UAAU,WAEpB;IAED;;OAEG;IACH,MAAM,KAAK,UAAU,WAEpB;gBAEW,OAAO,GAAE,sBAA2B;IAkMhD;;;;;;OAMG;IACG,OAAO,CAAC,EACZ,KAAK,EACL,WAAW,GACZ,EAAE,6BAA6B;IA+BhC;;OAEG;IACH,MAAM;CASP"}
@@ -0,0 +1,278 @@
1
+ // eslint-disable-next-line import/no-unresolved
2
+ import { DatabaseSync } from 'node:sqlite';
3
+ import { LRUCache } from 'lru-cache';
4
+ import pRetry, { AbortError } from 'p-retry';
5
+ import { loadPackageJson } from 'package-json-from-dist';
6
+ import { asDepID, splitDepID, joinDepIDTuple } from '@vltpkg/dep-id';
7
+ import { error } from '@vltpkg/error-cause';
8
+ import { XDG } from '@vltpkg/xdg';
9
+ import { asPackageReportData } from "./types.js";
10
+ const SOCKET_API_V0_URL = 'https://api.socket.dev/v0/purl?alerts=true';
11
+ const SOCKET_PUBLIC_API_TOKEN = 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api';
12
+ export const targetSecurityRegisty = 'https://registry.npmjs.org/';
13
+ /**
14
+ * Only the public npm registry has information available in the
15
+ * socket.dev API. This function checks if a given depID is pointing
16
+ * to the public npm registry and returns `true` if it does.
17
+ */
18
+ const usesTargetRegistry = (depID, specOptions) => {
19
+ const [nodeType, nodeRegistry] = splitDepID(depID);
20
+ if (nodeType !== 'registry') {
21
+ return false;
22
+ }
23
+ const reg = nodeRegistry === '' ?
24
+ specOptions.registry
25
+ : specOptions.registries?.[nodeRegistry];
26
+ return reg === targetSecurityRegisty;
27
+ };
28
+ // Loads the version number to be used in the User-Agent header
29
+ export const { version } = loadPackageJson(import.meta.filename, process.env.__VLT_INTERNAL_CLI_PACKAGE_JSON);
30
+ /**
31
+ * A database of security information for given packages in a graph.
32
+ *
33
+ * Using the SecurityArchive.refresh() method will update the local cache
34
+ * with information from the socket.dev APIs or load from the local storage
35
+ * if available. Information about package security is then available
36
+ * using the SecurityArchive.get() method.
37
+ */
38
+ export class SecurityArchive extends LRUCache {
39
+ #fetchedDepIDs = new Set();
40
+ #path;
41
+ #retries;
42
+ /**
43
+ * True if the refresh process was successful and report data
44
+ * is available for all public registry packages in the graph.
45
+ */
46
+ ok = false;
47
+ /**
48
+ * Creates a new security archive instance and starts the refresh process.
49
+ */
50
+ static async start(options) {
51
+ const archive = new SecurityArchive(options);
52
+ await archive.refresh(options);
53
+ return archive;
54
+ }
55
+ /**
56
+ * By default, limits to 100K entries in the in-memory archive.
57
+ */
58
+ static get defaultMax() {
59
+ return 100_000;
60
+ }
61
+ /**
62
+ * By default, entries are cached for 3 hours.
63
+ */
64
+ static get defaultTtl() {
65
+ return 1000 * 60 * 60 * 3;
66
+ }
67
+ constructor(options = {}) {
68
+ super({
69
+ max: SecurityArchive.defaultMax,
70
+ ttl: SecurityArchive.defaultTtl,
71
+ ...options,
72
+ });
73
+ this.#path =
74
+ options.path ?? new XDG('vlt').cache('security-archive.db');
75
+ this.#retries = options.retries ?? 3;
76
+ }
77
+ /**
78
+ * Opens a connection to the database at the given path.
79
+ */
80
+ #openDatabase() {
81
+ const db = new DatabaseSync(this.#path);
82
+ db.exec('CREATE TABLE IF NOT EXISTS cache ' +
83
+ '(depID TEXT PRIMARY KEY, report TEXT, ttl INTEGER, start INTEGER) ' +
84
+ // WITHOUT ROWID models a key=value storage,
85
+ // enforcing depID must be unique and not null
86
+ // while using less space and more efficient lookups
87
+ // https://www.sqlite.org/withoutrowid.html
88
+ 'WITHOUT ROWID');
89
+ db.exec('PRAGMA journal_mode = TRUNCATE');
90
+ db.exec('PRAGMA synchronous = NORMAL');
91
+ return db;
92
+ }
93
+ /**
94
+ * Loads all valid items found in the database into the in-memory cache.
95
+ */
96
+ #loadItemsFromDatabase(db, graph) {
97
+ const depIDs = [];
98
+ for (const node of graph.nodes.values()) {
99
+ depIDs.push(`'${node.id}'`);
100
+ }
101
+ // retrieves from the db packages using their dep-id reference
102
+ // and only include entries that have a valid TTL value
103
+ const dbRead = db.prepare('SELECT depID, report, start, ttl, ' +
104
+ `(SELECT UNIXEPOCH('subsecond')) as now ` +
105
+ 'FROM cache ' +
106
+ `WHERE depID IN (${depIDs.join(',')}) ` +
107
+ 'GROUP BY depID HAVING SUM(start + ttl) > now');
108
+ for (const entry of dbRead.all()) {
109
+ const { depID, report, start, ttl } = entry;
110
+ try {
111
+ this.set(asDepID(depID), asPackageReportData(JSON.parse(report)), {
112
+ ttl,
113
+ start,
114
+ });
115
+ }
116
+ catch {
117
+ // prune any invalid entries from the database
118
+ db.prepare('DELETE FROM cache WHERE depID = ?').run(depID);
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * Queues up all required packages that are missing from the archive.
124
+ */
125
+ #queueUpRequiredPackages(graph, specOptions) {
126
+ const queue = new Set();
127
+ const nodes = graph.nodes.values();
128
+ for (const node of nodes) {
129
+ // only queue up valid public registry
130
+ // references that are missing from the archive
131
+ if (usesTargetRegistry(node.id, specOptions) &&
132
+ !this.has(node.id)) {
133
+ const purl = `pkg:npm/${node.name}@${node.version}`;
134
+ queue.add({ purl });
135
+ }
136
+ }
137
+ return queue;
138
+ }
139
+ /**
140
+ * Fetches security information from the socket.dev API.
141
+ * Returns a string of NDJSON data.
142
+ */
143
+ async #retrieveRemoteData(queue) {
144
+ // fetch information from the socket.dev API
145
+ const req = await fetch(SOCKET_API_V0_URL, {
146
+ method: 'POST',
147
+ headers: {
148
+ Authorization: `Basic ${Buffer.from(`${SOCKET_PUBLIC_API_TOKEN}:`).toString('base64url')}`,
149
+ 'User-Agent': `@vltpkg/security-archive/${version}`,
150
+ },
151
+ body: JSON.stringify({
152
+ components: Array.from(queue),
153
+ }),
154
+ });
155
+ // on missing valid auth or API, it should abort the retry logic
156
+ if (req.status === 404) {
157
+ throw new AbortError('Missing API');
158
+ }
159
+ // on any other error throws an error that will trigger retry
160
+ if (!req.ok || !(req.status >= 200 && req.status <= 299)) {
161
+ throw error('Failed to fetch security data', { response: req });
162
+ }
163
+ const str = await req.text();
164
+ return str.trim() + '\n';
165
+ }
166
+ /**
167
+ * Parses and load NDJSON data into the in-memory cache.
168
+ */
169
+ #loadFromNDJSON(str, graph) {
170
+ const json = str.split('}\n');
171
+ for (const line of json) {
172
+ if (!line.trim())
173
+ continue;
174
+ const data = JSON.parse(line + '}');
175
+ const scope = data.namespace ? `${data.namespace}/` : '';
176
+ const depID = joinDepIDTuple([
177
+ 'registry',
178
+ '',
179
+ `${scope}${data.name}@${data.version}`,
180
+ ]);
181
+ const aliasedDepID = joinDepIDTuple([
182
+ 'registry',
183
+ 'npm',
184
+ `${scope}${data.name}@${data.version}`,
185
+ ]);
186
+ const node = graph.nodes.get(depID) ?? graph.nodes.get(aliasedDepID);
187
+ if (node) {
188
+ this.#fetchedDepIDs.add(node.id);
189
+ this.set(node.id, asPackageReportData(data));
190
+ }
191
+ else {
192
+ // eslint-disable-next-line no-console
193
+ console.warn(`security-archive: failed to find node for ${scope}${data.name}@${data.version} found in the response.`);
194
+ }
195
+ }
196
+ }
197
+ /**
198
+ * Store new in-memory items to the database.
199
+ */
200
+ #storeNewItemsToDatabase(db) {
201
+ const insertData = [];
202
+ for (const depID of this.#fetchedDepIDs) {
203
+ const entry = this.info(depID);
204
+ if (entry?.start && entry.ttl) {
205
+ insertData.push([
206
+ depID,
207
+ JSON.stringify(entry.value),
208
+ entry.start,
209
+ entry.ttl,
210
+ ]);
211
+ }
212
+ }
213
+ const dbWrite = db.prepare('INSERT OR REPLACE INTO cache (depID, report, start, ttl) ' +
214
+ 'VALUES (?, ?, ?, ?)');
215
+ for (const data of insertData) {
216
+ dbWrite.run(...data);
217
+ }
218
+ }
219
+ /**
220
+ * Validates that all public-registry packages in the graph
221
+ * have a valid report data in the current in-memory cache.
222
+ */
223
+ #validateReportData(graph, specOptions) {
224
+ for (const node of graph.nodes.values()) {
225
+ if (usesTargetRegistry(node.id, specOptions)) {
226
+ if (!this.has(node.id)) {
227
+ this.ok = false;
228
+ return;
229
+ }
230
+ }
231
+ }
232
+ this.ok = true;
233
+ }
234
+ /**
235
+ * Starts the security archive by providing a {@link GraphLike} instance,
236
+ * its registry-based nodes are going to be used as valid potential entries.
237
+ *
238
+ * Any entry that is missing from the persisted cached values are going
239
+ * to be requested in a batch-request to the remote socket.dev API.
240
+ */
241
+ async refresh({ graph, specOptions, }) {
242
+ // should start by clearing the current in-memory cache
243
+ this.clear();
244
+ const db = this.#openDatabase();
245
+ try {
246
+ this.#loadItemsFromDatabase(db, graph);
247
+ const queue = this.#queueUpRequiredPackages(graph, specOptions);
248
+ // only reach for the remote API if there are packages queued up
249
+ if (queue.size > 0) {
250
+ // Parse the response data
251
+ const res = await pRetry(() => this.#retrieveRemoteData(queue), { retries: this.#retries });
252
+ this.#loadFromNDJSON(res, graph);
253
+ this.#storeNewItemsToDatabase(db);
254
+ }
255
+ // validates the refresh process was successful
256
+ this.#validateReportData(graph, specOptions);
257
+ }
258
+ finally {
259
+ db.exec('PRAGMA optimize');
260
+ db.close();
261
+ // TODO: at this point we could spawn a deref process to
262
+ // remove entries with expired ttl values and vaccum the db
263
+ }
264
+ }
265
+ /**
266
+ * Outputs the current in-memory cache as a JSON object.
267
+ */
268
+ toJSON() {
269
+ if (!this.ok)
270
+ return undefined;
271
+ const obj = {};
272
+ for (const [key, value] of this.dump()) {
273
+ obj[key] = value.value;
274
+ }
275
+ return obj;
276
+ }
277
+ }
278
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["// eslint-disable-next-line import/no-unresolved\nimport { DatabaseSync } from 'node:sqlite'\nimport { LRUCache } from 'lru-cache'\nimport pRetry, { AbortError } from 'p-retry'\nimport { loadPackageJson } from 'package-json-from-dist'\nimport { asDepID, splitDepID, joinDepIDTuple } from '@vltpkg/dep-id'\nimport { error } from '@vltpkg/error-cause'\nimport { XDG } from '@vltpkg/xdg'\nimport { asPackageReportData } from './types.ts'\nimport type { DepID } from '@vltpkg/dep-id'\nimport type { GraphLike } from '@vltpkg/graph'\nimport type { SpecOptions } from '@vltpkg/spec'\nimport type {\n PackageReportData,\n SecurityArchiveLike,\n SecurityArchiveRefreshOptions,\n} from './types.ts'\n\nexport type * from './types.ts'\n\nconst SOCKET_API_V0_URL = 'https://api.socket.dev/v0/purl?alerts=true'\nconst SOCKET_PUBLIC_API_TOKEN =\n 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api'\n\nexport const targetSecurityRegisty = 'https://registry.npmjs.org/'\n\nexport type JSONItemResponse = {\n namespace?: `@{string}`\n name: string\n version: string\n}\n\nexport type DBReadEntry = {\n depID: string\n report: string\n start: number\n ttl: number\n}\n\nexport type DBWriteEntry = [string, string, number, number]\n\nexport type SecurityArchiveOptions = LRUCache.OptionsBase<\n DepID,\n PackageReportData,\n unknown\n> & {\n /**\n * Security archive does not supports a fetch-on-demand model.\n */\n fetchMethod?: undefined\n /**\n * An optional value for the path in which to store the sqlite db.\n */\n path?: string\n /**\n * Number of retries attempts to reach the remote security API.\n */\n retries?: number\n}\n\n/**\n * Only the public npm registry has information available in the\n * socket.dev API. This function checks if a given depID is pointing\n * to the public npm registry and returns `true` if it does.\n */\nconst usesTargetRegistry = (\n depID: DepID,\n specOptions: SpecOptions,\n): boolean => {\n const [nodeType, nodeRegistry] = splitDepID(depID)\n\n if (nodeType !== 'registry') {\n return false\n }\n\n const reg =\n nodeRegistry === '' ?\n specOptions.registry\n : specOptions.registries?.[nodeRegistry]\n return reg === targetSecurityRegisty\n}\n\n// Loads the version number to be used in the User-Agent header\nexport const { version } = loadPackageJson(\n import.meta.filename,\n process.env.__VLT_INTERNAL_CLI_PACKAGE_JSON,\n) as {\n version: string\n}\n\n/**\n * A database of security information for given packages in a graph.\n *\n * Using the SecurityArchive.refresh() method will update the local cache\n * with information from the socket.dev APIs or load from the local storage\n * if available. Information about package security is then available\n * using the SecurityArchive.get() method.\n */\nexport class SecurityArchive\n extends LRUCache<DepID, PackageReportData>\n implements SecurityArchiveLike\n{\n #fetchedDepIDs = new Set<DepID>()\n #path: string\n #retries: number\n\n /**\n * True if the refresh process was successful and report data\n * is available for all public registry packages in the graph.\n */\n ok = false\n\n /**\n * Creates a new security archive instance and starts the refresh process.\n */\n static async start(\n options: SecurityArchiveOptions & SecurityArchiveRefreshOptions,\n ) {\n const archive = new SecurityArchive(options)\n await archive.refresh(options)\n return archive\n }\n\n /**\n * By default, limits to 100K entries in the in-memory archive.\n */\n static get defaultMax() {\n return 100_000\n }\n\n /**\n * By default, entries are cached for 3 hours.\n */\n static get defaultTtl() {\n return 1000 * 60 * 60 * 3\n }\n\n constructor(options: SecurityArchiveOptions = {}) {\n super({\n max: SecurityArchive.defaultMax,\n ttl: SecurityArchive.defaultTtl,\n ...options,\n })\n\n this.#path =\n options.path ?? new XDG('vlt').cache('security-archive.db')\n this.#retries = options.retries ?? 3\n }\n\n /**\n * Opens a connection to the database at the given path.\n */\n #openDatabase(): DatabaseSync {\n const db = new DatabaseSync(this.#path)\n db.exec(\n 'CREATE TABLE IF NOT EXISTS cache ' +\n '(depID TEXT PRIMARY KEY, report TEXT, ttl INTEGER, start INTEGER) ' +\n // WITHOUT ROWID models a key=value storage,\n // enforcing depID must be unique and not null\n // while using less space and more efficient lookups\n // https://www.sqlite.org/withoutrowid.html\n 'WITHOUT ROWID',\n )\n db.exec('PRAGMA journal_mode = TRUNCATE')\n db.exec('PRAGMA synchronous = NORMAL')\n return db\n }\n\n /**\n * Loads all valid items found in the database into the in-memory cache.\n */\n #loadItemsFromDatabase(db: DatabaseSync, graph: GraphLike): void {\n const depIDs: string[] = []\n for (const node of graph.nodes.values()) {\n depIDs.push(`'${node.id}'`)\n }\n // retrieves from the db packages using their dep-id reference\n // and only include entries that have a valid TTL value\n const dbRead = db.prepare(\n 'SELECT depID, report, start, ttl, ' +\n `(SELECT UNIXEPOCH('subsecond')) as now ` +\n 'FROM cache ' +\n `WHERE depID IN (${depIDs.join(',')}) ` +\n 'GROUP BY depID HAVING SUM(start + ttl) > now',\n )\n for (const entry of dbRead.all() as DBReadEntry[]) {\n const { depID, report, start, ttl } = entry\n try {\n this.set(\n asDepID(depID),\n asPackageReportData(JSON.parse(report)),\n {\n ttl,\n start,\n },\n )\n } catch {\n // prune any invalid entries from the database\n db.prepare('DELETE FROM cache WHERE depID = ?').run(depID)\n }\n }\n }\n\n /**\n * Queues up all required packages that are missing from the archive.\n */\n #queueUpRequiredPackages(\n graph: GraphLike,\n specOptions: SpecOptions,\n ): Set<Record<'purl', string>> {\n const queue = new Set<Record<'purl', string>>()\n const nodes = graph.nodes.values()\n for (const node of nodes) {\n // only queue up valid public registry\n // references that are missing from the archive\n if (\n usesTargetRegistry(node.id, specOptions) &&\n !this.has(node.id)\n ) {\n const purl = `pkg:npm/${node.name}@${node.version}`\n queue.add({ purl })\n }\n }\n return queue\n }\n\n /**\n * Fetches security information from the socket.dev API.\n * Returns a string of NDJSON data.\n */\n async #retrieveRemoteData(\n queue: Set<Record<'purl', string>>,\n ): Promise<string> {\n // fetch information from the socket.dev API\n const req = await fetch(SOCKET_API_V0_URL, {\n method: 'POST',\n headers: {\n Authorization: `Basic ${Buffer.from(`${SOCKET_PUBLIC_API_TOKEN}:`).toString('base64url')}`,\n 'User-Agent': `@vltpkg/security-archive/${version}`,\n },\n body: JSON.stringify({\n components: Array.from(queue),\n }),\n })\n // on missing valid auth or API, it should abort the retry logic\n if (req.status === 404) {\n throw new AbortError('Missing API')\n }\n // on any other error throws an error that will trigger retry\n if (!req.ok || !(req.status >= 200 && req.status <= 299)) {\n throw error('Failed to fetch security data', { response: req })\n }\n\n const str = await req.text()\n return str.trim() + '\\n'\n }\n\n /**\n * Parses and load NDJSON data into the in-memory cache.\n */\n #loadFromNDJSON(str: string, graph: GraphLike): void {\n const json = str.split('}\\n')\n for (const line of json) {\n if (!line.trim()) continue\n const data = JSON.parse(line + '}') as JSONItemResponse\n const scope = data.namespace ? `${data.namespace}/` : ''\n const depID = joinDepIDTuple([\n 'registry',\n '',\n `${scope}${data.name}@${data.version}`,\n ])\n const aliasedDepID = joinDepIDTuple([\n 'registry',\n 'npm',\n `${scope}${data.name}@${data.version}`,\n ])\n const node =\n graph.nodes.get(depID) ?? graph.nodes.get(aliasedDepID)\n if (node) {\n this.#fetchedDepIDs.add(node.id)\n this.set(node.id, asPackageReportData(data))\n } else {\n // eslint-disable-next-line no-console\n console.warn(\n `security-archive: failed to find node for ${scope}${data.name}@${data.version} found in the response.`,\n )\n }\n }\n }\n\n /**\n * Store new in-memory items to the database.\n */\n #storeNewItemsToDatabase(db: DatabaseSync): void {\n const insertData: DBWriteEntry[] = []\n for (const depID of this.#fetchedDepIDs) {\n const entry = this.info(depID)\n if (entry?.start && entry.ttl) {\n insertData.push([\n depID,\n JSON.stringify(entry.value),\n entry.start,\n entry.ttl,\n ])\n }\n }\n const dbWrite = db.prepare(\n 'INSERT OR REPLACE INTO cache (depID, report, start, ttl) ' +\n 'VALUES (?, ?, ?, ?)',\n )\n for (const data of insertData) {\n dbWrite.run(...data)\n }\n }\n\n /**\n * Validates that all public-registry packages in the graph\n * have a valid report data in the current in-memory cache.\n */\n #validateReportData(graph: GraphLike, specOptions: SpecOptions) {\n for (const node of graph.nodes.values()) {\n if (usesTargetRegistry(node.id, specOptions)) {\n if (!this.has(node.id)) {\n this.ok = false\n return\n }\n }\n }\n this.ok = true\n }\n\n /**\n * Starts the security archive by providing a {@link GraphLike} instance,\n * its registry-based nodes are going to be used as valid potential entries.\n *\n * Any entry that is missing from the persisted cached values are going\n * to be requested in a batch-request to the remote socket.dev API.\n */\n async refresh({\n graph,\n specOptions,\n }: SecurityArchiveRefreshOptions) {\n // should start by clearing the current in-memory cache\n this.clear()\n\n const db = this.#openDatabase()\n\n try {\n this.#loadItemsFromDatabase(db, graph)\n\n const queue = this.#queueUpRequiredPackages(graph, specOptions)\n // only reach for the remote API if there are packages queued up\n if (queue.size > 0) {\n // Parse the response data\n const res = await pRetry(\n () => this.#retrieveRemoteData(queue),\n { retries: this.#retries },\n )\n this.#loadFromNDJSON(res, graph)\n this.#storeNewItemsToDatabase(db)\n }\n\n // validates the refresh process was successful\n this.#validateReportData(graph, specOptions)\n } finally {\n db.exec('PRAGMA optimize')\n db.close()\n // TODO: at this point we could spawn a deref process to\n // remove entries with expired ttl values and vaccum the db\n }\n }\n\n /**\n * Outputs the current in-memory cache as a JSON object.\n */\n toJSON() {\n if (!this.ok) return undefined\n\n const obj: Record<DepID, PackageReportData> = {}\n for (const [key, value] of this.dump()) {\n obj[key] = value.value\n }\n return obj\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,60 @@
1
+ import type { DepID } from '@vltpkg/dep-id';
2
+ import type { GraphLike } from '@vltpkg/graph';
3
+ import type { SpecOptions } from '@vltpkg/spec';
4
+ /**
5
+ * Parameter options for initializing a security archive.
6
+ */
7
+ export type SecurityArchiveRefreshOptions = {
8
+ /**
9
+ * A @link{GraphLike} instance to find what packages the
10
+ * security archive should have.
11
+ */
12
+ graph: GraphLike;
13
+ /**
14
+ * A @link{SpecOptions} instance to use for resolving dependencies.
15
+ */
16
+ specOptions: SpecOptions;
17
+ };
18
+ /**
19
+ * An interface for interacting with a security archive.
20
+ */
21
+ export interface SecurityArchiveLike {
22
+ get: (depId: DepID) => PackageReportData | undefined;
23
+ set: (depId: DepID, data: PackageReportData) => void;
24
+ delete: (depId: DepID) => void;
25
+ has: (depId: DepID) => boolean;
26
+ clear: () => void;
27
+ }
28
+ /**
29
+ * Package alert extra information.
30
+ */
31
+ export type PackageAlertProps = {
32
+ lastPublish: string;
33
+ };
34
+ /**
35
+ * A known alert for a given package.
36
+ */
37
+ export type PackageAlert = {
38
+ key: string;
39
+ type: string;
40
+ severity: 'low' | 'middle' | 'high' | 'critical';
41
+ category: string;
42
+ props: PackageAlertProps;
43
+ };
44
+ /**
45
+ * The report data for a given package.
46
+ */
47
+ export type PackageReportData = {
48
+ id: string;
49
+ author: string[];
50
+ size: number;
51
+ type: 'npm';
52
+ namespace?: `@${string}`;
53
+ name: string;
54
+ version: string;
55
+ license: string;
56
+ alerts: PackageAlert[];
57
+ };
58
+ export declare const isPackageReportData: (o: unknown) => o is PackageReportData;
59
+ export declare const asPackageReportData: (o: unknown) => PackageReportData;
60
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG;IAC1C;;;OAGG;IACH,KAAK,EAAE,SAAS,CAAA;IAChB;;OAEG;IACH,WAAW,EAAE,WAAW,CAAA;CACzB,CAAA;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,iBAAiB,GAAG,SAAS,CAAA;IACpD,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACpD,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAA;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAA;IAChD,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACzB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,KAAK,CAAA;IACX,SAAS,CAAC,EAAE,IAAI,MAAM,EAAE,CAAA;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,YAAY,EAAE,CAAA;CACvB,CAAA;AAED,eAAO,MAAM,mBAAmB,MAC3B,OAAO,KACT,CAAC,IAAI,iBAQU,CAAA;AAElB,eAAO,MAAM,mBAAmB,MAC3B,OAAO,KACT,iBAKF,CAAA"}
@@ -0,0 +1,16 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ export const isPackageReportData = (o) => typeof o === 'object' &&
3
+ o != null &&
4
+ 'id' in o &&
5
+ 'type' in o &&
6
+ 'name' in o &&
7
+ 'version' in o &&
8
+ 'alerts' in o &&
9
+ o.type === 'npm';
10
+ export const asPackageReportData = (o) => {
11
+ if (!isPackageReportData(o)) {
12
+ throw error('Invalid package report data', { found: o });
13
+ }
14
+ return o;
15
+ };
16
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAgE3C,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,CAAU,EACc,EAAE,CAC1B,OAAO,CAAC,KAAK,QAAQ;IACrB,CAAC,IAAI,IAAI;IACT,IAAI,IAAI,CAAC;IACT,MAAM,IAAI,CAAC;IACX,MAAM,IAAI,CAAC;IACX,SAAS,IAAI,CAAC;IACd,QAAQ,IAAI,CAAC;IACb,CAAC,CAAC,IAAI,KAAK,KAAK,CAAA;AAElB,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,CAAU,EACS,EAAE;IACrB,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5B,MAAM,KAAK,CAAC,6BAA6B,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;IAC1D,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC,CAAA","sourcesContent":["import { error } from '@vltpkg/error-cause'\nimport type { DepID } from '@vltpkg/dep-id'\nimport type { GraphLike } from '@vltpkg/graph'\nimport type { SpecOptions } from '@vltpkg/spec'\n\n/**\n * Parameter options for initializing a security archive.\n */\nexport type SecurityArchiveRefreshOptions = {\n /**\n * A @link{GraphLike} instance to find what packages the\n * security archive should have.\n */\n graph: GraphLike\n /**\n * A @link{SpecOptions} instance to use for resolving dependencies.\n */\n specOptions: SpecOptions\n}\n\n/**\n * An interface for interacting with a security archive.\n */\nexport interface SecurityArchiveLike {\n get: (depId: DepID) => PackageReportData | undefined\n set: (depId: DepID, data: PackageReportData) => void\n delete: (depId: DepID) => void\n has: (depId: DepID) => boolean\n clear: () => void\n}\n\n/**\n * Package alert extra information.\n */\nexport type PackageAlertProps = {\n lastPublish: string\n}\n\n/**\n * A known alert for a given package.\n */\nexport type PackageAlert = {\n key: string\n type: string\n severity: 'low' | 'middle' | 'high' | 'critical'\n category: string\n props: PackageAlertProps\n}\n\n/**\n * The report data for a given package.\n */\nexport type PackageReportData = {\n id: string\n author: string[]\n size: number\n type: 'npm'\n namespace?: `@${string}`\n name: string\n version: string\n license: string\n alerts: PackageAlert[]\n}\n\nexport const isPackageReportData = (\n o: unknown,\n): o is PackageReportData =>\n typeof o === 'object' &&\n o != null &&\n 'id' in o &&\n 'type' in o &&\n 'name' in o &&\n 'version' in o &&\n 'alerts' in o &&\n o.type === 'npm'\n\nexport const asPackageReportData = (\n o: unknown,\n): PackageReportData => {\n if (!isPackageReportData(o)) {\n throw error('Invalid package report data', { found: o })\n }\n return o\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@vltpkg/security-archive",
3
+ "description": "A database of security information of packages",
4
+ "version": "0.0.0-4",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/vltpkg/vltpkg.git",
8
+ "directory": "src/security-archive"
9
+ },
10
+ "tshy": {
11
+ "selfLink": false,
12
+ "liveDev": true,
13
+ "dialects": [
14
+ "esm"
15
+ ],
16
+ "exports": {
17
+ "./package.json": "./package.json",
18
+ ".": "./src/index.ts"
19
+ }
20
+ },
21
+ "dependencies": {
22
+ "lru-cache": "^11.0.2",
23
+ "p-retry": "^6.2.1",
24
+ "package-json-from-dist": "^1.0.0",
25
+ "@vltpkg/dep-id": "0.0.0-4",
26
+ "@vltpkg/error-cause": "0.0.0-4",
27
+ "@vltpkg/spec": "0.0.0-4",
28
+ "@vltpkg/graph": "0.0.0-4",
29
+ "@vltpkg/types": "0.0.0-4",
30
+ "@vltpkg/xdg": "0.0.0-4"
31
+ },
32
+ "devDependencies": {
33
+ "@eslint/js": "^9.20.0",
34
+ "@types/eslint__js": "^8.42.3",
35
+ "@types/node": "^22.13.1",
36
+ "eslint": "^9.20.0",
37
+ "prettier": "^3.4.2",
38
+ "tap": "^21.1.0",
39
+ "tshy": "^3.0.2",
40
+ "typedoc": "0.27.6",
41
+ "typescript": "5.7.3",
42
+ "typescript-eslint": "^8.23.0"
43
+ },
44
+ "license": "BSD-2-Clause-Patent",
45
+ "engines": {
46
+ "node": ">=22"
47
+ },
48
+ "tap": {
49
+ "extends": "../../tap-config.yaml"
50
+ },
51
+ "prettier": "../../.prettierrc.js",
52
+ "module": "./dist/esm/index.js",
53
+ "type": "module",
54
+ "exports": {
55
+ "./package.json": "./package.json",
56
+ ".": {
57
+ "import": {
58
+ "types": "./dist/esm/index.d.ts",
59
+ "default": "./dist/esm/index.js"
60
+ }
61
+ }
62
+ },
63
+ "files": [
64
+ "dist"
65
+ ],
66
+ "scripts": {
67
+ "format": "prettier --write . --log-level warn --ignore-path ../../.prettierignore --cache",
68
+ "format:check": "prettier --check . --ignore-path ../../.prettierignore --cache",
69
+ "lint": "eslint . --fix",
70
+ "lint:check": "eslint .",
71
+ "snap": "tap",
72
+ "test": "tap",
73
+ "posttest": "tsc --noEmit",
74
+ "typecheck": "tsc --noEmit"
75
+ }
76
+ }