@preventive/triage 1.0.0-alpha.37 → 1.0.0-alpha.39
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 +16 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +522 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-metadata.d.ts +6 -2
- package/common/bundle-metadata.js +63 -20
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-source-links.js +7 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +3 -1
- package/common/bundle-sources.js +47 -19
- package/common/code-language.js +16 -2
- package/common/default-scan-models.ts +2 -1
- package/common/dependency-reviews.d.ts +23 -0
- package/common/dependency-reviews.js +81 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/github-pr.ts +1 -1
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +56 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +1 -5
- package/common/managed/routes.js +123 -12
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +1 -1
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/workspace-app.js +23 -0
- package/common/workspace-groups.js +176 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2KBF5TZ7.js +2 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-35VE6CDH.js +2 -0
- package/out/chunk-3BZ7JERB.js +1 -0
- package/out/chunk-45WIN3SW.js +1 -0
- package/out/chunk-4XCDFGDP.js +10 -0
- package/out/chunk-6C76GAXK.js +1 -0
- package/out/chunk-7ZKDKGAH.js +1 -0
- package/out/chunk-ABW5FVSN.js +1 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-CMQA4DM6.js +1 -0
- package/out/chunk-CVVJAXVY.js +2 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-F4OMDNEY.js +1 -0
- package/out/chunk-GJ4IUCNS.js +1 -0
- package/out/chunk-GMFXFEUA.js +2 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-HIV7SVI7.js +1 -0
- package/out/chunk-IU3CCINR.js +3 -0
- package/out/chunk-JVBB4LZX.js +1 -0
- package/out/chunk-K3X5EJBQ.js +2 -0
- package/out/chunk-KEX2ATEW.js +2 -0
- package/out/chunk-KYCNJVB7.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-QSLWGJBQ.js +223 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-TINZZKP5.js +33 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-UNTRGE26.js +2 -0
- package/out/chunk-UYAXVRVL.js +1 -0
- package/out/chunk-V3ME6FI5.js +2 -0
- package/out/chunk-V5O7M2YT.js +1 -0
- package/out/chunk-VIHGO77Y.js +1 -0
- package/out/chunk-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-XNGEQO7Y.js +2 -0
- package/out/chunk-XRWAV3DX.js +7 -0
- package/out/chunk-XYDGU6BM.js +51 -0
- package/out/chunk-XYFXXB2G.js +10 -0
- package/out/chunk-YVZH5SHP.js +1 -0
- package/out/chunk-ZPD5R5RP.js +3 -0
- package/out/client-managed-import.js +3 -87
- package/out/client-managed.js +26 -338
- package/out/client-sync.js +8 -14
- package/out/graph.js +17 -30
- package/out/index.html +34 -0
- package/out/prism.js +6 -2
- package/out/terminal.js +241 -257
- package/out/view.css +1 -1
- package/out/view.js +76 -520
- package/package.json +89 -7
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +171 -0
- package/risk/files/runs-code.js +358 -0
- package/risk/files/script.js +114 -0
- package/risk/files/stamps.js +46 -0
- package/risk/index.d.ts +142 -0
- package/risk/index.js +7 -0
- package/risk/npm/assess.js +219 -0
- package/risk/npm/combined.js +65 -0
- package/risk/npm/compare.js +228 -0
- package/risk/npm/dependencies.js +153 -0
- package/risk/npm/diff.js +43 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +34 -0
- package/risk/npm/held.js +60 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +30 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +282 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +113 -0
- package/server-e2e/http.ts +7 -7
- package/server-e2e/npm-proxy.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +2 -2
- package/server-e2e/objstore/handlers.ts +1 -1
- package/server-e2e/objstore/tokens.ts +1 -1
- package/server-e2e/sign.ts +1 -1
- package/server-managed/bundle-advisories.ts +74 -12
- package/server-managed/bundle-cache.ts +37 -4
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-summary-cache.ts +24 -10
- package/server-managed/config.ts +4 -0
- package/server-managed/db-methods.ts +98 -11
- package/server-managed/db-neon.ts +39 -4
- package/server-managed/db-schema.ts +14 -2
- package/server-managed/db-table-names.ts +1 -1
- package/server-managed/db.ts +7 -3
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-app.ts +9 -3
- package/server-managed/github-errors.ts +21 -2
- package/server-managed/github-issue-oauth.ts +5 -6
- package/server-managed/github-metadata.ts +115 -0
- package/server-managed/github-oauth.ts +38 -2
- package/server-managed/http-response.ts +26 -0
- package/server-managed/http.ts +952 -53
- package/server-managed/index.ts +11 -5
- package/server-managed/kept.ts +28 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/npm-insights.ts +440 -0
- package/server-managed/npm-kept.ts +65 -0
- package/server-managed/npm-loads.ts +167 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +378 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +177 -0
- package/server-managed/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +78 -7
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -0
- package/server-managed/report-sources.ts +13 -2
- package/server-managed/repository-browser.ts +19 -5
- package/server-managed/repository-risk.ts +418 -0
- package/server-managed/session.ts +16 -2
- package/server-managed/sql.ts +5 -2
- package/server-managed/storage-encryption.ts +5 -3
- package/server-managed/storage-payload.ts +4 -0
- package/server-managed/storage-stores.ts +2 -0
- package/server-managed/storage.ts +6 -3
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-feed.ts +1 -1
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +25 -4
- package/server-managed/upstream-cache.ts +147 -5
- package/server-managed/view-as.ts +58 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// What the npm viewer and repository risk scans keep in the database
|
|
2
|
+
// (managed_upstream_cache, upstream-cache.ts), beyond the instance, so that
|
|
3
|
+
// neither asks for it again on every read, nor after a restart or on another
|
|
4
|
+
// instance: public versions' documents, of the fields read of them
|
|
5
|
+
// (npm-manifest.ts npmVersionFields), for a day (readNpmVersion asks npm again
|
|
6
|
+
// then, as a version can be deprecated, its package made private, or itself
|
|
7
|
+
// removed); and, for scans, each package's risk line, for as long as a scan
|
|
8
|
+
// keeps it (repository-risk.ts). Each store removes its own rows kept longer
|
|
9
|
+
// than that as it writes, at most once an hour a database, so that rows no
|
|
10
|
+
// read will take again don't pile up.
|
|
11
|
+
// The database is an optimization: a failure reads as a miss, and writes
|
|
12
|
+
// nothing.
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
import { plainObject } from './npm-manifest.ts'
|
|
15
|
+
import { type NpmVersionStore, VERSION_KEPT_MS } from './npm-packages.ts'
|
|
16
|
+
import type { UpstreamCacheStore } from './upstream-cache.ts'
|
|
17
|
+
|
|
18
|
+
// A value kept by key, as JSON, `at` when it was.
|
|
19
|
+
export interface KeptStore<T> {
|
|
20
|
+
read(key: string): Promise<{ at: number; value: T } | null>
|
|
21
|
+
write(key: string, value: T, at: number): Promise<void>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// When each database last had each type's old rows removed.
|
|
25
|
+
const PRUNE_EVERY_MS = 60 * 60_000
|
|
26
|
+
const pruned = new WeakMap<UpstreamCacheStore, Map<string, number>>()
|
|
27
|
+
|
|
28
|
+
// Values of `type`, each read for `keptMs` after it was kept.
|
|
29
|
+
function keptStore<T>(db: UpstreamCacheStore, type: string, keptMs: number, debug: boolean): KeptStore<T> {
|
|
30
|
+
const warn = (what: string, error: unknown) => { if (debug) console.warn(`managed: kept ${type} ${what} failed:`, error) }
|
|
31
|
+
const prune = async (at: number) => {
|
|
32
|
+
const times = pruned.get(db) ?? pruned.set(db, new Map()).get(db)!
|
|
33
|
+
if (at - (times.get(type) ?? -Infinity) < PRUNE_EVERY_MS) return
|
|
34
|
+
times.set(type, at)
|
|
35
|
+
try { await db.deleteUpstreamCacheEntries(`${type}/`, at - keptMs) } catch (error) { warn('prune', error) }
|
|
36
|
+
}
|
|
37
|
+
// Keys of any length or character, as the database's: hashed.
|
|
38
|
+
const keyOf = (key: string) => `${type}/${createHash('sha256').update(key).digest('hex')}`
|
|
39
|
+
return {
|
|
40
|
+
async read(key) {
|
|
41
|
+
try {
|
|
42
|
+
const kept: unknown = JSON.parse(await db.getUpstreamCacheEntry(keyOf(key)) ?? 'null')
|
|
43
|
+
return plainObject(kept) && typeof kept['at'] === 'number' && 'value' in kept ? { at: kept['at'], value: kept['value'] as T } : null
|
|
44
|
+
} catch (error) { warn('read', error); return null }
|
|
45
|
+
},
|
|
46
|
+
async write(key, value, at) {
|
|
47
|
+
try { await db.setUpstreamCacheEntry(keyOf(key), JSON.stringify({ at, value }), at) } catch (error) { warn('write', error) }
|
|
48
|
+
await prune(at)
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function npmVersionStore(db: UpstreamCacheStore, debug = false): NpmVersionStore {
|
|
54
|
+
const kept = keptStore<Record<string, unknown>>(db, 'npm/version-documents', VERSION_KEPT_MS, debug)
|
|
55
|
+
return {
|
|
56
|
+
async read(name, version) {
|
|
57
|
+
const hit = await kept.read(`${name}@${version}`)
|
|
58
|
+
return hit && plainObject(hit.value) ? { at: hit.at, json: hit.value } : null
|
|
59
|
+
},
|
|
60
|
+
write: (name, version, json) => kept.write(`${name}@${version}`, json, Date.now()),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Risk lines, each read for `keptMs`, as long as a scan keeps them.
|
|
65
|
+
export const riskLineStore = <T>(db: UpstreamCacheStore, keptMs: number, debug = false): KeptStore<T> => keptStore<T>(db, 'repository-risk/lines', keptMs, debug)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Loading a published version for the npm package viewer (npm-packages.ts):
|
|
2
|
+
// its tarball, kept on disk as bundle builds keep theirs, else read from the
|
|
3
|
+
// registry, and the viewer's response built from it, each shared by the
|
|
4
|
+
// readers asking for it meanwhile and held to a few at once per process.
|
|
5
|
+
import { Buffer } from 'node:buffer'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { join, resolve } from 'node:path'
|
|
8
|
+
import { promisify } from 'node:util'
|
|
9
|
+
import { gunzip } from 'node:zlib'
|
|
10
|
+
import { encodeBrotli } from './brotli.ts'
|
|
11
|
+
import { MAX_NPM_PACKAGE_BYTES, MAX_NPM_PACKAGE_FILES, NPM_REGISTRY, NpmPackageError, type NpmPackageFile, type NpmVersionDocument, askUpstream,
|
|
12
|
+
npmFileRows, npmPublishTime, npmToken, readNpmTar } from './npm-packages.ts'
|
|
13
|
+
import { keepFile, readKeptFile, upstreamCachePath } from './upstream-cache.ts'
|
|
14
|
+
|
|
15
|
+
// The tar stream a package's files may come in, headers and padding included.
|
|
16
|
+
// A tarball larger than that cannot unpack within it, so it is refused as it
|
|
17
|
+
// arrives.
|
|
18
|
+
export const MAX_TAR_BYTES = 96 * 1024 * 1024
|
|
19
|
+
const TARBALL_TIMEOUT_MS = 120_000
|
|
20
|
+
// Loads in flight per process; each can hold a tarball, its tar, its files
|
|
21
|
+
// and their encoded JSON.
|
|
22
|
+
const MAX_ACTIVE_LOADS = 4
|
|
23
|
+
|
|
24
|
+
const gunzipAsync = promisify(gunzip)
|
|
25
|
+
|
|
26
|
+
// A load's result, shared by its readers, and a reader's release of it.
|
|
27
|
+
export interface NpmLoad<T> { result: Promise<T>; release: () => void }
|
|
28
|
+
|
|
29
|
+
interface Load { job: Promise<unknown>; readers: number; settled: boolean }
|
|
30
|
+
|
|
31
|
+
// Loads by what they read; readers asking for the same share one. A load
|
|
32
|
+
// keeps its place among the active ones until its work is done and each
|
|
33
|
+
// reader has released it, as a reader does once its response is written or
|
|
34
|
+
// abandoned, so what responses still hold counts too.
|
|
35
|
+
const loads = new Map<string, Load>()
|
|
36
|
+
|
|
37
|
+
const refused = <T>(status: number, code: string): NpmLoad<T> => {
|
|
38
|
+
const result = Promise.reject(new NpmPackageError(status, code))
|
|
39
|
+
result.catch(() => {})
|
|
40
|
+
return { result, release() {} }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shared<T>(key: string, doc: NpmVersionDocument, work: () => Promise<T>): NpmLoad<T> {
|
|
44
|
+
const { unpackedSize, fileCount } = doc.dist
|
|
45
|
+
if ((unpackedSize ?? 0) > MAX_NPM_PACKAGE_BYTES || (fileCount ?? 0) > MAX_NPM_PACKAGE_FILES) return refused(413, 'package-too-large')
|
|
46
|
+
let load = loads.get(key)
|
|
47
|
+
if (!load) {
|
|
48
|
+
if (loads.size >= MAX_ACTIVE_LOADS) return refused(429, 'npm-busy')
|
|
49
|
+
const created: Load = { job: work(), readers: 0, settled: false }
|
|
50
|
+
const settle = () => {
|
|
51
|
+
created.settled = true
|
|
52
|
+
if (created.readers === 0 && loads.get(key) === created) loads.delete(key)
|
|
53
|
+
}
|
|
54
|
+
created.job.then(settle, settle)
|
|
55
|
+
loads.set(key, created)
|
|
56
|
+
load = created
|
|
57
|
+
}
|
|
58
|
+
const held = load
|
|
59
|
+
held.readers++
|
|
60
|
+
let released = false
|
|
61
|
+
return {
|
|
62
|
+
result: held.job as Promise<T>,
|
|
63
|
+
release() {
|
|
64
|
+
if (released) return
|
|
65
|
+
released = true
|
|
66
|
+
if (--held.readers === 0 && held.settled && loads.get(key) === held) loads.delete(key)
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The sha512 an integrity names, in base64, or null where it names none.
|
|
72
|
+
const sha512Of = (integrity: string) => /(?:^|\s)sha512-([\d+/A-Za-z]{86}==)(?=\s|$)/u.exec(integrity)?.[1] ?? null
|
|
73
|
+
// Hashed off the event loop: a tarball runs to tens of MiB.
|
|
74
|
+
const sha512 = async (bytes: Uint8Array) => Buffer.from(await crypto.subtle.digest('SHA-512', bytes as Uint8Array<ArrayBuffer>)).toString('base64')
|
|
75
|
+
|
|
76
|
+
// Upstream's disk cache (setCacheDir), where tarballs are kept between
|
|
77
|
+
// loads, or null to keep none and read none.
|
|
78
|
+
let tarballCache: string | null = null
|
|
79
|
+
|
|
80
|
+
export function setNpmTarballCache(dir: string | null) {
|
|
81
|
+
tarballCache = dir === null ? null : resolve(dir)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Where npm's own cache files content by its sha512, short of an .npmrc
|
|
85
|
+
// moving the cache.
|
|
86
|
+
function npmCachePath(hash: string): string | null {
|
|
87
|
+
const configured = process.env['npm_config_cache'] || process.env['NPM_CONFIG_CACHE']
|
|
88
|
+
let root
|
|
89
|
+
try {
|
|
90
|
+
root = configured ? resolve(configured.replace(/^~(?=[/\\])/u, homedir()))
|
|
91
|
+
: process.platform === 'win32' ? join(process.env['LOCALAPPDATA'] || homedir(), 'npm-cache') : join(homedir(), '.npm')
|
|
92
|
+
} catch { return null }
|
|
93
|
+
const hex = Buffer.from(hash, 'base64').toString('hex')
|
|
94
|
+
return join(root, '_cacache', 'content-v2', 'sha512', hex.slice(0, 2), hex.slice(2, 4), hex.slice(4))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The version's tarball from the registry, at the package's own path: with
|
|
98
|
+
// the token only for a private version, and its size bounded as it arrives.
|
|
99
|
+
async function downloadTarball(url: string, isPrivate: boolean): Promise<Buffer> {
|
|
100
|
+
const token = isPrivate ? npmToken() : null
|
|
101
|
+
const bytes = await askUpstream(url, { headers: token ? { authorization: `Bearer ${token}` } : {}, limit: MAX_TAR_BYTES, ms: TARBALL_TIMEOUT_MS, absent: [401, 403, 404] })
|
|
102
|
+
.catch((err: unknown) => { throw err instanceof NpmPackageError && err.message === 'upstream-too-large' ? new NpmPackageError(413, 'package-too-large') : err })
|
|
103
|
+
if (bytes === null) throw new NpmPackageError(404, 'package-not-found')
|
|
104
|
+
return bytes
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The version's tarball, its bytes held to the document's sha512 wherever
|
|
108
|
+
// they come from: kept in the cache, where bundle builds keep what they
|
|
109
|
+
// fetch with the token, or in npm's; else downloaded, and then kept. Bytes
|
|
110
|
+
// matching the sha512 the reader's own document names are the tarball that
|
|
111
|
+
// reader may download, wherever they were kept and whoever fetched them.
|
|
112
|
+
async function readTarball(doc: NpmVersionDocument): Promise<Uint8Array> {
|
|
113
|
+
const expected = sha512Of(doc.dist.integrity), url = doc.dist.tarball
|
|
114
|
+
if (!url.startsWith(`${NPM_REGISTRY}/${doc.name}/-/`) || URL.parse(url)?.href !== url || expected === null) throw new NpmPackageError(502, 'upstream-invalid')
|
|
115
|
+
const kept = tarballCache === null ? null : upstreamCachePath(tarballCache, 'npm/tarballs', `${doc.name}@${doc.version}.tgz`)
|
|
116
|
+
if (kept !== null) {
|
|
117
|
+
for (const path of [kept, npmCachePath(expected)]) {
|
|
118
|
+
const bytes = path === null ? null : await readKeptFile(path, MAX_TAR_BYTES)
|
|
119
|
+
if (bytes && await sha512(bytes) === expected) return bytes
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const bytes = await downloadTarball(url, doc.private)
|
|
123
|
+
if (await sha512(bytes) !== expected) throw new NpmPackageError(502, 'upstream-invalid')
|
|
124
|
+
if (kept !== null) await keepFile(kept, bytes)
|
|
125
|
+
return bytes
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function readFiles(tarball: Uint8Array): Promise<NpmPackageFile[]> {
|
|
129
|
+
let tar: Buffer
|
|
130
|
+
try { tar = await gunzipAsync(tarball, { maxOutputLength: MAX_TAR_BYTES }) }
|
|
131
|
+
catch (err) {
|
|
132
|
+
throw new NpmPackageError((err as { code?: string }).code === 'ERR_BUFFER_TOO_LARGE' ? 413 : 422,
|
|
133
|
+
(err as { code?: string }).code === 'ERR_BUFFER_TOO_LARGE' ? 'package-too-large' : 'bad-tarball')
|
|
134
|
+
}
|
|
135
|
+
return readNpmTar(tar)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The version's tarball, for download.
|
|
139
|
+
export function loadNpmTarball(doc: NpmVersionDocument): NpmLoad<Uint8Array> {
|
|
140
|
+
return shared(`tarball ${doc.dist.integrity} ${doc.dist.tarball}`, doc, () => readTarball(doc))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The version's files, for one of them to be pretty-printed (pretty-print.ts).
|
|
144
|
+
export function loadNpmFiles(doc: NpmVersionDocument): NpmLoad<NpmPackageFile[]> {
|
|
145
|
+
return shared(`files ${doc.dist.integrity} ${doc.dist.tarball}`, doc, async () => readFiles(await readTarball(doc)))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The version as the viewer reads it, as brotli-encoded JSON: `{ name,
|
|
149
|
+
// version, private, integrity, tarballSize, published, manifest, files,
|
|
150
|
+
// known }`, its files as npmFileRows gives them, and with them, as its files
|
|
151
|
+
// are encoded, when it was published where that is kept (npmPublishTime,
|
|
152
|
+
// else null) and what `known` gives: what is kept about its package
|
|
153
|
+
// (npm-insights.ts npmKnown), which the viewer need not then ask for. Encoding
|
|
154
|
+
// is where a load holds the most, so it takes its place among the active
|
|
155
|
+
// loads until its body is done, and readers asking meanwhile share that body,
|
|
156
|
+
// the first one's manifest and `known` in it; nothing is kept once its readers
|
|
157
|
+
// are done.
|
|
158
|
+
export function loadNpmPackageBody(doc: NpmVersionDocument, known: () => object = () => ({})): NpmLoad<Buffer> {
|
|
159
|
+
return shared(`package ${doc.dist.integrity} ${doc.dist.tarball} ${doc.private}`, doc, async () => {
|
|
160
|
+
const tarball = await readTarball(doc)
|
|
161
|
+
const files = await readFiles(tarball)
|
|
162
|
+
return encodeBrotli(Buffer.from(JSON.stringify({
|
|
163
|
+
name: doc.name, version: doc.version, private: doc.private, integrity: doc.dist.integrity, tarballSize: tarball.byteLength,
|
|
164
|
+
published: npmPublishTime(doc.name, doc.version), manifest: doc.manifest, files: npmFileRows(files), known: known(),
|
|
165
|
+
})))
|
|
166
|
+
})
|
|
167
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// What a published version's document tells of it as the npm viewer's
|
|
2
|
+
// Overview shows it (npmManifest), and the fields of the document read for it
|
|
3
|
+
// and for the version itself (npm-packages.ts readNpmVersion), the rest
|
|
4
|
+
// stripped where it is kept (npmVersionFields).
|
|
5
|
+
import publishers from '@preventive/publishers/npm.js'
|
|
6
|
+
import { getRepo } from '@preventive/upstream/package.js'
|
|
7
|
+
|
|
8
|
+
export const plainObject = (value: unknown): value is Record<string, unknown> => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
9
|
+
export const stringRecord = (value: unknown) => plainObject(value)
|
|
10
|
+
? Object.fromEntries(Object.entries(value).filter(([, item]) => typeof item === 'string')) as Record<string, string> : undefined
|
|
11
|
+
|
|
12
|
+
interface NpmPerson { name: string | null; email: string | null }
|
|
13
|
+
const NPM_ACCOUNT = /^[\w.-]{1,214}$/u
|
|
14
|
+
// The npm accounts known to publish: those with standing on one of npm's
|
|
15
|
+
// most downloaded packages (@preventive/publishers).
|
|
16
|
+
const KNOWN_PUBLISHERS = new Set(publishers.map(name => name.toLowerCase()))
|
|
17
|
+
|
|
18
|
+
// One of package.json's people: `{ name, email, url }`, or `Name <email>
|
|
19
|
+
// (url)` in one string.
|
|
20
|
+
function npmPerson(value: unknown): NpmPerson | null {
|
|
21
|
+
const text = (part: unknown) => typeof part === 'string' && part.trim() !== '' ? part.trim() : null
|
|
22
|
+
if (plainObject(value)) return { name: text(value['name']), email: text(value['email']) }
|
|
23
|
+
if (typeof value !== 'string') return null
|
|
24
|
+
const parts = /^([^<(]*)(?:<([^>]*)>)?\s*(?:\([^)]*\))?\s*$/u.exec(value)
|
|
25
|
+
return parts ? { name: text(parts[1]), email: text(parts[2]) } : { name: text(value), email: null }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The GitLab project (`group/project`, subgroups and all) a package names as
|
|
29
|
+
// its repository, in any of the forms npm takes (`gitlab:group/project`,
|
|
30
|
+
// `git@gitlab.com:…`, `git+https://gitlab.com/….git`); null for another.
|
|
31
|
+
// GitLab's are only on gitlab.com, as npm's provenance has them.
|
|
32
|
+
function gitlabProject(repository: unknown): string | null {
|
|
33
|
+
const url = typeof repository === 'string' ? repository : plainObject(repository) && typeof repository['url'] === 'string' ? repository['url'] : ''
|
|
34
|
+
if (url.length > 2048) return null
|
|
35
|
+
const project = /^(?:gitlab:|git@gitlab\.com:|(?:git\+)?(?:https|ssh|git):\/\/(?:git@)?gitlab\.com\/)([\w.-]+(?:\/[\w.-]+)+)\/?$/u
|
|
36
|
+
.exec(url.split('/-/')[0]!)?.[1]?.replace(/\.git$/u, '')
|
|
37
|
+
return project && !project.split('/').some(part => /^\.+$/u.test(part)) ? project : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The fields the Overview shows, in the shapes package.json gives them; each
|
|
41
|
+
// left out where the document has none, or has another shape. A homepage that
|
|
42
|
+
// leads only where its repository does (the repository, its directory there,
|
|
43
|
+
// or its readme, npm's homepage where the package names none) is left out.
|
|
44
|
+
export function npmManifest(json: Record<string, unknown>): Record<string, unknown> {
|
|
45
|
+
const pick: Record<string, unknown> = {}
|
|
46
|
+
for (const key of ['description', 'license', 'homepage', 'main', 'module', 'types', 'type', 'deprecated', 'gitHead']) {
|
|
47
|
+
if (typeof json[key] === 'string') pick[key] = json[key]
|
|
48
|
+
}
|
|
49
|
+
// Its author's name, and their npm account where it can be told: the
|
|
50
|
+
// publisher's or a maintainer's whose email or name is the author's. Emails
|
|
51
|
+
// only match people up; none is kept.
|
|
52
|
+
const author = npmPerson(json['author'])
|
|
53
|
+
if (author?.name) pick['author'] = author.name
|
|
54
|
+
const accounts = [json['_npmUser'], ...Array.isArray(json['maintainers']) ? json['maintainers'] : []].map(npmPerson)
|
|
55
|
+
.filter((person): person is NpmPerson & { name: string } => person?.name != null && NPM_ACCOUNT.test(person.name))
|
|
56
|
+
// The npm account that published it, for its profile; or, published by
|
|
57
|
+
// a CI npm trusts (npm's trusted publishing), the CI, as npm names it
|
|
58
|
+
// (`GitHub Actions`).
|
|
59
|
+
const publisher = npmPerson(json['_npmUser'])?.name
|
|
60
|
+
if (publisher && NPM_ACCOUNT.test(publisher)) pick['publisher'] = publisher
|
|
61
|
+
else if (publisher && publisher.length <= 64 && plainObject(json['_npmUser']) && plainObject(json['_npmUser']['trustedPublisher'])) pick['trustedPublisher'] = publisher
|
|
62
|
+
const same = (a: string | null, b: string | null) => a !== null && b !== null && a.toLowerCase() === b.toLowerCase()
|
|
63
|
+
const account = author && accounts.find(person => same(person.email, author.email) || same(person.name, author.name))
|
|
64
|
+
if (account) pick['authorAccount'] = account.name
|
|
65
|
+
// Which of those accounts are known to publish, for the Overview to tell
|
|
66
|
+
// them from the rest.
|
|
67
|
+
const known = [...new Set([publisher, account?.name])].filter(name => name && KNOWN_PUBLISHERS.has(name.toLowerCase()))
|
|
68
|
+
if (known.length > 0) pick['knownAccounts'] = known
|
|
69
|
+
if (Array.isArray(json['keywords'])) pick['keywords'] = json['keywords'].filter(item => typeof item === 'string').slice(0, 50)
|
|
70
|
+
if (typeof json['bin'] === 'string') pick['bin'] = { [String(json['name'])]: json['bin'] }
|
|
71
|
+
else if (stringRecord(json['bin'])) pick['bin'] = stringRecord(json['bin'])
|
|
72
|
+
for (const key of ['dependencies', 'peerDependencies', 'optionalDependencies', 'engines']) {
|
|
73
|
+
const record = stringRecord(json[key])
|
|
74
|
+
if (record && Object.keys(record).length > 0) pick[key] = record
|
|
75
|
+
}
|
|
76
|
+
if (json['hasInstallScript'] === true) pick['hasInstallScript'] = true
|
|
77
|
+
// Whether npm keeps a provenance statement for it (npm-provenance.ts).
|
|
78
|
+
if (plainObject(json['dist']) && plainObject(json['dist']['attestations']) && plainObject(json['dist']['attestations']['provenance'])) pick['provenance'] = true
|
|
79
|
+
// The scripts npm runs installing it from the registry; not `prepare`,
|
|
80
|
+
// which runs only for a local or git install.
|
|
81
|
+
const scripts = stringRecord(json['scripts'])
|
|
82
|
+
const install = scripts ? ['preinstall', 'install', 'postinstall'].filter(script => script in scripts) : []
|
|
83
|
+
if (install.length > 0) pick['installScripts'] = Object.fromEntries(install.map(script => [script, scripts![script]]))
|
|
84
|
+
// A GitLab project it names, as its provenance's source is matched with
|
|
85
|
+
// the repository it names, GitHub's below.
|
|
86
|
+
const gitlab = gitlabProject(json['repository'])
|
|
87
|
+
if (gitlab) pick['gitlab'] = gitlab
|
|
88
|
+
try {
|
|
89
|
+
const repo = getRepo(json)
|
|
90
|
+
if (repo.github) {
|
|
91
|
+
pick['github'] = { github: repo.github, ...(repo.directory === undefined ? {} : { directory: repo.directory }) }
|
|
92
|
+
const home = typeof pick['homepage'] === 'string' ? getRepo({ homepage: pick['homepage'] }) : {}
|
|
93
|
+
if (home.github?.toLowerCase() === repo.github.toLowerCase() && (home.directory ?? '') === (repo.directory ?? '')) delete pick['homepage']
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
return pick
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Of a version's document, the fields readNpmVersion and npmManifest read,
|
|
100
|
+
// of `dist` and `scripts` only theirs, and of a provenance statement only that
|
|
101
|
+
// there is one: what is kept of it (npm-kept.ts), so that both read a kept one
|
|
102
|
+
// as they read npm's.
|
|
103
|
+
const VERSION_FIELDS = ['name', 'version', 'description', 'license', 'homepage', 'main', 'module', 'types', 'type', 'deprecated', 'gitHead', 'author', '_npmUser',
|
|
104
|
+
'maintainers', 'keywords', 'bin', 'dependencies', 'peerDependencies', 'optionalDependencies', 'engines', 'hasInstallScript', 'repository', 'bugs']
|
|
105
|
+
const DIST_FIELDS = ['tarball', 'integrity', 'unpackedSize', 'fileCount']
|
|
106
|
+
const INSTALL_SCRIPTS = ['preinstall', 'install', 'postinstall']
|
|
107
|
+
const picked = (json: Record<string, unknown>, keys: string[]) => Object.fromEntries(keys.filter(key => Object.hasOwn(json, key)).map(key => [key, json[key]]))
|
|
108
|
+
|
|
109
|
+
export function npmVersionFields(json: Record<string, unknown>): Record<string, unknown> {
|
|
110
|
+
const dist = plainObject(json['dist']) ? json['dist'] : {}
|
|
111
|
+
const provenance = plainObject(dist['attestations']) && plainObject(dist['attestations']['provenance'])
|
|
112
|
+
const scripts = picked(stringRecord(json['scripts']) ?? {}, INSTALL_SCRIPTS)
|
|
113
|
+
return { ...picked(json, VERSION_FIELDS), dist: { ...picked(dist, DIST_FIELDS), ...provenance && { attestations: { provenance: {} } } },
|
|
114
|
+
...Object.keys(scripts).length > 0 && { scripts } }
|
|
115
|
+
}
|